Compare commits

...

87 commits
v0.10.0 ... dev

Author SHA1 Message Date
gumyr
a971cbbad6 Making project_to_viewport a proper method
Some checks failed
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (macos-15-intel, 3.10) (push) Has been cancelled
tests / tests (macos-15-intel, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled
2025-12-03 13:41:53 -05:00
gumyr
726a72a20b Eliminating copying exploration methods in higher order classes
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run
2025-12-03 11:35:20 -05:00
gumyr
3871345dcd Improving split to explicitly handle all Keep Enum values 2025-12-03 10:13:09 -05:00
gumyr
6605b676a3 Fixed problem with hollow STL files
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run
2025-12-02 20:25:34 -05:00
gumyr
17ccdd01cc Fixing OCCT typing problems 2025-12-02 20:24:55 -05:00
gumyr
3474dc61d2 Fixed typing @ OCCT level
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run
2025-12-02 13:03:58 -05:00
gumyr
5adf296fd8 Fixed typing and linting issues
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run
2025-12-02 11:04:08 -05:00
gumyr
8985220c79 Typing improvements
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run
2025-12-01 21:05:38 -05:00
gumyr
e7045ea856 Merge branch 'dev' of https://github.com/gumyr/build123d into dev 2025-12-01 20:05:02 -05:00
gumyr
2fa0dd22da Refactored Solid.extrude_until, moved split to Shape, fixed misc typing problems 2025-12-01 20:04:48 -05:00
Roger Maitland
ad77bf5f7f
Merge pull request #1158 from gumyr/pr1140
Some checks failed
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (macos-15-intel, 3.10) (push) Has been cancelled
tests / tests (macos-15-intel, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled
Pr1140 with typing and test coverage improvements
2025-11-29 12:01:43 -05:00
gumyr
0bedc9c9ad Fixed typing problems and increased coverage to 100% 2025-11-29 11:43:27 -05:00
gumyr
a8fc16b344 Replacing Mixin1D.discretize with enhanced Minxin1D.positions
Some checks failed
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (macos-15-intel, 3.10) (push) Has been cancelled
tests / tests (macos-15-intel, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled
2025-11-25 13:26:36 -05:00
Roger Maitland
6eb11ad9f6
Merge pull request #1081 from paulftw/dev
Add Mixin1D.discretize
2025-11-25 13:14:58 -05:00
Roger Maitland
05eb8fbd4d
Merge branch 'dev' into dev 2025-11-25 12:58:56 -05:00
gumyr
82aa0aa367 Updating positions tests 2025-11-25 11:39:39 -05:00
gumyr
2d82b2ca5c Adding tests for positions with deflection 2025-11-25 11:27:21 -05:00
gumyr
7f6d44249b Added GCPnts_UniformDeflection to positions 2025-11-25 11:27:21 -05:00
gumyr
bdad339e58 Merge branch 'dev' of https://github.com/gumyr/build123d into dev
Some checks failed
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (macos-15-intel, 3.10) (push) Has been cancelled
tests / tests (macos-15-intel, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled
2025-11-21 15:09:32 -05:00
gumyr
bc8d01dc7e Improve length accuracy Issue #1136, minor typing fixes 2025-11-21 15:09:11 -05:00
jdegenstein
7a4f1f7e55
Merge pull request #1141 from jdegenstein/bytesio
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run
Add BytesIO export support to SVG, DXF, STEP, and STL/3MF via a lib3mf/Mesher workaround
2025-11-20 15:56:02 -06:00
jdegenstein
70764bbe08 revert spurious docstring change for Mesher.write 2025-11-20 15:28:37 -06:00
gumyr
26caed754c Removing make_face changes keeping BoundBox.extent
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run
2025-11-20 13:31:25 -05:00
gumyr
02a8c07e0a Reapply "Enhanced make_face so faces can have holes. Added BoundBox.measure"
This reverts commit 607efade27.
2025-11-20 11:51:04 -05:00
gumyr
607efade27 Revert "Enhanced make_face so faces can have holes. Added BoundBox.measure"
This reverts commit a5e95fe72f.
2025-11-20 11:50:15 -05:00
gumyr
a5e95fe72f Enhanced make_face so faces can have holes. Added BoundBox.measure 2025-11-20 11:15:12 -05:00
jdegenstein
e6d272b2fa
Merge pull request #1144 from Birdulon/patch-1
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run
Fix Example 14 header in introductory_examples.rst
2025-11-19 10:43:47 -06:00
Luke H-W
a00cecbc38
Fix Example 14 header in introductory_examples.rst
Header had "1." instead of "14."
2025-11-20 02:36:46 +10:30
gumyr
4507d78fff Added Color.categorical_set that generates a creates a list of visually distinct colors 2025-11-19 10:01:58 -05:00
gumyr
f3b080e351 Merge branch 'dev' of https://github.com/gumyr/build123d into dev 2025-11-19 09:25:01 -05:00
Roger Maitland
bc96e84dc2
Merge pull request #1143 from jwagenet/doc-fixes
Documentation: Formatting corrections and code style fixes
2025-11-19 09:23:56 -05:00
Roger Maitland
8980120cb2
Merge pull request #1133 from jwagenet/color_hexstr_rgba
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run
Feature: add support for Color initialization with RGBA hex string
2025-11-18 13:11:59 -05:00
Jonathan Wagenet
f144ca5aa8 Fix tutorial links 2025-11-18 10:34:21 -05:00
jdegenstein
7f4e92f0bf enable BytesIO in STEP, STL and 3MF (via lib3mf/Mesher). Add necessary tests 2025-11-17 22:05:45 -06:00
Jonathan Wagenet
9707749c61 Merge branch 'dev' into doc-fixes 2025-11-17 18:42:32 -05:00
jdegenstein
d329cf1094 initial changes to support BytesIO 2025-11-17 10:09:54 -06:00
Roger Maitland
837b743a13
Merge pull request #1088 from jwagenet/lexer
Some checks failed
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (macos-15-intel, 3.10) (push) Has been cancelled
tests / tests (macos-15-intel, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled
Docs: Add improved syntax highlighting to code blocks
2025-11-16 10:45:16 -05:00
Roger Maitland
caa25671fb
Merge branch 'dev' into lexer 2025-11-16 10:21:51 -05:00
x0pherl
1095f3ee4c changes to make development more friendly on MacOS 2025-11-16 10:16:25 -05:00
Alex Verschoot
c7034202f3 Changed the tests to not expect a valueorrer when having a 0 radius, but add two assertEquals so the number of Circles and Lines should be correct 2025-11-16 16:15:13 +01:00
Alex Verschoot
dc90a4b15a Changed the FilletPolyLine to be compatible with 0-radius fillets, where it should behave like a normal Polyline 2025-11-16 15:48:30 +01:00
x0pherl
e92255cefc updated to handle polygons without closed lines
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run
2025-11-15 14:23:32 -05:00
x0pherl
173c7b08e2 added support for passing an iterable of radii to FilletPolyline. 2025-11-15 14:23:32 -05:00
gumyr
2768427087 Merge branch 'dev' of https://github.com/gumyr/build123d into dev 2025-11-15 13:47:06 -05:00
Roger Maitland
df17ae8698
Merge pull request #1120 from jwagenet/intersections-2d
Intersect Everything: 2D, 3D, Composite Shapes
2025-11-15 13:28:39 -05:00
Jonathan Wagenet
5f67a1932a Update for dev merge to Compound and Face(Plane) 2025-11-14 17:30:55 -05:00
Jonathan Wagenet
5ea2dab174 Merge branch 'dev' into intersections-2d 2025-11-14 14:41:37 -05:00
Jonathan Wagenet
5523a2184c Revert mode == Mode.INTERSECT iteration. pass Compound instead 2025-11-14 14:40:58 -05:00
Jonathan Wagenet
c384df21c7 Intersect: dissolve Wire, Shell after intersection, no need to process 0d, 1d separately 2025-11-14 13:31:40 -05:00
Jonathan Wagenet
68f6ef2125 Convert intersect to use _bool_op and split Wire after intersect 2025-11-14 13:26:17 -05:00
Jonathan Wagenet
3877fd5876 Ignore orderless Shapes in _bool_op 2025-11-14 12:58:46 -05:00
jdegenstein
6937501e79
Merge pull request #1134 from jdegenstein/pytest842
Some checks failed
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (macos-15-intel, 3.10) (push) Has been cancelled
tests / tests (macos-15-intel, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled
pyproject.toml -> pin to pytest==8.4.2 per pytest-dev/pytest-xdist/issues/1273
2025-11-12 16:06:01 -06:00
jdegenstein
20854b3d4d
pyproject.toml -> pin to pytest==8.4.2 per pytest-dev/pytest-xdist/issues/1273 2025-11-12 15:40:23 -06:00
Jonathan Wagenet
083cb1611c Remove depreciated Color.to_tuple 2025-11-12 12:29:48 -05:00
Jonathan Wagenet
cc34b5a743 Convert to pytest with parameterization and test ids 2025-11-12 12:18:30 -05:00
Jonathan Wagenet
5d84002aa5 Add Color support for RGBA hex string 2025-11-12 10:37:45 -05:00
gumyr
38e69844b3 Merge branch 'dev' of https://github.com/gumyr/build123d into dev 2025-11-08 10:29:53 -05:00
Roger Maitland
e6d98de840
Merge pull request #1113 from jwagenet/face_plane
Some checks failed
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (macos-15-intel, 3.10) (push) Has been cancelled
tests / tests (macos-15-intel, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled
Replace `Face.make_plane()` with `Face(Plane)`
2025-11-08 10:19:42 -05:00
gumyr
395ecc173e Merge branch 'dev' of https://github.com/gumyr/build123d into dev 2025-11-08 10:14:56 -05:00
gumyr
513c50530c Added support for Face/cone properties: enhanced axis_of_rotation added semi_angle 2025-11-08 10:13:03 -05:00
Roger Maitland
0416967a61
Merge pull request #1110 from snoyer/improve-shapes-typing
Improve shapes typing
2025-11-08 09:59:09 -05:00
Jonathan Wagenet
3bea4d3228 Re-add make_plane with depreciation warning 2025-11-07 16:11:33 -05:00
snoyer
27567a10ef fix typo 2025-11-07 21:29:06 +04:00
Jonathan Wagenet
b049e6a8ce Merge branch 'dev' into intersections-2d (fix import conflict) 2025-10-29 13:49:49 -04:00
Jonathan Wagenet
3713574519 Remove xfail notes from issue tests 2025-10-29 13:02:31 -04:00
Jonathan Wagenet
5d7b098379 Correct mode == Mode.INTERSECT to iterate intersections instead of pass all in to_intersect
Shape.intersect(A, B) through BRepAlgoAPI_Common appears to treat tool as a single object such that intersection is Shape ^ (A + B). The updated intersect methods treat this intersection as Shape ^ A ^ B. The intersections in this change need to be interated to accomadate.
2025-10-29 00:16:02 -04:00
Jonathan Wagenet
069b691964 Conform Shape.intersect to None | ShapeList 2025-10-28 23:56:29 -04:00
Jonathan Wagenet
315605f485 Correct area/volume calculations from intersect with new return type of ShapeList 2025-10-28 23:45:29 -04:00
Jonathan Wagenet
c13ef47cef Correct ex26 by revolving 180 and removing mirror which creates invalid shape 2025-10-28 23:33:29 -04:00
Jonathan Wagenet
a7b554001f Add intersect method to Compound, similar to 2d and 3d 2025-10-24 22:37:28 -04:00
Jonathan Wagenet
cfd4546585 Add Compound tests 2025-10-24 22:36:56 -04:00
Jonathan Wagenet
89dedd0888 Add lexer to surface tuts 2025-10-21 14:03:22 -04:00
Jonathan Wagenet
8c32e3bed3 Merge branch 'dev' into lexer 2025-10-21 13:59:45 -04:00
Jonathan Wagenet
c83aedaae2 Merge branch 'dev' into doc-fixes 2025-10-21 13:49:35 -04:00
Jonathan Wagenet
9a6c382ced Replace Face.make_plane() with Face(Plane) to match Edge(Axis) 2025-10-21 13:31:14 -04:00
Jonathan Wagenet
fb324adced Add 2d and 3d multi to_intersect cases, exception cases 2025-10-21 12:57:03 -04:00
snoyer
6ce4a31355 appease mypy 2025-10-21 10:31:41 +04:00
snoyer
a6d8f9bdc1 refactor .wrapped is None usages 2025-10-21 10:15:47 +04:00
snoyer
0013b9fa87 fix Mixins generic types 2025-10-21 08:28:24 +04:00
snoyer
5d485ee705 use _wrapped: TOPODS | None member and wrapped: TOPODS property 2025-10-21 08:12:29 +04:00
Jonathan Wagenet
c7bf48c80c Add intersect methods to Mixin2D and Mixin3D
These methods are very similar using a branching structure to pick intersection method.
2025-10-20 17:59:19 -04:00
Jonathan Wagenet
99da8912df Add 2d and 3d intersection tests 2025-10-17 11:45:11 -04:00
Jonathan Wagenet
640b530058 Fix doctrings for sphinx make 2025-09-24 23:48:46 -04:00
Jonathan Wagenet
bb9495a821 Reorder mirror / make_face bot best practice to resolve #1053 2025-09-24 23:28:22 -04:00
Jonathan Wagenet
f4c79db263 Change kwarg capitalization to fix #1026. Unindent code blocks, fix doublespace + formatting 2025-09-24 23:16:35 -04:00
Jonathan Wagenet
3d8bbcc539 Add basic b123d lexer and change pygments style 2025-09-09 23:21:05 -04:00
Paul Korzhyk
335f82d740 Add Mixin1D.discretize 2025-08-28 20:19:06 +03:00
76 changed files with 2887 additions and 1271 deletions

3
.gitignore vendored
View file

@ -41,3 +41,6 @@ venv.bak/
# Profiling debris. # Profiling debris.
prof/ prof/
# MacOS cruft
.DS_Store

View file

@ -3,8 +3,8 @@ tests, ensure they build and pass, and ensure that `pylint` and `mypy`
are happy with your code. are happy with your code.
- Install `pip` following their [documentation](https://pip.pypa.io/en/stable/installation/). - Install `pip` following their [documentation](https://pip.pypa.io/en/stable/installation/).
- Install development dependencies: `pip install -e .[development]` - Install development dependencies: `pip install -e ".[development]"`
- Install docs dependencies: `pip install -e .[docs]` - Install docs dependencies: `pip install -e ".[docs]"`
- Install `build123d` in editable mode from current dir: `pip install -e .` - Install `build123d` in editable mode from current dir: `pip install -e .`
- Run tests with: `python -m pytest -n auto` - Run tests with: `python -m pytest -n auto`
- Build docs with: `cd docs && make html` - Build docs with: `cd docs && make html`

View file

@ -124,7 +124,7 @@ build123d of a piece of angle iron:
**build123d Approach** **build123d Approach**
.. code-block:: python .. code-block:: build123d
# Builder mode # Builder mode
with BuildPart() as angle_iron: with BuildPart() as angle_iron:
@ -135,7 +135,7 @@ build123d of a piece of angle iron:
fillet(angle_iron.edges().filter_by(lambda e: e.is_interior), 5 * MM) fillet(angle_iron.edges().filter_by(lambda e: e.is_interior), 5 * MM)
.. code-block:: python .. code-block:: build123d
# Algebra mode # Algebra mode
profile = Rectangle(3 * CM, 4 * MM, align=Align.MIN) profile = Rectangle(3 * CM, 4 * MM, align=Align.MIN)

View file

@ -20,7 +20,7 @@ python context manager.
... ...
) )
.. code-block:: python .. code-block:: build123d
# build123d API # build123d API
with BuildPart() as pillow_block: with BuildPart() as pillow_block:
@ -43,7 +43,7 @@ Each object and operation is now a class instantiation that interacts with the
active context implicitly for the user. These instantiations can be assigned to active context implicitly for the user. These instantiations can be assigned to
an instance variable as with standard python programming for direct use. an instance variable as with standard python programming for direct use.
.. code-block:: python .. code-block:: build123d
with BuildSketch() as plan: with BuildSketch() as plan:
r = Rectangle(width, height) r = Rectangle(width, height)
@ -62,7 +62,7 @@ with tangents equal to the tangents of l5 and l6 at their end and beginning resp
Being able to extract information from existing features allows the user to "snap" new Being able to extract information from existing features allows the user to "snap" new
features to these points without knowing their numeric values. features to these points without knowing their numeric values.
.. code-block:: python .. code-block:: build123d
with BuildLine() as outline: with BuildLine() as outline:
... ...
@ -81,6 +81,7 @@ by the last operation and fillets them. Such a selection would be quite difficul
otherwise. otherwise.
.. literalinclude:: ../examples/intersecting_pipes.py .. literalinclude:: ../examples/intersecting_pipes.py
:language: build123d
:lines: 30, 39-49 :lines: 30, 39-49
@ -104,7 +105,7 @@ sorting which opens up the full functionality of python lists. To aid the
user, common operations have been optimized as shown here along with user, common operations have been optimized as shown here along with
a fully custom selection: a fully custom selection:
.. code-block:: python .. code-block:: build123d
top = rail.faces().filter_by(Axis.Z)[-1] top = rail.faces().filter_by(Axis.Z)[-1]
... ...

View file

@ -7,7 +7,7 @@ Creating lots of Shapes in a loop means for every step ``fuse`` and ``clean`` wi
In an example like the below, both functions get slower and slower the more objects are In an example like the below, both functions get slower and slower the more objects are
already fused. Overall it takes on an M1 Mac 4.76 sec. already fused. Overall it takes on an M1 Mac 4.76 sec.
.. code-block:: python .. code-block:: build123d
diam = 80 diam = 80
holes = Sketch() holes = Sketch()
@ -22,7 +22,7 @@ already fused. Overall it takes on an M1 Mac 4.76 sec.
One way to avoid it is to use lazy evaluation for the algebra operations. Just collect all objects and One way to avoid it is to use lazy evaluation for the algebra operations. Just collect all objects and
then call ``fuse`` (``+``) once with all objects and ``clean`` once. Overall it takes 0.19 sec. then call ``fuse`` (``+``) once with all objects and ``clean`` once. Overall it takes 0.19 sec.
.. code-block:: python .. code-block:: build123d
r = Rectangle(2, 2) r = Rectangle(2, 2)
holes = [ holes = [
@ -36,7 +36,7 @@ then call ``fuse`` (``+``) once with all objects and ``clean`` once. Overall it
Another way to leverage the vectorized algebra operations is to add a list comprehension of objects to Another way to leverage the vectorized algebra operations is to add a list comprehension of objects to
an empty ``Part``, ``Sketch`` or ``Curve``: an empty ``Part``, ``Sketch`` or ``Curve``:
.. code-block:: python .. code-block:: build123d
polygons = Sketch() + [ polygons = Sketch() + [
loc * RegularPolygon(radius=5, side_count=5) loc * RegularPolygon(radius=5, side_count=5)

View file

@ -22,6 +22,7 @@ Here we'll assign labels to all of the components that will be part of the box
assembly: assembly:
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Add labels] :start-after: [Add labels]
:end-before: [Create assembly] :end-before: [Create assembly]
@ -36,6 +37,7 @@ Creation of the assembly is done by simply creating a :class:`~topology.Compound
appropriate ``parent`` and ``children`` attributes as shown here: appropriate ``parent`` and ``children`` attributes as shown here:
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Create assembly] :start-after: [Create assembly]
:end-before: [Display assembly] :end-before: [Display assembly]
@ -43,6 +45,7 @@ To display the topology of an assembly :class:`~topology.Compound`, the :meth:`~
method can be used as follows: method can be used as follows:
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Display assembly] :start-after: [Display assembly]
:end-before: [Add to the assembly by assigning the parent attribute of an object] :end-before: [Add to the assembly by assigning the parent attribute of an object]
@ -59,6 +62,7 @@ which results in:
To add to an assembly :class:`~topology.Compound` one can change either ``children`` or ``parent`` attributes. To add to an assembly :class:`~topology.Compound` one can change either ``children`` or ``parent`` attributes.
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Add to the assembly by assigning the parent attribute of an object] :start-after: [Add to the assembly by assigning the parent attribute of an object]
:end-before: [Check that the components in the assembly don't intersect] :end-before: [Check that the components in the assembly don't intersect]

View file

@ -21,8 +21,8 @@ with BuildSketch(Location((0, -r1, y3))) as sk_body:
m3 = IntersectingLine(m2 @ 1, m2 % 1, c1) m3 = IntersectingLine(m2 @ 1, m2 % 1, c1)
m4 = Line(m3 @ 1, (r1, r1)) m4 = Line(m3 @ 1, (r1, r1))
m5 = JernArc(m4 @ 1, m4 % 1, r1, -90) m5 = JernArc(m4 @ 1, m4 % 1, r1, -90)
m6 = Line(m5 @ 1, m1 @ 0) mirror(about=Plane.YZ)
mirror(make_face(l.line), Plane.YZ) make_face()
fillet(sk_body.vertices().group_by(Axis.Y)[1], 12) fillet(sk_body.vertices().group_by(Axis.Y)[1], 12)
with Locations((x1 / 2, y_tot - 10), (-x1 / 2, y_tot - 10)): with Locations((x1 / 2, y_tot - 10), (-x1 / 2, y_tot - 10)):
Circle(r2, mode=Mode.SUBTRACT) Circle(r2, mode=Mode.SUBTRACT)

View file

@ -47,16 +47,17 @@ with BuildPart() as ppp109:
split(bisect_by=Plane.YZ) split(bisect_by=Plane.YZ)
extrude(amount=6) extrude(amount=6)
f = ppp109.faces().filter_by(Axis((0, 0, 0), (-1, 0, 1)))[0] f = ppp109.faces().filter_by(Axis((0, 0, 0), (-1, 0, 1)))[0]
# extrude(f, until=Until.NEXT) # throws a warning extrude(f, until=Until.NEXT)
extrude(f, amount=10) fillet(ppp109.edges().filter_by(Axis.Y).sort_by(Axis.Z)[2], 16)
fillet(ppp109.edge(Select.NEW), 16) # extrude(f, amount=10)
# fillet(ppp109.edges(Select.NEW), 16)
show(ppp109) show(ppp109)
got_mass = ppp109.part.volume*densb got_mass = ppp109.part.volume * densb
want_mass = 307.23 want_mass = 307.23
tolerance = 1 tolerance = 1
delta = abs(got_mass - want_mass) delta = abs(got_mass - want_mass)
print(f"Mass: {got_mass:0.2f} g") print(f"Mass: {got_mass:0.2f} g")
assert delta < tolerance, f'{got_mass=}, {want_mass=}, {delta=}, {tolerance=}' assert delta < tolerance, f"{got_mass=}, {want_mass=}, {delta=}, {tolerance=}"

75
docs/build123d_lexer.py Normal file
View file

@ -0,0 +1,75 @@
import inspect
import enum
import sys
import os
from pygments.lexers.python import PythonLexer
from pygments.token import Name
from sphinx.highlighting import lexers
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
import build123d
class Build123dLexer(PythonLexer):
"""
Python lexer extended with Build123d-specific highlighting.
Dynamically pulls symbols from build123d.__all__.
"""
EXTRA_SYMBOLS = set(getattr(build123d, "__all__", []))
EXTRA_CLASSES = {
n for n in EXTRA_SYMBOLS
if n[0].isupper()
}
EXTRA_CONSTANTS = {
n for n in EXTRA_SYMBOLS
if n.isupper() and not callable(getattr(build123d, n, None))
}
EXTRA_ENUMS = {
n for n in EXTRA_SYMBOLS
if inspect.isclass(getattr(build123d, n, None)) and issubclass(getattr(build123d, n), enum.Enum)
}
EXTRA_FUNCTIONS = EXTRA_SYMBOLS - EXTRA_CLASSES - EXTRA_CONSTANTS - EXTRA_ENUMS
def get_tokens_unprocessed(self, text):
"""
Yield tokens, highlighting Build123d symbols, including chained accesses.
"""
dot_chain = False
for index, token, value in super().get_tokens_unprocessed(text):
if value == ".":
dot_chain = True
yield index, token, value
continue
if dot_chain:
# In a chain, don't use top-level categories
if value[0].isupper():
yield index, Name.Class, value
elif value.isupper():
yield index, Name.Constant, value
else:
yield index, Name.Function, value
dot_chain = False
continue
# Top-level classification from __all__
if value in self.EXTRA_CLASSES:
yield index, Name.Class, value
elif value in self.EXTRA_FUNCTIONS:
yield index, Name.Function, value
elif value in self.EXTRA_CONSTANTS:
yield index, Name.Constant, value
elif value in self.EXTRA_ENUMS:
yield index, Name.Builtin, value
else:
yield index, token, value
def setup(app):
lexers["build123d"] = Build123dLexer()
return {"version": "0.1"}

View file

@ -15,6 +15,7 @@ Basic Functionality
The following is a simple BuildLine example: The following is a simple BuildLine example:
.. literalinclude:: objects_1d.py .. literalinclude:: objects_1d.py
:language: build123d
:start-after: [Ex. 1] :start-after: [Ex. 1]
:end-before: [Ex. 1] :end-before: [Ex. 1]
@ -50,6 +51,7 @@ point ``(0,0)`` and ``(2,0)``. This can be improved upon by specifying
constraints that lock the arc to those two end points, as follows: constraints that lock the arc to those two end points, as follows:
.. literalinclude:: objects_1d.py .. literalinclude:: objects_1d.py
:language: build123d
:start-after: [Ex. 2] :start-after: [Ex. 2]
:end-before: [Ex. 2] :end-before: [Ex. 2]
@ -63,6 +65,7 @@ This example can be improved on further by calculating the mid-point
of the arc as follows: of the arc as follows:
.. literalinclude:: objects_1d.py .. literalinclude:: objects_1d.py
:language: build123d
:start-after: [Ex. 3] :start-after: [Ex. 3]
:end-before: [Ex. 3] :end-before: [Ex. 3]
@ -73,6 +76,7 @@ To make the design even more parametric, the height of the arc can be calculated
from ``l1`` as follows: from ``l1`` as follows:
.. literalinclude:: objects_1d.py .. literalinclude:: objects_1d.py
:language: build123d
:start-after: [Ex. 4] :start-after: [Ex. 4]
:end-before: [Ex. 4] :end-before: [Ex. 4]
@ -87,6 +91,7 @@ The other operator that is commonly used within BuildLine is ``%`` the tangent a
operator. Here is another example: operator. Here is another example:
.. literalinclude:: objects_1d.py .. literalinclude:: objects_1d.py
:language: build123d
:start-after: [Ex. 5] :start-after: [Ex. 5]
:end-before: [Ex. 5] :end-before: [Ex. 5]
@ -124,6 +129,7 @@ Here is an example of using BuildLine to create an object that otherwise might b
difficult to create: difficult to create:
.. literalinclude:: objects_1d.py .. literalinclude:: objects_1d.py
:language: build123d
:start-after: [Ex. 6] :start-after: [Ex. 6]
:end-before: [Ex. 6] :end-before: [Ex. 6]
@ -155,6 +161,7 @@ The other primary reasons to use BuildLine is to create paths for BuildPart
define a path: define a path:
.. literalinclude:: objects_1d.py .. literalinclude:: objects_1d.py
:language: build123d
:start-after: [Ex. 7] :start-after: [Ex. 7]
:end-before: [Ex. 7] :end-before: [Ex. 7]
@ -184,6 +191,7 @@ to global coordinates. Sometimes it's convenient to work on another plane, espec
creating paths for BuildPart ``Sweep`` operations. creating paths for BuildPart ``Sweep`` operations.
.. literalinclude:: objects_1d.py .. literalinclude:: objects_1d.py
:language: build123d
:start-after: [Ex. 8] :start-after: [Ex. 8]
:end-before: [Ex. 8] :end-before: [Ex. 8]

View file

@ -15,6 +15,7 @@ Basic Functionality
The following is a simple BuildPart example: The following is a simple BuildPart example:
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 2] :start-after: [Ex. 2]
:end-before: [Ex. 2] :end-before: [Ex. 2]
@ -52,6 +53,7 @@ This tea cup example uses implicit parameters - note the :func:`~operations_gene
operation on the last line: operation on the last line:
.. literalinclude:: ../examples/tea_cup.py .. literalinclude:: ../examples/tea_cup.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
:emphasize-lines: 52 :emphasize-lines: 52

View file

@ -16,6 +16,7 @@ Basic Functionality
The following is a simple BuildSketch example: The following is a simple BuildSketch example:
.. literalinclude:: objects_2d.py .. literalinclude:: objects_2d.py
:language: build123d
:start-after: [Ex. 13] :start-after: [Ex. 13]
:end-before: [Ex. 13] :end-before: [Ex. 13]
@ -61,6 +62,7 @@ As an example, let's build the following simple control box with a display on an
Here is the code: Here is the code:
.. literalinclude:: objects_2d.py .. literalinclude:: objects_2d.py
:language: build123d
:start-after: [Ex. 14] :start-after: [Ex. 14]
:end-before: [Ex. 14] :end-before: [Ex. 14]
:emphasize-lines: 14-25 :emphasize-lines: 14-25
@ -88,14 +90,14 @@ on ``Plane.XY`` which one can see by looking at the ``sketch_local`` property of
sketch. For example, to display the local version of the ``display`` sketch from sketch. For example, to display the local version of the ``display`` sketch from
above, one would use: above, one would use:
.. code-block:: python .. code-block:: build123d
show_object(display.sketch_local, name="sketch on Plane.XY") show_object(display.sketch_local, name="sketch on Plane.XY")
while the sketches as applied to their target workplanes is accessible through while the sketches as applied to their target workplanes is accessible through
the ``sketch`` property, as follows: the ``sketch`` property, as follows:
.. code-block:: python .. code-block:: build123d
show_object(display.sketch, name="sketch on target workplane(s)") show_object(display.sketch, name="sketch on target workplane(s)")
@ -106,7 +108,7 @@ that the new Face may not be oriented as expected. To reorient the Face manually
to ``Plane.XY`` one can use the :meth:`~geometry.to_local_coords` method as to ``Plane.XY`` one can use the :meth:`~geometry.to_local_coords` method as
follows: follows:
.. code-block:: python .. code-block:: build123d
reoriented_face = plane.to_local_coords(face) reoriented_face = plane.to_local_coords(face)

View file

@ -49,6 +49,7 @@ extensions = [
"sphinx_design", "sphinx_design",
"sphinx_copybutton", "sphinx_copybutton",
"hoverxref.extension", "hoverxref.extension",
"build123d_lexer"
] ]
# Napoleon settings # Napoleon settings
@ -99,6 +100,7 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# #
# html_theme = "alabaster" # html_theme = "alabaster"
html_theme = "sphinx_rtd_theme" html_theme = "sphinx_rtd_theme"
pygments_style = "colorful"
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,

View file

@ -85,7 +85,7 @@ Sometimes the best debugging aid is just placing a print statement in your code.
of the build123d classes are setup to provide useful information beyond their class and of the build123d classes are setup to provide useful information beyond their class and
location in memory, as follows: location in memory, as follows:
.. code-block:: python .. code-block:: build123d
plane = Plane.XY.offset(1) plane = Plane.XY.offset(1)
print(f"{plane=}") print(f"{plane=}")

View file

@ -164,6 +164,7 @@ modify it by replacing chimney with a BREP version.
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/benchy.py .. literalinclude:: ../examples/benchy.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -184,6 +185,7 @@ surface.
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/bicycle_tire.py .. literalinclude:: ../examples/bicycle_tire.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -204,12 +206,14 @@ The builder mode example also generates the SVG file `logo.svg`.
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/build123d_logo.py .. literalinclude:: ../examples/build123d_logo.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/build123d_logo_algebra.py .. literalinclude:: ../examples/build123d_logo_algebra.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -228,6 +232,7 @@ using the `draft` operation to add appropriate draft angles for mold release.
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/cast_bearing_unit.py .. literalinclude:: ../examples/cast_bearing_unit.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -257,12 +262,14 @@ This example also demonstrates building complex lines that snap to existing feat
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/canadian_flag.py .. literalinclude:: ../examples/canadian_flag.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/canadian_flag_algebra.py .. literalinclude:: ../examples/canadian_flag_algebra.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -293,12 +300,14 @@ This example demonstrates placing holes around a part.
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/circuit_board.py .. literalinclude:: ../examples/circuit_board.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/circuit_board_algebra.py .. literalinclude:: ../examples/circuit_board_algebra.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -313,12 +322,14 @@ Clock Face
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/clock.py .. literalinclude:: ../examples/clock.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/clock_algebra.py .. literalinclude:: ../examples/clock_algebra.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -340,6 +351,7 @@ Fast Grid Holes
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/fast_grid_holes.py .. literalinclude:: ../examples/fast_grid_holes.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -367,12 +379,14 @@ Handle
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/handle.py .. literalinclude:: ../examples/handle.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/handle_algebra.py .. literalinclude:: ../examples/handle_algebra.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -388,12 +402,14 @@ Heat Exchanger
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/heat_exchanger.py .. literalinclude:: ../examples/heat_exchanger.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/heat_exchanger_algebra.py .. literalinclude:: ../examples/heat_exchanger_algebra.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -412,12 +428,14 @@ Key Cap
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/key_cap.py .. literalinclude:: ../examples/key_cap.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/key_cap_algebra.py .. literalinclude:: ../examples/key_cap_algebra.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -444,6 +462,7 @@ YouTube channel. There are two key features:
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/maker_coin.py .. literalinclude:: ../examples/maker_coin.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -462,12 +481,14 @@ the top and bottom by type, and shelling.
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/loft.py .. literalinclude:: ../examples/loft.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/loft_algebra.py .. literalinclude:: ../examples/loft_algebra.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -488,6 +509,7 @@ to aid 3D printing.
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/pegboard_j_hook.py .. literalinclude:: ../examples/pegboard_j_hook.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -495,6 +517,7 @@ to aid 3D printing.
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/pegboard_j_hook_algebra.py .. literalinclude:: ../examples/pegboard_j_hook_algebra.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -521,6 +544,7 @@ embodying ideals of symmetry and balance.
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/platonic_solids.py .. literalinclude:: ../examples/platonic_solids.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -539,6 +563,7 @@ imported as code from an SVG file and modified to the code found here.
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/playing_cards.py .. literalinclude:: ../examples/playing_cards.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -558,6 +583,7 @@ are used to position all of objects.
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/stud_wall.py .. literalinclude:: ../examples/stud_wall.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -571,12 +597,14 @@ Tea Cup
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/tea_cup.py .. literalinclude:: ../examples/tea_cup.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/tea_cup_algebra.py .. literalinclude:: ../examples/tea_cup_algebra.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -610,6 +638,7 @@ Toy Truck
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/toy_truck.py .. literalinclude:: ../examples/toy_truck.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -630,12 +659,14 @@ Vase
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/vase.py .. literalinclude:: ../examples/vase.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/vase_algebra.py .. literalinclude:: ../examples/vase_algebra.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
@ -677,11 +708,13 @@ selecting edges by position range and type for the application of fillets
.. dropdown:: |Builder| Reference Implementation (Builder Mode) .. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/boxes_on_faces.py .. literalinclude:: ../examples/boxes_on_faces.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode) .. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/boxes_on_faces_algebra.py .. literalinclude:: ../examples/boxes_on_faces_algebra.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]

View file

@ -6,7 +6,7 @@ Methods and functions specific to exporting and importing build123d objects are
For example: For example:
.. code-block:: python .. code-block:: build123d
with BuildPart() as box_builder: with BuildPart() as box_builder:
Box(1, 1, 1) Box(1, 1, 1)
@ -142,7 +142,7 @@ The shapes generated from the above steps are to be added as shapes
in one of the exporters described below and written as either a DXF or SVG file as shown in one of the exporters described below and written as either a DXF or SVG file as shown
in this example: in this example:
.. code-block:: python .. code-block:: build123d
view_port_origin=(-100, -50, 30) view_port_origin=(-100, -50, 30)
visible, hidden = part.project_to_viewport(view_port_origin) visible, hidden = part.project_to_viewport(view_port_origin)
@ -222,7 +222,7 @@ more complex API than the simple Shape exporters.
For example: For example:
.. code-block:: python .. code-block:: build123d
# Create the shapes and assign attributes # Create the shapes and assign attributes
blue_shape = Solid.make_cone(20, 0, 50) blue_shape = Solid.make_cone(20, 0, 50)
@ -276,7 +276,7 @@ Both 3MF and STL import (and export) are provided with the :class:`~mesher.Meshe
For example: For example:
.. code-block:: python .. code-block:: build123d
importer = Mesher() importer = Mesher()
cone, cyl = importer.read("example.3mf") cone, cyl = importer.read("example.3mf")

View file

@ -51,8 +51,7 @@ expressive, algebraic modeling. It offers:
* Operator-driven modeling (``obj += sub_obj``, ``Plane.XZ * Pos(X=5) * Rectangle(1, 1)``) * Operator-driven modeling (``obj += sub_obj``, ``Plane.XZ * Pos(X=5) * Rectangle(1, 1)``)
for algebraic, readable, and composable design logic for algebraic, readable, and composable design logic
The result is a framework that feels native to Python while providing the full power of .. code-block:: build123d
OpenCascade geometry underneath.
With build123d, intricate parametric models can be created in just a few lines of readable With build123d, intricate parametric models can be created in just a few lines of readable
@ -60,9 +59,10 @@ Python code—as demonstrated by the tea cup example below.
.. dropdown:: Teacup Example .. dropdown:: Teacup Example
.. literalinclude:: ../examples/tea_cup.py .. literalinclude:: ../examples/tea_cup.py
:start-after: [Code] :language: build123d
:end-before: [End] :start-after: [Code]
:end-before: [End]
.. raw:: html .. raw:: html

View file

@ -36,12 +36,14 @@ Just about the simplest possible example, a rectangular :class:`~objects_part.Bo
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 1] :start-after: [Ex. 1]
:end-before: [Ex. 1] :end-before: [Ex. 1]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 1] :start-after: [Ex. 1]
:end-before: [Ex. 1] :end-before: [Ex. 1]
@ -63,6 +65,7 @@ A rectangular box, but with a hole added.
from the :class:`~objects_part.Box`. from the :class:`~objects_part.Box`.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 2] :start-after: [Ex. 2]
:end-before: [Ex. 2] :end-before: [Ex. 2]
@ -73,6 +76,7 @@ A rectangular box, but with a hole added.
from the :class:`~objects_part.Box`. from the :class:`~objects_part.Box`.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 2] :start-after: [Ex. 2]
:end-before: [Ex. 2] :end-before: [Ex. 2]
@ -94,6 +98,7 @@ Build a prismatic solid using extrusion.
and then use :class:`~build_part.BuildPart`'s :meth:`~operations_part.extrude` feature. and then use :class:`~build_part.BuildPart`'s :meth:`~operations_part.extrude` feature.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 3] :start-after: [Ex. 3]
:end-before: [Ex. 3] :end-before: [Ex. 3]
@ -103,6 +108,7 @@ Build a prismatic solid using extrusion.
:class:`~objects_sketch.Rectangle`` and then use the :meth:`~operations_part.extrude` operation for parts. :class:`~objects_sketch.Rectangle`` and then use the :meth:`~operations_part.extrude` operation for parts.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 3] :start-after: [Ex. 3]
:end-before: [Ex. 3] :end-before: [Ex. 3]
@ -126,6 +132,7 @@ variables for the line segments, but it will be useful in a later example.
from :class:`~build_line.BuildLine` into a closed Face. from :class:`~build_line.BuildLine` into a closed Face.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 4] :start-after: [Ex. 4]
:end-before: [Ex. 4] :end-before: [Ex. 4]
@ -138,6 +145,7 @@ variables for the line segments, but it will be useful in a later example.
segments into a Face. segments into a Face.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 4] :start-after: [Ex. 4]
:end-before: [Ex. 4] :end-before: [Ex. 4]
@ -158,6 +166,7 @@ Note that to build a closed face it requires line segments that form a closed sh
at one (or multiple) places. at one (or multiple) places.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 5] :start-after: [Ex. 5]
:end-before: [Ex. 5] :end-before: [Ex. 5]
@ -168,6 +177,7 @@ Note that to build a closed face it requires line segments that form a closed sh
(with :class:`geometry.Rot`) would rotate the object. (with :class:`geometry.Rot`) would rotate the object.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 5] :start-after: [Ex. 5]
:end-before: [Ex. 5] :end-before: [Ex. 5]
@ -188,6 +198,7 @@ Sometimes you need to create a number of features at various
You can use a list of points to construct multiple objects at once. You can use a list of points to construct multiple objects at once.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 6] :start-after: [Ex. 6]
:end-before: [Ex. 6] :end-before: [Ex. 6]
@ -200,6 +211,7 @@ Sometimes you need to create a number of features at various
is short for ``obj - obj1 - obj2 - ob3`` (and more efficient, see :ref:`algebra_performance`). is short for ``obj - obj1 - obj2 - ob3`` (and more efficient, see :ref:`algebra_performance`).
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 6] :start-after: [Ex. 6]
:end-before: [Ex. 6] :end-before: [Ex. 6]
@ -218,6 +230,7 @@ Sometimes you need to create a number of features at various
you would like. you would like.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 7] :start-after: [Ex. 7]
:end-before: [Ex. 7] :end-before: [Ex. 7]
@ -227,6 +240,7 @@ Sometimes you need to create a number of features at various
for each location via loops or list comprehensions. for each location via loops or list comprehensions.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 7] :start-after: [Ex. 7]
:end-before: [Ex. 7] :end-before: [Ex. 7]
@ -247,12 +261,14 @@ create the final profile.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 8] :start-after: [Ex. 8]
:end-before: [Ex. 8] :end-before: [Ex. 8]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 8] :start-after: [Ex. 8]
:end-before: [Ex. 8] :end-before: [Ex. 8]
@ -273,12 +289,14 @@ edges, you could simply pass in ``ex9.edges()``.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 9] :start-after: [Ex. 9]
:end-before: [Ex. 9] :end-before: [Ex. 9]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 9] :start-after: [Ex. 9]
:end-before: [Ex. 9] :end-before: [Ex. 9]
@ -303,6 +321,7 @@ be the highest z-dimension group.
makes use of :class:`~objects_part.Hole` which automatically cuts through the entire part. makes use of :class:`~objects_part.Hole` which automatically cuts through the entire part.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 10] :start-after: [Ex. 10]
:end-before: [Ex. 10] :end-before: [Ex. 10]
@ -314,6 +333,7 @@ be the highest z-dimension group.
of :class:`~objects_part.Hole`. Different to the *context mode*, you have to add the ``depth`` of the whole. of :class:`~objects_part.Hole`. Different to the *context mode*, you have to add the ``depth`` of the whole.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 10] :start-after: [Ex. 10]
:end-before: [Ex. 10] :end-before: [Ex. 10]
@ -339,6 +359,7 @@ be the highest z-dimension group.
cut these from the parent. cut these from the parent.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 11] :start-after: [Ex. 11]
:end-before: [Ex. 11] :end-before: [Ex. 11]
@ -355,6 +376,7 @@ be the highest z-dimension group.
parent. parent.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 11] :start-after: [Ex. 11]
:end-before: [Ex. 11] :end-before: [Ex. 11]
@ -376,12 +398,14 @@ edge that needs a complex profile.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 12] :start-after: [Ex. 12]
:end-before: [Ex. 12] :end-before: [Ex. 12]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 12] :start-after: [Ex. 12]
:end-before: [Ex. 12] :end-before: [Ex. 12]
@ -401,6 +425,7 @@ Counter-sink and counter-bore holes are useful for creating recessed areas for f
We use a face to establish a location for :class:`~build_common.Locations`. We use a face to establish a location for :class:`~build_common.Locations`.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 13] :start-after: [Ex. 13]
:end-before: [Ex. 13] :end-before: [Ex. 13]
@ -410,6 +435,7 @@ Counter-sink and counter-bore holes are useful for creating recessed areas for f
onto this plane. onto this plane.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 13] :start-after: [Ex. 13]
:end-before: [Ex. 13] :end-before: [Ex. 13]
@ -417,7 +443,7 @@ Counter-sink and counter-bore holes are useful for creating recessed areas for f
.. _ex 14: .. _ex 14:
14. Position on a line with '\@', '\%' and introduce Sweep 14. Position on a line with '\@', '\%' and introduce Sweep
------------------------------------------------------------ ------------------------------------------------------------
build123d includes a feature for finding the position along a line segment. This build123d includes a feature for finding the position along a line segment. This
@ -440,6 +466,7 @@ path, please see example 37 for a way to make this placement easier.
:meth:`~operations_part.revolve` requires a single connected wire. :meth:`~operations_part.revolve` requires a single connected wire.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 14] :start-after: [Ex. 14]
:end-before: [Ex. 14] :end-before: [Ex. 14]
@ -449,6 +476,7 @@ path, please see example 37 for a way to make this placement easier.
path (in this case the path is taken from ``ex14_ln``). path (in this case the path is taken from ``ex14_ln``).
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 14] :start-after: [Ex. 14]
:end-before: [Ex. 14] :end-before: [Ex. 14]
@ -471,6 +499,7 @@ Additionally the '@' operator is used to simplify the line segment commands.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 15] :start-after: [Ex. 15]
:end-before: [Ex. 15] :end-before: [Ex. 15]
@ -479,6 +508,7 @@ Additionally the '@' operator is used to simplify the line segment commands.
Combine lines via the pattern ``Curve() + [l1, l2, l3, l4, l5]`` Combine lines via the pattern ``Curve() + [l1, l2, l3, l4, l5]``
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 15] :start-after: [Ex. 15]
:end-before: [Ex. 15] :end-before: [Ex. 15]
@ -496,12 +526,14 @@ The ``Plane.offset()`` method shifts the plane in the normal direction (positive
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 16] :start-after: [Ex. 16]
:end-before: [Ex. 16] :end-before: [Ex. 16]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 16] :start-after: [Ex. 16]
:end-before: [Ex. 16] :end-before: [Ex. 16]
@ -520,12 +552,14 @@ Here we select the farthest face in the Y-direction and turn it into a :class:`~
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 17] :start-after: [Ex. 17]
:end-before: [Ex. 17] :end-before: [Ex. 17]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 17] :start-after: [Ex. 17]
:end-before: [Ex. 17] :end-before: [Ex. 17]
@ -546,6 +580,7 @@ with a negative distance.
We then use ``Mode.SUBTRACT`` to cut it out from the main body. We then use ``Mode.SUBTRACT`` to cut it out from the main body.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 18] :start-after: [Ex. 18]
:end-before: [Ex. 18] :end-before: [Ex. 18]
@ -554,6 +589,7 @@ with a negative distance.
We then use ``-=`` to cut it out from the main body. We then use ``-=`` to cut it out from the main body.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 18] :start-after: [Ex. 18]
:end-before: [Ex. 18] :end-before: [Ex. 18]
@ -578,6 +614,7 @@ this custom Axis.
:class:`~build_common.Locations` then the part would be offset from the workplane by the vertex z-position. :class:`~build_common.Locations` then the part would be offset from the workplane by the vertex z-position.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 19] :start-after: [Ex. 19]
:end-before: [Ex. 19] :end-before: [Ex. 19]
@ -588,6 +625,7 @@ this custom Axis.
:class:`~geometry.Pos` then the part would be offset from the workplane by the vertex z-position. :class:`~geometry.Pos` then the part would be offset from the workplane by the vertex z-position.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 19] :start-after: [Ex. 19]
:end-before: [Ex. 19] :end-before: [Ex. 19]
@ -606,12 +644,14 @@ negative x-direction. The resulting Plane is offset from the original position.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 20] :start-after: [Ex. 20]
:end-before: [Ex. 20] :end-before: [Ex. 20]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 20] :start-after: [Ex. 20]
:end-before: [Ex. 20] :end-before: [Ex. 20]
@ -630,12 +670,14 @@ positioning another cylinder perpendicular and halfway along the first.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 21] :start-after: [Ex. 21]
:end-before: [Ex. 21] :end-before: [Ex. 21]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 21] :start-after: [Ex. 21]
:end-before: [Ex. 21] :end-before: [Ex. 21]
@ -656,6 +698,7 @@ example.
Use the :meth:`~geometry.Plane.rotated` method to rotate the workplane. Use the :meth:`~geometry.Plane.rotated` method to rotate the workplane.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 22] :start-after: [Ex. 22]
:end-before: [Ex. 22] :end-before: [Ex. 22]
@ -664,6 +707,7 @@ example.
Use the operator ``*`` to relocate the plane (post-multiplication!). Use the operator ``*`` to relocate the plane (post-multiplication!).
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 22] :start-after: [Ex. 22]
:end-before: [Ex. 22] :end-before: [Ex. 22]
@ -690,12 +734,14 @@ It is highly recommended to view your sketch before you attempt to call revolve.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 23] :start-after: [Ex. 23]
:end-before: [Ex. 23] :end-before: [Ex. 23]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 23] :start-after: [Ex. 23]
:end-before: [Ex. 23] :end-before: [Ex. 23]
@ -716,12 +762,14 @@ Loft can behave unexpectedly when the input faces are not parallel to each other
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 24] :start-after: [Ex. 24]
:end-before: [Ex. 24] :end-before: [Ex. 24]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 24] :start-after: [Ex. 24]
:end-before: [Ex. 24] :end-before: [Ex. 24]
@ -739,6 +787,7 @@ Loft can behave unexpectedly when the input faces are not parallel to each other
BuildSketch faces can be transformed with a 2D :meth:`~operations_generic.offset`. BuildSketch faces can be transformed with a 2D :meth:`~operations_generic.offset`.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 25] :start-after: [Ex. 25]
:end-before: [Ex. 25] :end-before: [Ex. 25]
@ -747,6 +796,7 @@ Loft can behave unexpectedly when the input faces are not parallel to each other
Sketch faces can be transformed with a 2D :meth:`~operations_generic.offset`. Sketch faces can be transformed with a 2D :meth:`~operations_generic.offset`.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 25] :start-after: [Ex. 25]
:end-before: [Ex. 25] :end-before: [Ex. 25]
@ -772,12 +822,14 @@ Note that self intersecting edges and/or faces can break both 2D and 3D offsets.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 26] :start-after: [Ex. 26]
:end-before: [Ex. 26] :end-before: [Ex. 26]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 26] :start-after: [Ex. 26]
:end-before: [Ex. 26] :end-before: [Ex. 26]
@ -796,12 +848,14 @@ a face and offset half the width of the box.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 27] :start-after: [Ex. 27]
:end-before: [Ex. 27] :end-before: [Ex. 27]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 27] :start-after: [Ex. 27]
:end-before: [Ex. 27] :end-before: [Ex. 27]
@ -820,6 +874,7 @@ a face and offset half the width of the box.
use the faces of this object to cut holes in a sphere. use the faces of this object to cut holes in a sphere.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 28] :start-after: [Ex. 28]
:end-before: [Ex. 28] :end-before: [Ex. 28]
@ -828,6 +883,7 @@ a face and offset half the width of the box.
We create a triangular prism and then later use the faces of this object to cut holes in a sphere. We create a triangular prism and then later use the faces of this object to cut holes in a sphere.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 28] :start-after: [Ex. 28]
:end-before: [Ex. 28] :end-before: [Ex. 28]
@ -849,12 +905,14 @@ the bottle opening.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 29] :start-after: [Ex. 29]
:end-before: [Ex. 29] :end-before: [Ex. 29]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 29] :start-after: [Ex. 29]
:end-before: [Ex. 29] :end-before: [Ex. 29]
@ -874,12 +932,14 @@ create a closed line that is made into a face and extruded.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 30] :start-after: [Ex. 30]
:end-before: [Ex. 30] :end-before: [Ex. 30]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 30] :start-after: [Ex. 30]
:end-before: [Ex. 30] :end-before: [Ex. 30]
@ -899,12 +959,14 @@ rotates any "children" groups by default.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 31] :start-after: [Ex. 31]
:end-before: [Ex. 31] :end-before: [Ex. 31]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 31] :start-after: [Ex. 31]
:end-before: [Ex. 31] :end-before: [Ex. 31]
@ -927,12 +989,14 @@ separate calls to :meth:`~operations_part.extrude`.
adding these faces until the for-loop. adding these faces until the for-loop.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 32] :start-after: [Ex. 32]
:end-before: [Ex. 32] :end-before: [Ex. 32]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 32] :start-after: [Ex. 32]
:end-before: [Ex. 32] :end-before: [Ex. 32]
@ -954,6 +1018,7 @@ progressively modify the size of each square.
The function returns a :class:`~build_sketch.BuildSketch`. The function returns a :class:`~build_sketch.BuildSketch`.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 33] :start-after: [Ex. 33]
:end-before: [Ex. 33] :end-before: [Ex. 33]
@ -962,6 +1027,7 @@ progressively modify the size of each square.
The function returns a ``Sketch`` object. The function returns a ``Sketch`` object.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 33] :start-after: [Ex. 33]
:end-before: [Ex. 33] :end-before: [Ex. 33]
@ -983,6 +1049,7 @@ progressively modify the size of each square.
the 2nd "World" text on the top of the "Hello" text. the 2nd "World" text on the top of the "Hello" text.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 34] :start-after: [Ex. 34]
:end-before: [Ex. 34] :end-before: [Ex. 34]
@ -993,6 +1060,7 @@ progressively modify the size of each square.
the ``topf`` variable to select the same face and deboss (indented) the text "World". the ``topf`` variable to select the same face and deboss (indented) the text "World".
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 34] :start-after: [Ex. 34]
:end-before: [Ex. 34] :end-before: [Ex. 34]
@ -1012,6 +1080,7 @@ progressively modify the size of each square.
arc for two instances of :class:`~objects_sketch.SlotArc`. arc for two instances of :class:`~objects_sketch.SlotArc`.
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 35] :start-after: [Ex. 35]
:end-before: [Ex. 35] :end-before: [Ex. 35]
@ -1021,6 +1090,7 @@ progressively modify the size of each square.
a :class:`~objects_curve.RadiusArc` to create an arc for two instances of :class:`~operations_sketch.SlotArc`. a :class:`~objects_curve.RadiusArc` to create an arc for two instances of :class:`~operations_sketch.SlotArc`.
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 35] :start-after: [Ex. 35]
:end-before: [Ex. 35] :end-before: [Ex. 35]
@ -1041,11 +1111,14 @@ with ``Until.NEXT`` or ``Until.LAST``.
* **Builder mode** * **Builder mode**
.. literalinclude:: general_examples.py .. literalinclude:: general_examples.py
:language: build123d
:start-after: [Ex. 36] :start-after: [Ex. 36]
:end-before: [Ex. 36] :end-before: [Ex. 36]
* **Algebra mode** * **Algebra mode**
.. literalinclude:: general_examples_algebra.py .. literalinclude:: general_examples_algebra.py
:language: build123d
:start-after: [Ex. 36] :start-after: [Ex. 36]
:end-before: [Ex. 36] :end-before: [Ex. 36]

View file

@ -46,14 +46,14 @@ A rigid joint positions two components relative to each another with no freedom
and a ``joint_location`` which defines both the position and orientation of the joint (see and a ``joint_location`` which defines both the position and orientation of the joint (see
:class:`~geometry.Location`) - as follows: :class:`~geometry.Location`) - as follows:
.. code-block:: python .. code-block:: build123d
RigidJoint(label="outlet", to_part=pipe, joint_location=path.location_at(1)) RigidJoint(label="outlet", to_part=pipe, joint_location=path.location_at(1))
Once a joint is bound to a part this way, the :meth:`~topology.Joint.connect_to` method can be used to Once a joint is bound to a part this way, the :meth:`~topology.Joint.connect_to` method can be used to
repositioning another part relative to ``self`` which stay fixed - as follows: repositioning another part relative to ``self`` which stay fixed - as follows:
.. code-block:: python .. code-block:: build123d
pipe.joints["outlet"].connect_to(flange_outlet.joints["pipe"]) pipe.joints["outlet"].connect_to(flange_outlet.joints["pipe"])
@ -70,6 +70,7 @@ flanges are attached to the ends of a curved pipe:
.. image:: assets/rigid_joints_pipe.png .. image:: assets/rigid_joints_pipe.png
.. literalinclude:: rigid_joints_pipe.py .. literalinclude:: rigid_joints_pipe.py
:language: build123d
:emphasize-lines: 19-20, 23-24 :emphasize-lines: 19-20, 23-24
Note how the locations of the joints are determined by the :meth:`~topology.Mixin1D.location_at` method Note how the locations of the joints are determined by the :meth:`~topology.Mixin1D.location_at` method
@ -132,6 +133,7 @@ Component moves along a single axis as with a sliding latch shown here:
The code to generate these components follows: The code to generate these components follows:
.. literalinclude:: slide_latch.py .. literalinclude:: slide_latch.py
:language: build123d
:emphasize-lines: 30, 52, 55 :emphasize-lines: 30, 52, 55
.. image:: assets/joint-latch.png .. image:: assets/joint-latch.png
@ -193,6 +195,7 @@ is found within a rod end as shown here:
.. image:: assets/rod_end.png .. image:: assets/rod_end.png
.. literalinclude:: rod_end.py .. literalinclude:: rod_end.py
:language: build123d
:emphasize-lines: 40-44,51,53 :emphasize-lines: 40-44,51,53
Note how limits are defined during the instantiation of the ball joint when ensures that the pin or bolt Note how limits are defined during the instantiation of the ball joint when ensures that the pin or bolt

View file

@ -12,26 +12,26 @@ Object arithmetic
- Creating a box and a cylinder centered at ``(0, 0, 0)`` - Creating a box and a cylinder centered at ``(0, 0, 0)``
.. code-block:: python .. code-block:: build123d
b = Box(1, 2, 3) b = Box(1, 2, 3)
c = Cylinder(0.2, 5) c = Cylinder(0.2, 5)
- Fusing a box and a cylinder - Fusing a box and a cylinder
.. code-block:: python .. code-block:: build123d
r = Box(1, 2, 3) + Cylinder(0.2, 5) r = Box(1, 2, 3) + Cylinder(0.2, 5)
- Cutting a cylinder from a box - Cutting a cylinder from a box
.. code-block:: python .. code-block:: build123d
r = Box(1, 2, 3) - Cylinder(0.2, 5) r = Box(1, 2, 3) - Cylinder(0.2, 5)
- Intersecting a box and a cylinder - Intersecting a box and a cylinder
.. code-block:: python .. code-block:: build123d
r = Box(1, 2, 3) & Cylinder(0.2, 5) r = Box(1, 2, 3) & Cylinder(0.2, 5)
@ -54,7 +54,7 @@ The generic forms of object placement are:
1. Placement on ``plane`` or at ``location`` relative to XY plane: 1. Placement on ``plane`` or at ``location`` relative to XY plane:
.. code-block:: python .. code-block:: build123d
plane * alg_compound plane * alg_compound
location * alg_compound location * alg_compound
@ -62,7 +62,7 @@ The generic forms of object placement are:
2. Placement on the ``plane`` and then moved relative to the ``plane`` by ``location`` 2. Placement on the ``plane`` and then moved relative to the ``plane`` by ``location``
(the location is relative to the local coordinate system of the plane). (the location is relative to the local coordinate system of the plane).
.. code-block:: python .. code-block:: build123d
plane * location * alg_compound plane * location * alg_compound
@ -73,7 +73,7 @@ Examples:
- Box on the ``XY`` plane, centered at `(0, 0, 0)` (both forms are equivalent): - Box on the ``XY`` plane, centered at `(0, 0, 0)` (both forms are equivalent):
.. code-block:: python .. code-block:: build123d
Plane.XY * Box(1, 2, 3) Plane.XY * Box(1, 2, 3)
@ -84,7 +84,7 @@ Examples:
- Box on the ``XY`` plane centered at `(0, 1, 0)` (all three are equivalent): - Box on the ``XY`` plane centered at `(0, 1, 0)` (all three are equivalent):
.. code-block:: python .. code-block:: build123d
Plane.XY * Pos(0, 1, 0) * Box(1, 2, 3) Plane.XY * Pos(0, 1, 0) * Box(1, 2, 3)
@ -96,21 +96,21 @@ Examples:
- Box on plane ``Plane.XZ``: - Box on plane ``Plane.XZ``:
.. code-block:: python .. code-block:: build123d
Plane.XZ * Box(1, 2, 3) Plane.XZ * Box(1, 2, 3)
- Box on plane ``Plane.XZ`` with a location ``(X=1, Y=2, Z=3)`` relative to the ``XZ`` plane, i.e., - Box on plane ``Plane.XZ`` with a location ``(X=1, Y=2, Z=3)`` relative to the ``XZ`` plane, i.e.,
using the x-, y- and z-axis of the ``XZ`` plane: using the x-, y- and z-axis of the ``XZ`` plane:
.. code-block:: python .. code-block:: build123d
Plane.XZ * Pos(1, 2, 3) * Box(1, 2, 3) Plane.XZ * Pos(1, 2, 3) * Box(1, 2, 3)
- Box on plane ``Plane.XZ`` moved to ``(X=1, Y=2, Z=3)`` relative to this plane and rotated there - Box on plane ``Plane.XZ`` moved to ``(X=1, Y=2, Z=3)`` relative to this plane and rotated there
by the angles `(X=0, Y=100, Z=45)` around ``Plane.XZ`` axes: by the angles `(X=0, Y=100, Z=45)` around ``Plane.XZ`` axes:
.. code-block:: python .. code-block:: build123d
Plane.XZ * Pos(1, 2, 3) * Rot(0, 100, 45) * Box(1, 2, 3) Plane.XZ * Pos(1, 2, 3) * Rot(0, 100, 45) * Box(1, 2, 3)
@ -121,7 +121,7 @@ Examples:
- Box on plane ``Plane.XZ`` rotated on this plane by the angles ``(X=0, Y=100, Z=45)`` (using the - Box on plane ``Plane.XZ`` rotated on this plane by the angles ``(X=0, Y=100, Z=45)`` (using the
x-, y- and z-axis of the ``XZ`` plane) and then moved to ``(X=1, Y=2, Z=3)`` relative to the ``XZ`` plane: x-, y- and z-axis of the ``XZ`` plane) and then moved to ``(X=1, Y=2, Z=3)`` relative to the ``XZ`` plane:
.. code-block:: python .. code-block:: build123d
Plane.XZ * Rot(0, 100, 45) * Pos(0,1,2) * Box(1, 2, 3) Plane.XZ * Rot(0, 100, 45) * Pos(0,1,2) * Box(1, 2, 3)
@ -131,7 +131,7 @@ Combing both concepts
**Object arithmetic** and **Placement at locations** can be combined: **Object arithmetic** and **Placement at locations** can be combined:
.. code-block:: python .. code-block:: build123d
b = Plane.XZ * Rot(X=30) * Box(1, 2, 3) + Plane.YZ * Pos(X=-1) * Cylinder(0.2, 5) b = Plane.XZ * Rot(X=30) * Box(1, 2, 3) + Plane.YZ * Pos(X=-1) * Cylinder(0.2, 5)

View file

@ -61,7 +61,7 @@ Example Workflow
Here is an example of using a Builder to create a simple part: Here is an example of using a Builder to create a simple part:
.. code-block:: python .. code-block:: build123d
from build123d import * from build123d import *
@ -117,21 +117,21 @@ class for further processing.
One can access the objects created by these builders by referencing the appropriate One can access the objects created by these builders by referencing the appropriate
instance variable. For example: instance variable. For example:
.. code-block:: python .. code-block:: build123d
with BuildPart() as my_part: with BuildPart() as my_part:
... ...
show_object(my_part.part) show_object(my_part.part)
.. code-block:: python .. code-block:: build123d
with BuildSketch() as my_sketch: with BuildSketch() as my_sketch:
... ...
show_object(my_sketch.sketch) show_object(my_sketch.sketch)
.. code-block:: python .. code-block:: build123d
with BuildLine() as my_line: with BuildLine() as my_line:
... ...
@ -144,7 +144,7 @@ Implicit Builder Instance Variables
One might expect to have to reference a builder's instance variable when using One might expect to have to reference a builder's instance variable when using
objects or operations that impact that builder like this: objects or operations that impact that builder like this:
.. code-block:: python .. code-block:: build123d
with BuildPart() as part_builder: with BuildPart() as part_builder:
Box(part_builder, 10,10,10) Box(part_builder, 10,10,10)
@ -153,7 +153,7 @@ Instead, build123d determines from the scope of the object or operation which
builder it applies to thus eliminating the need for the user to provide this builder it applies to thus eliminating the need for the user to provide this
information - as follows: information - as follows:
.. code-block:: python .. code-block:: build123d
with BuildPart() as part_builder: with BuildPart() as part_builder:
Box(10,10,10) Box(10,10,10)
@ -175,7 +175,7 @@ be generated on any plane which allows users to put a workplane where they are w
and then work in local 2D coordinate space. and then work in local 2D coordinate space.
.. code-block:: python .. code-block:: build123d
with BuildPart(Plane.XY) as example: with BuildPart(Plane.XY) as example:
... # a 3D-part ... # a 3D-part
@ -199,7 +199,7 @@ One is not limited to a single workplane at a time. In the following example all
faces of the first box are used to define workplanes which are then used to position faces of the first box are used to define workplanes which are then used to position
rotated boxes. rotated boxes.
.. code-block:: python .. code-block:: build123d
import build123d as bd import build123d as bd
@ -223,7 +223,7 @@ When positioning objects or operations within a builder Location Contexts are us
function in a very similar was to the builders in that they create a context where one or function in a very similar was to the builders in that they create a context where one or
more locations are active within a scope. For example: more locations are active within a scope. For example:
.. code-block:: python .. code-block:: build123d
with BuildPart(): with BuildPart():
with Locations((0,10),(0,-10)): with Locations((0,10),(0,-10)):
@ -244,7 +244,7 @@ its scope - much as the hour and minute indicator on an analogue clock.
Also note that the locations are local to the current location(s) - i.e. ``Locations`` can be Also note that the locations are local to the current location(s) - i.e. ``Locations`` can be
nested. It's easy for a user to retrieve the global locations: nested. It's easy for a user to retrieve the global locations:
.. code-block:: python .. code-block:: build123d
with Locations(Plane.XY, Plane.XZ): with Locations(Plane.XY, Plane.XZ):
locs = GridLocations(1, 1, 2, 2) locs = GridLocations(1, 1, 2, 2)
@ -271,7 +271,7 @@ an iterable of objects is often required (often a ShapeList).
Here is the definition of :meth:`~operations_generic.fillet` to help illustrate: Here is the definition of :meth:`~operations_generic.fillet` to help illustrate:
.. code-block:: python .. code-block:: build123d
def fillet( def fillet(
objects: Union[Union[Edge, Vertex], Iterable[Union[Edge, Vertex]]], objects: Union[Union[Edge, Vertex], Iterable[Union[Edge, Vertex]]],
@ -281,7 +281,7 @@ Here is the definition of :meth:`~operations_generic.fillet` to help illustrate:
To use this fillet operation, an edge or vertex or iterable of edges or To use this fillet operation, an edge or vertex or iterable of edges or
vertices must be provided followed by a fillet radius with or without the keyword as follows: vertices must be provided followed by a fillet radius with or without the keyword as follows:
.. code-block:: python .. code-block:: build123d
with BuildPart() as pipes: with BuildPart() as pipes:
Box(10, 10, 10, rotation=(10, 20, 30)) Box(10, 10, 10, rotation=(10, 20, 30))
@ -297,7 +297,7 @@ Combination Modes
Almost all objects or operations have a ``mode`` parameter which is defined by the Almost all objects or operations have a ``mode`` parameter which is defined by the
``Mode`` Enum class as follows: ``Mode`` Enum class as follows:
.. code-block:: python .. code-block:: build123d
class Mode(Enum): class Mode(Enum):
ADD = auto() ADD = auto()
@ -329,7 +329,7 @@ build123d stores points (to be specific ``Location`` (s)) internally to be used
positions for the placement of new objects. By default, a single location positions for the placement of new objects. By default, a single location
will be created at the origin of the given workplane such that: will be created at the origin of the given workplane such that:
.. code-block:: python .. code-block:: build123d
with BuildPart() as pipes: with BuildPart() as pipes:
Box(10, 10, 10, rotation=(10, 20, 30)) Box(10, 10, 10, rotation=(10, 20, 30))
@ -338,7 +338,7 @@ will create a single 10x10x10 box centered at (0,0,0) - by default objects are
centered. One can create multiple objects by pushing points prior to creating centered. One can create multiple objects by pushing points prior to creating
objects as follows: objects as follows:
.. code-block:: python .. code-block:: build123d
with BuildPart() as pipes: with BuildPart() as pipes:
with Locations((-10, -10, -10), (10, 10, 10)): with Locations((-10, -10, -10), (10, 10, 10)):
@ -370,7 +370,7 @@ Builder's Pending Objects
When a builder exits, it will push the object created back to its parent if When a builder exits, it will push the object created back to its parent if
there was one. Here is an example: there was one. Here is an example:
.. code-block:: python .. code-block:: build123d
height, width, thickness, f_rad = 60, 80, 20, 10 height, width, thickness, f_rad = 60, 80, 20, 10

View file

@ -3,13 +3,12 @@
Location arithmetic for algebra mode Location arithmetic for algebra mode
====================================== ======================================
Position a shape relative to the XY plane Position a shape relative to the XY plane
--------------------------------------------- ---------------------------------------------
For the following use the helper function: For the following use the helper function:
.. code-block:: python .. code-block:: build123d
def location_symbol(location: Location, scale: float = 1) -> Compound: def location_symbol(location: Location, scale: float = 1) -> Compound:
return Compound.make_triad(axes_scale=scale).locate(location) return Compound.make_triad(axes_scale=scale).locate(location)
@ -19,134 +18,131 @@ For the following use the helper function:
circle = Circle(scale * .8).edge() circle = Circle(scale * .8).edge()
return (triad + circle).locate(plane.location) return (triad + circle).locate(plane.location)
1. **Positioning at a location** 1. **Positioning at a location**
.. code-block:: python .. code-block:: build123d
loc = Location((0.1, 0.2, 0.3), (10, 20, 30)) loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
face = loc * Rectangle(1,2) face = loc * Rectangle(1, 2)
show_object(face, name="face") show_object(face, name="face")
show_object(location_symbol(loc), name="location") show_object(location_symbol(loc), name="location")
.. image:: assets/location-example-01.png .. image:: assets/location-example-01.png
2) **Positioning on a plane** 2) **Positioning on a plane**
.. code-block:: python .. code-block:: build123d
plane = Plane.XZ plane = Plane.XZ
face = plane * Rectangle(1, 2) face = plane * Rectangle(1, 2)
show_object(face, name="face") show_object(face, name="face")
show_object(plane_symbol(plane), name="plane") show_object(plane_symbol(plane), name="plane")
.. image:: assets/location-example-07.png .. image:: assets/location-example-07.png
Note that the ``x``-axis and the ``y``-axis of the plane are on the ``x``-axis and the ``z``-axis of the world coordinate system (red and blue axis)
Note: The ``x``-axis and the ``y``-axis of the plane are on the ``x``-axis and the ``z``-axis of the world coordinate system (red and blue axis).
Relative positioning to a plane Relative positioning to a plane
------------------------------------ ------------------------------------
1. **Position an object on a plane relative to the plane** 1. **Position an object on a plane relative to the plane**
.. code-block:: python .. code-block:: build123d
loc = Location((0.1, 0.2, 0.3), (10, 20, 30)) loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
face = loc * Rectangle(1,2) face = loc * Rectangle(1,2)
box = Plane(loc) * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2) box = Plane(loc) * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
# box = Plane(face.location) * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2) # box = Plane(face.location) * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
# box = loc * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2) # box = loc * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
show_object(face, name="face") show_object(face, name="face")
show_object(location_symbol(loc), name="location") show_object(location_symbol(loc), name="location")
show_object(box, name="box") show_object(box, name="box")
.. image:: assets/location-example-02.png .. image:: assets/location-example-02.png
The ``x``, ``y``, ``z`` components of ``Pos(0.2, 0.4, 0.1)`` are relative to the ``x``-axis, ``y``-axis or The ``X``, ``Y``, ``Z`` components of ``Pos(0.2, 0.4, 0.1)`` are relative to the ``x``-axis, ``y``-axis or
``z``-axis of the underlying location ``loc``. ``z``-axis of the underlying location ``loc``.
Note: ``Plane(loc) *``, ``Plane(face.location) *`` and ``loc *`` are equivalent in this example. Note: ``Plane(loc) *``, ``Plane(face.location) *`` and ``loc *`` are equivalent in this example.
2. **Rotate an object on a plane relative to the plane** 2. **Rotate an object on a plane relative to the plane**
.. code-block:: python .. code-block:: build123d
loc = Location((0.1, 0.2, 0.3), (10, 20, 30)) loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
face = loc * Rectangle(1,2) face = loc * Rectangle(1,2)
box = Plane(loc) * Rot(z=80) * Box(0.2, 0.2, 0.2) box = Plane(loc) * Rot(Z=80) * Box(0.2, 0.2, 0.2)
show_object(face, name="face") show_object(face, name="face")
show_object(location_symbol(loc), name="location") show_object(location_symbol(loc), name="location")
show_object(box, name="box") show_object(box, name="box")
.. image:: assets/location-example-03.png .. image:: assets/location-example-03.png
The box is rotated via ``Rot(z=80)`` around the ``z``-axis of the underlying location The box is rotated via ``Rot(Z=80)`` around the ``z``-axis of the underlying location
(and not of the z-axis of the world). (and not of the z-axis of the world).
More general: More general:
.. code-block:: python .. code-block:: build123d
loc = Location((0.1, 0.2, 0.3), (10, 20, 30)) loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
face = loc * Rectangle(1,2) face = loc * Rectangle(1,2)
box = loc * Rot(20, 40, 80) * Box(0.2, 0.2, 0.2) box = loc * Rot(20, 40, 80) * Box(0.2, 0.2, 0.2)
show_object(face, name="face") show_object(face, name="face")
show_object(location_symbol(loc), name="location") show_object(location_symbol(loc), name="location")
show_object(box, name="box") show_object(box, name="box")
.. image:: assets/location-example-04.png .. image:: assets/location-example-04.png
The box is rotated via ``Rot(20, 40, 80)`` around all three axes relative to the plane. The box is rotated via ``Rot(20, 40, 80)`` around all three axes relative to the plane.
3. **Rotate and position an object relative to a location** 3. **Rotate and position an object relative to a location**
.. code-block:: python .. code-block:: build123d
loc = Location((0.1, 0.2, 0.3), (10, 20, 30)) loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
face = loc * Rectangle(1,2) face = loc * Rectangle(1,2)
box = loc * Rot(20, 40, 80) * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2) box = loc * Rot(20, 40, 80) * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
show_object(face, name="face") show_object(face, name="face")
show_object(location_symbol(loc), name="location") show_object(location_symbol(loc), name="location")
show_object(box, name="box") show_object(box, name="box")
show_object(location_symbol(loc * Rot(20, 40, 80), 0.5), options={"color":(0, 255, 255)}, name="local_location") show_object(location_symbol(loc * Rot(20, 40, 80), 0.5), options={"color":(0, 255, 255)}, name="local_location")
.. image:: assets/location-example-05.png .. image:: assets/location-example-05.png
The box is positioned via ``Pos(0.2, 0.4, 0.1)`` relative to the location ``loc * Rot(20, 40, 80)`` The box is positioned via ``Pos(0.2, 0.4, 0.1)`` relative to the location ``loc * Rot(20, 40, 80)``
4. **Position and rotate an object relative to a location** 4. **Position and rotate an object relative to a location**
.. code-block:: python .. code-block:: build123d
loc = Location((0.1, 0.2, 0.3), (10, 20, 30)) loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
face = loc * Rectangle(1,2) face = loc * Rectangle(1,2)
box = loc * Pos(0.2, 0.4, 0.1) * Rot(20, 40, 80) * Box(0.2, 0.2, 0.2) box = loc * Pos(0.2, 0.4, 0.1) * Rot(20, 40, 80) * Box(0.2, 0.2, 0.2)
show_object(face, name="face") show_object(face, name="face")
show_object(location_symbol(loc), name="location") show_object(location_symbol(loc), name="location")
show_object(box, name="box") show_object(box, name="box")
show_object(location_symbol(loc * Pos(0.2, 0.4, 0.1), 0.5), options={"color":(0, 255, 255)}, name="local_location") show_object(location_symbol(loc * Pos(0.2, 0.4, 0.1), 0.5), options={"color":(0, 255, 255)}, name="local_location")
.. image:: assets/location-example-06.png .. image:: assets/location-example-06.png
Note: This is the same as `box = loc * Location((0.2, 0.4, 0.1), (20, 40, 80)) * Box(0.2, 0.2, 0.2)`
Note: This is the same as ``box = loc * Location((0.2, 0.4, 0.1), (20, 40, 80)) * Box(0.2, 0.2, 0.2)``

View file

@ -22,7 +22,7 @@ construction process. The following tools are commonly used to specify locations
Example: Example:
.. code-block:: python .. code-block:: build123d
with Locations((10, 20, 30)): with Locations((10, 20, 30)):
Box(5, 5, 5) Box(5, 5, 5)
@ -42,7 +42,7 @@ an existing one.
Example: Example:
.. code-block:: python .. code-block:: build123d
rotated_box = Rotation(45, 0, 0) * box rotated_box = Rotation(45, 0, 0) * box
@ -55,13 +55,13 @@ Position
^^^^^^^^ ^^^^^^^^
- **Absolute Position:** Set the position directly. - **Absolute Position:** Set the position directly.
.. code-block:: python .. code-block:: build123d
shape.position = (x, y, z) shape.position = (x, y, z)
- **Relative Position:** Adjust the position incrementally. - **Relative Position:** Adjust the position incrementally.
.. code-block:: python .. code-block:: build123d
shape.position += (x, y, z) shape.position += (x, y, z)
shape.position -= (x, y, z) shape.position -= (x, y, z)
@ -71,13 +71,13 @@ Orientation
^^^^^^^^^^^ ^^^^^^^^^^^
- **Absolute Orientation:** Set the orientation directly. - **Absolute Orientation:** Set the orientation directly.
.. code-block:: python .. code-block:: build123d
shape.orientation = (X, Y, Z) shape.orientation = (X, Y, Z)
- **Relative Orientation:** Adjust the orientation incrementally. - **Relative Orientation:** Adjust the orientation incrementally.
.. code-block:: python .. code-block:: build123d
shape.orientation += (X, Y, Z) shape.orientation += (X, Y, Z)
shape.orientation -= (X, Y, Z) shape.orientation -= (X, Y, Z)
@ -86,25 +86,25 @@ Movement Methods
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
- **Relative Move:** - **Relative Move:**
.. code-block:: python .. code-block:: build123d
shape.move(Location) shape.move(Location)
- **Relative Move of Copy:** - **Relative Move of Copy:**
.. code-block:: python .. code-block:: build123d
relocated_shape = shape.moved(Location) relocated_shape = shape.moved(Location)
- **Absolute Move:** - **Absolute Move:**
.. code-block:: python .. code-block:: build123d
shape.locate(Location) shape.locate(Location)
- **Absolute Move of Copy:** - **Absolute Move of Copy:**
.. code-block:: python .. code-block:: build123d
relocated_shape = shape.located(Location) relocated_shape = shape.located(Location)
@ -119,12 +119,12 @@ Transformation a.k.a. Translation and Rotation
- **Translation:** Move a shape relative to its current position. - **Translation:** Move a shape relative to its current position.
.. code-block:: python .. code-block:: build123d
relocated_shape = shape.translate(x, y, z) relocated_shape = shape.translate(x, y, z)
- **Rotation:** Rotate a shape around a specified axis by a given angle. - **Rotation:** Rotate a shape around a specified axis by a given angle.
.. code-block:: python .. code-block:: build123d
rotated_shape = shape.rotate(Axis, angle_in_degrees) rotated_shape = shape.rotate(Axis, angle_in_degrees)

View file

@ -7,7 +7,7 @@ For example, a :class:`~objects_part.Torus` is defined by a major and minor radi
Builder mode, objects are positioned with ``Locations`` while in Algebra mode, objects Builder mode, objects are positioned with ``Locations`` while in Algebra mode, objects
are positioned with the ``*`` operator and shown in these examples: are positioned with the ``*`` operator and shown in these examples:
.. code-block:: python .. code-block:: build123d
with BuildPart() as disk: with BuildPart() as disk:
with BuildSketch(): with BuildSketch():
@ -18,7 +18,7 @@ are positioned with the ``*`` operator and shown in these examples:
Circle(d, mode=Mode.SUBTRACT) Circle(d, mode=Mode.SUBTRACT)
extrude(amount=c) extrude(amount=c)
.. code-block:: python .. code-block:: build123d
sketch = Circle(a) - Pos(b, 0.0) * Rectangle(c, c) - Pos(0.0, b) * Circle(d) sketch = Circle(a) - Pos(b, 0.0) * Rectangle(c, c) - Pos(0.0, b) * Circle(d)
disk = extrude(sketch, c) disk = extrude(sketch, c)
@ -36,7 +36,7 @@ right or left of each Axis. The following diagram shows how this alignment works
For example: For example:
.. code-block:: python .. code-block:: build123d
with BuildSketch(): with BuildSketch():
Circle(1, align=(Align.MIN, Align.MIN)) Circle(1, align=(Align.MIN, Align.MIN))
@ -49,7 +49,7 @@ In 3D the ``align`` parameter also contains a Z align value but otherwise works
Note that the ``align`` will also accept a single ``Align`` value which will be used on all axes - Note that the ``align`` will also accept a single ``Align`` value which will be used on all axes -
as shown here: as shown here:
.. code-block:: python .. code-block:: build123d
with BuildSketch(): with BuildSketch():
Circle(1, align=Align.MIN) Circle(1, align=Align.MIN)
@ -519,6 +519,7 @@ Here is an example of a custom sketch object specially created as part of the de
this playing card storage box (:download:`see the playing_cards.py example <../examples/playing_cards.py>`): this playing card storage box (:download:`see the playing_cards.py example <../examples/playing_cards.py>`):
.. literalinclude:: ../examples/playing_cards.py .. literalinclude:: ../examples/playing_cards.py
:language: build123d
:start-after: [Club] :start-after: [Club]
:end-before: [Club] :end-before: [Club]

View file

@ -6,14 +6,14 @@ Operations are functions that take objects as inputs and transform them into new
Here are a couple ways to use :func:`~operations_part.extrude`, in Builder and Algebra mode: Here are a couple ways to use :func:`~operations_part.extrude`, in Builder and Algebra mode:
.. code-block:: python .. code-block:: build123d
with BuildPart() as cylinder: with BuildPart() as cylinder:
with BuildSketch(): with BuildSketch():
Circle(radius) Circle(radius)
extrude(amount=height) extrude(amount=height)
.. code-block:: python .. code-block:: build123d
cylinder = extrude(Circle(radius), amount=height) cylinder = extrude(Circle(radius), amount=height)

View file

@ -74,7 +74,7 @@ It is important to note that standard list methods such as `sorted` or `filtered
be used to easily build complex selectors beyond what is available with the predefined be used to easily build complex selectors beyond what is available with the predefined
sorts and filters. Here is an example of a custom filters: sorts and filters. Here is an example of a custom filters:
.. code-block:: python .. code-block:: build123d
with BuildSketch() as din: with BuildSketch() as din:
... ...
@ -88,7 +88,7 @@ The :meth:`~topology.ShapeList.filter_by` method can take lambda expressions as
fluent chain of operations which enables integration of custom filters into a larger change of fluent chain of operations which enables integration of custom filters into a larger change of
selectors as shown in this example: selectors as shown in this example:
.. code-block:: python .. code-block:: build123d
obj = Box(1, 1, 1) - Cylinder(0.2, 1) obj = Box(1, 1, 1) - Cylinder(0.2, 1)
faces_with_holes = obj.faces().filter_by(lambda f: f.inner_wires()) faces_with_holes = obj.faces().filter_by(lambda f: f.inner_wires())

View file

@ -59,7 +59,7 @@ Code
---- ----
.. literalinclude:: technical_drawing.py .. literalinclude:: technical_drawing.py
:language: python :language: build123d
:start-after: [code] :start-after: [code]
:end-before: [end] :end-before: [end]

View file

@ -92,7 +92,7 @@ consider a plate with four chamfered holes like this:
When selecting edges to be chamfered one might first select the face that these edges When selecting edges to be chamfered one might first select the face that these edges
belong to then select the edges as shown here: belong to then select the edges as shown here:
.. code-block:: python .. code-block:: build123d
from build123d import * from build123d import *
@ -118,7 +118,7 @@ a common OpenCascade Python wrapper (`OCP <https://github.com/CadQuery/OCP>`_) i
interchange objects both from CadQuery to build123d and vice-versa by transferring the ``wrapped`` interchange objects both from CadQuery to build123d and vice-versa by transferring the ``wrapped``
objects as follows (first from CadQuery to build123d): objects as follows (first from CadQuery to build123d):
.. code-block:: python .. code-block:: build123d
import build123d as b3d import build123d as b3d
b3d_solid = b3d.Solid.make_box(1,1,1) b3d_solid = b3d.Solid.make_box(1,1,1)
@ -129,7 +129,7 @@ objects as follows (first from CadQuery to build123d):
Secondly, from build123d to CadQuery as follows: Secondly, from build123d to CadQuery as follows:
.. code-block:: python .. code-block:: build123d
import build123d as b3d import build123d as b3d
import cadquery as cq import cadquery as cq
@ -209,7 +209,7 @@ Why doesn't BuildSketch(Plane.XZ) work?
When creating a sketch not on the default ``Plane.XY`` users may expect that they are drawing directly When creating a sketch not on the default ``Plane.XY`` users may expect that they are drawing directly
on the workplane / coordinate system provided. For example: on the workplane / coordinate system provided. For example:
.. code-block:: python .. code-block:: build123d
with BuildSketch(Plane.XZ) as vertical_sketch: with BuildSketch(Plane.XZ) as vertical_sketch:
Rectangle(1, 1) Rectangle(1, 1)
@ -229,7 +229,7 @@ Why does ``BuildSketch`` work this way? Consider an example where the user wants
plane not aligned with any Axis, as follows (this is often done when creating a sketch on a ``Face`` plane not aligned with any Axis, as follows (this is often done when creating a sketch on a ``Face``
of a 3D part but is simulated here by rotating a ``Plane``): of a 3D part but is simulated here by rotating a ``Plane``):
.. code-block:: python .. code-block:: build123d
with BuildSketch(Plane.YZ.rotated((123, 45, 6))) as custom_plane: with BuildSketch(Plane.YZ.rotated((123, 45, 6))) as custom_plane:
Rectangle(1, 1, align=Align.MIN) Rectangle(1, 1, align=Align.MIN)
@ -251,7 +251,7 @@ Why is BuildLine not working as expected within the scope of BuildSketch?
As described above, all sketching is done on a local ``Plane.XY``; however, the following As described above, all sketching is done on a local ``Plane.XY``; however, the following
is a common issue: is a common issue:
.. code-block:: python .. code-block:: build123d
with BuildSketch() as sketch: with BuildSketch() as sketch:
with BuildLine(Plane.XZ): with BuildLine(Plane.XZ):

View file

@ -40,7 +40,7 @@ Overview
Both shape objects and builder objects have access to selector methods to select all of Both shape objects and builder objects have access to selector methods to select all of
a feature as long as they can contain the feature being selected. a feature as long as they can contain the feature being selected.
.. code-block:: python .. code-block:: build123d
# In context # In context
with BuildSketch() as context: with BuildSketch() as context:
@ -70,7 +70,7 @@ existed in the referenced object before the last operation, nor the modifying ob
:class:`~build_enums.Select` as selector criteria is only valid for builder objects! :class:`~build_enums.Select` as selector criteria is only valid for builder objects!
.. code-block:: python .. code-block:: build123d
# In context # In context
with BuildPart() as context: with BuildPart() as context:
@ -85,7 +85,7 @@ existed in the referenced object before the last operation, nor the modifying ob
Create a simple part to demonstrate selectors. Select using the default criteria Create a simple part to demonstrate selectors. Select using the default criteria
``Select.ALL``. Specifying ``Select.ALL`` for the selector is not required. ``Select.ALL``. Specifying ``Select.ALL`` for the selector is not required.
.. code-block:: python .. code-block:: build123d
with BuildPart() as part: with BuildPart() as part:
Box(5, 5, 1) Box(5, 5, 1)
@ -107,7 +107,7 @@ Create a simple part to demonstrate selectors. Select using the default criteria
Select features changed in the last operation with criteria ``Select.LAST``. Select features changed in the last operation with criteria ``Select.LAST``.
.. code-block:: python .. code-block:: build123d
with BuildPart() as part: with BuildPart() as part:
Box(5, 5, 1) Box(5, 5, 1)
@ -125,7 +125,7 @@ Select features changed in the last operation with criteria ``Select.LAST``.
Select only new edges from the last operation with ``Select.NEW``. This option is only Select only new edges from the last operation with ``Select.NEW``. This option is only
available for a ``ShapeList`` of edges! available for a ``ShapeList`` of edges!
.. code-block:: python .. code-block:: build123d
with BuildPart() as part: with BuildPart() as part:
Box(5, 5, 1) Box(5, 5, 1)
@ -142,7 +142,7 @@ This only returns new edges which are not reused from Box or Cylinder, in this c
the objects `intersect`. But what happens if the objects don't intersect and all the the objects `intersect`. But what happens if the objects don't intersect and all the
edges are reused? edges are reused?
.. code-block:: python .. code-block:: build123d
with BuildPart() as part: with BuildPart() as part:
Box(5, 5, 1, align=(Align.CENTER, Align.CENTER, Align.MAX)) Box(5, 5, 1, align=(Align.CENTER, Align.CENTER, Align.MAX))
@ -164,7 +164,7 @@ only completely new edges created by the operation.
Chamfer and fillet modify the current object, but do not have new edges via Chamfer and fillet modify the current object, but do not have new edges via
``Select.NEW``. ``Select.NEW``.
.. code-block:: python .. code-block:: build123d
with BuildPart() as part: with BuildPart() as part:
Box(5, 5, 1) Box(5, 5, 1)
@ -187,7 +187,7 @@ another "combined" shape object and returns the edges new to the combined shape.
``new_edges`` is available both Algebra mode or Builder mode, but is necessary in ``new_edges`` is available both Algebra mode or Builder mode, but is necessary in
Algebra Mode where ``Select.NEW`` is unavailable Algebra Mode where ``Select.NEW`` is unavailable
.. code-block:: python .. code-block:: build123d
box = Box(5, 5, 1) box = Box(5, 5, 1)
circle = Cylinder(2, 5) circle = Cylinder(2, 5)
@ -200,7 +200,7 @@ Algebra Mode where ``Select.NEW`` is unavailable
``new_edges`` can also find edges created during a chamfer or fillet operation by ``new_edges`` can also find edges created during a chamfer or fillet operation by
comparing the object before the operation to the "combined" object. comparing the object before the operation to the "combined" object.
.. code-block:: python .. code-block:: build123d
box = Box(5, 5, 1) box = Box(5, 5, 1)
circle = Cylinder(2, 5) circle = Cylinder(2, 5)
@ -263,7 +263,7 @@ Finally, the vertices can be captured with a list slice for the last 4 list item
items are sorted from least to greatest ``X`` position. Remember, ``ShapeList`` is a items are sorted from least to greatest ``X`` position. Remember, ``ShapeList`` is a
subclass of ``list``, so any list slice can be used. subclass of ``list``, so any list slice can be used.
.. code-block:: python .. code-block:: build123d
part.vertices().sort_by(Axis.X)[-4:] part.vertices().sort_by(Axis.X)[-4:]
@ -320,7 +320,7 @@ group by ``SortBy.AREA``. The ``ShapeList`` of smallest faces is available from
list index. Finally, a ``ShapeList`` has access to selectors, so calling |edges| will list index. Finally, a ``ShapeList`` has access to selectors, so calling |edges| will
return a new list of all edges in the previous list. return a new list of all edges in the previous list.
.. code-block:: python .. code-block:: build123d
part.faces().group_by(SortBy.AREA)[0].edges()) part.faces().group_by(SortBy.AREA)[0].edges())
@ -368,7 +368,7 @@ might be with a list comprehension, however |filter_by| has the capability to ta
lambda function as a filter condition on the entire list. In this case, the normal of lambda function as a filter condition on the entire list. In this case, the normal of
each face can be checked against a vector direction and filtered accordingly. each face can be checked against a vector direction and filtered accordingly.
.. code-block:: python .. code-block:: build123d
part.faces().filter_by(lambda f: f.normal_at() == Vector(0, 0, 1)) part.faces().filter_by(lambda f: f.normal_at() == Vector(0, 0, 1))

View file

@ -18,11 +18,11 @@ operations, and are sometimes necessary e.g. before sorting or filtering by radi
.. dropdown:: Setup .. dropdown:: Setup
.. literalinclude:: examples/filter_geomtype.py .. literalinclude:: examples/filter_geomtype.py
:language: python :language: build123d
:lines: 3, 8-13 :lines: 3, 8-13
.. literalinclude:: examples/filter_geomtype.py .. literalinclude:: examples/filter_geomtype.py
:language: python :language: build123d
:lines: 15 :lines: 15
.. figure:: ../assets/topology_selection/filter_geomtype_line.png .. figure:: ../assets/topology_selection/filter_geomtype_line.png
@ -31,7 +31,7 @@ operations, and are sometimes necessary e.g. before sorting or filtering by radi
| |
.. literalinclude:: examples/filter_geomtype.py .. literalinclude:: examples/filter_geomtype.py
:language: python :language: build123d
:lines: 17 :lines: 17
.. figure:: ../assets/topology_selection/filter_geomtype_cylinder.png .. figure:: ../assets/topology_selection/filter_geomtype_cylinder.png
@ -52,11 +52,11 @@ circular edges selects the counterbore faces that meet the joint criteria.
.. dropdown:: Setup .. dropdown:: Setup
.. literalinclude:: examples/filter_all_edges_circle.py .. literalinclude:: examples/filter_all_edges_circle.py
:language: python :language: build123d
:lines: 3, 8-41 :lines: 3, 8-41
.. literalinclude:: examples/filter_all_edges_circle.py .. literalinclude:: examples/filter_all_edges_circle.py
:language: python :language: build123d
:lines: 43-47 :lines: 43-47
.. figure:: ../assets/topology_selection/filter_all_edges_circle.png .. figure:: ../assets/topology_selection/filter_all_edges_circle.png
@ -74,14 +74,14 @@ Plane will select faces parallel to the plane.
.. dropdown:: Setup .. dropdown:: Setup
.. code-block:: python .. code-block:: build123d
from build123d import * from build123d import *
with BuildPart() as part: with BuildPart() as part:
Box(1, 1, 1) Box(1, 1, 1)
.. code-block:: python .. code-block:: build123d
part.faces().filter_by(Axis.Z) part.faces().filter_by(Axis.Z)
part.faces().filter_by(Plane.XY) part.faces().filter_by(Plane.XY)
@ -96,7 +96,7 @@ accomplish this with feature properties or methods. Here, we are looking for fac
the dot product of face normal and either the axis direction or the plane normal is about the dot product of face normal and either the axis direction or the plane normal is about
to 0. The result is faces parallel to the axis or perpendicular to the plane. to 0. The result is faces parallel to the axis or perpendicular to the plane.
.. code-block:: python .. code-block:: build123d
part.faces().filter_by(lambda f: abs(f.normal_at().dot(Axis.Z.direction) < 1e-6) part.faces().filter_by(lambda f: abs(f.normal_at().dot(Axis.Z.direction) < 1e-6)
part.faces().filter_by(lambda f: abs(f.normal_at().dot(Plane.XY.z_dir)) < 1e-6) part.faces().filter_by(lambda f: abs(f.normal_at().dot(Plane.XY.z_dir)) < 1e-6)
@ -122,11 +122,11 @@ and then filtering for the specific inner wire by radius.
.. dropdown:: Setup .. dropdown:: Setup
.. literalinclude:: examples/filter_inner_wire_count.py .. literalinclude:: examples/filter_inner_wire_count.py
:language: python :language: build123d
:lines: 4, 9-16 :lines: 4, 9-16
.. literalinclude:: examples/filter_inner_wire_count.py .. literalinclude:: examples/filter_inner_wire_count.py
:language: python :language: build123d
:lines: 18-21 :lines: 18-21
.. figure:: ../assets/topology_selection/filter_inner_wire_count.png .. figure:: ../assets/topology_selection/filter_inner_wire_count.png
@ -140,7 +140,7 @@ axis and range. To do that we can filter for faces with 6 inner wires, sort for
select the top face, and then filter for the circular edges of the inner wires. select the top face, and then filter for the circular edges of the inner wires.
.. literalinclude:: examples/filter_inner_wire_count.py .. literalinclude:: examples/filter_inner_wire_count.py
:language: python :language: build123d
:lines: 25-32 :lines: 25-32
.. figure:: ../assets/topology_selection/filter_inner_wire_count_linear.png .. figure:: ../assets/topology_selection/filter_inner_wire_count_linear.png
@ -163,11 +163,11 @@ any line edges.
.. dropdown:: Setup .. dropdown:: Setup
.. literalinclude:: examples/filter_nested.py .. literalinclude:: examples/filter_nested.py
:language: python :language: build123d
:lines: 4, 9-22 :lines: 4, 9-22
.. literalinclude:: examples/filter_nested.py .. literalinclude:: examples/filter_nested.py
:language: python :language: build123d
:lines: 26-32 :lines: 26-32
.. figure:: ../assets/topology_selection/filter_nested.png .. figure:: ../assets/topology_selection/filter_nested.png
@ -186,7 +186,7 @@ different fillets accordingly. Then the ``Face`` ``is_circular_*`` properties ar
to highlight the resulting fillets. to highlight the resulting fillets.
.. literalinclude:: examples/filter_shape_properties.py .. literalinclude:: examples/filter_shape_properties.py
:language: python :language: build123d
:lines: 3-4, 8-22 :lines: 3-4, 8-22
.. figure:: ../assets/topology_selection/filter_shape_properties.png .. figure:: ../assets/topology_selection/filter_shape_properties.png

View file

@ -14,7 +14,7 @@ result knowing how many edges to expect.
.. dropdown:: Setup .. dropdown:: Setup
.. literalinclude:: examples/group_axis.py .. literalinclude:: examples/group_axis.py
:language: python :language: build123d
:lines: 4, 9-17 :lines: 4, 9-17
.. figure:: ../assets/topology_selection/group_axis_without.png .. figure:: ../assets/topology_selection/group_axis_without.png
@ -26,7 +26,7 @@ However, ``group_by`` can be used to first group all the edges by z-axis positio
group again by length. In both cases, you can select the desired edges from the last group. group again by length. In both cases, you can select the desired edges from the last group.
.. literalinclude:: examples/group_axis.py .. literalinclude:: examples/group_axis.py
:language: python :language: build123d
:lines: 21-22 :lines: 21-22
.. figure:: ../assets/topology_selection/group_axis_with.png .. figure:: ../assets/topology_selection/group_axis_with.png
@ -46,11 +46,11 @@ with the largest hole.
.. dropdown:: Setup .. dropdown:: Setup
.. literalinclude:: examples/group_hole_area.py .. literalinclude:: examples/group_hole_area.py
:language: python :language: build123d
:lines: 4, 9-17 :lines: 4, 9-17
.. literalinclude:: examples/group_hole_area.py .. literalinclude:: examples/group_hole_area.py
:language: python :language: build123d
:lines: 21-24 :lines: 21-24
.. figure:: ../assets/topology_selection/group_hole_area.png .. figure:: ../assets/topology_selection/group_hole_area.png
@ -72,11 +72,11 @@ then the desired groups are selected with the ``group`` method using the lengths
.. dropdown:: Setup .. dropdown:: Setup
.. literalinclude:: examples/group_properties_with_keys.py .. literalinclude:: examples/group_properties_with_keys.py
:language: python :language: build123d
:lines: 4, 9-26 :lines: 4, 9-26
.. literalinclude:: examples/group_properties_with_keys.py .. literalinclude:: examples/group_properties_with_keys.py
:language: python :language: build123d
:lines: 30, 31 :lines: 30, 31
.. figure:: ../assets/topology_selection/group_length_key.png .. figure:: ../assets/topology_selection/group_length_key.png
@ -94,11 +94,11 @@ and then further specify only the edges the bearings and pins are installed from
.. dropdown:: Adding holes .. dropdown:: Adding holes
.. literalinclude:: examples/group_properties_with_keys.py .. literalinclude:: examples/group_properties_with_keys.py
:language: python :language: build123d
:lines: 35-43 :lines: 35-43
.. literalinclude:: examples/group_properties_with_keys.py .. literalinclude:: examples/group_properties_with_keys.py
:language: python :language: build123d
:lines: 47-50 :lines: 47-50
.. figure:: ../assets/topology_selection/group_radius_key.png .. figure:: ../assets/topology_selection/group_radius_key.png
@ -109,7 +109,7 @@ and then further specify only the edges the bearings and pins are installed from
Note that ``group_by`` is not the only way to capture edges with a known property Note that ``group_by`` is not the only way to capture edges with a known property
value! ``filter_by`` with a lambda expression can be used as well: value! ``filter_by`` with a lambda expression can be used as well:
.. code-block:: python .. code-block:: build123d
radius_groups = part.edges().filter_by(GeomType.CIRCLE) radius_groups = part.edges().filter_by(GeomType.CIRCLE)
bearing_edges = radius_groups.filter_by(lambda e: e.radius == 8) bearing_edges = radius_groups.filter_by(lambda e: e.radius == 8)

View file

@ -23,11 +23,11 @@ be used with``group_by``.
.. dropdown:: Setup .. dropdown:: Setup
.. literalinclude:: examples/sort_sortby.py .. literalinclude:: examples/sort_sortby.py
:language: python :language: build123d
:lines: 3, 8-13 :lines: 3, 8-13
.. literalinclude:: examples/sort_sortby.py .. literalinclude:: examples/sort_sortby.py
:language: python :language: build123d
:lines: 19-22 :lines: 19-22
.. figure:: ../assets/topology_selection/sort_sortby_length.png .. figure:: ../assets/topology_selection/sort_sortby_length.png
@ -36,7 +36,7 @@ be used with``group_by``.
| |
.. literalinclude:: examples/sort_sortby.py .. literalinclude:: examples/sort_sortby.py
:language: python :language: build123d
:lines: 24-27 :lines: 24-27
.. figure:: ../assets/topology_selection/sort_sortby_distance.png .. figure:: ../assets/topology_selection/sort_sortby_distance.png
@ -57,11 +57,11 @@ the order is random.
.. dropdown:: Setup .. dropdown:: Setup
.. literalinclude:: examples/sort_along_wire.py .. literalinclude:: examples/sort_along_wire.py
:language: python :language: build123d
:lines: 3, 8-12 :lines: 3, 8-12
.. literalinclude:: examples/sort_along_wire.py .. literalinclude:: examples/sort_along_wire.py
:language: python :language: build123d
:lines: 14-15 :lines: 14-15
.. figure:: ../assets/topology_selection/sort_not_along_wire.png .. figure:: ../assets/topology_selection/sort_not_along_wire.png
@ -73,7 +73,7 @@ Vertices may be sorted along the wire they fall on to create order. Notice the f
radii now increase in order. radii now increase in order.
.. literalinclude:: examples/sort_along_wire.py .. literalinclude:: examples/sort_along_wire.py
:language: python :language: build123d
:lines: 26-28 :lines: 26-28
.. figure:: ../assets/topology_selection/sort_along_wire.png .. figure:: ../assets/topology_selection/sort_along_wire.png
@ -94,11 +94,11 @@ edge can be found sorting along y-axis.
.. dropdown:: Setup .. dropdown:: Setup
.. literalinclude:: examples/sort_axis.py .. literalinclude:: examples/sort_axis.py
:language: python :language: build123d
:lines: 4, 9-18 :lines: 4, 9-18
.. literalinclude:: examples/sort_axis.py .. literalinclude:: examples/sort_axis.py
:language: python :language: build123d
:lines: 22-24 :lines: 22-24
.. figure:: ../assets/topology_selection/sort_axis.png .. figure:: ../assets/topology_selection/sort_axis.png
@ -118,11 +118,11 @@ Here we are sorting the boxes by distance from the origin, using an empty ``Vert
.. dropdown:: Setup .. dropdown:: Setup
.. literalinclude:: examples/sort_distance_from.py .. literalinclude:: examples/sort_distance_from.py
:language: python :language: build123d
:lines: 2-5, 9-13 :lines: 2-5, 9-13
.. literalinclude:: examples/sort_distance_from.py .. literalinclude:: examples/sort_distance_from.py
:language: python :language: build123d
:lines: 15-16 :lines: 15-16
.. figure:: ../assets/topology_selection/sort_distance_from_origin.png .. figure:: ../assets/topology_selection/sort_distance_from_origin.png
@ -135,7 +135,7 @@ property ``volume``, and getting the last (largest) box. Then, the boxes sorted
their distance from the largest box. their distance from the largest box.
.. literalinclude:: examples/sort_distance_from.py .. literalinclude:: examples/sort_distance_from.py
:language: python :language: build123d
:lines: 19-20 :lines: 19-20
.. figure:: ../assets/topology_selection/sort_distance_from_largest.png .. figure:: ../assets/topology_selection/sort_distance_from_largest.png

View file

@ -98,6 +98,7 @@ Party Pack 01-01 Bearing Bracket
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0101.py .. literalinclude:: assets/ttt/ttt-ppp0101.py
:language: build123d
.. _ttt-ppp0102: .. _ttt-ppp0102:
@ -114,6 +115,7 @@ Party Pack 01-02 Post Cap
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0102.py .. literalinclude:: assets/ttt/ttt-ppp0102.py
:language: build123d
.. _ttt-ppp0103: .. _ttt-ppp0103:
@ -129,6 +131,7 @@ Party Pack 01-03 C Clamp Base
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0103.py .. literalinclude:: assets/ttt/ttt-ppp0103.py
:language: build123d
.. _ttt-ppp0104: .. _ttt-ppp0104:
@ -144,6 +147,7 @@ Party Pack 01-04 Angle Bracket
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0104.py .. literalinclude:: assets/ttt/ttt-ppp0104.py
:language: build123d
.. _ttt-ppp0105: .. _ttt-ppp0105:
@ -159,6 +163,7 @@ Party Pack 01-05 Paste Sleeve
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0105.py .. literalinclude:: assets/ttt/ttt-ppp0105.py
:language: build123d
.. _ttt-ppp0106: .. _ttt-ppp0106:
@ -174,6 +179,7 @@ Party Pack 01-06 Bearing Jig
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0106.py .. literalinclude:: assets/ttt/ttt-ppp0106.py
:language: build123d
.. _ttt-ppp0107: .. _ttt-ppp0107:
@ -189,6 +195,7 @@ Party Pack 01-07 Flanged Hub
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0107.py .. literalinclude:: assets/ttt/ttt-ppp0107.py
:language: build123d
.. _ttt-ppp0108: .. _ttt-ppp0108:
@ -204,6 +211,7 @@ Party Pack 01-08 Tie Plate
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0108.py .. literalinclude:: assets/ttt/ttt-ppp0108.py
:language: build123d
.. _ttt-ppp0109: .. _ttt-ppp0109:
@ -219,6 +227,7 @@ Party Pack 01-09 Corner Tie
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0109.py .. literalinclude:: assets/ttt/ttt-ppp0109.py
:language: build123d
.. _ttt-ppp0110: .. _ttt-ppp0110:
@ -234,6 +243,7 @@ Party Pack 01-10 Light Cap
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0110.py .. literalinclude:: assets/ttt/ttt-ppp0110.py
:language: build123d
.. _ttt-23-02-02-sm_hanger: .. _ttt-23-02-02-sm_hanger:
@ -249,6 +259,7 @@ Party Pack 01-10 Light Cap
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-23-02-02-sm_hanger.py .. literalinclude:: assets/ttt/ttt-23-02-02-sm_hanger.py
:language: build123d
.. _ttt-23-t-24: .. _ttt-23-t-24:
@ -265,6 +276,7 @@ Party Pack 01-10 Light Cap
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-23-t-24-curved_support.py .. literalinclude:: assets/ttt/ttt-23-t-24-curved_support.py
:language: build123d
.. _ttt-24-spo-06: .. _ttt-24-spo-06:
@ -281,3 +293,4 @@ Party Pack 01-10 Light Cap
.. dropdown:: Reference Implementation .. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-24-SPO-06-Buffer_Stand.py .. literalinclude:: assets/ttt/ttt-24-SPO-06-Buffer_Stand.py
:language: build123d

View file

@ -69,7 +69,7 @@ Mirror parts of profiles across any axes of symmetry identified earlier.
*The build123d code to generate this profile is as follows:* *The build123d code to generate this profile is as follows:*
.. code-block:: python .. code-block:: build123d
with BuildSketch() as sketch: with BuildSketch() as sketch:
with BuildLine() as profile: with BuildLine() as profile:
@ -109,7 +109,7 @@ Use the resulting geometry as sub-parts if needed.
*The next step in implementing our design in build123d is to convert the above sketch into *The next step in implementing our design in build123d is to convert the above sketch into
a part by extruding it as shown in this code:* a part by extruding it as shown in this code:*
.. code-block:: python .. code-block:: build123d
with BuildPart() as bracket: with BuildPart() as bracket:
with BuildSketch() as sketch: with BuildSketch() as sketch:
@ -156,7 +156,7 @@ ensure the correct edges have been modified.
define these corners need to be isolated. The following code, placed to follow the previous define these corners need to be isolated. The following code, placed to follow the previous
code block, captures just these edges:* code block, captures just these edges:*
.. code-block:: python .. code-block:: build123d
corners = bracket.edges().filter_by(Axis.X).group_by(Axis.Y)[-1] corners = bracket.edges().filter_by(Axis.X).group_by(Axis.Y)[-1]
fillet(corners, fillet_radius) fillet(corners, fillet_radius)
@ -191,7 +191,7 @@ and functionality in the final assembly.
*Our example has two circular holes and a slot that need to be created. First we'll create *Our example has two circular holes and a slot that need to be created. First we'll create
the two circular holes:* the two circular holes:*
.. code-block:: python .. code-block:: build123d
with Locations(bracket.faces().sort_by(Axis.X)[-1]): with Locations(bracket.faces().sort_by(Axis.X)[-1]):
Hole(hole_diameter / 2) Hole(hole_diameter / 2)
@ -219,7 +219,7 @@ the two circular holes:*
*Next the slot needs to be created in the bracket with will be done by sketching a slot on *Next the slot needs to be created in the bracket with will be done by sketching a slot on
the front of the bracket and extruding the sketch through the part.* the front of the bracket and extruding the sketch through the part.*
.. code-block:: python .. code-block:: build123d
with BuildSketch(bracket.faces().sort_by(Axis.Y)[0]): with BuildSketch(bracket.faces().sort_by(Axis.Y)[0]):
SlotOverall(20 * MM, hole_diameter) SlotOverall(20 * MM, hole_diameter)
@ -262,7 +262,7 @@ or if variations of the part are needed.
*The dimensions of the bracket are defined as follows:* *The dimensions of the bracket are defined as follows:*
.. code-block:: python .. code-block:: build123d
thickness = 3 * MM thickness = 3 * MM
width = 25 * MM width = 25 * MM
@ -285,7 +285,7 @@ These steps should guide you through a logical and efficient workflow in build12
*The entire code block for the bracket example is shown here:* *The entire code block for the bracket example is shown here:*
.. code-block:: python .. code-block:: build123d
from build123d import * from build123d import *
from ocp_vscode import show_all from ocp_vscode import show_all

View file

@ -19,6 +19,7 @@ Before getting to the CAD operations, this selector script needs to import the b
environment. environment.
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [import] :start-after: [import]
:end-before: [Hinge Class] :end-before: [Hinge Class]
@ -32,6 +33,7 @@ tutorial is the joints and not the CAD operations to create objects, this code i
described in detail. described in detail.
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Hinge Class] :start-after: [Hinge Class]
:end-before: [Create the Joints] :end-before: [Create the Joints]
@ -62,6 +64,7 @@ The first joint to add is a :class:`~topology.RigidJoint` that is used to fix th
or lid. or lid.
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Create the Joints] :start-after: [Create the Joints]
:end-before: [Hinge Axis] :end-before: [Hinge Axis]
@ -78,6 +81,7 @@ The second joint to add is either a :class:`~topology.RigidJoint` (on the inner
(on the outer leaf) that describes the hinge axis. (on the outer leaf) that describes the hinge axis.
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Create the Joints] :start-after: [Create the Joints]
:end-before: [Fastener holes] :end-before: [Fastener holes]
:emphasize-lines: 10-24 :emphasize-lines: 10-24
@ -96,6 +100,7 @@ The third set of joints to add are :class:`~topology.CylindricalJoint`'s that de
screws used to attach the leaves move. screws used to attach the leaves move.
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Fastener holes] :start-after: [Fastener holes]
:end-before: [End Fastener holes] :end-before: [End Fastener holes]
@ -115,6 +120,7 @@ Step 3d: Call Super
To finish off, the base class for the Hinge class is initialized: To finish off, the base class for the Hinge class is initialized:
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [End Fastener holes] :start-after: [End Fastener holes]
:end-before: [Hinge Class] :end-before: [Hinge Class]
@ -125,6 +131,7 @@ Now that the Hinge class is complete it can be used to instantiate the two hinge
required to attach the box and lid together. required to attach the box and lid together.
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Create instances of the two leaves of the hinge] :start-after: [Create instances of the two leaves of the hinge]
:end-before: [Create the box with a RigidJoint to mount the hinge] :end-before: [Create the box with a RigidJoint to mount the hinge]
@ -139,6 +146,7 @@ the joint used to attach the outer hinge leaf.
:align: center :align: center
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Create the box with a RigidJoint to mount the hinge] :start-after: [Create the box with a RigidJoint to mount the hinge]
:end-before: [Demonstrate that objects with Joints can be moved and the joints follow] :end-before: [Demonstrate that objects with Joints can be moved and the joints follow]
:emphasize-lines: 13-16 :emphasize-lines: 13-16
@ -157,6 +165,7 @@ having to recreate or modify :class:`~topology.Joint`'s. Here is the box is move
property. property.
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Demonstrate that objects with Joints can be moved and the joints follow] :start-after: [Demonstrate that objects with Joints can be moved and the joints follow]
:end-before: [The lid with a RigidJoint for the hinge] :end-before: [The lid with a RigidJoint for the hinge]
@ -170,6 +179,7 @@ Much like the box, the lid is created in a :class:`~build_part.BuildPart` contex
:align: center :align: center
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [The lid with a RigidJoint for the hinge] :start-after: [The lid with a RigidJoint for the hinge]
:end-before: [A screw to attach the hinge to the box] :end-before: [A screw to attach the hinge to the box]
:emphasize-lines: 6-9 :emphasize-lines: 6-9
@ -191,6 +201,7 @@ screw.
:align: center :align: center
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [A screw to attach the hinge to the box] :start-after: [A screw to attach the hinge to the box]
:end-before: [End of screw creation] :end-before: [End of screw creation]
@ -210,6 +221,7 @@ Step 7a: Hinge to Box
To start, the outer hinge leaf will be connected to the box, as follows: To start, the outer hinge leaf will be connected to the box, as follows:
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Connect Box to Outer Hinge] :start-after: [Connect Box to Outer Hinge]
:end-before: [Connect Box to Outer Hinge] :end-before: [Connect Box to Outer Hinge]
@ -227,6 +239,7 @@ Next, the hinge inner leaf is connected to the hinge outer leaf which is attache
box. box.
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Connect Hinge Leaves] :start-after: [Connect Hinge Leaves]
:end-before: [Connect Hinge Leaves] :end-before: [Connect Hinge Leaves]
@ -243,6 +256,7 @@ Step 7c: Lid to Hinge
Now the ``lid`` is connected to the ``hinge_inner``: Now the ``lid`` is connected to the ``hinge_inner``:
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Connect Hinge to Lid] :start-after: [Connect Hinge to Lid]
:end-before: [Connect Hinge to Lid] :end-before: [Connect Hinge to Lid]
@ -260,6 +274,7 @@ Step 7d: Screw to Hinge
The last step in this example is to place a screw in one of the hinges: The last step in this example is to place a screw in one of the hinges:
.. literalinclude:: tutorial_joints.py .. literalinclude:: tutorial_joints.py
:language: build123d
:start-after: [Connect Screw to Hole] :start-after: [Connect Screw to Hole]
:end-before: [Connect Screw to Hole] :end-before: [Connect Screw to Hole]

View file

@ -21,6 +21,7 @@ The dimensions of the Lego block follow. A key parameter is ``pip_count``, the l
of the Lego blocks in pips. This parameter must be at least 2. of the Lego blocks in pips. This parameter must be at least 2.
.. literalinclude:: ../examples/lego.py .. literalinclude:: ../examples/lego.py
:language: build123d
:lines: 30,31, 34-47 :lines: 30,31, 34-47
******************** ********************
@ -31,6 +32,7 @@ The Lego block will be created by the ``BuildPart`` builder as it's a discrete t
dimensional part; therefore, we'll instantiate a ``BuildPart`` with the name ``lego``. dimensional part; therefore, we'll instantiate a ``BuildPart`` with the name ``lego``.
.. literalinclude:: ../examples/lego.py .. literalinclude:: ../examples/lego.py
:language: build123d
:lines: 49 :lines: 49
********************** **********************
@ -43,6 +45,7 @@ object. As this sketch will be part of the lego part, we'll create a sketch bui
in the context of the part builder as follows: in the context of the part builder as follows:
.. literalinclude:: ../examples/lego.py .. literalinclude:: ../examples/lego.py
:language: build123d
:lines: 49-51 :lines: 49-51
:emphasize-lines: 3 :emphasize-lines: 3
@ -59,6 +62,7 @@ of the Lego block. The following step is going to refer to this rectangle, so it
be assigned the identifier ``perimeter``. be assigned the identifier ``perimeter``.
.. literalinclude:: ../examples/lego.py .. literalinclude:: ../examples/lego.py
:language: build123d
:lines: 49-53 :lines: 49-53
:emphasize-lines: 5 :emphasize-lines: 5
@ -76,6 +80,7 @@ hollowed out. This will be done with the ``Offset`` operation which is going to
create a new object from ``perimeter``. create a new object from ``perimeter``.
.. literalinclude:: ../examples/lego.py .. literalinclude:: ../examples/lego.py
:language: build123d
:lines: 49-53,58-64 :lines: 49-53,58-64
:emphasize-lines: 7-12 :emphasize-lines: 7-12
@ -104,6 +109,7 @@ objects are in the scope of a location context (``GridLocations`` in this case)
that defined multiple points, multiple rectangles are created. that defined multiple points, multiple rectangles are created.
.. literalinclude:: ../examples/lego.py .. literalinclude:: ../examples/lego.py
:language: build123d
:lines: 49-53,58-64,69-73 :lines: 49-53,58-64,69-73
:emphasize-lines: 13-17 :emphasize-lines: 13-17
@ -125,6 +131,7 @@ To convert the internal grid to ridges, the center needs to be removed. This wil
with another ``Rectangle``. with another ``Rectangle``.
.. literalinclude:: ../examples/lego.py .. literalinclude:: ../examples/lego.py
:language: build123d
:lines: 49-53,58-64,69-73,78-83 :lines: 49-53,58-64,69-73,78-83
:emphasize-lines: 18-23 :emphasize-lines: 18-23
@ -142,6 +149,7 @@ Lego blocks use a set of internal hollow cylinders that the pips push against
to hold two blocks together. These will be created with ``Circle``. to hold two blocks together. These will be created with ``Circle``.
.. literalinclude:: ../examples/lego.py .. literalinclude:: ../examples/lego.py
:language: build123d
:lines: 49-53,58-64,69-73,78-83,88-93 :lines: 49-53,58-64,69-73,78-83,88-93
:emphasize-lines: 24-29 :emphasize-lines: 24-29
@ -162,6 +170,7 @@ Now that the sketch is complete it needs to be extruded into the three dimension
wall object. wall object.
.. literalinclude:: ../examples/lego.py .. literalinclude:: ../examples/lego.py
:language: build123d
:lines: 49-53,58-64,69-73,78-83,88-93,98-99 :lines: 49-53,58-64,69-73,78-83,88-93,98-99
:emphasize-lines: 30-31 :emphasize-lines: 30-31
@ -183,6 +192,7 @@ Now that the walls are complete, the top of the block needs to be added. Althoug
could be done with another sketch, we'll add a box to the top of the walls. could be done with another sketch, we'll add a box to the top of the walls.
.. literalinclude:: ../examples/lego.py .. literalinclude:: ../examples/lego.py
:language: build123d
:lines: 49-53,58-64,69-73,78-83,88-93,98-99,110-118 :lines: 49-53,58-64,69-73,78-83,88-93,98-99,110-118
:emphasize-lines: 32-40 :emphasize-lines: 32-40
@ -211,6 +221,7 @@ The final step is to add the pips to the top of the Lego block. To do this we'll
a new workplane on top of the block where we can position the pips. a new workplane on top of the block where we can position the pips.
.. literalinclude:: ../examples/lego.py .. literalinclude:: ../examples/lego.py
:language: build123d
:lines: 49-53,58-64,69-73,78-83,88-93,98-99,110-118,129-137 :lines: 49-53,58-64,69-73,78-83,88-93,98-99,110-118,129-137
:emphasize-lines: 41-49 :emphasize-lines: 41-49

View file

@ -22,6 +22,7 @@ Before getting to the CAD operations, this selector script needs to import the b
environment. environment.
.. literalinclude:: selector_example.py .. literalinclude:: selector_example.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
:lines: 1-2 :lines: 1-2
@ -34,6 +35,7 @@ To start off, the part will be based on a cylinder so we'll use the :class:`~obj
of :class:`~build_part.BuildPart`: of :class:`~build_part.BuildPart`:
.. literalinclude:: selector_example.py .. literalinclude:: selector_example.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
:lines: 1-5 :lines: 1-5
@ -50,6 +52,7 @@ surfaces) , so we'll create a sketch centered on the top of the cylinder. To lo
this sketch we'll use the cylinder's top Face as shown here: this sketch we'll use the cylinder's top Face as shown here:
.. literalinclude:: selector_example.py .. literalinclude:: selector_example.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
:lines: 1-6 :lines: 1-6
@ -82,6 +85,7 @@ The object has a hexagonal hole in the top with a central cylinder which we'll d
in the sketch. in the sketch.
.. literalinclude:: selector_example.py .. literalinclude:: selector_example.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
:lines: 1-8 :lines: 1-8
@ -107,6 +111,7 @@ To create the hole we'll :func:`~operations_part.extrude` the sketch we just cre
the :class:`~objects_part.Cylinder` and subtract it. the :class:`~objects_part.Cylinder` and subtract it.
.. literalinclude:: selector_example.py .. literalinclude:: selector_example.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
:lines: 1-9 :lines: 1-9
@ -128,6 +133,7 @@ Step 6: Fillet the top perimeter Edge
The final step is to apply a fillet to the top perimeter. The final step is to apply a fillet to the top perimeter.
.. literalinclude:: selector_example.py .. literalinclude:: selector_example.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
:lines: 1-9,18-24,33-34 :lines: 1-9,18-24,33-34

View file

@ -34,6 +34,7 @@ We model a single wing (halfspan), with an elliptic leading and trailing edge
These two edges act as the *guides* for the Gordon surface. These two edges act as the *guides* for the Gordon surface.
.. literalinclude:: spitfire_wing_gordon.py .. literalinclude:: spitfire_wing_gordon.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [AirfoilSizes] :end-before: [AirfoilSizes]
@ -45,6 +46,7 @@ We intersect the guides with planes normal to the span to size the airfoil secti
The resulting chord lengths define uniform scales for each airfoil curve. The resulting chord lengths define uniform scales for each airfoil curve.
.. literalinclude:: spitfire_wing_gordon.py .. literalinclude:: spitfire_wing_gordon.py
:language: build123d
:start-after: [AirfoilSizes] :start-after: [AirfoilSizes]
:end-before: [Airfoils] :end-before: [Airfoils]
@ -56,6 +58,7 @@ shifted so the leading edge fraction is aligned—then scale to the chord length
from Step 2. from Step 2.
.. literalinclude:: spitfire_wing_gordon.py .. literalinclude:: spitfire_wing_gordon.py
:language: build123d
:start-after: [Airfoils] :start-after: [Airfoils]
:end-before: [Profiles] :end-before: [Profiles]
@ -68,6 +71,7 @@ profiles; the elliptic edges are the guides. We also add the wing tip section
so the profile grid closes at the tip. so the profile grid closes at the tip.
.. literalinclude:: spitfire_wing_gordon.py .. literalinclude:: spitfire_wing_gordon.py
:language: build123d
:start-after: [Profiles] :start-after: [Profiles]
:end-before: [Solid] :end-before: [Solid]
@ -82,6 +86,7 @@ Step 5 — Cap the root and create the solid
We extract the closed root edge loop, make a planar cap, and form a solid shell. We extract the closed root edge loop, make a planar cap, and form a solid shell.
.. literalinclude:: spitfire_wing_gordon.py .. literalinclude:: spitfire_wing_gordon.py
:language: build123d
:start-after: [Solid] :start-after: [Solid]
:end-before: [End] :end-before: [End]
@ -102,5 +107,6 @@ Complete listing
For convenience, here is the full script in one block: For convenience, here is the full script in one block:
.. literalinclude:: spitfire_wing_gordon.py .. literalinclude:: spitfire_wing_gordon.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]

View file

@ -20,7 +20,7 @@ the object. To illustrate this process, we will create the following game token:
Useful :class:`~topology.Face` creation methods include Useful :class:`~topology.Face` creation methods include
:meth:`~topology.Face.make_surface`, :meth:`~topology.Face.make_bezier_surface`, :meth:`~topology.Face.make_surface`, :meth:`~topology.Face.make_bezier_surface`,
and :meth:`~topology.Face.make_surface_from_array_of_points`. See the and :meth:`~topology.Face.make_surface_from_array_of_points`. See the
:doc:`surface_modeling` overview for the full list. :doc:`tutorial_surface_modeling` overview for the full list.
In this case, we'll use the ``make_surface`` method, providing it with the edges that define In this case, we'll use the ``make_surface`` method, providing it with the edges that define
the perimeter of the surface and a central point on that surface. the perimeter of the surface and a central point on that surface.
@ -29,6 +29,7 @@ To create the perimeter, we'll define the perimeter edges. Since the heart is
symmetric, we'll only create half of its surface here: symmetric, we'll only create half of its surface here:
.. literalinclude:: heart_token.py .. literalinclude:: heart_token.py
:language: build123d
:start-after: [Code] :start-after: [Code]
:end-before: [SurfaceEdges] :end-before: [SurfaceEdges]
@ -42,12 +43,14 @@ of the heart and archs up off ``Plane.XY``.
In preparation for creating the surface, we'll define a point on the surface: In preparation for creating the surface, we'll define a point on the surface:
.. literalinclude:: heart_token.py .. literalinclude:: heart_token.py
:language: build123d
:start-after: [SurfaceEdges] :start-after: [SurfaceEdges]
:end-before: [SurfacePoint] :end-before: [SurfacePoint]
We will then use this point to create a non-planar ``Face``: We will then use this point to create a non-planar ``Face``:
.. literalinclude:: heart_token.py .. literalinclude:: heart_token.py
:language: build123d
:start-after: [SurfacePoint] :start-after: [SurfacePoint]
:end-before: [Surface] :end-before: [Surface]
@ -63,6 +66,7 @@ Now that one half of the top of the heart has been created, the remainder of the
and bottom can be created by mirroring: and bottom can be created by mirroring:
.. literalinclude:: heart_token.py .. literalinclude:: heart_token.py
:language: build123d
:start-after: [Surface] :start-after: [Surface]
:end-before: [Surfaces] :end-before: [Surfaces]
@ -70,6 +74,7 @@ The sides of the heart are going to be created by extruding the outside of the p
as follows: as follows:
.. literalinclude:: heart_token.py .. literalinclude:: heart_token.py
:language: build123d
:start-after: [Surfaces] :start-after: [Surfaces]
:end-before: [Sides] :end-before: [Sides]
@ -82,6 +87,7 @@ now put them together, first into a :class:`~topology.Shell` and then into a
:class:`~topology.Solid`: :class:`~topology.Solid`:
.. literalinclude:: heart_token.py .. literalinclude:: heart_token.py
:language: build123d
:start-after: [Sides] :start-after: [Sides]
:end-before: [Solid] :end-before: [Solid]
@ -99,6 +105,7 @@ Finally, we'll create the frame around the heart as a simple extrusion of a plan
shape defined by the perimeter of the heart and merge all of the components together: shape defined by the perimeter of the heart and merge all of the components together:
.. literalinclude:: heart_token.py .. literalinclude:: heart_token.py
:language: build123d
:start-after: [Solid] :start-after: [Solid]
:end-before: [End] :end-before: [End]
@ -121,5 +128,5 @@ from the heart.
Next steps Next steps
---------- ----------
Continue to :doc:`tutorial_heart_token` for an advanced example using Continue to :doc:`tutorial_spitfire_wing_gordon` for an advanced example using
:meth:`~topology.Face.make_gordon_surface` to create a Supermarine Spitfire wing. :meth:`~topology.Face.make_gordon_surface` to create a Supermarine Spitfire wing.

View file

@ -68,8 +68,7 @@ with BuildPart() as ex26:
with BuildSketch() as ex26_sk: with BuildSketch() as ex26_sk:
with Locations((0, rev)): with Locations((0, rev)):
Circle(rad) Circle(rad)
revolve(axis=Axis.X, revolution_arc=90) revolve(axis=Axis.X, revolution_arc=180)
mirror(about=Plane.XZ)
with BuildSketch() as ex26_sk2: with BuildSketch() as ex26_sk2:
Rectangle(rad, rev) Rectangle(rad, rev)
ex26_target = ex26.part ex26_target = ex26.part

View file

@ -26,8 +26,8 @@ rad, rev = 3, 25
# Extrude last # Extrude last
circle = Pos(0, rev) * Circle(rad) circle = Pos(0, rev) * Circle(rad)
ex26_target = revolve(circle, Axis.X, revolution_arc=90) ex26_target = revolve(circle, Axis.X, revolution_arc=180)
ex26_target = ex26_target + mirror(ex26_target, Plane.XZ) ex26_target = ex26_target
rect = Rectangle(rad, rev) rect = Rectangle(rad, rev)

View file

@ -68,7 +68,7 @@ development = [
"black", "black",
"mypy", "mypy",
"pylint", "pylint",
"pytest", "pytest==8.4.2", # TODO: unpin on resolution of pytest-dev/pytest-xdist/issues/1273
"pytest-benchmark", "pytest-benchmark",
"pytest-cov", "pytest-cov",
"pytest-xdist", "pytest-xdist",

View file

@ -466,7 +466,7 @@ class Builder(ABC, Generic[ShapeT]):
elif mode == Mode.INTERSECT: elif mode == Mode.INTERSECT:
if self._obj is None: if self._obj is None:
raise RuntimeError("Nothing to intersect with") raise RuntimeError("Nothing to intersect with")
combined = self._obj.intersect(*typed[self._shape]) combined = self._obj.intersect(Compound(typed[self._shape]))
elif mode == Mode.REPLACE: elif mode == Mode.REPLACE:
combined = self._sub_class(list(typed[self._shape])) combined = self._sub_class(list(typed[self._shape]))

View file

@ -453,7 +453,7 @@ class DimensionLine(BaseSketchObject):
if self_intersection is None: if self_intersection is None:
self_intersection_area = 0.0 self_intersection_area = 0.0
else: else:
self_intersection_area = self_intersection.area self_intersection_area = sum(f.area for f in self_intersection.faces())
d_line += placed_label d_line += placed_label
bbox_size = d_line.bounding_box().diagonal bbox_size = d_line.bounding_box().diagonal
@ -467,7 +467,7 @@ class DimensionLine(BaseSketchObject):
if line_intersection is None: if line_intersection is None:
common_area = 0.0 common_area = 0.0
else: else:
common_area = line_intersection.area common_area = sum(f.area for f in line_intersection.faces())
common_area += self_intersection_area common_area += self_intersection_area
score = (d_line.area - 10 * common_area) / bbox_size score = (d_line.area - 10 * common_area) / bbox_size
d_lines[d_line] = score d_lines[d_line] = score

View file

@ -34,8 +34,10 @@ import math
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from copy import copy from copy import copy
from enum import Enum, auto from enum import Enum, auto
from io import BytesIO
from os import PathLike, fsdecode from os import PathLike, fsdecode
from typing import Any, TypeAlias from typing import Any, TypeAlias
from typing import cast as tcast
from warnings import warn from warnings import warn
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
@ -47,7 +49,7 @@ from ezdxf.colors import RGB, aci2rgb
from ezdxf.math import Vec2 from ezdxf.math import Vec2
from OCP.BRepLib import BRepLib from OCP.BRepLib import BRepLib
from OCP.BRepTools import BRepTools_WireExplorer from OCP.BRepTools import BRepTools_WireExplorer
from OCP.Geom import Geom_BezierCurve from OCP.Geom import Geom_BezierCurve, Geom_BSplineCurve
from OCP.GeomConvert import GeomConvert from OCP.GeomConvert import GeomConvert
from OCP.GeomConvert import GeomConvert_BSplineCurveToBezierCurve from OCP.GeomConvert import GeomConvert_BSplineCurveToBezierCurve
from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Vec, gp_XYZ from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Vec, gp_XYZ
@ -636,13 +638,13 @@ class ExportDXF(Export2D):
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def write(self, file_name: PathLike | str | bytes): def write(self, file_name: PathLike | str | bytes | BytesIO):
"""write """write
Writes the DXF data to the specified file name. Writes the DXF data to the specified file name.
Args: Args:
file_name (PathLike | str | bytes): The file name (including path) where file_name (PathLike | str | bytes | BytesIO): The file name (including path) where
the DXF data will be written. the DXF data will be written.
""" """
# Reset the main CAD viewport of the model space to the # Reset the main CAD viewport of the model space to the
@ -650,7 +652,12 @@ class ExportDXF(Export2D):
# https://github.com/gumyr/build123d/issues/382 tracks # https://github.com/gumyr/build123d/issues/382 tracks
# exposing viewport control to the user. # exposing viewport control to the user.
zoom.extents(self._modelspace) zoom.extents(self._modelspace)
self._document.saveas(fsdecode(file_name))
if not isinstance(file_name, BytesIO):
file_name = fsdecode(file_name)
self._document.saveas(file_name)
else:
self._document.write(file_name, fmt="bin")
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -751,14 +758,14 @@ class ExportDXF(Export2D):
# Extract the relevant segment of the curve. # Extract the relevant segment of the curve.
spline = GeomConvert.SplitBSplineCurve_s( spline = GeomConvert.SplitBSplineCurve_s(
curve, tcast(Geom_BSplineCurve, curve),
u1, u1,
u2, u2,
Export2D.PARAMETRIC_TOLERANCE, Export2D.PARAMETRIC_TOLERANCE,
) )
# need to apply the transform on the geometry level # need to apply the transform on the geometry level
if edge.wrapped is None or edge.location is None: if not edge or edge.location is None:
raise ValueError(f"Edge is empty {edge}.") raise ValueError(f"Edge is empty {edge}.")
t = edge.location.wrapped.Transformation() t = edge.location.wrapped.Transformation()
spline.Transform(t) spline.Transform(t)
@ -1130,7 +1137,7 @@ class ExportSVG(Export2D):
) )
while explorer.More(): while explorer.More():
topo_edge = explorer.Current() topo_edge = explorer.Current()
loose_edges.append(Edge(topo_edge)) loose_edges.append(Edge(TopoDS.Edge_s(topo_edge)))
explorer.Next() explorer.Next()
# print(f"{len(loose_edges)} loose edges") # print(f"{len(loose_edges)} loose edges")
loose_edge_elements = [self._edge_element(edge) for edge in loose_edges] loose_edge_elements = [self._edge_element(edge) for edge in loose_edges]
@ -1257,7 +1264,7 @@ class ExportSVG(Export2D):
(u0, u1) = (lp, fp) if reverse else (fp, lp) (u0, u1) = (lp, fp) if reverse else (fp, lp)
start = self._path_point(curve.Value(u0)) start = self._path_point(curve.Value(u0))
end = self._path_point(curve.Value(u1)) end = self._path_point(curve.Value(u1))
radius = complex(radius, radius) radius = complex(radius, radius) # type: ignore[assignment]
rotation = math.degrees(gp_Dir(1, 0, 0).AngleWithRef(x_axis, gp_Dir(0, 0, 1))) rotation = math.degrees(gp_Dir(1, 0, 0).AngleWithRef(x_axis, gp_Dir(0, 0, 1)))
if curve.IsClosed(): if curve.IsClosed():
midway = self._path_point(curve.Value((u0 + u1) / 2)) midway = self._path_point(curve.Value((u0 + u1) / 2))
@ -1310,7 +1317,7 @@ class ExportSVG(Export2D):
(u0, u1) = (lp, fp) if reverse else (fp, lp) (u0, u1) = (lp, fp) if reverse else (fp, lp)
start = self._path_point(curve.Value(u0)) start = self._path_point(curve.Value(u0))
end = self._path_point(curve.Value(u1)) end = self._path_point(curve.Value(u1))
radius = complex(major_radius, minor_radius) radius = complex(major_radius, minor_radius) # type: ignore[assignment]
rotation = math.degrees(gp_Dir(1, 0, 0).AngleWithRef(x_axis, gp_Dir(0, 0, 1))) rotation = math.degrees(gp_Dir(1, 0, 0).AngleWithRef(x_axis, gp_Dir(0, 0, 1)))
if curve.IsClosed(): if curve.IsClosed():
midway = self._path_point(curve.Value((u0 + u1) / 2)) midway = self._path_point(curve.Value((u0 + u1) / 2))
@ -1345,7 +1352,7 @@ class ExportSVG(Export2D):
u2 = adaptor.LastParameter() u2 = adaptor.LastParameter()
# Apply the shape location to the geometry. # Apply the shape location to the geometry.
if edge.wrapped is None or edge.location is None: if not edge or edge.location is None:
raise ValueError(f"Edge is empty {edge}.") raise ValueError(f"Edge is empty {edge}.")
t = edge.location.wrapped.Transformation() t = edge.location.wrapped.Transformation()
spline.Transform(t) spline.Transform(t)
@ -1355,7 +1362,7 @@ class ExportSVG(Export2D):
# According to the OCCT 7.6.0 documentation, # According to the OCCT 7.6.0 documentation,
# "ParametricTolerance is not used." # "ParametricTolerance is not used."
converter = GeomConvert_BSplineCurveToBezierCurve( converter = GeomConvert_BSplineCurveToBezierCurve(
spline, u1, u2, Export2D.PARAMETRIC_TOLERANCE tcast(Geom_BSplineCurve, spline), u1, u2, Export2D.PARAMETRIC_TOLERANCE
) )
def make_segment(bezier: Geom_BezierCurve, reverse: bool) -> PathSegment: def make_segment(bezier: Geom_BezierCurve, reverse: bool) -> PathSegment:
@ -1411,7 +1418,7 @@ class ExportSVG(Export2D):
} }
def _edge_segments(self, edge: Edge, reverse: bool) -> list[PathSegment]: def _edge_segments(self, edge: Edge, reverse: bool) -> list[PathSegment]:
if edge.wrapped is None: if not edge:
raise ValueError(f"Edge is empty {edge}.") raise ValueError(f"Edge is empty {edge}.")
edge_reversed = edge.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED edge_reversed = edge.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED
geom_type = edge.geom_type geom_type = edge.geom_type
@ -1497,13 +1504,13 @@ class ExportSVG(Export2D):
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def write(self, path: PathLike | str | bytes): def write(self, path: PathLike | str | bytes | BytesIO):
"""write """write
Writes the SVG data to the specified file path. Writes the SVG data to the specified file path.
Args: Args:
path (PathLike | str | bytes): The file path where the SVG data will be written. path (PathLike | str | bytes | BytesIO): The file path where the SVG data will be written.
""" """
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
bb = self._bounds bb = self._bounds
@ -1549,5 +1556,9 @@ class ExportSVG(Export2D):
xml = ET.ElementTree(svg) xml = ET.ElementTree(svg)
ET.indent(xml, " ") ET.indent(xml, " ")
if not isinstance(path, BytesIO):
path = fsdecode(path)
# xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=False) # xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=False)
xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=None) xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=None)

View file

@ -244,7 +244,7 @@ def export_gltf(
messenger = Message.DefaultMessenger_s() messenger = Message.DefaultMessenger_s()
for printer in messenger.Printers(): for printer in messenger.Printers():
printer.SetTraceLevel(Message_Gravity(Message_Gravity.Message_Fail)) printer.SetTraceLevel(Message_Gravity.Message_Fail)
status = writer.Perform(doc, index_map, progress) status = writer.Perform(doc, index_map, progress)
@ -262,7 +262,7 @@ def export_gltf(
def export_step( def export_step(
to_export: Shape, to_export: Shape,
file_path: PathLike | str | bytes, file_path: PathLike | str | bytes | BytesIO,
unit: Unit = Unit.MM, unit: Unit = Unit.MM,
write_pcurves: bool = True, write_pcurves: bool = True,
precision_mode: PrecisionMode = PrecisionMode.AVERAGE, precision_mode: PrecisionMode = PrecisionMode.AVERAGE,
@ -277,7 +277,7 @@ def export_step(
Args: Args:
to_export (Shape): object or assembly to_export (Shape): object or assembly
file_path (Union[PathLike, str, bytes]): step file path file_path (Union[PathLike, str, bytes, BytesIO]): step file path
unit (Unit, optional): shape units. Defaults to Unit.MM. unit (Unit, optional): shape units. Defaults to Unit.MM.
write_pcurves (bool, optional): write parametric curves to the STEP file. write_pcurves (bool, optional): write parametric curves to the STEP file.
Defaults to True. Defaults to True.
@ -297,7 +297,7 @@ def export_step(
# Disable writing OCCT info to console # Disable writing OCCT info to console
messenger = Message.DefaultMessenger_s() messenger = Message.DefaultMessenger_s()
for printer in messenger.Printers(): for printer in messenger.Printers():
printer.SetTraceLevel(Message_Gravity(Message_Gravity.Message_Fail)) printer.SetTraceLevel(Message_Gravity.Message_Fail)
session = XSControl_WorkSession() session = XSControl_WorkSession()
writer = STEPCAFControl_Writer(session, False) writer = STEPCAFControl_Writer(session, False)
@ -326,7 +326,13 @@ def export_step(
Interface_Static.SetIVal_s("write.precision.mode", precision_mode.value) Interface_Static.SetIVal_s("write.precision.mode", precision_mode.value)
writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs) writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs)
status = writer.Write(fspath(file_path)) == IFSelect_ReturnStatus.IFSelect_RetDone if not isinstance(file_path, BytesIO):
status = (
writer.Write(fsdecode(file_path)) == IFSelect_ReturnStatus.IFSelect_RetDone
)
else:
status = writer.WriteStream(file_path) == IFSelect_ReturnStatus.IFSelect_RetDone
if not status: if not status:
raise RuntimeError("Failed to write STEP file") raise RuntimeError("Failed to write STEP file")
@ -346,7 +352,7 @@ def export_stl(
Args: Args:
to_export (Shape): object or assembly to_export (Shape): object or assembly
file_path (str): The path and file name to write the STL output to. file_path (Union[PathLike, str, bytes]): The path and file name to write the STL output to.
tolerance (float, optional): A linear deflection setting which limits the distance tolerance (float, optional): A linear deflection setting which limits the distance
between a curve and its tessellation. Setting this value too low will result in between a curve and its tessellation. Setting this value too low will result in
large meshes that can consume computing resources. Setting the value too high can large meshes that can consume computing resources. Setting the value too high can
@ -368,7 +374,4 @@ def export_stl(
writer = StlAPI_Writer() writer = StlAPI_Writer()
writer.ASCIIMode = ascii_format writer.ASCIIMode = ascii_format
return writer.Write(to_export.wrapped, fsdecode(file_path))
file_path = str(file_path)
return writer.Write(to_export.wrapped, file_path)

View file

@ -34,13 +34,14 @@ from __future__ import annotations
# other pylint warning to temp remove: # other pylint warning to temp remove:
# too-many-arguments, too-many-locals, too-many-public-methods, # too-many-arguments, too-many-locals, too-many-public-methods,
# too-many-statements, too-many-instance-attributes, too-many-branches # too-many-statements, too-many-instance-attributes, too-many-branches
import colorsys
import copy as copy_module import copy as copy_module
import itertools import itertools
import json import json
import logging import logging
import warnings import warnings
from collections.abc import Callable, Iterable, Sequence from collections.abc import Callable, Iterable, Sequence
from math import degrees, isclose, log10, pi, radians from math import degrees, isclose, log10, pi, radians, prod
from typing import TYPE_CHECKING, Any, TypeAlias, overload from typing import TYPE_CHECKING, Any, TypeAlias, overload
import numpy as np import numpy as np
@ -1000,6 +1001,16 @@ class BoundBox:
self.max = Vector(x_max, y_max, z_max) #: location of maximum corner self.max = Vector(x_max, y_max, z_max) #: location of maximum corner
self.size = Vector(x_max - x_min, y_max - y_min, z_max - z_min) #: overall size self.size = Vector(x_max - x_min, y_max - y_min, z_max - z_min) #: overall size
@property
def measure(self) -> float:
"""Return the overall Lebesgue measure of the bounding box.
- For 1D objects: length
- For 2D objects: area
- For 3D objects: volume
"""
return prod([x for x in self.size if x > TOLERANCE])
@property @property
def diagonal(self) -> float: def diagonal(self) -> float:
"""body diagonal length (i.e. object maximum size)""" """body diagonal length (i.e. object maximum size)"""
@ -1160,8 +1171,8 @@ class Color:
Args: Args:
color_like (ColorLike): color_like (ColorLike):
name, ex: "red", name, ex: "red" or "#ff0000",
name + alpha, ex: ("red", 0.5), name + alpha, ex: ("red", 0.5) or "#ff000080",
rgb, ex: (1., 0., 0.), rgb, ex: (1., 0., 0.),
rgb + alpha, ex: (1., 0., 0., 0.5), rgb + alpha, ex: (1., 0., 0., 0.5),
hex, ex: 0xff0000, hex, ex: 0xff0000,
@ -1172,7 +1183,7 @@ class Color:
@overload @overload
def __init__(self, name: str, alpha: float = 1.0): def __init__(self, name: str, alpha: float = 1.0):
"""Color from name """Color from name or hexadecimal string
`CSS3 Color Names `CSS3 Color Names
<https://en.wikipedia.org/wiki/Web_colors#Extended_colors>` <https://en.wikipedia.org/wiki/Web_colors#Extended_colors>`
@ -1180,8 +1191,10 @@ class Color:
`OCCT Color Names `OCCT Color Names
<https://dev.opencascade.org/doc/refman/html/_quantity___name_of_color_8hxx.html>`_ <https://dev.opencascade.org/doc/refman/html/_quantity___name_of_color_8hxx.html>`_
Hexadecimal string may be RGB or RGBA format with leading "#"
Args: Args:
name (str): color, e.g. "blue" name (str): color, e.g. "blue" or "#0000ff""
alpha (float, optional): 0.0 <= alpha <= 1.0. Defaults to 1.0 alpha (float, optional): 0.0 <= alpha <= 1.0. Defaults to 1.0
""" """
@ -1237,6 +1250,27 @@ class Color:
return return
case str(): case str():
name, alpha = fill_defaults(args, (name, alpha)) name, alpha = fill_defaults(args, (name, alpha))
name = name.strip()
if "#" in name:
# extract alpha from hex string
hex_a = format(int(alpha * 255), "x")
if len(name) == 5:
hex_a = name[4] * 2
name = name[:4]
elif len(name) == 9:
hex_a = name[7:9]
name = name[:7]
elif len(name) not in [4, 5, 7, 9]:
raise ValueError(
f'"{name}" is not a valid hexadecimal color value.'
)
try:
if hex_a:
alpha = int(hex_a, 16) / 0xFF
except ValueError as ex:
raise ValueError(
f"Invald alpha hex string: {hex_a}"
) from ex
case int(): case int():
color_code, alpha = fill_defaults(args, (color_code, alpha)) color_code, alpha = fill_defaults(args, (color_code, alpha))
case float(): case float():
@ -1296,16 +1330,6 @@ class Color:
self.iter_index += 1 self.iter_index += 1
return value return value
def to_tuple(self):
"""Value as tuple"""
warnings.warn(
"to_tuple is deprecated and will be removed in a future version. "
"Use 'tuple(Color)' instead.",
DeprecationWarning,
stacklevel=2,
)
return tuple(self)
def __copy__(self) -> Color: def __copy__(self) -> Color:
"""Return copy of self""" """Return copy of self"""
return Color(*tuple(self)) return Color(*tuple(self))
@ -1332,6 +1356,74 @@ class Color:
"""Color repr""" """Color repr"""
return f"Color{str(tuple(self))}" return f"Color{str(tuple(self))}"
@classmethod
def categorical_set(
cls,
color_count: int,
starting_hue: ColorLike | float = 0.0,
alpha: float | Iterable[float] = 1.0,
) -> list[Color]:
"""Generate a palette of evenly spaced colors.
Creates a list of visually distinct colors suitable for representing
discrete categories (such as different parts, assemblies, or data
series). Colors are evenly spaced around the hue circle and share
consistent lightness and saturation levels, resulting in balanced
perceptual contrast across all hues.
Produces palettes similar in appearance to the **Tableau 10** and **D3
Category10** color setsboth widely recognized standards in data
visualization for their clarity and accessibility. These values have
been empirically chosen to maintain consistent perceived brightness
across hues while avoiding overly vivid or dark colors.
Args:
color_count (int): Number of colors to generate.
starting_hue (ColorLike | float): Either a Color-like object or
a hue value in the range [0.0, 1.0] that defines the starting color.
alpha (float | Iterable[float]): Alpha value(s) for the colors. Can be a
single float or an iterable of length `color_count`.
Returns:
list[Color]: List of generated colors.
Raises:
ValueError: If starting_hue is out of range or alpha length mismatch.
"""
# --- Determine starting hue ---
if isinstance(starting_hue, float):
if not (0.0 <= starting_hue <= 1.0):
raise ValueError("Starting hue must be within range 0.01.0")
elif isinstance(starting_hue, int):
if starting_hue < 0:
raise ValueError("Starting color integer must be non-negative")
rgb = tuple(Color(starting_hue))[:3]
starting_hue = colorsys.rgb_to_hls(*rgb)[0]
else:
raise TypeError(
"Starting hue must be a float in [0,1] or an integer color literal"
)
# --- Normalize alpha values ---
if isinstance(alpha, (float, int)):
alphas = [float(alpha)] * color_count
else:
alphas = list(alpha)
if len(alphas) != color_count:
raise ValueError("Number of alpha values must match color_count")
# --- Generate color list ---
hues = np.linspace(
starting_hue, starting_hue + 1.0, color_count, endpoint=False
)
colors = [
cls(*colorsys.hls_to_rgb(h % 1.0, 0.55, 0.9), a)
for h, a in zip(hues, alphas)
]
return colors
@staticmethod @staticmethod
def _rgb_from_int(triplet: int) -> tuple[float, float, float]: def _rgb_from_int(triplet: int) -> tuple[float, float, float]:
red, remainder = divmod(triplet, 256**2) red, remainder = divmod(triplet, 256**2)
@ -1340,7 +1432,6 @@ class Color:
@staticmethod @staticmethod
def _rgb_from_str(name: str) -> tuple: def _rgb_from_str(name: str) -> tuple:
name = name.strip()
if "#" not in name: if "#" not in name:
try: try:
# Use css3 color names by default # Use css3 color names by default

View file

@ -83,6 +83,7 @@ license:
# pylint: disable=no-name-in-module, import-error # pylint: disable=no-name-in-module, import-error
import copy as copy_module import copy as copy_module
import ctypes import ctypes
from io import BytesIO
import math import math
import os import os
import sys import sys
@ -105,15 +106,23 @@ from OCP.BRepGProp import BRepGProp
from OCP.BRepMesh import BRepMesh_IncrementalMesh from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.gp import gp_Pnt from OCP.gp import gp_Pnt
from OCP.GProp import GProp_GProps from OCP.GProp import GProp_GProps
from OCP.Standard import Standard_TypeMismatch
from OCP.TopAbs import TopAbs_ShapeEnum from OCP.TopAbs import TopAbs_ShapeEnum
from OCP.TopExp import TopExp_Explorer from OCP.TopExp import TopExp_Explorer
from OCP.TopLoc import TopLoc_Location from OCP.TopLoc import TopLoc_Location
from OCP.TopoDS import TopoDS_Compound from OCP.TopoDS import TopoDS, TopoDS_Compound, TopoDS_Shell
from lib3mf import Lib3MF from lib3mf import Lib3MF
from build123d.build_enums import MeshType, Unit from build123d.build_enums import MeshType, Unit
from build123d.geometry import TOLERANCE, Color from build123d.geometry import TOLERANCE, Color
from build123d.topology import Compound, Shape, Shell, Solid, downcast from build123d.topology import (
Compound,
Shape,
Shell,
Solid,
downcast,
unwrap_topods_compound,
)
class Mesher: class Mesher:
@ -295,7 +304,7 @@ class Mesher:
ocp_mesh_vertices.append(pnt) ocp_mesh_vertices.append(pnt)
# Store the triangles from the triangulated faces # Store the triangles from the triangulated faces
if facet.wrapped is None: if not facet:
continue continue
facet_reversed = facet.wrapped.Orientation() == ta.TopAbs_REVERSED facet_reversed = facet.wrapped.Orientation() == ta.TopAbs_REVERSED
order = [1, 3, 2] if facet_reversed else [1, 2, 3] order = [1, 3, 2] if facet_reversed else [1, 2, 3]
@ -328,8 +337,7 @@ class Mesher:
# Create vertices array in one shot # Create vertices array in one shot
vertices_3mf = [ vertices_3mf = [
Lib3MF.Position((ctypes.c_float * 3)(*v)) Lib3MF.Position((ctypes.c_float * 3)(*v)) for v in vertex_to_idx.keys()
for v in vertex_to_idx.keys()
] ]
# Pre-allocate triangles array and process in bulk # Pre-allocate triangles array and process in bulk
@ -346,7 +354,9 @@ class Mesher:
# Quick degenerate check without set creation # Quick degenerate check without set creation
if mapped_a != mapped_b and mapped_b != mapped_c and mapped_c != mapped_a: if mapped_a != mapped_b and mapped_b != mapped_c and mapped_c != mapped_a:
triangles_3mf.append(Lib3MF.Triangle(c_uint3(mapped_a, mapped_b, mapped_c))) triangles_3mf.append(
Lib3MF.Triangle(c_uint3(mapped_a, mapped_b, mapped_c))
)
return (vertices_3mf, triangles_3mf) return (vertices_3mf, triangles_3mf)
@ -464,7 +474,9 @@ class Mesher:
# Convert to a list of gp_Pnt # Convert to a list of gp_Pnt
ocp_vertices = [gp_pnts[tri_indices[i]] for i in range(3)] ocp_vertices = [gp_pnts[tri_indices[i]] for i in range(3)]
# Create the triangular face using the polygon # Create the triangular face using the polygon
polygon_builder = BRepBuilderAPI_MakePolygon(*ocp_vertices, Close=True) polygon_builder = BRepBuilderAPI_MakePolygon(
ocp_vertices[0], ocp_vertices[1], ocp_vertices[2], Close=True
)
face_builder = BRepBuilderAPI_MakeFace(polygon_builder.Wire()) face_builder = BRepBuilderAPI_MakeFace(polygon_builder.Wire())
facet = face_builder.Face() facet = face_builder.Face()
facet_properties = GProp_GProps() facet_properties = GProp_GProps()
@ -477,19 +489,27 @@ class Mesher:
occ_sewed_shape = downcast(shell_builder.SewedShape()) occ_sewed_shape = downcast(shell_builder.SewedShape())
if isinstance(occ_sewed_shape, TopoDS_Compound): if isinstance(occ_sewed_shape, TopoDS_Compound):
occ_shells = [] bd_shells = []
explorer = TopExp_Explorer(occ_sewed_shape, TopAbs_ShapeEnum.TopAbs_SHELL) explorer = TopExp_Explorer(occ_sewed_shape, TopAbs_ShapeEnum.TopAbs_SHELL)
while explorer.More(): while explorer.More():
occ_shells.append(downcast(explorer.Current())) # occ_shells.append(downcast(explorer.Current()))
bd_shells.append(Shell(TopoDS.Shell_s(explorer.Current())))
explorer.Next() explorer.Next()
else: else:
occ_shells = [occ_sewed_shape] assert isinstance(occ_sewed_shape, TopoDS_Shell)
bd_shells = [Shell(occ_sewed_shape)]
# Create a solid if manifold outer_shell = max(bd_shells, key=lambda s: math.prod(s.bounding_box().size))
shape_obj = Shell(occ_sewed_shape) inner_shells = [s for s in bd_shells if s is not outer_shell]
if shape_obj.is_manifold:
solid_builder = BRepBuilderAPI_MakeSolid(*occ_shells) # The the shell isn't water tight just return it else create a solid
shape_obj = Solid(solid_builder.Solid()) if not outer_shell.is_manifold:
return outer_shell
solid_builder = BRepBuilderAPI_MakeSolid(outer_shell.wrapped)
for inner_shell in inner_shells:
solid_builder.Add(inner_shell.wrapped)
shape_obj = Solid(solid_builder.Solid())
return shape_obj return shape_obj
@ -551,3 +571,8 @@ class Mesher:
raise ValueError(f"Unknown file format {output_file_extension}") raise ValueError(f"Unknown file format {output_file_extension}")
writer = self.model.QueryWriter(output_file_extension[1:]) writer = self.model.QueryWriter(output_file_extension[1:])
writer.WriteToFile(file_name) writer.WriteToFile(file_name)
def write_stream(self, stream: BytesIO, file_type: str):
writer = self.model.QueryWriter(file_type)
result = bytes(writer.WriteToBuffer())
stream.write(result)

View file

@ -50,7 +50,7 @@ from build123d.build_enums import (
) )
from build123d.build_line import BuildLine from build123d.build_line import BuildLine
from build123d.geometry import Axis, Plane, Vector, VectorLike, TOLERANCE from build123d.geometry import Axis, Plane, Vector, VectorLike, TOLERANCE
from build123d.topology import Edge, Face, Wire, Curve from build123d.topology import Curve, Edge, Face, Vertex, Wire
from build123d.topology.shape_core import ShapeList from build123d.topology.shape_core import ShapeList
@ -787,20 +787,22 @@ class Helix(BaseEdgeObject):
class FilletPolyline(BaseLineObject): class FilletPolyline(BaseLineObject):
"""Line Object: Fillet Polyline """Line Object: Fillet Polyline
Create a sequence of straight lines defined by successive points that are filleted Create a sequence of straight lines defined by successive points that are filleted
to a given radius. to a given radius.
Args: Args:
pts (VectorLike | Iterable[VectorLike]): sequence of two or more points pts (VectorLike | Iterable[VectorLike]): sequence of two or more points
radius (float): fillet radius radius (float | Iterable[float]): radius to fillet at each vertex or a single value for all vertices.
A radius of 0 will create a sharp corner (vertex without fillet).
close (bool, optional): close end points with extra Edge and corner fillets. close (bool, optional): close end points with extra Edge and corner fillets.
Defaults to False Defaults to False
mode (Mode, optional): combination mode. Defaults to Mode.ADD mode (Mode, optional): combination mode. Defaults to Mode.ADD
Raises: Raises:
ValueError: Two or more points not provided ValueError: Two or more points not provided
ValueError: radius must be positive ValueError: radius must be non-negative
""" """
_applies_to = [BuildLine._tag] _applies_to = [BuildLine._tag]
@ -808,34 +810,49 @@ class FilletPolyline(BaseLineObject):
def __init__( def __init__(
self, self,
*pts: VectorLike | Iterable[VectorLike], *pts: VectorLike | Iterable[VectorLike],
radius: float, radius: float | Iterable[float],
close: bool = False, close: bool = False,
mode: Mode = Mode.ADD, mode: Mode = Mode.ADD,
): ):
context: BuildLine | None = BuildLine._get_context(self) context: BuildLine | None = BuildLine._get_context(self)
validate_inputs(context, self) validate_inputs(context, self)
points = flatten_sequence(*pts) points = flatten_sequence(*pts)
if len(points) < 2: if len(points) < 2:
raise ValueError("FilletPolyline requires two or more pts") raise ValueError("FilletPolyline requires two or more pts")
if radius <= 0:
raise ValueError("radius must be positive") if isinstance(radius, (int, float)):
radius_list = [radius] * len(points) # Single radius for all points
else:
radius_list = list(radius)
if len(radius_list) != len(points) - int(not close) * 2:
raise ValueError(
f"radius list length ({len(radius_list)}) must match angle count ({ len(points) - int(not close) * 2})"
)
for r in radius_list:
if r < 0:
raise ValueError(f"radius {r} must be non-negative")
lines_pts = WorkplaneList.localize(*points) lines_pts = WorkplaneList.localize(*points)
# Create the polyline # Create the polyline
new_edges = [ new_edges = [
Edge.make_line(lines_pts[i], lines_pts[i + 1]) Edge.make_line(lines_pts[i], lines_pts[i + 1])
for i in range(len(lines_pts) - 1) for i in range(len(lines_pts) - 1)
] ]
if close and (new_edges[0] @ 0 - new_edges[-1] @ 1).length > 1e-5: if close and (new_edges[0] @ 0 - new_edges[-1] @ 1).length > 1e-5:
new_edges.append(Edge.make_line(new_edges[-1] @ 1, new_edges[0] @ 0)) new_edges.append(Edge.make_line(new_edges[-1] @ 1, new_edges[0] @ 0))
wire_of_lines = Wire(new_edges) wire_of_lines = Wire(new_edges)
# Create a list of vertices from wire_of_lines in the same order as # Create a list of vertices from wire_of_lines in the same order as
# the original points so the resulting fillet edges are ordered # the original points so the resulting fillet edges are ordered
ordered_vertices = [] ordered_vertices: list[Vertex] = []
for pnts in lines_pts: for pnts in lines_pts:
distance = { distance = {
v: (Vector(pnts) - Vector(*v)).length for v in wire_of_lines.vertices() v: (Vector(pnts) - Vector(*v)).length for v in wire_of_lines.vertices()
@ -843,40 +860,93 @@ class FilletPolyline(BaseLineObject):
ordered_vertices.append(sorted(distance.items(), key=lambda x: x[1])[0][0]) ordered_vertices.append(sorted(distance.items(), key=lambda x: x[1])[0][0])
# Fillet the corners # Fillet the corners
# Create a map of vertices to edges containing that vertex # Create a map of vertices to edges containing that vertex
vertex_to_edges = { vertex_to_edges = {
v: [e for e in wire_of_lines.edges() if v in e.vertices()] v: [e for e in wire_of_lines.edges() if v in e.vertices()]
for v in ordered_vertices for v in ordered_vertices
} }
# For each corner vertex create a new fillet Edge # For each corner vertex create a new fillet Edge (or keep as vertex if radius is 0)
fillets = [] fillets: list[None | Edge] = []
for vertex, edges in vertex_to_edges.items():
for i, (vertex, edges) in enumerate(vertex_to_edges.items()):
if len(edges) != 2: if len(edges) != 2:
continue continue
other_vertices = {ve for e in edges for ve in e.vertices() if ve != vertex} current_radius = radius_list[i - int(not close)]
third_edge = Edge.make_line(*[v for v in other_vertices])
fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(radius, [vertex]) if current_radius == 0:
fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0]) # For 0 radius, store the vertex as a marker for a sharp corner
fillets.append(None)
else:
other_vertices = {
ve for e in edges for ve in e.vertices() if ve != vertex
}
third_edge = Edge.make_line(*[v for v in other_vertices])
fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(
current_radius, [vertex]
)
fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0])
# Create the Edges that join the fillets # Create the Edges that join the fillets
if close: if close:
interior_edges = [ interior_edges = []
Edge.make_line(fillets[i - 1] @ 1, fillets[i] @ 0)
for i in range(len(fillets))
]
end_edges = []
else:
interior_edges = [
Edge.make_line(fillets[i] @ 1, f @ 0) for i, f in enumerate(fillets[1:])
]
end_edges = [
Edge.make_line(wire_of_lines @ 0, fillets[0] @ 0),
Edge.make_line(fillets[-1] @ 1, wire_of_lines @ 1),
]
new_wire = Wire(end_edges + interior_edges + fillets) for i in range(len(fillets)):
prev_fillet = fillets[i - 1]
curr_fillet = fillets[i]
prev_idx = i - 1
curr_idx = i
# Determine start and end points
if prev_fillet is None:
start_pt: Vertex | Vector = ordered_vertices[prev_idx]
else:
start_pt = prev_fillet @ 1
if curr_fillet is None:
end_pt: Vertex | Vector = ordered_vertices[curr_idx]
else:
end_pt = curr_fillet @ 0
interior_edges.append(Edge.make_line(start_pt, end_pt))
end_edges = []
else:
interior_edges = []
for i in range(len(fillets) - 1):
next_fillet = fillets[i + 1]
curr_fillet = fillets[i]
curr_idx = i
next_idx = i + 1
# Determine start and end points
if curr_fillet is None:
start_pt = ordered_vertices[
curr_idx + 1
] # +1 because first vertex has no fillet
else:
start_pt = curr_fillet @ 1
if next_fillet is None:
end_pt = ordered_vertices[next_idx + 1]
else:
end_pt = next_fillet @ 0
interior_edges.append(Edge.make_line(start_pt, end_pt))
# Handle end edges
if fillets[0] is None:
start_edge = Edge.make_line(wire_of_lines @ 0, ordered_vertices[1])
else:
start_edge = Edge.make_line(wire_of_lines @ 0, fillets[0] @ 0)
if fillets[-1] is None:
end_edge = Edge.make_line(ordered_vertices[-2], wire_of_lines @ 1)
else:
end_edge = Edge.make_line(fillets[-1] @ 1, wire_of_lines @ 1)
end_edges = [start_edge, end_edge]
# Filter out None values from fillets (these are 0-radius corners)
actual_fillets = [f for f in fillets if f is not None]
new_wire = Wire(end_edges + interior_edges + actual_fillets)
super().__init__(new_wire, mode=mode) super().__init__(new_wire, mode=mode)
@ -1597,6 +1667,7 @@ class ArcArcTangentLine(BaseEdgeObject):
Defaults to Keep.INSIDE Defaults to Keep.INSIDE
mode (Mode, optional): combination mode. Defaults to Mode.ADD mode (Mode, optional): combination mode. Defaults to Mode.ADD
""" """
warnings.warn( warnings.warn(
"The 'ArcArcTangentLine' object is deprecated and will be removed in a future version.", "The 'ArcArcTangentLine' object is deprecated and will be removed in a future version.",
DeprecationWarning, DeprecationWarning,

View file

@ -365,7 +365,7 @@ def chamfer(
if target._dim == 1: if target._dim == 1:
if isinstance(target, BaseLineObject): if isinstance(target, BaseLineObject):
if target.wrapped is None: if not target:
target = Wire([]) # empty wire target = Wire([]) # empty wire
else: else:
target = Wire(target.wrapped) target = Wire(target.wrapped)
@ -465,7 +465,7 @@ def fillet(
if target._dim == 1: if target._dim == 1:
if isinstance(target, BaseLineObject): if isinstance(target, BaseLineObject):
if target.wrapped is None: if not target:
target = Wire([]) # empty wire target = Wire([]) # empty wire
else: else:
target = Wire(target.wrapped) target = Wire(target.wrapped)

View file

@ -223,8 +223,8 @@ def extrude(
new_solids.append( new_solids.append(
Solid.extrude_until( Solid.extrude_until(
section=face, face,
target_object=target_object, target=target_object,
direction=plane.z_dir * direction, direction=plane.z_dir * direction,
until=until, until=until,
) )

View file

@ -43,7 +43,6 @@ from build123d.topology import (
Wire, Wire,
Sketch, Sketch,
topo_explore_connected_edges, topo_explore_connected_edges,
topo_explore_common_vertex,
) )
from build123d.geometry import Plane, Vector, TOLERANCE from build123d.geometry import Plane, Vector, TOLERANCE
from build123d.build_common import flatten_sequence, validate_inputs from build123d.build_common import flatten_sequence, validate_inputs

View file

@ -58,13 +58,13 @@ import copy
import os import os
import sys import sys
import warnings import warnings
from itertools import combinations
from typing import Type, Union
from collections.abc import Iterable, Iterator, Sequence from collections.abc import Iterable, Iterator, Sequence
from itertools import combinations
from typing_extensions import Self
import OCP.TopAbs as ta import OCP.TopAbs as ta
from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Fuse, BRepAlgoAPI_Section
from OCP.Font import ( from OCP.Font import (
Font_FA_Bold, Font_FA_Bold,
Font_FA_BoldItalic, Font_FA_BoldItalic,
@ -107,7 +107,6 @@ from build123d.geometry import (
VectorLike, VectorLike,
logger, logger,
) )
from typing_extensions import Self
from .one_d import Edge, Wire, Mixin1D from .one_d import Edge, Wire, Mixin1D
from .shape_core import ( from .shape_core import (
@ -130,7 +129,7 @@ from .utils import (
from .zero_d import Vertex from .zero_d import Vertex
class Compound(Mixin3D, Shape[TopoDS_Compound]): class Compound(Mixin3D[TopoDS_Compound]):
"""A Compound in build123d is a topological entity representing a collection of """A Compound in build123d is a topological entity representing a collection of
geometric shapes grouped together within a single structure. It serves as a geometric shapes grouped together within a single structure. It serves as a
container for organizing diverse shapes like edges, faces, or solids. This container for organizing diverse shapes like edges, faces, or solids. This
@ -142,7 +141,6 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
order = 4.0 order = 4.0
project_to_viewport = Mixin1D.project_to_viewport
# ---- Constructor ---- # ---- Constructor ----
def __init__( def __init__(
@ -166,7 +164,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
parent (Compound, optional): assembly parent. Defaults to None. parent (Compound, optional): assembly parent. Defaults to None.
children (Sequence[Shape], optional): assembly children. Defaults to None. children (Sequence[Shape], optional): assembly children. Defaults to None.
""" """
topods_compound: TopoDS_Compound | None
if isinstance(obj, Iterable): if isinstance(obj, Iterable):
topods_compound = _make_topods_compound_from_shapes( topods_compound = _make_topods_compound_from_shapes(
[s.wrapped for s in obj] [s.wrapped for s in obj]
@ -378,8 +376,14 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
) )
text_flat = Compound( text_flat = Compound(
builder.Perform( TopoDS.Compound_s(
font_i, NCollection_Utf8String(txt), gp_Ax3(), horiz_align, vert_align builder.Perform(
font_i,
NCollection_Utf8String(txt),
gp_Ax3(),
horiz_align,
vert_align,
)
) )
) )
@ -455,7 +459,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
will be a Wire, otherwise a Shape. will be a Wire, otherwise a Shape.
""" """
if self._dim == 1: if self._dim == 1:
curve = Curve() if self.wrapped is None else Curve(self.wrapped) curve = Curve() if self._wrapped is None else Curve(self.wrapped)
sum1d: Edge | Wire | ShapeList[Edge] = curve + other sum1d: Edge | Wire | ShapeList[Edge] = curve + other
if isinstance(sum1d, ShapeList): if isinstance(sum1d, ShapeList):
result1d: Curve | Wire = Curve(sum1d) result1d: Curve | Wire = Curve(sum1d)
@ -506,6 +510,8 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
def __and__(self, other: Shape | Iterable[Shape]) -> Compound: def __and__(self, other: Shape | Iterable[Shape]) -> Compound:
"""Intersect other to self `&` operator""" """Intersect other to self `&` operator"""
intersection = Shape.__and__(self, other) intersection = Shape.__and__(self, other)
if intersection is None:
return Compound()
intersection = Compound( intersection = Compound(
intersection if isinstance(intersection, list) else [intersection] intersection if isinstance(intersection, list) else [intersection]
) )
@ -517,7 +523,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
Check if empty. Check if empty.
""" """
return TopoDS_Iterator(self.wrapped).More() return self._wrapped is not None and TopoDS_Iterator(self.wrapped).More()
def __iter__(self) -> Iterator[Shape]: def __iter__(self) -> Iterator[Shape]:
""" """
@ -534,7 +540,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
def __len__(self) -> int: def __len__(self) -> int:
"""Return the number of subshapes""" """Return the number of subshapes"""
count = 0 count = 0
if self.wrapped is not None: if self._wrapped is not None:
for _ in self: for _ in self:
count += 1 count += 1
return count return count
@ -593,7 +599,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
"""Return the Compound""" """Return the Compound"""
shape_list = self.compounds() shape_list = self.compounds()
entity_count = len(shape_list) entity_count = len(shape_list)
if entity_count != 1: if entity_count > 1:
warnings.warn( warnings.warn(
f"Found {entity_count} compounds, returning first", f"Found {entity_count} compounds, returning first",
stacklevel=2, stacklevel=2,
@ -602,7 +608,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
def compounds(self) -> ShapeList[Compound]: def compounds(self) -> ShapeList[Compound]:
"""compounds - all the compounds in this Shape""" """compounds - all the compounds in this Shape"""
if self.wrapped is None: if self._wrapped is None:
return ShapeList() return ShapeList()
if isinstance(self.wrapped, TopoDS_Compound): if isinstance(self.wrapped, TopoDS_Compound):
# pylint: disable=not-an-iterable # pylint: disable=not-an-iterable
@ -651,11 +657,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
children[child_index_pair[1]] children[child_index_pair[1]]
) )
if obj_intersection is not None: if obj_intersection is not None:
common_volume = ( common_volume = sum(s.volume for s in obj_intersection.solids())
0.0
if isinstance(obj_intersection, list)
else obj_intersection.volume
)
if common_volume > tolerance: if common_volume > tolerance:
return ( return (
True, True,
@ -706,11 +708,182 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
while iterator.More(): while iterator.More():
child = iterator.Value() child = iterator.Value()
if child.ShapeType() == type_map[obj_type]: if child.ShapeType() == type_map[obj_type]:
results.append(obj_type(downcast(child))) results.append(obj_type(downcast(child))) # type: ignore
iterator.Next() iterator.Next()
return results return results
def intersect(
self, *to_intersect: Shape | Vector | Location | Axis | Plane
) -> None | ShapeList[Vertex | Edge | Face | Solid]:
"""Intersect Compound with Shape or geometry object
Args:
to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
Returns:
ShapeList[Vertex | Edge | Face | Solid] | None: ShapeList of vertices, edges,
faces, and/or solids.
"""
def to_vector(objs: Iterable) -> ShapeList:
return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])
def to_vertex(objs: Iterable) -> ShapeList:
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
def bool_op(
args: Sequence,
tools: Sequence,
operation: BRepAlgoAPI_Common | BRepAlgoAPI_Section,
) -> ShapeList:
# Wrap Shape._bool_op for corrected output
intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
if isinstance(intersections, ShapeList):
return intersections
if isinstance(intersections, Shape) and not intersections.is_null:
return ShapeList([intersections])
return ShapeList()
def expand_compound(compound: Compound) -> ShapeList:
shapes = ShapeList(compound.children)
for shape_type in [Vertex, Edge, Wire, Face, Shell, Solid]:
shapes.extend(compound.get_type(shape_type))
return shapes
def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
# Remove lower order shapes from list which *appear* to be part of
# a higher order shape using a lazy distance check
# (sufficient for vertices, may be an issue for higher orders)
order_groups = []
for order in orders:
order_groups.append(
ShapeList([s for s in shapes if isinstance(s, order)])
)
filtered_shapes = order_groups[-1]
for i in range(len(order_groups) - 1):
los = order_groups[i]
his: list = sum(order_groups[i + 1 :], [])
filtered_shapes.extend(
ShapeList(
lo
for lo in los
if all(lo.distance_to(hi) > TOLERANCE for hi in his)
)
)
return filtered_shapes
common_set: ShapeList[Shape] = expand_compound(self)
target: ShapeList | Shape
for other in to_intersect:
# Conform target type
match other:
case Axis():
# BRepAlgoAPI_Section seems happier if Edge isnt infinite
bbox = self.bounding_box()
dist = self.distance_to(other.position)
dist = dist if dist >= 1 else 1
target = Edge.make_line(
other.position - other.direction * bbox.diagonal * dist,
other.position + other.direction * bbox.diagonal * dist,
)
case Plane():
target = Face(other)
case Vector():
target = Vertex(other)
case Location():
target = Vertex(other.position)
case Compound():
target = expand_compound(other)
case _ if issubclass(type(other), Shape):
target = other
case _:
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
# Find common matches
common: list[Vertex | Edge | Wire | Face | Shell | Solid] = []
result: ShapeList
for obj in common_set:
if isinstance(target, Shape):
target = ShapeList([target])
result = ShapeList()
for t in target:
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common = (
BRepAlgoAPI_Section()
)
result.extend(bool_op((obj,), (t,), operation))
if (
not isinstance(obj, Edge | Wire)
and not isinstance(t, Edge | Wire)
) or (
isinstance(obj, Solid | Compound)
or isinstance(t, Solid | Compound)
):
# Face + Edge combinations may produce an intersection
# with Common but always with Section.
# No easy way to deduplicate
# Many Solid + Edge combinations need Common
operation = BRepAlgoAPI_Common()
result.extend(bool_op((obj,), (t,), operation))
if result:
common.extend(result)
expanded: ShapeList = ShapeList()
if common:
for shape in common:
if isinstance(shape, Compound):
expanded.extend(expand_compound(shape))
else:
expanded.append(shape)
if expanded:
common_set = ShapeList()
for shape in expanded:
if isinstance(shape, Wire):
common_set.extend(shape.edges())
elif isinstance(shape, Shell):
common_set.extend(shape.faces())
else:
common_set.append(shape)
common_set = to_vertex(set(to_vector(common_set)))
common_set = filter_shapes_by_order(
common_set, [Vertex, Edge, Face, Solid]
)
else:
return None
return ShapeList(common_set)
def project_to_viewport(
self,
viewport_origin: VectorLike,
viewport_up: VectorLike = (0, 0, 1),
look_at: VectorLike | None = None,
focus: float | None = None,
) -> tuple[ShapeList[Edge], ShapeList[Edge]]:
"""project_to_viewport
Project a shape onto a viewport returning visible and hidden Edges.
Args:
viewport_origin (VectorLike): location of viewport
viewport_up (VectorLike, optional): direction of the viewport y axis.
Defaults to (0, 0, 1).
look_at (VectorLike, optional): point to look at.
Defaults to None (center of shape).
focus (float, optional): the focal length for perspective projection
Defaults to None (orthographic projection)
Returns:
tuple[ShapeList[Edge],ShapeList[Edge]]: visible & hidden Edges
"""
return Mixin1D.project_to_viewport(
self, viewport_origin, viewport_up, look_at, focus
)
def unwrap(self, fully: bool = True) -> Self | Shape: def unwrap(self, fully: bool = True) -> Self | Shape:
"""Strip unnecessary Compound wrappers """Strip unnecessary Compound wrappers
@ -764,8 +937,8 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
parent.wrapped = _make_topods_compound_from_shapes( parent.wrapped = _make_topods_compound_from_shapes(
[c.wrapped for c in parent.children] [c.wrapped for c in parent.children]
) )
else: # else:
parent.wrapped = None # parent.wrapped = None
def _post_detach_children(self, children): def _post_detach_children(self, children):
"""Method call before detaching `children`.""" """Method call before detaching `children`."""

View file

@ -174,7 +174,7 @@ def _as_gcc_arg(obj: Edge | Vector, constaint: Tangency) -> tuple[
- Edge -> (QualifiedCurve, h2d, first, last, True) - Edge -> (QualifiedCurve, h2d, first, last, True)
- Vector -> (CartesianPoint, None, None, None, False) - Vector -> (CartesianPoint, None, None, None, False)
""" """
if obj.wrapped is None: if not obj:
raise TypeError("Can't create a qualified curve from empty edge") raise TypeError("Can't create a qualified curve from empty edge")
if isinstance(obj.wrapped, TopoDS_Edge): if isinstance(obj.wrapped, TopoDS_Edge):

View file

@ -52,12 +52,11 @@ license:
from __future__ import annotations from __future__ import annotations
import copy import copy
import numpy as np
import warnings import warnings
from collections.abc import Iterable from collections.abc import Iterable, Sequence
from itertools import combinations from itertools import combinations
from math import atan2, ceil, copysign, cos, floor, inf, isclose, pi, radians from math import atan2, ceil, copysign, cos, floor, inf, isclose, pi, radians
from typing import TYPE_CHECKING, Literal, TypeAlias, overload from typing import TYPE_CHECKING, Literal, overload
from typing import cast as tcast from typing import cast as tcast
import numpy as np import numpy as np
@ -90,8 +89,12 @@ from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace
from OCP.BRepProj import BRepProj_Projection from OCP.BRepProj import BRepProj_Projection
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
from OCP.GC import GC_MakeArcOfCircle, GC_MakeArcOfEllipse from OCP.GC import GC_MakeArcOfCircle, GC_MakeArcOfEllipse
from OCP.GccEnt import GccEnt_unqualified, GccEnt_Position from OCP.GCPnts import (
from OCP.GCPnts import GCPnts_AbscissaPoint GCPnts_AbscissaPoint,
GCPnts_QuasiUniformDeflection,
GCPnts_UniformDeflection,
)
from OCP.GProp import GProp_GProps
from OCP.Geom import ( from OCP.Geom import (
Geom_BezierCurve, Geom_BezierCurve,
Geom_BSplineCurve, Geom_BSplineCurve,
@ -117,6 +120,9 @@ from OCP.GeomAbs import (
GeomAbs_C0, GeomAbs_C0,
GeomAbs_C1, GeomAbs_C1,
GeomAbs_C2, GeomAbs_C2,
GeomAbs_C3,
GeomAbs_CN,
GeomAbs_C1,
GeomAbs_G1, GeomAbs_G1,
GeomAbs_G2, GeomAbs_G2,
GeomAbs_JoinType, GeomAbs_JoinType,
@ -217,6 +223,7 @@ from build123d.geometry import (
) )
from .shape_core import ( from .shape_core import (
TOPODS,
Shape, Shape,
ShapeList, ShapeList,
SkipClean, SkipClean,
@ -226,11 +233,11 @@ from .shape_core import (
shapetype, shapetype,
topods_dim, topods_dim,
unwrap_topods_compound, unwrap_topods_compound,
_topods_bool_op,
) )
from .utils import ( from .utils import (
_extrude_topods_shape, _extrude_topods_shape,
_make_topods_face_from_wires, _make_topods_face_from_wires,
_topods_bool_op,
isclose_b, isclose_b,
) )
from .zero_d import Vertex, topo_explore_common_vertex from .zero_d import Vertex, topo_explore_common_vertex
@ -250,7 +257,7 @@ if TYPE_CHECKING: # pragma: no cover
from .two_d import Face, Shell # pylint: disable=R0801 from .two_d import Face, Shell # pylint: disable=R0801
class Mixin1D(Shape): class Mixin1D(Shape[TOPODS]):
"""Methods to add to the Edge and Wire classes""" """Methods to add to the Edge and Wire classes"""
# ---- Properties ---- # ---- Properties ----
@ -263,14 +270,14 @@ class Mixin1D(Shape):
@property @property
def is_closed(self) -> bool: def is_closed(self) -> bool:
"""Are the start and end points equal?""" """Are the start and end points equal?"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't determine if empty Edge or Wire is closed") raise ValueError("Can't determine if empty Edge or Wire is closed")
return BRep_Tool.IsClosed_s(self.wrapped) return BRep_Tool.IsClosed_s(self.wrapped)
@property @property
def is_forward(self) -> bool: def is_forward(self) -> bool:
"""Does the Edge/Wire loop forward or reverse""" """Does the Edge/Wire loop forward or reverse"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't determine direction of empty Edge or Wire") raise ValueError("Can't determine direction of empty Edge or Wire")
return self.wrapped.Orientation() == TopAbs_Orientation.TopAbs_FORWARD return self.wrapped.Orientation() == TopAbs_Orientation.TopAbs_FORWARD
@ -305,7 +312,9 @@ class Mixin1D(Shape):
@property @property
def length(self) -> float: def length(self) -> float:
"""Edge or Wire length""" """Edge or Wire length"""
return GCPnts_AbscissaPoint.Length_s(self.geom_adaptor()) props = GProp_GProps()
BRepGProp.LinearProperties_s(self.wrapped, props)
return props.Mass()
@property @property
def radius(self) -> float: def radius(self) -> float:
@ -388,8 +397,7 @@ class Mixin1D(Shape):
shape shape
# for o in (other if isinstance(other, (list, tuple)) else [other]) # for o in (other if isinstance(other, (list, tuple)) else [other])
for o in ([other] if isinstance(other, Shape) else other) for o in ([other] if isinstance(other, Shape) else other)
if o is not None for shape in get_top_level_topods_shapes(o.wrapped if o else None)
for shape in get_top_level_topods_shapes(o.wrapped)
] ]
# If there is nothing to add return the original object # If there is nothing to add return the original object
if not topods_summands: if not topods_summands:
@ -404,7 +412,7 @@ class Mixin1D(Shape):
) )
summand_edges = [e for summand in summands for e in summand.edges()] summand_edges = [e for summand in summands for e in summand.edges()]
if self.wrapped is None: # an empty object if self._wrapped is None: # an empty object
if len(summands) == 1: if len(summands) == 1:
sum_shape: Edge | Wire | ShapeList[Edge] = summands[0] sum_shape: Edge | Wire | ShapeList[Edge] = summands[0]
else: else:
@ -452,7 +460,7 @@ class Mixin1D(Shape):
Returns: Returns:
Vector: center Vector: center
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find center of empty edge/wire") raise ValueError("Can't find center of empty edge/wire")
if center_of == CenterOf.GEOMETRY: if center_of == CenterOf.GEOMETRY:
@ -571,14 +579,14 @@ class Mixin1D(Shape):
- On straight segments, κ = 0 so no teeth are drawn. - On straight segments, κ = 0 so no teeth are drawn.
- At inflection points κ0 and the tooth flips direction. - At inflection points κ0 and the tooth flips direction.
- At C0 corners the tangent is discontinuous; nearby teeth may jump. - At C0 corners the tangent is discontinuous; nearby teeth may jump.
C1 yields continuous direction; C2 yields continuous magnitude as well. C1 yields continuous direction; C2 yields continuous magnitude as well.
Example: Example:
>>> comb = my_wire.curvature_comb(count=200, max_tooth_size=2.0) >>> comb = my_wire.curvature_comb(count=200, max_tooth_size=2.0)
>>> show(my_wire, Curve(comb)) >>> show(my_wire, Curve(comb))
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't create curvature_comb for empty curve") raise ValueError("Can't create curvature_comb for empty curve")
pln = self.common_plane() pln = self.common_plane()
if pln is None or not isclose(abs(pln.z_dir.Z), 1.0, abs_tol=TOLERANCE): if pln is None or not isclose(abs(pln.z_dir.Z), 1.0, abs_tol=TOLERANCE):
@ -676,30 +684,30 @@ class Mixin1D(Shape):
return derivative return derivative
def edge(self) -> Edge | None: # def edge(self) -> Edge | None:
"""Return the Edge""" # """Return the Edge"""
return Shape.get_single_shape(self, "Edge") # return Shape.get_single_shape(self, "Edge")
def edges(self) -> ShapeList[Edge]: # def edges(self) -> ShapeList[Edge]:
"""edges - all the edges in this Shape""" # """edges - all the edges in this Shape"""
if isinstance(self, Wire) and self.wrapped is not None: # if isinstance(self, Wire) and self.wrapped is not None:
# The WireExplorer is a tool to explore the edges of a wire in a connection order. # # The WireExplorer is a tool to explore the edges of a wire in a connection order.
explorer = BRepTools_WireExplorer(self.wrapped) # explorer = BRepTools_WireExplorer(self.wrapped)
edge_list: ShapeList[Edge] = ShapeList() # edge_list: ShapeList[Edge] = ShapeList()
while explorer.More(): # while explorer.More():
next_edge = Edge(explorer.Current()) # next_edge = Edge(explorer.Current())
next_edge.topo_parent = ( # next_edge.topo_parent = (
self if self.topo_parent is None else self.topo_parent # self if self.topo_parent is None else self.topo_parent
) # )
edge_list.append(next_edge) # edge_list.append(next_edge)
explorer.Next() # explorer.Next()
return edge_list # return edge_list
edge_list = Shape.get_shape_list(self, "Edge") # edge_list = Shape.get_shape_list(self, "Edge")
return edge_list.filter_by( # return edge_list.filter_by(
lambda e: BRep_Tool.Degenerated_s(e.wrapped), reverse=True # lambda e: BRep_Tool.Degenerated_s(e.wrapped), reverse=True
) # )
def end_point(self) -> Vector: def end_point(self) -> Vector:
"""The end point of this edge. """The end point of this edge.
@ -729,122 +737,104 @@ class Mixin1D(Shape):
def to_vertex(objs: Iterable) -> ShapeList: def to_vertex(objs: Iterable) -> ShapeList:
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs]) return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
common_set: ShapeList[Vertex | Edge] = ShapeList(self.edges()) def bool_op(
target: ShapeList | Shape | Plane args: Sequence,
tools: Sequence,
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common,
) -> ShapeList:
# Wrap Shape._bool_op for corrected output
intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
if isinstance(intersections, ShapeList):
return intersections or ShapeList()
if isinstance(intersections, Shape) and not intersections.is_null:
return ShapeList([intersections])
return ShapeList()
def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
# Remove lower order shapes from list which *appear* to be part of
# a higher order shape using a lazy distance check
# (sufficient for vertices, may be an issue for higher orders)
order_groups = []
for order in orders:
order_groups.append(
ShapeList([s for s in shapes if isinstance(s, order)])
)
filtered_shapes = order_groups[-1]
for i in range(len(order_groups) - 1):
los = order_groups[i]
his: list = sum(order_groups[i + 1 :], [])
filtered_shapes.extend(
ShapeList(
lo
for lo in los
if all(lo.distance_to(hi) > TOLERANCE for hi in his)
)
)
return filtered_shapes
common_set: ShapeList[Vertex | Edge | Wire] = ShapeList([self])
target: Shape | Plane
for other in to_intersect: for other in to_intersect:
# Conform target type # Conform target type
# Vertices need to be Vector for set()
match other: match other:
case Axis(): case Axis():
target = ShapeList([Edge(other)]) # BRepAlgoAPI_Section seems happier if Edge isnt infinite
bbox = self.bounding_box()
dist = self.distance_to(other.position)
dist = dist if dist >= 1 else 1
target = Edge.make_line(
other.position - other.direction * bbox.diagonal * dist,
other.position + other.direction * bbox.diagonal * dist,
)
case Plane(): case Plane():
target = other target = other
case Vector(): case Vector():
target = Vertex(other) target = Vertex(other)
case Location(): case Location():
target = Vertex(other.position) target = Vertex(other.position)
case Edge():
target = ShapeList([other])
case Wire():
target = ShapeList(other.edges())
case _ if issubclass(type(other), Shape): case _ if issubclass(type(other), Shape):
target = other target = other
case _: case _:
raise ValueError(f"Unsupported type to_intersect: {type(other)}") raise ValueError(f"Unsupported type to_intersect: {type(other)}")
# Find common matches # Find common matches
common: list[Vector | Edge] = [] common: list[Vertex | Edge | Wire] = []
result: ShapeList | Shape | None result: ShapeList | None
for obj in common_set: for obj in common_set:
match (obj, target): match (obj, target):
case obj, Shape() as target: case (_, Plane()):
# Find Shape with Edge/Wire assert isinstance(other.wrapped, gp_Pln)
if isinstance(target, Vertex): target = Shape(BRepBuilderAPI_MakeFace(other.wrapped).Face())
result = Shape.intersect(obj, target) operation1 = BRepAlgoAPI_Section()
else: result = bool_op((obj,), (target,), operation1)
result = target.intersect(obj) operation2 = BRepAlgoAPI_Common()
result.extend(bool_op((obj,), (target,), operation2))
if result: case (_, Vertex() | Edge() | Wire()):
if not isinstance(result, list): operation1 = BRepAlgoAPI_Section()
result = ShapeList([result]) section = bool_op((obj,), (target,), operation1)
common.extend(to_vector(result)) result = section
if not section:
operation2 = BRepAlgoAPI_Common()
result.extend(bool_op((obj,), (target,), operation2))
case Vertex() as obj, target: case _ if issubclass(type(target), Shape):
if not isinstance(target, ShapeList): result = target.intersect(obj)
target = ShapeList([target])
for tar in target: if result:
if isinstance(tar, Edge): common.extend(result)
result = Shape.intersect(obj, tar)
else:
result = obj.intersect(tar)
if result:
if not isinstance(result, list):
result = ShapeList([result])
common.extend(to_vector(result))
case Edge() as obj, ShapeList() as targets:
# Find any edge / edge intersection points
for tar in targets:
# Find crossing points
try:
intersection_points = obj.find_intersection_points(tar)
common.extend(intersection_points)
except ValueError:
pass
# Find common end points
obj_end_points = set(Vector(v) for v in obj.vertices())
tar_end_points = set(Vector(v) for v in tar.vertices())
points = set.intersection(obj_end_points, tar_end_points)
common.extend(points)
# Find Edge/Edge overlaps
result = obj._bool_op(
(obj,), targets, BRepAlgoAPI_Common()
).edges()
common.extend(result if isinstance(result, list) else [result])
case Edge() as obj, Plane() as plane:
# Find any edge / plane intersection points & edges
# Find point intersections
if obj.wrapped is None:
continue
geom_line = BRep_Tool.Curve_s(
obj.wrapped, obj.param_at(0), obj.param_at(1)
)
geom_plane = Geom_Plane(plane.local_coord_system)
intersection_calculator = GeomAPI_IntCS(geom_line, geom_plane)
plane_intersection_points: list[Vector] = []
if intersection_calculator.IsDone():
plane_intersection_points = [
Vector(intersection_calculator.Point(i + 1))
for i in range(intersection_calculator.NbPoints())
]
common.extend(plane_intersection_points)
# Find edge intersections
if all(
plane.contains(v)
for v in obj.positions(i / 7 for i in range(8))
): # is a 2D edge
common.append(obj)
if common: if common:
common_set = to_vertex(set(common)) common_set = ShapeList()
# Remove Vertex intersections coincident to Edge intersections for shape in common:
vts = common_set.vertices() if isinstance(shape, Wire):
eds = common_set.edges() common_set.extend(shape.edges())
if vts and eds: else:
filtered_vts = ShapeList( common_set.append(shape)
[ common_set = to_vertex(set(to_vector(common_set)))
v common_set = filter_shapes_by_order(common_set, [Vertex, Edge])
for v in vts
if all(v.distance_to(e) > TOLERANCE for e in eds)
]
)
common_set = filtered_vts + eds
else: else:
return None return None
@ -991,7 +981,7 @@ class Mixin1D(Shape):
Returns: Returns:
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find normal of empty edge/wire") raise ValueError("Can't find normal of empty edge/wire")
curve = self.geom_adaptor() curve = self.geom_adaptor()
@ -1117,16 +1107,16 @@ class Mixin1D(Shape):
The meaning of the returned parameter depends on the type of self: The meaning of the returned parameter depends on the type of self:
- **Edge**: Returns the native OCCT curve parameter corresponding to the - **Edge**: Returns the native OCCT curve parameter corresponding to the
given normalized `position` (0.0 start, 1.0 end). For closed/periodic given normalized `position` (0.0 start, 1.0 end). For closed/periodic
edges, OCCT may return a value **outside** the edge's nominal parameter edges, OCCT may return a value **outside** the edge's nominal parameter
range `[param_min, param_max]` (e.g., by adding/subtracting multiples of range `[param_min, param_max]` (e.g., by adding/subtracting multiples of
the period). If you require a value folded into the edge's range, apply a the period). If you require a value folded into the edge's range, apply a
modulo with the parameter span. modulo with the parameter span.
- **Wire**: Returns a *composite* parameter encoding both the edge index - **Wire**: Returns a *composite* parameter encoding both the edge index
and the position within that edge: the **integer part** is the zero-based and the position within that edge: the **integer part** is the zero-based
count of fully traversed edges, and the **fractional part** is the count of fully traversed edges, and the **fractional part** is the
normalized position in `[0.0, 1.0]` along the current edge. normalized position in `[0.0, 1.0]` along the current edge.
Args: Args:
position (float): Normalized arc-length position along the shape, position (float): Normalized arc-length position along the shape,
@ -1195,22 +1185,52 @@ class Mixin1D(Shape):
def positions( def positions(
self, self,
distances: Iterable[float], distances: Iterable[float] | None = None,
position_mode: PositionMode = PositionMode.PARAMETER, position_mode: PositionMode = PositionMode.PARAMETER,
deflection: float | None = None,
) -> list[Vector]: ) -> list[Vector]:
"""Positions along curve """Positions along curve
Generate positions along the underlying curve Generate positions along the underlying curve
Args: Args:
distances (Iterable[float]): distance or parameter values distances (Iterable[float] | None, optional): distance or parameter values.
position_mode (PositionMode, optional): position calculation mode. Defaults to None.
Defaults to PositionMode.PARAMETER. position_mode (PositionMode, optional): position calculation mode only applies
when using distances. Defaults to PositionMode.PARAMETER.
deflection (float | None, optional): maximum deflection between the curve and
the polygon that results from the computed points. Defaults to None.
Returns: Returns:
list[Vector]: positions along curve list[Vector]: positions along curve
""" """
return [self.position_at(d, position_mode) for d in distances] if deflection is not None:
curve: BRepAdaptor_Curve | BRepAdaptor_CompCurve = self.geom_adaptor()
# GCPnts_UniformDeflection provides the best results but is limited
if curve.Continuity() in (GeomAbs_C2, GeomAbs_C3, GeomAbs_CN):
discretizer: (
GCPnts_UniformDeflection | GCPnts_QuasiUniformDeflection
) = GCPnts_UniformDeflection()
else:
discretizer = GCPnts_QuasiUniformDeflection()
discretizer.Initialize(
curve,
deflection,
curve.FirstParameter(),
curve.LastParameter(),
)
if not discretizer.IsDone() or discretizer.NbPoints() == 0:
raise RuntimeError("Deflection calculation failed")
return [
Vector(curve.Value(discretizer.Parameter(i + 1)))
for i in range(discretizer.NbPoints())
]
elif distances is not None:
return [self.position_at(d, position_mode) for d in distances]
else:
raise ValueError("Either distances or deflection must be provided")
def project( def project(
self, face: Face, direction: VectorLike, closest: bool = True self, face: Face, direction: VectorLike, closest: bool = True
@ -1225,7 +1245,7 @@ class Mixin1D(Shape):
Returns: Returns:
""" """
if self.wrapped is None or face.wrapped is None: if self._wrapped is None or not face:
raise ValueError("Can't project an empty Edge or Wire onto empty Face") raise ValueError("Can't project an empty Edge or Wire onto empty Face")
bldr = BRepProj_Projection( bldr = BRepProj_Projection(
@ -1297,7 +1317,7 @@ class Mixin1D(Shape):
return edges return edges
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't project empty edge/wire") raise ValueError("Can't project empty edge/wire")
# Setup the projector # Setup the projector
@ -1357,144 +1377,6 @@ class Mixin1D(Shape):
return (visible_edges, hidden_edges) return (visible_edges, hidden_edges)
@overload
def split(
self, tool: TrimmingTool, keep: Literal[Keep.TOP, Keep.BOTTOM]
) -> Self | list[Self] | None:
"""split and keep inside or outside"""
@overload
def split(self, tool: TrimmingTool, keep: Literal[Keep.ALL]) -> list[Self]:
"""split and return the unordered pieces"""
@overload
def split(self, tool: TrimmingTool, keep: Literal[Keep.BOTH]) -> tuple[
Self | list[Self] | None,
Self | list[Self] | None,
]:
"""split and keep inside and outside"""
@overload
def split(self, tool: TrimmingTool) -> Self | list[Self] | None:
"""split and keep inside (default)"""
def split(self, tool: TrimmingTool, keep: Keep = Keep.TOP):
"""split
Split this shape by the provided plane or face.
Args:
surface (Plane | Face): surface to segment shape
keep (Keep, optional): which object(s) to save. Defaults to Keep.TOP.
Returns:
Shape: result of split
Returns:
Self | list[Self] | None,
Tuple[Self | list[Self] | None]: The result of the split operation.
- **Keep.TOP**: Returns the top as a `Self` or `list[Self]`, or `None`
if no top is found.
- **Keep.BOTTOM**: Returns the bottom as a `Self` or `list[Self]`, or `None`
if no bottom is found.
- **Keep.BOTH**: Returns a tuple `(inside, outside)` where each element is
either a `Self` or `list[Self]`, or `None` if no corresponding part is found.
"""
if self.wrapped is None or tool.wrapped is None:
raise ValueError("Can't split an empty edge/wire/tool")
shape_list = TopTools_ListOfShape()
shape_list.Append(self.wrapped)
# Define the splitting tool
trim_tool = (
BRepBuilderAPI_MakeFace(tool.wrapped).Face() # gp_Pln to Face
if isinstance(tool, Plane)
else tool.wrapped
)
tool_list = TopTools_ListOfShape()
tool_list.Append(trim_tool)
# Create the splitter algorithm
splitter = BRepAlgoAPI_Splitter()
# Set the shape to be split and the splitting tool (plane face)
splitter.SetArguments(shape_list)
splitter.SetTools(tool_list)
# Perform the splitting operation
splitter.Build()
split_result = downcast(splitter.Shape())
# Remove unnecessary TopoDS_Compound around single shape
if isinstance(split_result, TopoDS_Compound):
split_result = unwrap_topods_compound(split_result, True)
# For speed the user may just want all the objects which they
# can sort more efficiently then the generic algorithm below
if keep == Keep.ALL:
return ShapeList(
self.__class__.cast(part)
for part in get_top_level_topods_shapes(split_result)
)
if not isinstance(tool, Plane):
# Get a TopoDS_Face to work with from the tool
if isinstance(trim_tool, TopoDS_Shell):
face_explorer = TopExp_Explorer(trim_tool, ta.TopAbs_FACE)
tool_face = TopoDS.Face_s(face_explorer.Current())
else:
tool_face = trim_tool
# Create a reference point off the +ve side of the tool
surface_gppnt = gp_Pnt()
surface_normal = gp_Vec()
u_min, u_max, v_min, v_max = BRepTools.UVBounds_s(tool_face)
BRepGProp_Face(tool_face).Normal(
(u_min + u_max) / 2, (v_min + v_max) / 2, surface_gppnt, surface_normal
)
normalized_surface_normal = Vector(
surface_normal.X(), surface_normal.Y(), surface_normal.Z()
).normalized()
surface_point = Vector(surface_gppnt)
ref_point = surface_point + normalized_surface_normal
# Create a HalfSpace - Solidish object to determine top/bottom
# Note: BRepPrimAPI_MakeHalfSpace takes either a TopoDS_Shell or TopoDS_Face but the
# mypy expects only a TopoDS_Shell here
half_space_maker = BRepPrimAPI_MakeHalfSpace(trim_tool, ref_point.to_pnt())
# type: ignore
tool_solid = half_space_maker.Solid()
tops: list[Shape] = []
bottoms: list[Shape] = []
properties = GProp_GProps()
for part in get_top_level_topods_shapes(split_result):
sub_shape = self.__class__.cast(part)
if isinstance(tool, Plane):
is_up = tool.to_local_coords(sub_shape).center().Z >= 0
else:
# Intersect self and the thickened tool
is_up_obj = _topods_bool_op(
(part,), (tool_solid,), BRepAlgoAPI_Common()
)
# Check for valid intersections
BRepGProp.LinearProperties_s(is_up_obj, properties)
# Mass represents the total length for linear properties
is_up = properties.Mass() >= TOLERANCE
(tops if is_up else bottoms).append(sub_shape)
top = None if not tops else tops[0] if len(tops) == 1 else tops
bottom = None if not bottoms else bottoms[0] if len(bottoms) == 1 else bottoms
if keep == Keep.BOTH:
return (top, bottom)
if keep == Keep.TOP:
return top
if keep == Keep.BOTTOM:
return bottom
return None
def start_point(self) -> Vector: def start_point(self) -> Vector:
"""The start point of this edge """The start point of this edge
@ -1549,24 +1431,24 @@ class Mixin1D(Shape):
""" """
return self.derivative_at(position, 1, position_mode).normalized() return self.derivative_at(position, 1, position_mode).normalized()
def vertex(self) -> Vertex | None: # def vertex(self) -> Vertex | None:
"""Return the Vertex""" # """Return the Vertex"""
return Shape.get_single_shape(self, "Vertex") # return Shape.get_single_shape(self, "Vertex")
def vertices(self) -> ShapeList[Vertex]: # def vertices(self) -> ShapeList[Vertex]:
"""vertices - all the vertices in this Shape""" # """vertices - all the vertices in this Shape"""
return Shape.get_shape_list(self, "Vertex") # return Shape.get_shape_list(self, "Vertex")
def wire(self) -> Wire | None: # def wire(self) -> Wire | None:
"""Return the Wire""" # """Return the Wire"""
return Shape.get_single_shape(self, "Wire") # return Shape.get_single_shape(self, "Wire")
def wires(self) -> ShapeList[Wire]: # def wires(self) -> ShapeList[Wire]:
"""wires - all the wires in this Shape""" # """wires - all the wires in this Shape"""
return Shape.get_shape_list(self, "Wire") # return Shape.get_shape_list(self, "Wire")
class Edge(Mixin1D, Shape[TopoDS_Edge]): class Edge(Mixin1D[TopoDS_Edge]):
"""An Edge in build123d is a fundamental element in the topological data structure """An Edge in build123d is a fundamental element in the topological data structure
representing a one-dimensional geometric entity within a 3D model. It encapsulates representing a one-dimensional geometric entity within a 3D model. It encapsulates
information about a curve, which could be a line, arc, or other parametrically information about a curve, which could be a line, arc, or other parametrically
@ -1647,7 +1529,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
Returns: Returns:
Edge: extruded shape Edge: extruded shape
""" """
if obj.wrapped is None: if not obj:
raise ValueError("Can't extrude empty vertex") raise ValueError("Can't extrude empty vertex")
return Edge(TopoDS.Edge_s(_extrude_topods_shape(obj.wrapped, direction))) return Edge(TopoDS.Edge_s(_extrude_topods_shape(obj.wrapped, direction)))
@ -2538,7 +2420,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
extension_factor: float = 0.1, extension_factor: float = 0.1,
): ):
"""Helper method to slightly extend an edge that is bound to a surface""" """Helper method to slightly extend an edge that is bound to a surface"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't extend empty spline") raise ValueError("Can't extend empty spline")
if self.geom_type != GeomType.BSPLINE: if self.geom_type != GeomType.BSPLINE:
raise TypeError("_extend_spline only works with splines") raise TypeError("_extend_spline only works with splines")
@ -2595,7 +2477,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
Returns: Returns:
ShapeList[Vector]: list of intersection points ShapeList[Vector]: list of intersection points
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find intersections of empty edge") raise ValueError("Can't find intersections of empty edge")
# Convert an Axis into an edge at least as large as self and Axis start point # Convert an Axis into an edge at least as large as self and Axis start point
@ -2723,7 +2605,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
def geom_adaptor(self) -> BRepAdaptor_Curve: def geom_adaptor(self) -> BRepAdaptor_Curve:
"""Return the Geom Curve from this Edge""" """Return the Geom Curve from this Edge"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find adaptor for empty edge") raise ValueError("Can't find adaptor for empty edge")
return BRepAdaptor_Curve(self.wrapped) return BRepAdaptor_Curve(self.wrapped)
@ -2811,7 +2693,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
float: Normalized parameter in [0.0, 1.0] corresponding to the point's float: Normalized parameter in [0.0, 1.0] corresponding to the point's
closest location on the edge. closest location on the edge.
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find param on empty edge") raise ValueError("Can't find param on empty edge")
pnt = Vector(point) pnt = Vector(point)
@ -2945,7 +2827,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
Returns: Returns:
Edge: reversed Edge: reversed
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("An empty edge can't be reversed") raise ValueError("An empty edge can't be reversed")
assert isinstance(self.wrapped, TopoDS_Edge) assert isinstance(self.wrapped, TopoDS_Edge)
@ -2960,7 +2842,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
topods_edge = BRepBuilderAPI_MakeEdge(curve.Reversed(), last, first).Edge() topods_edge = BRepBuilderAPI_MakeEdge(curve.Reversed(), last, first).Edge()
reversed_edge.wrapped = topods_edge reversed_edge.wrapped = topods_edge
else: else:
reversed_edge.wrapped = downcast(self.wrapped.Reversed()) reversed_edge.wrapped = TopoDS.Edge_s(self.wrapped.Reversed())
return reversed_edge return reversed_edge
def to_axis(self) -> Axis: def to_axis(self) -> Axis:
@ -3025,7 +2907,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
# if start_u >= end_u: # if start_u >= end_u:
# raise ValueError(f"start ({start_u}) must be less than end ({end_u})") # raise ValueError(f"start ({start_u}) must be less than end ({end_u})")
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't trim empty edge") raise ValueError("Can't trim empty edge")
self_copy = copy.deepcopy(self) self_copy = copy.deepcopy(self)
@ -3060,7 +2942,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
Returns: Returns:
Edge: trimmed edge Edge: trimmed edge
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't trim empty edge") raise ValueError("Can't trim empty edge")
start_u = Mixin1D._to_param(self, start, "start") start_u = Mixin1D._to_param(self, start, "start")
@ -3089,7 +2971,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
return Edge(new_edge) return Edge(new_edge)
class Wire(Mixin1D, Shape[TopoDS_Wire]): class Wire(Mixin1D[TopoDS_Wire]):
"""A Wire in build123d is a topological entity representing a connected sequence """A Wire in build123d is a topological entity representing a connected sequence
of edges forming a continuous curve or path in 3D space. Wires are essential of edges forming a continuous curve or path in 3D space. Wires are essential
components in modeling complex objects, defining boundaries for surfaces or components in modeling complex objects, defining boundaries for surfaces or
@ -3623,7 +3505,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
Returns: Returns:
Wire: chamfered wire Wire: chamfered wire
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't chamfer empty wire") raise ValueError("Can't chamfer empty wire")
reference_edge = edge reference_edge = edge
@ -3638,7 +3520,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
) )
for v in vertices: for v in vertices:
if v.wrapped is None: if not v:
continue continue
edge_list = vertex_edge_map.FindFromKey(v.wrapped) edge_list = vertex_edge_map.FindFromKey(v.wrapped)
@ -3679,6 +3561,21 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
return return_value return return_value
def edges(self) -> ShapeList[Edge]:
"""edges - all the edges in this Shape"""
# The WireExplorer is a tool to explore the edges of a wire in a connection order.
explorer = BRepTools_WireExplorer(self.wrapped)
edge_list: ShapeList[Edge] = ShapeList()
while explorer.More():
next_edge = Edge(explorer.Current())
next_edge.topo_parent = (
self if self.topo_parent is None else self.topo_parent
)
edge_list.append(next_edge)
explorer.Next()
return edge_list
def fillet_2d(self, radius: float, vertices: Iterable[Vertex]) -> Wire: def fillet_2d(self, radius: float, vertices: Iterable[Vertex]) -> Wire:
"""fillet_2d """fillet_2d
@ -3695,7 +3592,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
Returns: Returns:
Wire: filleted wire Wire: filleted wire
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't fillet an empty wire") raise ValueError("Can't fillet an empty wire")
# Create a face to fillet # Create a face to fillet
@ -3723,7 +3620,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
Returns: Returns:
Wire: fixed wire Wire: fixed wire
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't fix an empty edge") raise ValueError("Can't fix an empty edge")
sf_w = ShapeFix_Wireframe(self.wrapped) sf_w = ShapeFix_Wireframe(self.wrapped)
@ -3735,7 +3632,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
def geom_adaptor(self) -> BRepAdaptor_CompCurve: def geom_adaptor(self) -> BRepAdaptor_CompCurve:
"""Return the Geom Comp Curve for this Wire""" """Return the Geom Comp Curve for this Wire"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't get geom adaptor of empty wire") raise ValueError("Can't get geom adaptor of empty wire")
return BRepAdaptor_CompCurve(self.wrapped) return BRepAdaptor_CompCurve(self.wrapped)
@ -3779,7 +3676,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
float: Normalized parameter in [0.0, 1.0] representing the relative float: Normalized parameter in [0.0, 1.0] representing the relative
position of the projected point along the wire. position of the projected point along the wire.
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find point on empty wire") raise ValueError("Can't find point on empty wire")
point_on_curve = Vector(point) point_on_curve = Vector(point)
@ -3932,7 +3829,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
""" """
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
if self.wrapped is None or target_object.wrapped is None: if self._wrapped is None or not target_object:
raise ValueError("Can't project empty Wires or to empty Shapes") raise ValueError("Can't project empty Wires or to empty Shapes")
if direction is not None and center is None: if direction is not None and center is None:
@ -4021,7 +3918,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
Returns: Returns:
Wire: stitched wires Wire: stitched wires
""" """
if self.wrapped is None or other.wrapped is None: if self._wrapped is None or not other:
raise ValueError("Can't stitch empty wires") raise ValueError("Can't stitch empty wires")
wire_builder = BRepBuilderAPI_MakeWire() wire_builder = BRepBuilderAPI_MakeWire()
@ -4065,7 +3962,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
""" """
# Build a single Geom_BSplineCurve from the wire, in *topological order* # Build a single Geom_BSplineCurve from the wire, in *topological order*
builder = GeomConvert_CompCurveToBSplineCurve() builder = GeomConvert_CompCurveToBSplineCurve()
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't convert an empty wire") raise ValueError("Can't convert an empty wire")
wire_explorer = BRepTools_WireExplorer(self.wrapped) wire_explorer = BRepTools_WireExplorer(self.wrapped)
@ -4217,9 +4114,9 @@ def topo_explore_connected_edges(
parent = parent if parent is not None else edge.topo_parent parent = parent if parent is not None else edge.topo_parent
if parent is None: if parent is None:
raise ValueError("edge has no valid parent") raise ValueError("edge has no valid parent")
given_topods_edge = edge.wrapped if not edge:
if given_topods_edge is None:
raise ValueError("edge is empty") raise ValueError("edge is empty")
given_topods_edge = edge.wrapped
connected_edges = set() connected_edges = set()
# Find all the TopoDS_Edges for this Shape # Find all the TopoDS_Edges for this Shape
@ -4262,11 +4159,11 @@ def topo_explore_connected_faces(
) -> list[TopoDS_Face]: ) -> list[TopoDS_Face]:
"""Given an edge extracted from a Shape, return the topods_faces connected to it""" """Given an edge extracted from a Shape, return the topods_faces connected to it"""
if edge.wrapped is None: if not edge:
raise ValueError("Can't explore from an empty edge") raise ValueError("Can't explore from an empty edge")
parent = parent if parent is not None else edge.topo_parent parent = parent if parent is not None else edge.topo_parent
if parent is None or parent.wrapped is None: if not parent:
raise ValueError("edge has no valid parent") raise ValueError("edge has no valid parent")
# make a edge --> faces mapping # make a edge --> faces mapping

View file

@ -100,7 +100,8 @@ from OCP.BRepFeat import BRepFeat_SplitShape
from OCP.BRepGProp import BRepGProp, BRepGProp_Face from OCP.BRepGProp import BRepGProp, BRepGProp_Face
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
from OCP.BRepMesh import BRepMesh_IncrementalMesh from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.BRepTools import BRepTools from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
from OCP.gce import gce_MakeLin from OCP.gce import gce_MakeLin
from OCP.Geom import Geom_Line from OCP.Geom import Geom_Line
from OCP.GeomAPI import GeomAPI_ProjectPointOnSurf from OCP.GeomAPI import GeomAPI_ProjectPointOnSurf
@ -162,6 +163,7 @@ if TYPE_CHECKING: # pragma: no cover
Shapes = Literal["Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "Compound"] Shapes = Literal["Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "Compound"]
TrimmingTool = Union[Plane, "Shell", "Face"] TrimmingTool = Union[Plane, "Shell", "Face"]
TOPODS = TypeVar("TOPODS", bound=TopoDS_Shape) TOPODS = TypeVar("TOPODS", bound=TopoDS_Shape)
CalcFn = Callable[[TopoDS_Shape, GProp_GProps], None]
class Shape(NodeMixin, Generic[TOPODS]): class Shape(NodeMixin, Generic[TOPODS]):
@ -196,7 +198,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
ta.TopAbs_COMPSOLID: "CompSolid", ta.TopAbs_COMPSOLID: "CompSolid",
} }
shape_properties_LUT = { shape_properties_LUT: dict[TopAbs_ShapeEnum, CalcFn | None] = {
ta.TopAbs_VERTEX: None, ta.TopAbs_VERTEX: None,
ta.TopAbs_EDGE: BRepGProp.LinearProperties_s, ta.TopAbs_EDGE: BRepGProp.LinearProperties_s,
ta.TopAbs_WIRE: BRepGProp.LinearProperties_s, ta.TopAbs_WIRE: BRepGProp.LinearProperties_s,
@ -287,7 +289,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
color: ColorLike | None = None, color: ColorLike | None = None,
parent: Compound | None = None, parent: Compound | None = None,
): ):
self.wrapped: TOPODS | None = ( self._wrapped: TOPODS | None = (
tcast(Optional[TOPODS], downcast(obj)) if obj is not None else None tcast(Optional[TOPODS], downcast(obj)) if obj is not None else None
) )
self.for_construction = False self.for_construction = False
@ -304,6 +306,18 @@ class Shape(NodeMixin, Generic[TOPODS]):
# pylint: disable=too-many-instance-attributes, too-many-public-methods # pylint: disable=too-many-instance-attributes, too-many-public-methods
@property
def wrapped(self):
assert self._wrapped
return self._wrapped
@wrapped.setter
def wrapped(self, shape: TOPODS):
self._wrapped = shape
def __bool__(self):
return self._wrapped is not None
@property @property
@abstractmethod @abstractmethod
def _dim(self) -> int | None: def _dim(self) -> int | None:
@ -312,7 +326,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
@property @property
def area(self) -> float: def area(self) -> float:
"""area -the surface area of all faces in this Shape""" """area -the surface area of all faces in this Shape"""
if self.wrapped is None: if self._wrapped is None:
return 0.0 return 0.0
properties = GProp_GProps() properties = GProp_GProps()
BRepGProp.SurfaceProperties_s(self.wrapped, properties) BRepGProp.SurfaceProperties_s(self.wrapped, properties)
@ -351,7 +365,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
GeomType: The geometry type of the shape GeomType: The geometry type of the shape
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot determine geometry type of an empty shape") raise ValueError("Cannot determine geometry type of an empty shape")
shape: TopAbs_ShapeEnum = shapetype(self.wrapped) shape: TopAbs_ShapeEnum = shapetype(self.wrapped)
@ -380,7 +394,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
bool: is the shape manifold or water tight bool: is the shape manifold or water tight
""" """
# Extract one or more (if a Compound) shape from self # Extract one or more (if a Compound) shape from self
if self.wrapped is None: if self._wrapped is None:
return False return False
shape_stack = get_top_level_topods_shapes(self.wrapped) shape_stack = get_top_level_topods_shapes(self.wrapped)
@ -431,12 +445,12 @@ class Shape(NodeMixin, Generic[TOPODS]):
underlying shape with the potential to be given a location and an underlying shape with the potential to be given a location and an
orientation. orientation.
""" """
return self.wrapped is None or self.wrapped.IsNull() return self._wrapped is None or self.wrapped.IsNull()
@property @property
def is_planar_face(self) -> bool: def is_planar_face(self) -> bool:
"""Is the shape a planar face even though its geom_type may not be PLANE""" """Is the shape a planar face even though its geom_type may not be PLANE"""
if self.wrapped is None or not isinstance(self.wrapped, TopoDS_Face): if self._wrapped is None or not isinstance(self.wrapped, TopoDS_Face):
return False return False
surface = BRep_Tool.Surface_s(self.wrapped) surface = BRep_Tool.Surface_s(self.wrapped)
is_face_planar = GeomLib_IsPlanarSurface(surface, TOLERANCE) is_face_planar = GeomLib_IsPlanarSurface(surface, TOLERANCE)
@ -448,7 +462,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full
description of what is checked. description of what is checked.
""" """
if self.wrapped is None: if self._wrapped is None:
return True return True
chk = BRepCheck_Analyzer(self.wrapped) chk = BRepCheck_Analyzer(self.wrapped)
chk.SetParallel(True) chk.SetParallel(True)
@ -474,7 +488,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
@property @property
def location(self) -> Location: def location(self) -> Location:
"""Get this Shape's Location""" """Get this Shape's Location"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find the location of an empty shape") raise ValueError("Can't find the location of an empty shape")
return Location(self.wrapped.Location()) return Location(self.wrapped.Location())
@ -518,7 +532,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
- It is commonly used in structural analysis, mechanical simulations, - It is commonly used in structural analysis, mechanical simulations,
and physics-based motion calculations. and physics-based motion calculations.
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't calculate matrix for empty shape") raise ValueError("Can't calculate matrix for empty shape")
properties = GProp_GProps() properties = GProp_GProps()
BRepGProp.VolumeProperties_s(self.wrapped, properties) BRepGProp.VolumeProperties_s(self.wrapped, properties)
@ -546,7 +560,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
@property @property
def position(self) -> Vector: def position(self) -> Vector:
"""Get the position component of this Shape's Location""" """Get the position component of this Shape's Location"""
if self.wrapped is None or self.location is None: if self._wrapped is None or self.location is None:
raise ValueError("Can't find the position of an empty shape") raise ValueError("Can't find the position of an empty shape")
return self.location.position return self.location.position
@ -575,7 +589,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
(Vector(0, 1, 0), 1000.0), (Vector(0, 1, 0), 1000.0),
(Vector(0, 0, 1), 300.0)] (Vector(0, 0, 1), 300.0)]
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't calculate properties for empty shape") raise ValueError("Can't calculate properties for empty shape")
properties = GProp_GProps() properties = GProp_GProps()
@ -615,7 +629,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
(150.0, 200.0, 50.0) (150.0, 200.0, 50.0)
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't calculate moments for empty shape") raise ValueError("Can't calculate moments for empty shape")
properties = GProp_GProps() properties = GProp_GProps()
@ -785,13 +799,13 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if obj.wrapped is None: if not obj:
return 0.0 return 0.0
properties = GProp_GProps() properties = GProp_GProps()
calc_function = Shape.shape_properties_LUT[shapetype(obj.wrapped)] calc_function = Shape.shape_properties_LUT[shapetype(obj.wrapped)]
if not calc_function: if calc_function is None:
raise NotImplementedError raise NotImplementedError
calc_function(obj.wrapped, properties) calc_function(obj.wrapped, properties)
@ -805,7 +819,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
], ],
) -> ShapeList: ) -> ShapeList:
"""Helper to extract entities of a specific type from a shape.""" """Helper to extract entities of a specific type from a shape."""
if shape.wrapped is None: if not shape:
return ShapeList() return ShapeList()
shape_list = ShapeList( shape_list = ShapeList(
[shape.__class__.cast(i) for i in shape.entities(entity_type)] [shape.__class__.cast(i) for i in shape.entities(entity_type)]
@ -825,7 +839,9 @@ class Shape(NodeMixin, Generic[TOPODS]):
with a warning if count != 1.""" with a warning if count != 1."""
shape_list = Shape.get_shape_list(shape, entity_type) shape_list = Shape.get_shape_list(shape, entity_type)
entity_count = len(shape_list) entity_count = len(shape_list)
if entity_count != 1: if entity_count == 0:
return None
elif entity_count > 1:
warnings.warn( warnings.warn(
f"Found {entity_count} {entity_type.lower()}s, returning first", f"Found {entity_count} {entity_type.lower()}s, returning first",
stacklevel=3, stacklevel=3,
@ -859,7 +875,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
if not all(summand._dim == addend_dim for summand in summands): if not all(summand._dim == addend_dim for summand in summands):
raise ValueError("Only shapes with the same dimension can be added") raise ValueError("Only shapes with the same dimension can be added")
if self.wrapped is None: # an empty object if self._wrapped is None: # an empty object
if len(summands) == 1: if len(summands) == 1:
sum_shape = summands[0] sum_shape = summands[0]
else: else:
@ -876,7 +892,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
"""intersect shape with self operator &""" """intersect shape with self operator &"""
others = other if isinstance(other, (list, tuple)) else [other] others = other if isinstance(other, (list, tuple)) else [other]
if self.wrapped is None or (isinstance(other, Shape) and other.wrapped is None): if not self or (isinstance(other, Shape) and not other):
raise ValueError("Cannot intersect shape with empty compound") raise ValueError("Cannot intersect shape with empty compound")
new_shape = self.intersect(*others) new_shape = self.intersect(*others)
@ -948,7 +964,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def __hash__(self) -> int: def __hash__(self) -> int:
"""Return hash code""" """Return hash code"""
if self.wrapped is None: if self._wrapped is None:
return 0 return 0
return hash(self.wrapped) return hash(self.wrapped)
@ -966,7 +982,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def __sub__(self, other: None | Shape | Iterable[Shape]) -> Self | ShapeList[Self]: def __sub__(self, other: None | Shape | Iterable[Shape]) -> Self | ShapeList[Self]:
"""cut shape from self operator -""" """cut shape from self operator -"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot subtract shape from empty compound") raise ValueError("Cannot subtract shape from empty compound")
# Convert `other` to list of base objects and filter out None values # Convert `other` to list of base objects and filter out None values
@ -1014,7 +1030,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
BoundBox: A box sized to contain this Shape BoundBox: A box sized to contain this Shape
""" """
if self.wrapped is None: if self._wrapped is None:
return BoundBox(Bnd_Box()) return BoundBox(Bnd_Box())
tolerance = TOLERANCE if tolerance is None else tolerance tolerance = TOLERANCE if tolerance is None else tolerance
return BoundBox.from_topo_ds(self.wrapped, tolerance=tolerance, optimal=optimal) return BoundBox.from_topo_ds(self.wrapped, tolerance=tolerance, optimal=optimal)
@ -1033,7 +1049,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Shape: Original object with extraneous internal edges removed Shape: Original object with extraneous internal edges removed
""" """
if self.wrapped is None: if self._wrapped is None:
return self return self
upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True) upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True)
upgrader.AllowInternalEdges(False) upgrader.AllowInternalEdges(False)
@ -1112,7 +1128,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None or other.wrapped is None: if self._wrapped is None or not other:
raise ValueError("Cannot calculate distance to or from an empty shape") raise ValueError("Cannot calculate distance to or from an empty shape")
return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value() return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value()
@ -1125,7 +1141,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
self, other: Shape | VectorLike self, other: Shape | VectorLike
) -> tuple[float, Vector, Vector]: ) -> tuple[float, Vector, Vector]:
"""Minimal distance between two shapes and the points on each shape""" """Minimal distance between two shapes and the points on each shape"""
if self.wrapped is None or (isinstance(other, Shape) and other.wrapped is None): if self._wrapped is None or (isinstance(other, Shape) and not other):
raise ValueError("Cannot calculate distance to or from an empty shape") raise ValueError("Cannot calculate distance to or from an empty shape")
if isinstance(other, Shape): if isinstance(other, Shape):
@ -1155,14 +1171,14 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot calculate distance to or from an empty shape") raise ValueError("Cannot calculate distance to or from an empty shape")
dist_calc = BRepExtrema_DistShapeShape() dist_calc = BRepExtrema_DistShapeShape()
dist_calc.LoadS1(self.wrapped) dist_calc.LoadS1(self.wrapped)
for other_shape in others: for other_shape in others:
if other_shape.wrapped is None: if not other_shape:
raise ValueError("Cannot calculate distance to or from an empty shape") raise ValueError("Cannot calculate distance to or from an empty shape")
dist_calc.LoadS2(other_shape.wrapped) dist_calc.LoadS2(other_shape.wrapped)
dist_calc.Perform() dist_calc.Perform()
@ -1171,27 +1187,28 @@ class Shape(NodeMixin, Generic[TOPODS]):
def edge(self) -> Edge | None: def edge(self) -> Edge | None:
"""Return the Edge""" """Return the Edge"""
return None return Shape.get_single_shape(self, "Edge")
# Note all sub-classes have vertices and vertex methods
def edges(self) -> ShapeList[Edge]: def edges(self) -> ShapeList[Edge]:
"""edges - all the edges in this Shape - subclasses may override""" """edges - all the edges in this Shape - subclasses may override"""
return ShapeList() edge_list = Shape.get_shape_list(self, "Edge")
return edge_list.filter_by(
lambda e: BRep_Tool.Degenerated_s(e.wrapped), reverse=True
)
def entities(self, topo_type: Shapes) -> list[TopoDS_Shape]: def entities(self, topo_type: Shapes) -> list[TopoDS_Shape]:
"""Return all of the TopoDS sub entities of the given type""" """Return all of the TopoDS sub entities of the given type"""
if self.wrapped is None: if self._wrapped is None:
return [] return []
return _topods_entities(self.wrapped, topo_type) return _topods_entities(self.wrapped, topo_type)
def face(self) -> Face | None: def face(self) -> Face | None:
"""Return the Face""" """Return the Face"""
return None return Shape.get_single_shape(self, "Face")
def faces(self) -> ShapeList[Face]: def faces(self) -> ShapeList[Face]:
"""faces - all the faces in this Shape""" """faces - all the faces in this Shape"""
return ShapeList() return Shape.get_shape_list(self, "Face")
def faces_intersected_by_axis( def faces_intersected_by_axis(
self, self,
@ -1209,7 +1226,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
list[Face]: A list of intersected faces sorted by distance from axis.position list[Face]: A list of intersected faces sorted by distance from axis.position
""" """
if self.wrapped is None: if self._wrapped is None:
return ShapeList() return ShapeList()
line = gce_MakeLin(axis.wrapped).Value() line = gce_MakeLin(axis.wrapped).Value()
@ -1239,7 +1256,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def fix(self) -> Self: def fix(self) -> Self:
"""fix - try to fix shape if not valid""" """fix - try to fix shape if not valid"""
if self.wrapped is None: if self._wrapped is None:
return self return self
if not self.is_valid: if not self.is_valid:
shape_copy: Shape = copy.deepcopy(self, None) shape_copy: Shape = copy.deepcopy(self, None)
@ -1281,7 +1298,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
# self, child_type: Shapes, parent_type: Shapes # self, child_type: Shapes, parent_type: Shapes
# ) -> Dict[Shape, list[Shape]]: # ) -> Dict[Shape, list[Shape]]:
# """This function is very slow on M1 macs and is currently unused""" # """This function is very slow on M1 macs and is currently unused"""
# if self.wrapped is None: # if self._wrapped is None:
# return {} # return {}
# res = TopTools_IndexedDataMapOfShapeListOfShape() # res = TopTools_IndexedDataMapOfShapeListOfShape()
@ -1319,7 +1336,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
(e.g., edges, vertices) and other compounds, the method returns a list (e.g., edges, vertices) and other compounds, the method returns a list
of only the simple shapes directly contained at the top level. of only the simple shapes directly contained at the top level.
""" """
if self.wrapped is None: if self._wrapped is None:
return ShapeList() return ShapeList()
return ShapeList( return ShapeList(
self.__class__.cast(s) for s in get_top_level_topods_shapes(self.wrapped) self.__class__.cast(s) for s in get_top_level_topods_shapes(self.wrapped)
@ -1327,7 +1344,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def intersect( def intersect(
self, *to_intersect: Shape | Vector | Location | Axis | Plane self, *to_intersect: Shape | Vector | Location | Axis | Plane
) -> None | Self | ShapeList[Self]: ) -> None | ShapeList[Self]:
"""Intersection of the arguments and this shape """Intersection of the arguments and this shape
Args: Args:
@ -1335,8 +1352,8 @@ class Shape(NodeMixin, Generic[TOPODS]):
intersect with intersect with
Returns: Returns:
Self | ShapeList[Self]: Resulting object may be of a different class than self None | ShapeList[Self]: Resulting ShapeList may contain different class
or a ShapeList if multiple non-Compound object created than self
""" """
def _to_vertex(vec: Vector) -> Vertex: def _to_vertex(vec: Vector) -> Vertex:
@ -1380,15 +1397,12 @@ class Shape(NodeMixin, Generic[TOPODS]):
# Find the shape intersections # Find the shape intersections
intersect_op = BRepAlgoAPI_Common() intersect_op = BRepAlgoAPI_Common()
shape_intersections = self._bool_op((self,), objs, intersect_op) intersections = self._bool_op((self,), objs, intersect_op)
if isinstance(shape_intersections, ShapeList) and not shape_intersections: if isinstance(intersections, ShapeList):
return None return intersections or None
if ( if isinstance(intersections, Shape) and not intersections.is_null:
not isinstance(shape_intersections, ShapeList) return ShapeList([intersections])
and shape_intersections.is_null return None
):
return None
return shape_intersections
def is_equal(self, other: Shape) -> bool: def is_equal(self, other: Shape) -> bool:
"""Returns True if two shapes are equal, i.e. if they share the same """Returns True if two shapes are equal, i.e. if they share the same
@ -1401,7 +1415,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None or other.wrapped is None: if self._wrapped is None or not other:
return False return False
return self.wrapped.IsEqual(other.wrapped) return self.wrapped.IsEqual(other.wrapped)
@ -1416,7 +1430,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None or other.wrapped is None: if self._wrapped is None or not other:
return False return False
return self.wrapped.IsSame(other.wrapped) return self.wrapped.IsSame(other.wrapped)
@ -1429,7 +1443,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot locate an empty shape") raise ValueError("Cannot locate an empty shape")
if loc.wrapped is None: if loc.wrapped is None:
raise ValueError("Cannot locate a shape at an empty location") raise ValueError("Cannot locate a shape at an empty location")
@ -1448,7 +1462,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Shape: copy of Shape at location Shape: copy of Shape at location
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot locate an empty shape") raise ValueError("Cannot locate an empty shape")
if loc.wrapped is None: if loc.wrapped is None:
raise ValueError("Cannot locate a shape at an empty location") raise ValueError("Cannot locate a shape at an empty location")
@ -1466,7 +1480,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot mesh an empty shape") raise ValueError("Cannot mesh an empty shape")
if not BRepTools.Triangulation_s(self.wrapped, tolerance): if not BRepTools.Triangulation_s(self.wrapped, tolerance):
@ -1487,7 +1501,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
if not mirror_plane: if not mirror_plane:
mirror_plane = Plane.XY mirror_plane = Plane.XY
if self.wrapped is None: if self._wrapped is None:
return self return self
transformation = gp_Trsf() transformation = gp_Trsf()
transformation.SetMirror( transformation.SetMirror(
@ -1505,7 +1519,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot move an empty shape") raise ValueError("Cannot move an empty shape")
if loc.wrapped is None: if loc.wrapped is None:
raise ValueError("Cannot move a shape at an empty location") raise ValueError("Cannot move a shape at an empty location")
@ -1525,7 +1539,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Shape: copy of Shape moved to relative location Shape: copy of Shape moved to relative location
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot move an empty shape") raise ValueError("Cannot move an empty shape")
if loc.wrapped is None: if loc.wrapped is None:
raise ValueError("Cannot move a shape at an empty location") raise ValueError("Cannot move a shape at an empty location")
@ -1539,7 +1553,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
OrientedBoundBox: A box oriented and sized to contain this Shape OrientedBoundBox: A box oriented and sized to contain this Shape
""" """
if self.wrapped is None: if self._wrapped is None:
return OrientedBoundBox(Bnd_OBB()) return OrientedBoundBox(Bnd_OBB())
return OrientedBoundBox(self) return OrientedBoundBox(self)
@ -1641,7 +1655,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
- The radius of gyration is computed based on the shapes mass properties. - The radius of gyration is computed based on the shapes mass properties.
- It is useful for evaluating structural stability and rotational behavior. - It is useful for evaluating structural stability and rotational behavior.
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't calculate radius of gyration for empty shape") raise ValueError("Can't calculate radius of gyration for empty shape")
properties = GProp_GProps() properties = GProp_GProps()
@ -1660,7 +1674,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
DeprecationWarning, DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot relocate an empty shape") raise ValueError("Cannot relocate an empty shape")
if loc.wrapped is None: if loc.wrapped is None:
raise ValueError("Cannot relocate a shape at an empty location") raise ValueError("Cannot relocate a shape at an empty location")
@ -1713,11 +1727,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
def shell(self) -> Shell | None: def shell(self) -> Shell | None:
"""Return the Shell""" """Return the Shell"""
return None return Shape.get_single_shape(self, "Shell")
def shells(self) -> ShapeList[Shell]: def shells(self) -> ShapeList[Shell]:
"""shells - all the shells in this Shape""" """shells - all the shells in this Shape"""
return ShapeList() return Shape.get_shape_list(self, "Shell")
def show_topology( def show_topology(
self, self,
@ -1771,11 +1785,157 @@ class Shape(NodeMixin, Generic[TOPODS]):
def solid(self) -> Solid | None: def solid(self) -> Solid | None:
"""Return the Solid""" """Return the Solid"""
return None return Shape.get_single_shape(self, "Solid")
def solids(self) -> ShapeList[Solid]: def solids(self) -> ShapeList[Solid]:
"""solids - all the solids in this Shape""" """solids - all the solids in this Shape"""
return ShapeList() return Shape.get_shape_list(self, "Solid")
@overload
def split(
self, tool: TrimmingTool, keep: Literal[Keep.TOP, Keep.BOTTOM]
) -> Self | list[Self] | None:
"""split and keep inside or outside"""
@overload
def split(self, tool: TrimmingTool, keep: Literal[Keep.ALL]) -> list[Self]:
"""split and return the unordered pieces"""
@overload
def split(self, tool: TrimmingTool, keep: Literal[Keep.BOTH]) -> tuple[
Self | list[Self] | None,
Self | list[Self] | None,
]:
"""split and keep inside and outside"""
@overload
def split(
self, tool: TrimmingTool, keep: Literal[Keep.INSIDE, Keep.OUTSIDE]
) -> None:
"""invalid split"""
@overload
def split(self, tool: TrimmingTool) -> Self | list[Self] | None:
"""split and keep inside (default)"""
def split(self, tool: TrimmingTool, keep: Keep = Keep.TOP):
"""split
Split this shape by the provided plane or face.
Args:
surface (Plane | Face): surface to segment shape
keep (Keep, optional): which object(s) to save. Defaults to Keep.TOP.
Returns:
Shape: result of split
Returns:
Self | list[Self] | None,
Tuple[Self | list[Self] | None]: The result of the split operation.
- **Keep.TOP**: Returns the top as a `Self` or `list[Self]`, or `None`
if no top is found.
- **Keep.BOTTOM**: Returns the bottom as a `Self` or `list[Self]`, or `None`
if no bottom is found.
- **Keep.BOTH**: Returns a tuple `(inside, outside)` where each element is
either a `Self` or `list[Self]`, or `None` if no corresponding part is found.
"""
if self._wrapped is None or not tool:
raise ValueError("Can't split an empty edge/wire/tool")
if keep in [Keep.INSIDE, Keep.OUTSIDE]:
raise ValueError(f"{keep} is invalid")
shape_list = TopTools_ListOfShape()
shape_list.Append(self.wrapped)
# Define the splitting tool
trim_tool = (
BRepBuilderAPI_MakeFace(tool.wrapped).Face() # gp_Pln to Face
if isinstance(tool, Plane)
else tool.wrapped
)
tool_list = TopTools_ListOfShape()
tool_list.Append(trim_tool)
# Create the splitter algorithm
splitter = BRepAlgoAPI_Splitter()
# Set the shape to be split and the splitting tool (plane face)
splitter.SetArguments(shape_list)
splitter.SetTools(tool_list)
# Perform the splitting operation
splitter.Build()
split_result = downcast(splitter.Shape())
# Remove unnecessary TopoDS_Compound around single shape
if isinstance(split_result, TopoDS_Compound):
split_result = unwrap_topods_compound(split_result, True)
# For speed the user may just want all the objects which they
# can sort more efficiently then the generic algorithm below
if keep == Keep.ALL:
return ShapeList(
self.__class__.cast(part)
for part in get_top_level_topods_shapes(split_result)
)
if not isinstance(tool, Plane):
# Get a TopoDS_Face to work with from the tool
if isinstance(trim_tool, TopoDS_Shell):
face_explorer = TopExp_Explorer(trim_tool, ta.TopAbs_FACE)
tool_face = TopoDS.Face_s(face_explorer.Current())
else:
tool_face = trim_tool
# Create a reference point off the +ve side of the tool
surface_gppnt = gp_Pnt()
surface_normal = gp_Vec()
u_min, u_max, v_min, v_max = BRepTools.UVBounds_s(tool_face)
BRepGProp_Face(tool_face).Normal(
(u_min + u_max) / 2, (v_min + v_max) / 2, surface_gppnt, surface_normal
)
normalized_surface_normal = Vector(
surface_normal.X(), surface_normal.Y(), surface_normal.Z()
).normalized()
surface_point = Vector(surface_gppnt)
ref_point = surface_point + normalized_surface_normal
# Create a HalfSpace - Solidish object to determine top/bottom
# Note: BRepPrimAPI_MakeHalfSpace takes either a TopoDS_Shell or TopoDS_Face but the
# mypy expects only a TopoDS_Shell here
half_space_maker = BRepPrimAPI_MakeHalfSpace(trim_tool, ref_point.to_pnt())
# type: ignore
tool_solid = half_space_maker.Solid()
tops: list[Shape] = []
bottoms: list[Shape] = []
properties = GProp_GProps()
for part in get_top_level_topods_shapes(split_result):
sub_shape = self.__class__.cast(part)
if isinstance(tool, Plane):
is_up = tool.to_local_coords(sub_shape).center().Z >= 0
else:
# Intersect self and the thickened tool
is_up_obj = _topods_bool_op(
(part,), (tool_solid,), BRepAlgoAPI_Common()
)
# Check for valid intersections
BRepGProp.LinearProperties_s(is_up_obj, properties)
# Mass represents the total length for linear properties
is_up = properties.Mass() >= TOLERANCE
(tops if is_up else bottoms).append(sub_shape)
top = None if not tops else tops[0] if len(tops) == 1 else tops
bottom = None if not bottoms else bottoms[0] if len(bottoms) == 1 else bottoms
if keep == Keep.BOTH:
return (top, bottom)
if keep == Keep.TOP:
return top
if keep == Keep.BOTTOM:
return bottom
@overload @overload
def split_by_perimeter( def split_by_perimeter(
@ -1855,7 +2015,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
"keep must be one of Keep.INSIDE, Keep.OUTSIDE, or Keep.BOTH" "keep must be one of Keep.INSIDE, Keep.OUTSIDE, or Keep.BOTH"
) )
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot split an empty shape") raise ValueError("Cannot split an empty shape")
# Process the perimeter # Process the perimeter
@ -1863,7 +2023,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
raise ValueError("perimeter must be a closed Wire or Edge") raise ValueError("perimeter must be a closed Wire or Edge")
perimeter_edges = TopTools_SequenceOfShape() perimeter_edges = TopTools_SequenceOfShape()
for perimeter_edge in perimeter.edges(): for perimeter_edge in perimeter.edges():
if perimeter_edge.wrapped is None: if not perimeter_edge:
continue continue
perimeter_edges.Append(perimeter_edge.wrapped) perimeter_edges.Append(perimeter_edge.wrapped)
@ -1871,7 +2031,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
lefts: list[Shell] = [] lefts: list[Shell] = []
rights: list[Shell] = [] rights: list[Shell] = []
for target_shell in self.shells(): for target_shell in self.shells():
if target_shell.wrapped is None: if not target_shell:
continue continue
constructor = BRepFeat_SplitShape(target_shell.wrapped) constructor = BRepFeat_SplitShape(target_shell.wrapped)
constructor.Add(perimeter_edges) constructor.Add(perimeter_edges)
@ -1900,7 +2060,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
self, tolerance: float, angular_tolerance: float = 0.1 self, tolerance: float, angular_tolerance: float = 0.1
) -> tuple[list[Vector], list[tuple[int, int, int]]]: ) -> tuple[list[Vector], list[tuple[int, int, int]]]:
"""General triangulated approximation""" """General triangulated approximation"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot tessellate an empty shape") raise ValueError("Cannot tessellate an empty shape")
self.mesh(tolerance, angular_tolerance) self.mesh(tolerance, angular_tolerance)
@ -1962,7 +2122,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Self: Approximated shape Self: Approximated shape
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot approximate an empty shape") raise ValueError("Cannot approximate an empty shape")
params = ShapeCustom_RestrictionParameters() params = ShapeCustom_RestrictionParameters()
@ -1999,7 +2159,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Shape: a copy of the object, but with geometry transformed Shape: a copy of the object, but with geometry transformed
""" """
if self.wrapped is None: if self._wrapped is None:
return self return self
new_shape = copy.deepcopy(self, None) new_shape = copy.deepcopy(self, None)
transformed = downcast( transformed = downcast(
@ -2022,7 +2182,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Shape: copy of transformed shape with all objects keeping their type Shape: copy of transformed shape with all objects keeping their type
""" """
if self.wrapped is None: if self._wrapped is None:
return self return self
new_shape = copy.deepcopy(self, None) new_shape = copy.deepcopy(self, None)
transformed = downcast( transformed = downcast(
@ -2078,11 +2238,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
def wire(self) -> Wire | None: def wire(self) -> Wire | None:
"""Return the Wire""" """Return the Wire"""
return None return Shape.get_single_shape(self, "Wire")
def wires(self) -> ShapeList[Wire]: def wires(self) -> ShapeList[Wire]:
"""wires - all the wires in this Shape""" """wires - all the wires in this Shape"""
return ShapeList() return Shape.get_shape_list(self, "Wire")
def _apply_transform(self, transformation: gp_Trsf) -> Self: def _apply_transform(self, transformation: gp_Trsf) -> Self:
"""Private Apply Transform """Private Apply Transform
@ -2095,7 +2255,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Shape: copy of transformed Shape Shape: copy of transformed Shape
""" """
if self.wrapped is None: if self._wrapped is None:
return self return self
shape_copy: Shape = copy.deepcopy(self, None) shape_copy: Shape = copy.deepcopy(self, None)
transformed_shape = BRepBuilderAPI_Transform( transformed_shape = BRepBuilderAPI_Transform(
@ -2126,7 +2286,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
args = list(args) args = list(args)
tools = list(tools) tools = list(tools)
# Find the highest order class from all the inputs Solid > Vertex # Find the highest order class from all the inputs Solid > Vertex
order_dict = {type(s): type(s).order for s in [self] + args + tools} order_dict = {
type(s): type(s).order
for s in [self] + args + tools
if hasattr(type(s), "order")
}
highest_order = sorted(order_dict.items(), key=lambda item: item[1])[-1] highest_order = sorted(order_dict.items(), key=lambda item: item[1])[-1]
# The base of the operation # The base of the operation
@ -2200,7 +2364,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
tuple[ShapeList[Vertex], ShapeList[Edge]]: section results tuple[ShapeList[Vertex], ShapeList[Edge]]: section results
""" """
if self.wrapped is None or other.wrapped is None: if self._wrapped is None or not other:
return (ShapeList(), ShapeList()) return (ShapeList(), ShapeList())
section = BRepAlgoAPI_Section(self.wrapped, other.wrapped) section = BRepAlgoAPI_Section(self.wrapped, other.wrapped)
@ -2234,6 +2398,14 @@ class Shape(NodeMixin, Generic[TOPODS]):
return shape_to_html(self)._repr_html_() return shape_to_html(self)._repr_html_()
def vertex(self) -> Vertex | None:
"""Return the Vertex"""
return Shape.get_single_shape(self, "Vertex")
def vertices(self) -> ShapeList[Vertex]:
"""vertices - all the vertices in this Shape"""
return Shape.get_shape_list(self, "Vertex")
class Comparable(ABC): class Comparable(ABC):
"""Abstract base class that requires comparison methods""" """Abstract base class that requires comparison methods"""
@ -2701,15 +2873,16 @@ class ShapeList(list[T]):
tol_digits, tol_digits,
) )
elif hasattr(group_by, "wrapped"): elif not group_by:
if group_by.wrapped is None: raise ValueError("Cannot group by an empty object")
raise ValueError("Cannot group by an empty object")
if isinstance(group_by.wrapped, (TopoDS_Edge, TopoDS_Wire)): elif hasattr(group_by, "wrapped") and isinstance(
group_by.wrapped, (TopoDS_Edge, TopoDS_Wire)
):
def key_f(obj): def key_f(obj):
pnt1, _pnt2 = group_by.closest_points(obj.center()) pnt1, _pnt2 = group_by.closest_points(obj.center())
return round(group_by.param_at_point(pnt1), tol_digits) return round(group_by.param_at_point(pnt1), tol_digits)
elif isinstance(group_by, SortBy): elif isinstance(group_by, SortBy):
if group_by == SortBy.LENGTH: if group_by == SortBy.LENGTH:
@ -2815,22 +2988,22 @@ class ShapeList(list[T]):
).position.Z, ).position.Z,
reverse=reverse, reverse=reverse,
) )
elif hasattr(sort_by, "wrapped"): elif not sort_by:
if sort_by.wrapped is None: raise ValueError("Cannot sort by an empty object")
raise ValueError("Cannot sort by an empty object") elif hasattr(sort_by, "wrapped") and isinstance(
sort_by.wrapped, (TopoDS_Edge, TopoDS_Wire)
):
if isinstance(sort_by.wrapped, (TopoDS_Edge, TopoDS_Wire)): def u_of_closest_center(obj) -> float:
"""u-value of closest point between object center and sort_by"""
assert not isinstance(sort_by, SortBy)
pnt1, _pnt2 = sort_by.closest_points(obj.center())
return sort_by.param_at_point(pnt1)
def u_of_closest_center(obj) -> float: # pylint: disable=unnecessary-lambda
"""u-value of closest point between object center and sort_by""" objects = sorted(
assert not isinstance(sort_by, SortBy) self, key=lambda o: u_of_closest_center(o), reverse=reverse
pnt1, _pnt2 = sort_by.closest_points(obj.center()) )
return sort_by.param_at_point(pnt1)
# pylint: disable=unnecessary-lambda
objects = sorted(
self, key=lambda o: u_of_closest_center(o), reverse=reverse
)
elif isinstance(sort_by, SortBy): elif isinstance(sort_by, SortBy):
if sort_by == SortBy.LENGTH: if sort_by == SortBy.LENGTH:
@ -2997,6 +3170,45 @@ def _sew_topods_faces(faces: Iterable[TopoDS_Face]) -> TopoDS_Shape:
return downcast(shell_builder.SewedShape()) return downcast(shell_builder.SewedShape())
def _topods_bool_op(
args: Iterable[TopoDS_Shape],
tools: Iterable[TopoDS_Shape],
operation: BRepAlgoAPI_BooleanOperation | BRepAlgoAPI_Splitter,
) -> TopoDS_Shape:
"""Generic boolean operation for TopoDS_Shapes
Args:
args: Iterable[TopoDS_Shape]:
tools: Iterable[TopoDS_Shape]:
operation: BRepAlgoAPI_BooleanOperation | BRepAlgoAPI_Splitter:
Returns: TopoDS_Shape
"""
args = list(args)
tools = list(tools)
arg = TopTools_ListOfShape()
for obj in args:
arg.Append(obj)
tool = TopTools_ListOfShape()
for obj in tools:
tool.Append(obj)
operation.SetArguments(arg)
operation.SetTools(tool)
operation.SetRunParallel(True)
operation.Build()
result = downcast(operation.Shape())
# Remove unnecessary TopoDS_Compound around single shape
if isinstance(result, TopoDS_Compound):
result = unwrap_topods_compound(result, True)
return result
def _topods_entities(shape: TopoDS_Shape, topo_type: Shapes) -> list[TopoDS_Shape]: def _topods_entities(shape: TopoDS_Shape, topo_type: Shapes) -> list[TopoDS_Shape]:
"""Return the TopoDS_Shapes of topo_type from this TopoDS_Shape""" """Return the TopoDS_Shapes of topo_type from this TopoDS_Shape"""
out = {} # using dict to prevent duplicates out = {} # using dict to prevent duplicates

View file

@ -54,15 +54,13 @@ license:
from __future__ import annotations from __future__ import annotations
import platform from collections.abc import Iterable, Sequence
import warnings
from math import radians, cos, tan from math import radians, cos, tan
from typing import Union, TYPE_CHECKING from typing import TYPE_CHECKING, Literal
from typing_extensions import Self
from collections.abc import Iterable
import OCP.TopAbs as ta import OCP.TopAbs as ta
from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Cut, BRepAlgoAPI_Section
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeSolid from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeSolid
from OCP.BRepClass3d import BRepClass3d_SolidClassifier from OCP.BRepClass3d import BRepClass3d_SolidClassifier
from OCP.BRepFeat import BRepFeat_MakeDPrism from OCP.BRepFeat import BRepFeat_MakeDPrism
@ -86,15 +84,23 @@ from OCP.GProp import GProp_GProps
from OCP.GeomAbs import GeomAbs_Intersection, GeomAbs_JoinType from OCP.GeomAbs import GeomAbs_Intersection, GeomAbs_JoinType
from OCP.LocOpe import LocOpe_DPrism from OCP.LocOpe import LocOpe_DPrism
from OCP.ShapeFix import ShapeFix_Solid from OCP.ShapeFix import ShapeFix_Solid
from OCP.Standard import Standard_Failure from OCP.Standard import Standard_Failure, Standard_TypeMismatch
from OCP.StdFail import StdFail_NotDone from OCP.StdFail import StdFail_NotDone
from OCP.TopExp import TopExp from OCP.TopExp import TopExp, TopExp_Explorer
from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape
from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape, TopoDS_Solid, TopoDS_Wire from OCP.TopoDS import (
TopoDS,
TopoDS_Face,
TopoDS_Shape,
TopoDS_Shell,
TopoDS_Solid,
TopoDS_Wire,
)
from OCP.gp import gp_Ax2, gp_Pnt from OCP.gp import gp_Ax2, gp_Pnt
from build123d.build_enums import CenterOf, GeomType, Kind, Transition, Until from build123d.build_enums import CenterOf, GeomType, Keep, Kind, Transition, Until
from build123d.geometry import ( from build123d.geometry import (
DEG2RAD, DEG2RAD,
TOLERANCE,
Axis, Axis,
BoundBox, BoundBox,
Color, Color,
@ -104,10 +110,20 @@ from build123d.geometry import (
Vector, Vector,
VectorLike, VectorLike,
) )
from typing_extensions import Self
from .one_d import Edge, Wire, Mixin1D from .one_d import Edge, Wire, Mixin1D
from .shape_core import Shape, ShapeList, Joint, downcast, shapetype from .shape_core import (
TOPODS,
Shape,
ShapeList,
Joint,
downcast,
shapetype,
_sew_topods_faces,
get_top_level_topods_shapes,
unwrap_topods_compound,
)
from .two_d import sort_wires_by_build_order, Mixin2D, Face, Shell from .two_d import sort_wires_by_build_order, Mixin2D, Face, Shell
from .utils import ( from .utils import (
_extrude_topods_shape, _extrude_topods_shape,
@ -119,26 +135,14 @@ from .zero_d import Vertex
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from .composite import Compound, Curve, Sketch, Part # pylint: disable=R0801 from .composite import Compound # pylint: disable=R0801
class Mixin3D(Shape): class Mixin3D(Shape[TOPODS]):
"""Additional methods to add to 3D Shape classes""" """Additional methods to add to 3D Shape classes"""
project_to_viewport = Mixin1D.project_to_viewport
split = Mixin1D.split
find_intersection_points = Mixin2D.find_intersection_points find_intersection_points = Mixin2D.find_intersection_points
vertices = Mixin1D.vertices
vertex = Mixin1D.vertex
edges = Mixin1D.edges
edge = Mixin1D.edge
wires = Mixin1D.wires
wire = Mixin1D.wire
faces = Mixin2D.faces
face = Mixin2D.face
shells = Mixin2D.shells
shell = Mixin2D.shell
# ---- Properties ---- # ---- Properties ----
@property @property
@ -195,6 +199,7 @@ class Mixin3D(Shape):
if center_of == CenterOf.MASS: if center_of == CenterOf.MASS:
properties = GProp_GProps() properties = GProp_GProps()
calc_function = Shape.shape_properties_LUT[shapetype(self.wrapped)] calc_function = Shape.shape_properties_LUT[shapetype(self.wrapped)]
assert calc_function is not None
calc_function(self.wrapped, properties) calc_function(self.wrapped, properties)
middle = Vector(properties.CentreOfMass()) middle = Vector(properties.CentreOfMass())
elif center_of == CenterOf.BOUNDING_BOX: elif center_of == CenterOf.BOUNDING_BOX:
@ -420,6 +425,132 @@ class Mixin3D(Shape):
return return_value return return_value
def intersect(
self, *to_intersect: Shape | Vector | Location | Axis | Plane
) -> None | ShapeList[Vertex | Edge | Face | Solid]:
"""Intersect Solid with Shape or geometry object
Args:
to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
Returns:
ShapeList[Vertex | Edge | Face | Solid] | None: ShapeList of vertices, edges,
faces, and/or solids.
"""
def to_vector(objs: Iterable) -> ShapeList:
return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])
def to_vertex(objs: Iterable) -> ShapeList:
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
def bool_op(
args: Sequence,
tools: Sequence,
operation: BRepAlgoAPI_Common | BRepAlgoAPI_Section,
) -> ShapeList:
# Wrap Shape._bool_op for corrected output
intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
if isinstance(intersections, ShapeList):
return intersections or ShapeList()
if isinstance(intersections, Shape) and not intersections.is_null:
return ShapeList([intersections])
return ShapeList()
def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
# Remove lower order shapes from list which *appear* to be part of
# a higher order shape using a lazy distance check
# (sufficient for vertices, may be an issue for higher orders)
order_groups = []
for order in orders:
order_groups.append(
ShapeList([s for s in shapes if isinstance(s, order)])
)
filtered_shapes = order_groups[-1]
for i in range(len(order_groups) - 1):
los = order_groups[i]
his: list = sum(order_groups[i + 1 :], [])
filtered_shapes.extend(
ShapeList(
lo
for lo in los
if all(lo.distance_to(hi) > TOLERANCE for hi in his)
)
)
return filtered_shapes
common_set: ShapeList[Vertex | Edge | Face | Solid] = ShapeList([self])
target: Shape
for other in to_intersect:
# Conform target type
match other:
case Axis():
# BRepAlgoAPI_Section seems happier if Edge isnt infinite
bbox = self.bounding_box()
dist = self.distance_to(other.position)
dist = dist if dist >= 1 else 1
target = Edge.make_line(
other.position - other.direction * bbox.diagonal * dist,
other.position + other.direction * bbox.diagonal * dist,
)
case Plane():
target = Face(other)
case Vector():
target = Vertex(other)
case Location():
target = Vertex(other.position)
case _ if issubclass(type(other), Shape):
target = other
case _:
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
# Find common matches
common: list[Vertex | Edge | Wire | Face | Shell | Solid] = []
result: ShapeList | None
for obj in common_set:
match (obj, target):
case (_, Vertex() | Edge() | Wire() | Face() | Shell() | Solid()):
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common = (
BRepAlgoAPI_Section()
)
result = bool_op((obj,), (target,), operation)
if (
not isinstance(obj, Edge | Wire)
and not isinstance(target, (Edge | Wire))
) or (isinstance(obj, Solid) or isinstance(target, Solid)):
# Face + Edge combinations may produce an intersection
# with Common but always with Section.
# No easy way to deduplicate
# Many Solid + Edge combinations need Common
operation = BRepAlgoAPI_Common()
result.extend(bool_op((obj,), (target,), operation))
case _ if issubclass(type(target), Shape):
result = target.intersect(obj)
if result:
common.extend(result)
if common:
common_set = ShapeList()
for shape in common:
if isinstance(shape, Wire):
common_set.extend(shape.edges())
elif isinstance(shape, Shell):
common_set.extend(shape.faces())
else:
common_set.append(shape)
common_set = to_vertex(set(to_vector(common_set)))
common_set = filter_shapes_by_order(
common_set, [Vertex, Edge, Face, Solid]
)
else:
return None
return ShapeList(common_set)
def is_inside(self, point: VectorLike, tolerance: float = 1.0e-6) -> bool: def is_inside(self, point: VectorLike, tolerance: float = 1.0e-6) -> bool:
"""Returns whether or not the point is inside a solid or compound """Returns whether or not the point is inside a solid or compound
object within the specified tolerance. object within the specified tolerance.
@ -486,8 +617,10 @@ class Mixin3D(Shape):
try: try:
new_shape = self.__class__(fillet_builder.Shape()) new_shape = self.__class__(fillet_builder.Shape())
if not new_shape.is_valid: if not new_shape.is_valid:
raise fillet_exception # raise fillet_exception
except fillet_exception: raise Standard_Failure
# except fillet_exception:
except (Standard_Failure, StdFail_NotDone):
return __max_fillet(window_min, window_mid, current_iteration + 1) return __max_fillet(window_min, window_mid, current_iteration + 1)
# These numbers work, are they close enough? - if not try larger window # These numbers work, are they close enough? - if not try larger window
@ -506,10 +639,10 @@ class Mixin3D(Shape):
# Unfortunately, MacOS doesn't support the StdFail_NotDone exception so platform # Unfortunately, MacOS doesn't support the StdFail_NotDone exception so platform
# specific exceptions are required. # specific exceptions are required.
if platform.system() == "Darwin": # if platform.system() == "Darwin":
fillet_exception = Standard_Failure # fillet_exception = Standard_Failure
else: # else:
fillet_exception = StdFail_NotDone # fillet_exception = StdFail_NotDone
max_radius = __max_fillet(0.0, 2 * self.bounding_box().diagonal, 0) max_radius = __max_fillet(0.0, 2 * self.bounding_box().diagonal, 0)
@ -581,16 +714,35 @@ class Mixin3D(Shape):
return offset_solid return offset_solid
def solid(self) -> Solid | None: def project_to_viewport(
"""Return the Solid""" self,
return Shape.get_single_shape(self, "Solid") viewport_origin: VectorLike,
viewport_up: VectorLike = (0, 0, 1),
look_at: VectorLike | None = None,
focus: float | None = None,
) -> tuple[ShapeList[Edge], ShapeList[Edge]]:
"""project_to_viewport
def solids(self) -> ShapeList[Solid]: Project a shape onto a viewport returning visible and hidden Edges.
"""solids - all the solids in this Shape"""
return Shape.get_shape_list(self, "Solid") Args:
viewport_origin (VectorLike): location of viewport
viewport_up (VectorLike, optional): direction of the viewport y axis.
Defaults to (0, 0, 1).
look_at (VectorLike, optional): point to look at.
Defaults to None (center of shape).
focus (float, optional): the focal length for perspective projection
Defaults to None (orthographic projection)
Returns:
tuple[ShapeList[Edge],ShapeList[Edge]]: visible & hidden Edges
"""
return Mixin1D.project_to_viewport(
self, viewport_origin, viewport_up, look_at, focus
)
class Solid(Mixin3D, Shape[TopoDS_Solid]): class Solid(Mixin3D[TopoDS_Solid]):
"""A Solid in build123d represents a three-dimensional solid geometry """A Solid in build123d represents a three-dimensional solid geometry
in a topological structure. A solid is a closed and bounded volume, enclosing in a topological structure. A solid is a closed and bounded volume, enclosing
a region in 3D space. It comprises faces, edges, and vertices connected in a a region in 3D space. It comprises faces, edges, and vertices connected in a
@ -768,7 +920,17 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
inner_comp = _make_topods_compound_from_shapes(inner_solids) inner_comp = _make_topods_compound_from_shapes(inner_solids)
# subtract from the outer solid # subtract from the outer solid
return Solid(BRepAlgoAPI_Cut(outer_solid, inner_comp).Shape()) difference = BRepAlgoAPI_Cut(outer_solid, inner_comp).Shape()
# convert to a TopoDS_Solid - might be wrapped in a TopoDS_Compound
try:
result = TopoDS.Solid_s(difference)
except Standard_TypeMismatch:
result = TopoDS.Solid_s(
unwrap_topods_compound(TopoDS.Compound_s(difference), True)
)
return Solid(result)
@classmethod @classmethod
def extrude_taper( def extrude_taper(
@ -809,7 +971,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
direction.length / cos(radians(taper)), direction.length / cos(radians(taper)),
radians(taper), radians(taper),
) )
new_solid = Solid(prism_builder.Shape()) new_solid = Solid(TopoDS.Solid_s(prism_builder.Shape()))
else: else:
# Determine the offset to get the taper # Determine the offset to get the taper
offset_amt = -direction.length * tan(radians(taper)) offset_amt = -direction.length * tan(radians(taper))
@ -848,110 +1010,116 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
@classmethod @classmethod
def extrude_until( def extrude_until(
cls, cls,
section: Face, profile: Face,
target_object: Compound | Solid, target: Compound | Solid,
direction: VectorLike, direction: VectorLike,
until: Until = Until.NEXT, until: Until = Until.NEXT,
) -> Compound | Solid: ) -> Solid:
"""extrude_until """extrude_until
Extrude section in provided direction until it encounters either the Extrude `profile` in the provided `direction` until it encounters a
NEXT or LAST surface of target_object. Note that the bounding surface bounding surface on the `target`. The termination surface is chosen
must be larger than the extruded face where they contact. according to the `until` option:
* ``Until.NEXT`` Extrude forward until the first intersecting surface.
* ``Until.LAST`` Extrude forward through all intersections, stopping at
the farthest surface.
* ``Until.PREVIOUS`` Reverse the extrusion direction and stop at the
first intersecting surface behind the profile.
* ``Until.FIRST`` Reverse the direction and stop at the farthest
surface behind the profile.
When ``Until.PREVIOUS`` or ``Until.FIRST`` are used, the extrusion
direction is automatically inverted before execution.
Note:
The bounding surface on the target must be large enough to
completely cover the extruded profile at the contact region.
Partial overlaps may yield open or invalid solids.
Args: Args:
section (Face): Face to extrude profile (Face): The face to extrude.
target_object (Union[Compound, Solid]): object to limit extrusion target (Union[Compound, Solid]): The object that limits the extrusion.
direction (VectorLike): extrusion direction direction (VectorLike): Extrusion direction.
until (Until, optional): surface to limit extrusion. Defaults to Until.NEXT. until (Until, optional): Surface selection mode controlling which
intersection to stop at. Defaults to ``Until.NEXT``.
Raises: Raises:
ValueError: provided face does not intersect target_object ValueError: If the provided profile does not intersect the target.
Returns: Returns:
Union[Compound, Solid]: extruded Face Solid: The extruded and limited solid.
""" """
direction = Vector(direction) direction = Vector(direction)
if until in [Until.PREVIOUS, Until.FIRST]: if until in [Until.PREVIOUS, Until.FIRST]:
direction *= -1 direction *= -1
until = Until.NEXT if until == Until.PREVIOUS else Until.LAST until = Until.NEXT if until == Until.PREVIOUS else Until.LAST
max_dimension = find_max_dimension([section, target_object]) # 1: Create extrusion of length the maximum distance between profile and target
clipping_direction = ( max_dimension = find_max_dimension([profile, target])
direction * max_dimension extrusion = Solid.extrude(profile, direction * max_dimension)
if until == Until.NEXT
else -direction * max_dimension # 2: Intersect the extrusion with the target to find the target's modified faces
intersect_op = BRepAlgoAPI_Common(target.wrapped, extrusion.wrapped)
intersect_op.Build()
intersection = intersect_op.Shape()
face_exp = TopExp_Explorer(intersection, ta.TopAbs_FACE)
if not face_exp.More():
raise ValueError("No intersection: extrusion does not contact target")
# Find the faces from the intersection that originated on the target
history = intersect_op.History()
modified_target_faces = []
face_explorer = TopExp_Explorer(target.wrapped, ta.TopAbs_FACE)
while face_explorer.More():
target_face = TopoDS.Face_s(face_explorer.Current())
modified_los: TopTools_ListOfShape = history.Modified(target_face)
while not modified_los.IsEmpty():
modified_face = TopoDS.Face_s(modified_los.First())
modified_los.RemoveFirst()
modified_target_faces.append(modified_face)
face_explorer.Next()
# 3: Sew the resulting faces into shells - one for each surface the extrusion
# passes through and sort by distance from the profile
sewed_shape = _sew_topods_faces(modified_target_faces)
# From the sewed shape extract the shells and single faces
top_level_shapes = get_top_level_topods_shapes(sewed_shape)
modified_target_surfaces: ShapeList[Face | Shell] = ShapeList()
# For each of the top level Shells and Faces
for top_level_shape in top_level_shapes:
if isinstance(top_level_shape, TopoDS_Face):
modified_target_surfaces.append(Face(top_level_shape))
elif isinstance(top_level_shape, TopoDS_Shell):
modified_target_surfaces.append(Shell(top_level_shape))
else:
raise RuntimeError(f"Invalid sewn shape {type(top_level_shape)}")
modified_target_surfaces = modified_target_surfaces.sort_by(
lambda s: s.distance_to(profile)
) )
direction_axis = Axis(section.center(), clipping_direction) limit = modified_target_surfaces[
# Create a linear extrusion to start 0 if until in [Until.NEXT, Until.PREVIOUS] else -1
extrusion = Solid.extrude(section, direction * max_dimension)
# Project section onto the shape to generate faces that will clip the extrusion
# and exclude the planar faces normal to the direction of extrusion and these
# will have no volume when extruded
faces = []
for face in section.project_to_shape(target_object, direction):
if isinstance(face, Face):
faces.append(face)
else:
faces += face.faces()
clip_faces = [
f
for f in faces
if not (f.is_planar and f.normal_at().dot(direction) == 0.0)
] ]
if not clip_faces: keep: Literal[Keep.TOP, Keep.BOTTOM] = (
raise ValueError("provided face does not intersect target_object") Keep.TOP if until in [Until.NEXT, Until.PREVIOUS] else Keep.BOTTOM
)
# Create the objects that will clip the linear extrusion # 4: Split the extrusion by the appropriate shell
clipping_objects = [ clipped_extrusion = extrusion.split(limit, keep=keep)
Solid.extrude(f, clipping_direction).fix() for f in clip_faces
]
clipping_objects = [o for o in clipping_objects if o.volume > 1e-9]
if until == Until.NEXT: # 5: Return the appropriate type
trimmed_extrusion = extrusion.cut(target_object) if clipped_extrusion is None:
if isinstance(trimmed_extrusion, ShapeList): raise RuntimeError("Extrusion is None") # None isn't an option here
closest_extrusion = trimmed_extrusion.sort_by(direction_axis)[0] elif isinstance(clipped_extrusion, Solid):
else: return clipped_extrusion
closest_extrusion = trimmed_extrusion
for clipping_object in clipping_objects:
# It's possible for clipping faces to self intersect when they are extruded
# thus they could be non manifold which results failed boolean operations
# - so skip these objects
try:
extrusion_shapes = closest_extrusion.cut(clipping_object)
except Exception:
warnings.warn(
"clipping error - extrusion may be incorrect",
stacklevel=2,
)
else: else:
base_part = extrusion.intersect(target_object) # isinstance(clipped_extrusion, list):
if isinstance(base_part, ShapeList): return ShapeList(clipped_extrusion).sort_by(
extrusion_parts = base_part Axis(profile.center(), direction)
elif base_part is None: )[0]
extrusion_parts = ShapeList()
else:
extrusion_parts = ShapeList([base_part])
for clipping_object in clipping_objects:
try:
clipped_extrusion = extrusion.intersect(clipping_object)
if clipped_extrusion is not None:
extrusion_parts.append(
clipped_extrusion.solids().sort_by(direction_axis)[0]
)
except Exception:
warnings.warn(
"clipping error - extrusion may be incorrect",
stacklevel=2,
)
extrusion_shapes = Solid.fuse(*extrusion_parts)
result = extrusion_shapes.solids().sort_by(direction_axis)[0]
return result
@classmethod @classmethod
def from_bounding_box(cls, bbox: BoundBox | OrientedBoundBox) -> Solid: def from_bounding_box(cls, bbox: BoundBox | OrientedBoundBox) -> Solid:
@ -982,12 +1150,14 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
Solid: Box Solid: Box
""" """
return cls( return cls(
BRepPrimAPI_MakeBox( TopoDS.Solid_s(
plane.to_gp_ax2(), BRepPrimAPI_MakeBox(
length, plane.to_gp_ax2(),
width, length,
height, width,
).Shape() height,
).Shape()
)
) )
@classmethod @classmethod
@ -1014,13 +1184,15 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
Solid: Full or partial cone Solid: Full or partial cone
""" """
return cls( return cls(
BRepPrimAPI_MakeCone( TopoDS.Solid_s(
plane.to_gp_ax2(), BRepPrimAPI_MakeCone(
base_radius, plane.to_gp_ax2(),
top_radius, base_radius,
height, top_radius,
angle * DEG2RAD, height,
).Shape() angle * DEG2RAD,
).Shape()
)
) )
@classmethod @classmethod
@ -1045,12 +1217,14 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
Solid: Full or partial cylinder Solid: Full or partial cylinder
""" """
return cls( return cls(
BRepPrimAPI_MakeCylinder( TopoDS.Solid_s(
plane.to_gp_ax2(), BRepPrimAPI_MakeCylinder(
radius, plane.to_gp_ax2(),
height, radius,
angle * DEG2RAD, height,
).Shape() angle * DEG2RAD,
).Shape()
)
) )
@classmethod @classmethod
@ -1071,7 +1245,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
Returns: Returns:
Solid: Lofted object Solid: Lofted object
""" """
return cls(_make_loft(objs, True, ruled)) return cls(TopoDS.Solid_s(_make_loft(objs, True, ruled)))
@classmethod @classmethod
def make_sphere( def make_sphere(
@ -1097,13 +1271,15 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
Solid: sphere Solid: sphere
""" """
return cls( return cls(
BRepPrimAPI_MakeSphere( TopoDS.Solid_s(
plane.to_gp_ax2(), BRepPrimAPI_MakeSphere(
radius, plane.to_gp_ax2(),
angle1 * DEG2RAD, radius,
angle2 * DEG2RAD, angle1 * DEG2RAD,
angle3 * DEG2RAD, angle2 * DEG2RAD,
).Shape() angle3 * DEG2RAD,
).Shape()
)
) )
@classmethod @classmethod
@ -1131,14 +1307,16 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
Solid: Full or partial torus Solid: Full or partial torus
""" """
return cls( return cls(
BRepPrimAPI_MakeTorus( TopoDS.Solid_s(
plane.to_gp_ax2(), BRepPrimAPI_MakeTorus(
major_radius, plane.to_gp_ax2(),
minor_radius, major_radius,
start_angle * DEG2RAD, minor_radius,
end_angle * DEG2RAD, start_angle * DEG2RAD,
major_angle * DEG2RAD, end_angle * DEG2RAD,
).Shape() major_angle * DEG2RAD,
).Shape()
)
) )
@classmethod @classmethod
@ -1169,16 +1347,18 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
Solid: wedge Solid: wedge
""" """
return cls( return cls(
BRepPrimAPI_MakeWedge( TopoDS.Solid_s(
plane.to_gp_ax2(), BRepPrimAPI_MakeWedge(
delta_x, plane.to_gp_ax2(),
delta_y, delta_x,
delta_z, delta_y,
min_x, delta_z,
min_z, min_x,
max_x, min_z,
max_z, max_x,
).Solid() max_z,
).Solid()
)
) )
@classmethod @classmethod
@ -1216,7 +1396,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
True, True,
) )
return cls(revol_builder.Shape()) return cls(TopoDS.Solid_s(revol_builder.Shape()))
@classmethod @classmethod
def sweep( def sweep(
@ -1269,7 +1449,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
outer_wire = section outer_wire = section
inner_wires = inner_wires if inner_wires else [] inner_wires = inner_wires if inner_wires else []
shapes = [] shapes: list[Mixin3D[TopoDS_Shape]] = []
for wire in [outer_wire] + inner_wires: for wire in [outer_wire] + inner_wires:
builder = BRepOffsetAPI_MakePipeShell(Wire(path).wrapped) builder = BRepOffsetAPI_MakePipeShell(Wire(path).wrapped)
@ -1364,7 +1544,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
if make_solid: if make_solid:
builder.MakeSolid() builder.MakeSolid()
return cls(builder.Shape()) return cls(TopoDS.Solid_s(builder.Shape()))
@classmethod @classmethod
def thicken( def thicken(
@ -1420,7 +1600,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
) )
offset_builder.MakeOffsetShape() offset_builder.MakeOffsetShape()
try: try:
result = Solid(offset_builder.Shape()) result = Solid(TopoDS.Solid_s(offset_builder.Shape()))
except StdFail_NotDone as err: except StdFail_NotDone as err:
raise RuntimeError("Error applying thicken to given surface") from err raise RuntimeError("Error applying thicken to given surface") from err
@ -1467,7 +1647,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
try: try:
draft_angle_builder.Build() draft_angle_builder.Build()
result = Solid(draft_angle_builder.Shape()) result = Solid(TopoDS.Solid_s(draft_angle_builder.Shape()))
except StdFail_NotDone as err: except StdFail_NotDone as err:
raise DraftAngleError( raise DraftAngleError(
"Draft build failed on the given solid.", "Draft build failed on the given solid.",

View file

@ -60,13 +60,15 @@ import sys
import warnings import warnings
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Iterable, Sequence from collections.abc import Iterable, Sequence
from math import degrees
from typing import TYPE_CHECKING, Any, TypeVar, overload from typing import TYPE_CHECKING, Any, TypeVar, overload
from typing import cast as tcast
import OCP.TopAbs as ta import OCP.TopAbs as ta
from OCP.BRep import BRep_Builder, BRep_Tool from OCP.BRep import BRep_Builder, BRep_Tool
from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
from OCP.BRepAlgo import BRepAlgo from OCP.BRepAlgo import BRepAlgo
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Section
from OCP.BRepBuilderAPI import ( from OCP.BRepBuilderAPI import (
BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeEdge,
BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakeFace,
@ -103,6 +105,7 @@ from OCP.Standard import (
Standard_ConstructionError, Standard_ConstructionError,
Standard_Failure, Standard_Failure,
Standard_NoSuchObject, Standard_NoSuchObject,
Standard_TypeMismatch,
) )
from OCP.StdFail import StdFail_NotDone from OCP.StdFail import StdFail_NotDone
from OCP.TColgp import TColgp_Array1OfPnt, TColgp_HArray2OfPnt from OCP.TColgp import TColgp_Array1OfPnt, TColgp_HArray2OfPnt
@ -139,10 +142,12 @@ from build123d.geometry import (
from .one_d import Edge, Mixin1D, Wire from .one_d import Edge, Mixin1D, Wire
from .shape_core import ( from .shape_core import (
TOPODS,
Shape, Shape,
ShapeList, ShapeList,
SkipClean, SkipClean,
_sew_topods_faces, _sew_topods_faces,
_topods_bool_op,
_topods_entities, _topods_entities,
_topods_face_normal_at, _topods_face_normal_at,
downcast, downcast,
@ -153,7 +158,6 @@ from .utils import (
_extrude_topods_shape, _extrude_topods_shape,
_make_loft, _make_loft,
_make_topods_face_from_wires, _make_topods_face_from_wires,
_topods_bool_op,
find_max_dimension, find_max_dimension,
) )
from .zero_d import Vertex from .zero_d import Vertex
@ -165,17 +169,11 @@ if TYPE_CHECKING: # pragma: no cover
T = TypeVar("T", Edge, Wire, "Face") T = TypeVar("T", Edge, Wire, "Face")
class Mixin2D(ABC, Shape): class Mixin2D(ABC, Shape[TOPODS]):
"""Additional methods to add to Face and Shell class""" """Additional methods to add to Face and Shell class"""
project_to_viewport = Mixin1D.project_to_viewport # project_to_viewport = Mixin1D.project_to_viewport
split = Mixin1D.split
vertices = Mixin1D.vertices
vertex = Mixin1D.vertex
edges = Mixin1D.edges
edge = Mixin1D.edge
wires = Mixin1D.wires
# ---- Properties ---- # ---- Properties ----
@property @property
@ -213,23 +211,23 @@ class Mixin2D(ABC, Shape):
def __neg__(self) -> Self: def __neg__(self) -> Self:
"""Reverse normal operator -""" """Reverse normal operator -"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Invalid Shape") raise ValueError("Invalid Shape")
new_surface = copy.deepcopy(self) new_surface = copy.deepcopy(self)
new_surface.wrapped = downcast(self.wrapped.Complemented()) new_surface.wrapped = tcast(TOPODS, downcast(self.wrapped.Complemented()))
# As the surface has been modified, the parent is no longer valid # As the surface has been modified, the parent is no longer valid
new_surface.topo_parent = None new_surface.topo_parent = None
return new_surface return new_surface
def face(self) -> Face | None: # def face(self) -> Face | None:
"""Return the Face""" # """Return the Face"""
return Shape.get_single_shape(self, "Face") # return Shape.get_single_shape(self, "Face")
def faces(self) -> ShapeList[Face]: # def faces(self) -> ShapeList[Face]:
"""faces - all the faces in this Shape""" # """faces - all the faces in this Shape"""
return Shape.get_shape_list(self, "Face") # return Shape.get_shape_list(self, "Face")
def find_intersection_points( def find_intersection_points(
self, other: Axis, tolerance: float = TOLERANCE self, other: Axis, tolerance: float = TOLERANCE
@ -244,7 +242,7 @@ class Mixin2D(ABC, Shape):
Returns: Returns:
list[tuple[Vector, Vector]]: Point and normal of intersection list[tuple[Vector, Vector]]: Point and normal of intersection
""" """
if self.wrapped is None: if self._wrapped is None:
return [] return []
intersection_line = gce_MakeLin(other.wrapped).Value() intersection_line = gce_MakeLin(other.wrapped).Value()
@ -278,6 +276,128 @@ class Mixin2D(ABC, Shape):
return result return result
def intersect(
self, *to_intersect: Shape | Vector | Location | Axis | Plane
) -> None | ShapeList[Vertex | Edge | Face]:
"""Intersect Face with Shape or geometry object
Args:
to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
Returns:
ShapeList[Vertex | Edge | Face] | None: ShapeList of vertices, edges, and/or
faces.
"""
def to_vector(objs: Iterable) -> ShapeList:
return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])
def to_vertex(objs: Iterable) -> ShapeList:
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
def bool_op(
args: Sequence,
tools: Sequence,
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common,
) -> ShapeList:
# Wrap Shape._bool_op for corrected output
intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
if isinstance(intersections, ShapeList):
return intersections or ShapeList()
if isinstance(intersections, Shape) and not intersections.is_null:
return ShapeList([intersections])
return ShapeList()
def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
# Remove lower order shapes from list which *appear* to be part of
# a higher order shape using a lazy distance check
# (sufficient for vertices, may be an issue for higher orders)
order_groups = []
for order in orders:
order_groups.append(
ShapeList([s for s in shapes if isinstance(s, order)])
)
filtered_shapes = order_groups[-1]
for i in range(len(order_groups) - 1):
los = order_groups[i]
his: list = sum(order_groups[i + 1 :], [])
filtered_shapes.extend(
ShapeList(
lo
for lo in los
if all(lo.distance_to(hi) > TOLERANCE for hi in his)
)
)
return filtered_shapes
common_set: ShapeList[Vertex | Edge | Face | Shell] = ShapeList([self])
target: Shape
for other in to_intersect:
# Conform target type
match other:
case Axis():
# BRepAlgoAPI_Section seems happier if Edge isnt infinite
bbox = self.bounding_box()
dist = self.distance_to(other.position)
dist = dist if dist >= 1 else 1
target = Edge.make_line(
other.position - other.direction * bbox.diagonal * dist,
other.position + other.direction * bbox.diagonal * dist,
)
case Plane():
target = Face(other)
case Vector():
target = Vertex(other)
case Location():
target = Vertex(other.position)
case _ if issubclass(type(other), Shape):
target = other
case _:
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
# Find common matches
common: list[Vertex | Edge | Wire | Face | Shell] = []
result: ShapeList | None
for obj in common_set:
match (obj, target):
case (_, Vertex() | Edge() | Wire() | Face() | Shell()):
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common = (
BRepAlgoAPI_Section()
)
result = bool_op((obj,), (target,), operation)
if not isinstance(obj, Edge | Wire) and not isinstance(
target, (Edge | Wire)
):
# Face + Edge combinations may produce an intersection
# with Common but always with Section.
# No easy way to deduplicate
operation = BRepAlgoAPI_Common()
result.extend(bool_op((obj,), (target,), operation))
case _ if issubclass(type(target), Shape):
result = target.intersect(obj)
if result:
common.extend(result)
if common:
common_set = ShapeList()
for shape in common:
if isinstance(shape, Wire):
common_set.extend(shape.edges())
elif isinstance(shape, Shell):
common_set.extend(shape.faces())
else:
common_set.append(shape)
common_set = to_vertex(set(to_vector(common_set)))
common_set = filter_shapes_by_order(common_set, [Vertex, Edge, Face])
else:
return None
return ShapeList(common_set)
@abstractmethod @abstractmethod
def location_at(self, *args: Any, **kwargs: Any) -> Location: def location_at(self, *args: Any, **kwargs: Any) -> Location:
"""A location from a face or shell""" """A location from a face or shell"""
@ -287,13 +407,32 @@ class Mixin2D(ABC, Shape):
"""Return a copy of self moved along the normal by amount""" """Return a copy of self moved along the normal by amount"""
return copy.deepcopy(self).moved(Location(self.normal_at() * amount)) return copy.deepcopy(self).moved(Location(self.normal_at() * amount))
def shell(self) -> Shell | None: def project_to_viewport(
"""Return the Shell""" self,
return Shape.get_single_shape(self, "Shell") viewport_origin: VectorLike,
viewport_up: VectorLike = (0, 0, 1),
look_at: VectorLike | None = None,
focus: float | None = None,
) -> tuple[ShapeList[Edge], ShapeList[Edge]]:
"""project_to_viewport
def shells(self) -> ShapeList[Shell]: Project a shape onto a viewport returning visible and hidden Edges.
"""shells - all the shells in this Shape"""
return Shape.get_shape_list(self, "Shell") Args:
viewport_origin (VectorLike): location of viewport
viewport_up (VectorLike, optional): direction of the viewport y axis.
Defaults to (0, 0, 1).
look_at (VectorLike, optional): point to look at.
Defaults to None (center of shape).
focus (float, optional): the focal length for perspective projection
Defaults to None (orthographic projection)
Returns:
tuple[ShapeList[Edge],ShapeList[Edge]]: visible & hidden Edges
"""
return Mixin1D.project_to_viewport(
self, viewport_origin, viewport_up, look_at, focus
)
def _wrap_edge( def _wrap_edge(
self, self,
@ -350,7 +489,7 @@ class Mixin2D(ABC, Shape):
world_point, world_point - target_object_center world_point, world_point - target_object_center
) )
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't wrap around an empty face") raise ValueError("Can't wrap around an empty face")
# Initial setup # Initial setup
@ -411,7 +550,7 @@ class Mixin2D(ABC, Shape):
raise RuntimeError( raise RuntimeError(
f"Length error of {length_error:.6f} exceeds tolerance {tolerance}" f"Length error of {length_error:.6f} exceeds tolerance {tolerance}"
) )
if wrapped_edge.wrapped is None or not wrapped_edge.is_valid: if not wrapped_edge or not wrapped_edge.is_valid:
raise RuntimeError("Wrapped edge is invalid") raise RuntimeError("Wrapped edge is invalid")
if not snap_to_face: if not snap_to_face:
@ -434,7 +573,7 @@ class Mixin2D(ABC, Shape):
return projected_edge return projected_edge
class Face(Mixin2D, Shape[TopoDS_Face]): class Face(Mixin2D[TopoDS_Face]):
"""A Face in build123d represents a 3D bounded surface within the topological data """A Face in build123d represents a 3D bounded surface within the topological data
structure. It encapsulates geometric information, defining a face of a 3D shape. structure. It encapsulates geometric information, defining a face of a 3D shape.
These faces are integral components of complex structures, such as solids and These faces are integral components of complex structures, such as solids and
@ -449,7 +588,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
@overload @overload
def __init__( def __init__(
self, self,
obj: TopoDS_Face, obj: TopoDS_Face | Plane,
label: str = "", label: str = "",
color: Color | None = None, color: Color | None = None,
parent: Compound | None = None, parent: Compound | None = None,
@ -457,7 +596,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
"""Build a Face from an OCCT TopoDS_Shape/TopoDS_Face """Build a Face from an OCCT TopoDS_Shape/TopoDS_Face
Args: Args:
obj (TopoDS_Shape, optional): OCCT Face. obj (TopoDS_Shape | Plane, optional): OCCT Face or Plane.
label (str, optional): Defaults to ''. label (str, optional): Defaults to ''.
color (Color, optional): Defaults to None. color (Color, optional): Defaults to None.
parent (Compound, optional): assembly parent. Defaults to None. parent (Compound, optional): assembly parent. Defaults to None.
@ -483,11 +622,14 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
""" """
def __init__(self, *args: Any, **kwargs: Any): def __init__(self, *args: Any, **kwargs: Any):
obj: TopoDS_Face | Plane | None
outer_wire, inner_wires, obj, label, color, parent = (None,) * 6 outer_wire, inner_wires, obj, label, color, parent = (None,) * 6
if args: if args:
l_a = len(args) l_a = len(args)
if isinstance(args[0], TopoDS_Shape): if isinstance(args[0], Plane):
obj = args[0]
elif isinstance(args[0], TopoDS_Shape):
obj, label, color, parent = args[:4] + (None,) * (4 - l_a) obj, label, color, parent = args[:4] + (None,) * (4 - l_a)
elif isinstance(args[0], Wire): elif isinstance(args[0], Wire):
outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * ( outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * (
@ -516,6 +658,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
color = kwargs.get("color", color) color = kwargs.get("color", color)
parent = kwargs.get("parent", parent) parent = kwargs.get("parent", parent)
if isinstance(obj, Plane):
obj = BRepBuilderAPI_MakeFace(obj.wrapped).Face()
if outer_wire is not None: if outer_wire is not None:
inner_topods_wires = ( inner_topods_wires = (
[w.wrapped for w in inner_wires] if inner_wires is not None else [] [w.wrapped for w in inner_wires] if inner_wires is not None else []
@ -545,7 +690,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
float: The total surface area, including the area of holes. Returns 0.0 if float: The total surface area, including the area of holes. Returns 0.0 if
the face is empty. the face is empty.
""" """
if self.wrapped is None: if self._wrapped is None:
return 0.0 return 0.0
return self.without_holes().area return self.without_holes().area
@ -556,6 +701,11 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
if type(self.geom_adaptor()) == Geom_RectangularTrimmedSurface: if type(self.geom_adaptor()) == Geom_RectangularTrimmedSurface:
return None return None
if self.geom_type == GeomType.CONE:
return Axis(
self.geom_adaptor().Cone().Axis() # type:ignore[attr-defined]
)
if self.geom_type == GeomType.CYLINDER: if self.geom_type == GeomType.CYLINDER:
return Axis( return Axis(
self.geom_adaptor().Cylinder().Axis() # type:ignore[attr-defined] self.geom_adaptor().Cylinder().Axis() # type:ignore[attr-defined]
@ -605,7 +755,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
ValueError: If the face or its underlying representation is empty. ValueError: If the face or its underlying representation is empty.
ValueError: If the face is not planar. ValueError: If the face is not planar.
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't determine axes_of_symmetry of empty face") raise ValueError("Can't determine axes_of_symmetry of empty face")
if not self.is_planar_face: if not self.is_planar_face:
@ -671,15 +821,13 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
).sort_by(Axis(cog, cross_dir)) ).sort_by(Axis(cog, cross_dir))
bottom_area = sum(f.area for f in bottom_list) bottom_area = sum(f.area for f in bottom_list)
intersect_area = 0.0
for flipped_face, bottom_face in zip(top_flipped_list, bottom_list): for flipped_face, bottom_face in zip(top_flipped_list, bottom_list):
intersection = flipped_face.intersect(bottom_face) intersection = flipped_face.intersect(bottom_face)
if intersection is None or isinstance(intersection, list): if intersection is None:
intersect_area = -1.0 intersect_area = -1.0
break break
else: else:
assert isinstance(intersection, Face) intersect_area = sum(f.area for f in intersection.faces())
intersect_area += intersection.area
if intersect_area == -1.0: if intersect_area == -1.0:
continue continue
@ -837,6 +985,17 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
else: else:
return None return None
@property
def semi_angle(self) -> None | float:
"""Return the semi angle of a cone, otherwise None"""
if (
self.geom_type == GeomType.CONE
and type(self.geom_adaptor()) != Geom_RectangularTrimmedSurface
):
return degrees(self.geom_adaptor().SemiAngle()) # type:ignore[attr-defined]
else:
return None
@property @property
def volume(self) -> float: def volume(self) -> float:
"""volume - the volume of this Face, which is always zero""" """volume - the volume of this Face, which is always zero"""
@ -871,7 +1030,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
Returns: Returns:
Face: extruded shape Face: extruded shape
""" """
if obj.wrapped is None: if not obj:
raise ValueError("Can't extrude empty object") raise ValueError("Can't extrude empty object")
return Face(TopoDS.Face_s(_extrude_topods_shape(obj.wrapped, direction))) return Face(TopoDS.Face_s(_extrude_topods_shape(obj.wrapped, direction)))
@ -981,7 +1140,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
) )
return single_point_curve return single_point_curve
if shape.wrapped is None: if not shape:
raise ValueError("input Edge cannot be empty") raise ValueError("input Edge cannot be empty")
adaptor = BRepAdaptor_Curve(shape.wrapped) adaptor = BRepAdaptor_Curve(shape.wrapped)
@ -1015,6 +1174,12 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
plane: Plane = Plane.XY, plane: Plane = Plane.XY,
) -> Face: ) -> Face:
"""Create a unlimited size Face aligned with plane""" """Create a unlimited size Face aligned with plane"""
warnings.warn(
"The 'make_plane' method is deprecated and will be removed in a future version.",
DeprecationWarning,
stacklevel=2,
)
pln_shape = BRepBuilderAPI_MakeFace(plane.wrapped).Face() pln_shape = BRepBuilderAPI_MakeFace(plane.wrapped).Face()
return cls(pln_shape) return cls(pln_shape)
@ -1104,7 +1269,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
raise ValueError("exterior must be a Wire or list of Edges") raise ValueError("exterior must be a Wire or list of Edges")
for edge in outside_edges: for edge in outside_edges:
if edge.wrapped is None: if not edge:
raise ValueError("exterior contains empty edges") raise ValueError("exterior contains empty edges")
surface.Add(edge.wrapped, GeomAbs_C0) surface.Add(edge.wrapped, GeomAbs_C0)
@ -1135,7 +1300,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
if interior_wires: if interior_wires:
makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped) makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped)
for wire in interior_wires: for wire in interior_wires:
if wire.wrapped is None: if not wire:
raise ValueError("interior_wires contain an empty wire") raise ValueError("interior_wires contain an empty wire")
makeface_object.Add(wire.wrapped) makeface_object.Add(wire.wrapped)
try: try:
@ -1317,7 +1482,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
try: try:
patch.Build() patch.Build()
result = cls(patch.Shape()) result = cls(TopoDS.Face_s(patch.Shape()))
except ( except (
Standard_Failure, Standard_Failure,
StdFail_NotDone, StdFail_NotDone,
@ -1329,7 +1494,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
) from err ) from err
result = result.fix() result = result.fix()
if not result.is_valid or result.wrapped is None: if not result.is_valid or not result:
raise RuntimeError("Non planar face is invalid") raise RuntimeError("Non planar face is invalid")
return result return result
@ -1433,8 +1598,12 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
if len(profile.edges()) != 1 or len(path.edges()) != 1: if len(profile.edges()) != 1 or len(path.edges()) != 1:
raise ValueError("Use Shell.sweep for multi Edge objects") raise ValueError("Use Shell.sweep for multi Edge objects")
profile = Wire([profile.edge()]) profile_edge = profile.edge()
path = Wire([path.edge()]) path_edge = path.edge()
assert profile_edge is not None
assert path_edge is not None
profile = Wire([profile_edge])
path = Wire([path_edge])
builder = BRepOffsetAPI_MakePipeShell(path.wrapped) builder = BRepOffsetAPI_MakePipeShell(path.wrapped)
builder.Add(profile.wrapped, False, False) builder.Add(profile.wrapped, False, False)
builder.SetTransitionMode(Shape._transModeDict[transition]) builder.SetTransitionMode(Shape._transModeDict[transition])
@ -1458,6 +1627,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
Returns: Returns:
Vector: center Vector: center
""" """
center_point: Vector | gp_Pnt
if (center_of == CenterOf.MASS) or ( if (center_of == CenterOf.MASS) or (
center_of == CenterOf.GEOMETRY and self.is_planar center_of == CenterOf.GEOMETRY and self.is_planar
): ):
@ -1517,7 +1687,10 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
# Index or iterator access to OCP.TopTools.TopTools_ListOfShape is slow on M1 macs # Index or iterator access to OCP.TopTools.TopTools_ListOfShape is slow on M1 macs
# Using First() and Last() to omit # Using First() and Last() to omit
edges = (Edge(edge_list.First()), Edge(edge_list.Last())) edges = (
Edge(TopoDS.Edge_s(edge_list.First())),
Edge(TopoDS.Edge_s(edge_list.Last())),
)
edge1, edge2 = Wire.order_chamfer_edges(reference_edge, edges) edge1, edge2 = Wire.order_chamfer_edges(reference_edge, edges)
@ -1907,7 +2080,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
BRepAlgoAPI_Common(), BRepAlgoAPI_Common(),
) )
for topods_shell in get_top_level_topods_shapes(topods_shape): for topods_shell in get_top_level_topods_shapes(topods_shape):
intersected_shapes.append(Shell(topods_shell)) intersected_shapes.append(Shell(TopoDS.Shell_s(topods_shell)))
intersected_shapes = intersected_shapes.sort_by(Axis(self.center(), direction)) intersected_shapes = intersected_shapes.sort_by(Axis(self.center(), direction))
projected_shapes: ShapeList[Face | Shell] = ShapeList() projected_shapes: ShapeList[Face | Shell] = ShapeList()
@ -1940,7 +2113,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
DeprecationWarning, DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot approximate an empty shape") raise ValueError("Cannot approximate an empty shape")
return self.__class__.cast(BRepAlgo.ConvertFace_s(self.wrapped, tolerance)) return self.__class__.cast(BRepAlgo.ConvertFace_s(self.wrapped, tolerance))
@ -1953,7 +2126,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
Returns: Returns:
Face: A new Face instance identical to the original but without any holes. Face: A new Face instance identical to the original but without any holes.
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot remove holes from an empty face") raise ValueError("Cannot remove holes from an empty face")
if not (inner_wires := self.inner_wires()): if not (inner_wires := self.inner_wires()):
@ -1964,7 +2137,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
for hole_wire in inner_wires: for hole_wire in inner_wires:
reshaper.Remove(hole_wire.wrapped) reshaper.Remove(hole_wire.wrapped)
modified_shape = downcast(reshaper.Apply(self.wrapped)) modified_shape = downcast(reshaper.Apply(self.wrapped))
holeless.wrapped = modified_shape holeless.wrapped = TopoDS.Face_s(modified_shape)
return holeless return holeless
def wire(self) -> Wire: def wire(self) -> Wire:
@ -2327,7 +2500,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
return wrapped_wire return wrapped_wire
class Shell(Mixin2D, Shape[TopoDS_Shell]): class Shell(Mixin2D[TopoDS_Shell]):
"""A Shell is a fundamental component in build123d's topological data structure """A Shell is a fundamental component in build123d's topological data structure
representing a connected set of faces forming a closed surface in 3D space. As representing a connected set of faces forming a closed surface in 3D space. As
part of a geometric model, it defines a watertight enclosure, commonly encountered part of a geometric model, it defines a watertight enclosure, commonly encountered
@ -2359,7 +2532,7 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
obj = obj_list[0] obj = obj_list[0]
if isinstance(obj, Face): if isinstance(obj, Face):
if obj.wrapped is None: if not obj:
raise ValueError(f"Can't create a Shell from empty Face") raise ValueError(f"Can't create a Shell from empty Face")
builder = BRep_Builder() builder = BRep_Builder()
shell = TopoDS_Shell() shell = TopoDS_Shell()
@ -2367,7 +2540,10 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
builder.Add(shell, obj.wrapped) builder.Add(shell, obj.wrapped)
obj = shell obj = shell
elif isinstance(obj, Iterable): elif isinstance(obj, Iterable):
obj = _sew_topods_faces([f.wrapped for f in obj]) try:
obj = TopoDS.Shell_s(_sew_topods_faces([f.wrapped for f in obj]))
except Standard_TypeMismatch:
raise TypeError("Unable to create Shell, invalid input type")
super().__init__( super().__init__(
obj=obj, obj=obj,
@ -2385,6 +2561,7 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
solid_shell = ShapeFix_Solid().SolidFromShell(self.wrapped) solid_shell = ShapeFix_Solid().SolidFromShell(self.wrapped)
properties = GProp_GProps() properties = GProp_GProps()
calc_function = Shape.shape_properties_LUT[shapetype(solid_shell)] calc_function = Shape.shape_properties_LUT[shapetype(solid_shell)]
assert calc_function is not None
calc_function(solid_shell, properties) calc_function(solid_shell, properties)
return properties.Mass() return properties.Mass()
return 0.0 return 0.0
@ -2427,7 +2604,7 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
Returns: Returns:
Shell: Lofted object Shell: Lofted object
""" """
return cls(_make_loft(objs, False, ruled)) return cls(TopoDS.Shell_s(_make_loft(objs, False, ruled)))
@classmethod @classmethod
def revolve( def revolve(
@ -2453,7 +2630,7 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
profile.wrapped, axis.wrapped, angle * DEG2RAD, True profile.wrapped, axis.wrapped, angle * DEG2RAD, True
) )
return cls(revol_builder.Shape()) return cls(TopoDS.Shell_s(revol_builder.Shape()))
@classmethod @classmethod
def sweep( def sweep(
@ -2481,7 +2658,7 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
builder.Add(profile.wrapped, False, False) builder.Add(profile.wrapped, False, False)
builder.SetTransitionMode(Shape._transModeDict[transition]) builder.SetTransitionMode(Shape._transModeDict[transition])
builder.Build() builder.Build()
result = Shell(builder.Shape()) result = Shell(TopoDS.Shell_s(builder.Shape()))
if SkipClean.clean: if SkipClean.clean:
result = result.clean() result = result.clean()

View file

@ -24,7 +24,6 @@ Key Features:
- `_make_topods_face_from_wires`: Generates planar faces with optional holes. - `_make_topods_face_from_wires`: Generates planar faces with optional holes.
- **Boolean Operations**: - **Boolean Operations**:
- `_topods_bool_op`: Generic Boolean operations for TopoDS_Shapes.
- `new_edges`: Identifies newly created edges from combined shapes. - `new_edges`: Identifies newly created edges from combined shapes.
- **Enhanced Math**: - **Enhanced Math**:
@ -282,45 +281,6 @@ def _make_topods_face_from_wires(
return TopoDS.Face_s(sf_f.Result()) return TopoDS.Face_s(sf_f.Result())
def _topods_bool_op(
args: Iterable[TopoDS_Shape],
tools: Iterable[TopoDS_Shape],
operation: BRepAlgoAPI_BooleanOperation | BRepAlgoAPI_Splitter,
) -> TopoDS_Shape:
"""Generic boolean operation for TopoDS_Shapes
Args:
args: Iterable[TopoDS_Shape]:
tools: Iterable[TopoDS_Shape]:
operation: BRepAlgoAPI_BooleanOperation | BRepAlgoAPI_Splitter:
Returns: TopoDS_Shape
"""
args = list(args)
tools = list(tools)
arg = TopTools_ListOfShape()
for obj in args:
arg.Append(obj)
tool = TopTools_ListOfShape()
for obj in tools:
tool.Append(obj)
operation.SetArguments(arg)
operation.SetTools(tool)
operation.SetRunParallel(True)
operation.Build()
result = downcast(operation.Shape())
# Remove unnecessary TopoDS_Compound around single shape
if isinstance(result, TopoDS_Compound):
result = unwrap_topods_compound(result, True)
return result
def delta(shapes_one: Iterable[Shape], shapes_two: Iterable[Shape]) -> list[Shape]: def delta(shapes_one: Iterable[Shape], shapes_two: Iterable[Shape]) -> list[Shape]:
"""Compare the OCCT objects of each list and return the differences""" """Compare the OCCT objects of each list and return the differences"""
shapes_one = list(shapes_one) shapes_one = list(shapes_one)

View file

@ -68,8 +68,8 @@ from OCP.TopExp import TopExp_Explorer
from OCP.TopoDS import TopoDS, TopoDS_Shape, TopoDS_Vertex, TopoDS_Edge from OCP.TopoDS import TopoDS, TopoDS_Shape, TopoDS_Vertex, TopoDS_Edge
from OCP.gp import gp_Pnt from OCP.gp import gp_Pnt
from build123d.geometry import Matrix, Vector, VectorLike, Location, Axis, Plane from build123d.geometry import Matrix, Vector, VectorLike, Location, Axis, Plane
from build123d.build_enums import Keep
from .shape_core import Shape, ShapeList, downcast, shapetype from .shape_core import Shape, ShapeList, TrimmingTool, downcast, shapetype
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
@ -161,7 +161,7 @@ class Vertex(Shape[TopoDS_Vertex]):
shape_type = shapetype(obj) shape_type = shapetype(obj)
# NB downcast is needed to handle TopoDS_Shape types # NB downcast is needed to handle TopoDS_Shape types
return constructor_lut[shape_type](downcast(obj)) return constructor_lut[shape_type](TopoDS.Vertex_s(obj))
@classmethod @classmethod
def extrude(cls, obj: Shape, direction: VectorLike) -> Vertex: def extrude(cls, obj: Shape, direction: VectorLike) -> Vertex:
@ -312,6 +312,10 @@ class Vertex(Shape[TopoDS_Vertex]):
"""The center of a vertex is itself!""" """The center of a vertex is itself!"""
return Vector(self) return Vector(self)
def split(self, tool: TrimmingTool, keep: Keep = Keep.TOP):
"""split - not implemented"""
raise NotImplementedError("Vertices cannot be split.")
def to_tuple(self) -> tuple[float, float, float]: def to_tuple(self) -> tuple[float, float, float]:
"""Return vertex as three tuple of floats""" """Return vertex as three tuple of floats"""
warnings.warn( warnings.warn(

View file

@ -80,7 +80,7 @@ def to_vtk_poly_data(
if not HAS_VTK: if not HAS_VTK:
warnings.warn("VTK not supported", stacklevel=2) warnings.warn("VTK not supported", stacklevel=2)
if obj.wrapped is None: if not obj:
raise ValueError("Cannot convert an empty shape") raise ValueError("Cannot convert an empty shape")
vtk_shape = IVtkOCC_Shape(obj.wrapped) vtk_shape = IVtkOCC_Shape(obj.wrapped)

View file

@ -98,14 +98,14 @@ class BuildLineTests(unittest.TestCase):
powerup @ 0, powerup @ 0,
tangents=(screw % 1, powerup % 0), tangents=(screw % 1, powerup % 0),
) )
self.assertAlmostEqual(roller_coaster.wires()[0].length, 678.983628932414, 5) self.assertAlmostEqual(roller_coaster.wires()[0].length, 678.9785865257071, 5)
def test_bezier(self): def test_bezier(self):
pts = [(0, 0), (20, 20), (40, 0), (0, -40), (-60, 0), (0, 100), (100, 0)] pts = [(0, 0), (20, 20), (40, 0), (0, -40), (-60, 0), (0, 100), (100, 0)]
wts = [1.0, 1.0, 2.0, 3.0, 4.0, 2.0, 1.0] wts = [1.0, 1.0, 2.0, 3.0, 4.0, 2.0, 1.0]
with BuildLine() as bz: with BuildLine() as bz:
b1 = Bezier(*pts, weights=wts) b1 = Bezier(*pts, weights=wts)
self.assertAlmostEqual(bz.wires()[0].length, 225.86389406824566, 5) self.assertAlmostEqual(bz.wires()[0].length, 225.98661946375782, 5)
self.assertTrue(isinstance(b1, Edge)) self.assertTrue(isinstance(b1, Edge))
def test_double_tangent_arc(self): def test_double_tangent_arc(self):
@ -183,6 +183,61 @@ class BuildLineTests(unittest.TestCase):
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 2) self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 2)
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 3) self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 3)
with BuildLine(Plane.YZ):
p = FilletPolyline(
(0, 0),
(10, 0),
(10, 10),
(0, 10),
radius=(1, 2, 3, 0),
close=True,
)
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 3)
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 4)
with self.assertRaises(ValueError):
p = FilletPolyline(
(0, 0),
(10, 0),
(10, 10),
(0, 10),
radius=(1, 2, 3, 4),
close=False,
)
with self.assertRaises(ValueError):
p = FilletPolyline(
(0, 0),
(10, 0),
(10, 10),
(0, 10),
radius=-1,
close=True,
)
with self.assertRaises(ValueError):
p = FilletPolyline(
(0, 0),
(10, 0),
(10, 10),
(0, 10),
radius=(1, 2),
close=True,
)
with BuildLine(Plane.YZ):
p = FilletPolyline(
(0, 0),
(10, 0),
(10, 10),
(0, 10),
radius=(1, 2, 3, 4),
close=True,
)
self.assertEqual(len(p.edges()), 8)
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 4)
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 4)
with BuildLine(Plane.YZ): with BuildLine(Plane.YZ):
p = FilletPolyline( p = FilletPolyline(
(0, 0, 0), (0, 0, 10), (10, 2, 10), (10, 0, 0), radius=2, close=True (0, 0, 0), (0, 0, 10), (10, 2, 10), (10, 0, 0), radius=2, close=True
@ -197,6 +252,33 @@ class BuildLineTests(unittest.TestCase):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
FilletPolyline((0, 0), (1, 0), (1, 1), radius=-1) FilletPolyline((0, 0), (1, 0), (1, 1), radius=-1)
# test filletpolyline curr_fillet None
# Middle corner radius = 0 → curr_fillet is None
with BuildLine():
p = FilletPolyline(
(0, 0),
(10, 0),
(10, 10),
(20, 10),
radius=(0, 1), # middle corner is sharp
close=False,
)
# 1 circular fillet, 3 line fillets
assert len(p.edges().filter_by(GeomType.CIRCLE)) == 1
# test filletpolyline next_fillet None:
# Second corner is sharp (radius 0) → next_fillet is None
with BuildLine():
p = FilletPolyline(
(0, 0),
(10, 0),
(10, 10),
(0, 10),
radius=(1, 0), # next_fillet is None at last interior corner
close=False,
)
assert len(p.edges()) > 0
def test_intersecting_line(self): def test_intersecting_line(self):
with BuildLine(): with BuildLine():
l1 = Line((0, 0), (10, 0)) l1 = Line((0, 0), (10, 0))
@ -805,9 +887,9 @@ class BuildLineTests(unittest.TestCase):
min_r = 0 if case[2][0] is None else (flip_min * case[0] + case[2][0]) / 2 min_r = 0 if case[2][0] is None else (flip_min * case[0] + case[2][0]) / 2
max_r = 1e6 if case[2][1] is None else (flip_max * case[0] + case[2][1]) / 2 max_r = 1e6 if case[2][1] is None else (flip_max * case[0] + case[2][1]) / 2
print(case[1], min_r, max_r, case[0]) # print(case[1], min_r, max_r, case[0])
print(min_r + 0.01, min_r * 0.99, max_r - 0.01, max_r + 0.01) # print(min_r + 0.01, min_r * 0.99, max_r - 0.01, max_r + 0.01)
print((case[0] - 1 * (r1 + r2)) / 2) # print((case[0] - 1 * (r1 + r2)) / 2)
# Greater than min # Greater than min
l1 = ArcArcTangentArc(start_arc, end_arc, min_r + 0.01, keep=case[1]) l1 = ArcArcTangentArc(start_arc, end_arc, min_r + 0.01, keep=case[1])

View file

@ -330,6 +330,60 @@ class TestExtrude(unittest.TestCase):
extrude(until=Until.NEXT) extrude(until=Until.NEXT)
self.assertAlmostEqual(test.part.volume, 10**3 - 8**3 + 1**2 * 8, 5) self.assertAlmostEqual(test.part.volume, 10**3 - 8**3 + 1**2 * 8, 5)
def test_extrude_until2(self):
target = Box(10, 5, 5) - Pos(X=2.5) * Cylinder(0.5, 5)
pln = Plane((7, 0, 7), z_dir=(-1, 0, -1))
profile = (pln * Circle(1)).face()
extrusion = extrude(profile, dir=pln.z_dir, until=Until.NEXT, target=target)
self.assertLess(extrusion.bounding_box().min.Z, 2.5)
def test_extrude_until3(self):
with BuildPart() as p:
with BuildSketch(Plane.XZ):
Rectangle(8, 8, align=Align.MIN)
with Locations((1, 1)):
Rectangle(7, 7, align=Align.MIN, mode=Mode.SUBTRACT)
extrude(amount=2, both=True)
with BuildSketch(
Plane((-2, 0, -2), x_dir=(0, 1, 0), z_dir=(1, 0, 1))
) as profile:
Rectangle(4, 1)
extrude(until=Until.NEXT)
self.assertAlmostEqual(p.part.volume, 72.313, 2)
def test_extrude_until_errors(self):
with self.assertRaises(ValueError):
extrude(
Rectangle(1, 1),
until=Until.NEXT,
dir=(0, 0, 1),
target=Pos(Z=-10) * Box(1, 1, 1),
)
def test_extrude_until_invalid_sewn_shape(self):
profile = Face.make_rect(1, 1)
target = Box(2, 2, 2)
direction = Vector(0, 0, 1)
bad_shape = Box(1, 1, 1).wrapped # not a Face or Shell → forces RuntimeError
with patch(
"build123d.topology.three_d.get_top_level_topods_shapes",
return_value=[bad_shape],
):
with self.assertRaises(RuntimeError):
extrude(profile, dir=direction, until=Until.NEXT, target=target)
def test_extrude_until_invalid_split(self):
profile = Face.make_rect(1, 1)
target = Box(2, 2, 2)
direction = Vector(0, 0, 1)
with patch("build123d.topology.three_d.Solid.split", return_value=None):
with self.assertRaises(RuntimeError):
extrude(profile, dir=direction, until=Until.NEXT, target=target)
def test_extrude_face(self): def test_extrude_face(self):
with BuildPart(Plane.XZ) as box: with BuildPart(Plane.XZ) as box:
with BuildSketch(Plane.XZ, mode=Mode.PRIVATE) as square: with BuildSketch(Plane.XZ, mode=Mode.PRIVATE) as square:

View file

@ -43,6 +43,7 @@ class TestBoundBox(unittest.TestCase):
# OCC uses some approximations # OCC uses some approximations
self.assertAlmostEqual(bb1.size.X, 1.0, 1) self.assertAlmostEqual(bb1.size.X, 1.0, 1)
self.assertAlmostEqual(bb1.measure, 1.0, 5)
# Test adding to an existing bounding box # Test adding to an existing bounding box
v0 = Vertex(0, 0, 0) v0 = Vertex(0, 0, 0)
@ -50,6 +51,7 @@ class TestBoundBox(unittest.TestCase):
bb3 = bb1.add(bb2) bb3 = bb1.add(bb2)
self.assertAlmostEqual(bb3.size, (2, 2, 2), 7) self.assertAlmostEqual(bb3.size, (2, 2, 2), 7)
self.assertAlmostEqual(bb3.measure, 8, 5)
bb3 = bb2.add((3, 3, 3)) bb3 = bb2.add((3, 3, 3))
self.assertAlmostEqual(bb3.size, (3, 3, 3), 7) self.assertAlmostEqual(bb3.size, (3, 3, 3), 7)
@ -61,6 +63,7 @@ class TestBoundBox(unittest.TestCase):
bb1 = Vertex(1, 1, 0).bounding_box().add(Vertex(2, 2, 0).bounding_box()) bb1 = Vertex(1, 1, 0).bounding_box().add(Vertex(2, 2, 0).bounding_box())
bb2 = Vertex(0, 0, 0).bounding_box().add(Vertex(3, 3, 0).bounding_box()) bb2 = Vertex(0, 0, 0).bounding_box().add(Vertex(3, 3, 0).bounding_box())
bb3 = Vertex(0, 0, 0).bounding_box().add(Vertex(1.5, 1.5, 0).bounding_box()) bb3 = Vertex(0, 0, 0).bounding_box().add(Vertex(1.5, 1.5, 0).bounding_box())
self.assertAlmostEqual(bb2.measure, 9, 5)
# Test that bb2 contains bb1 # Test that bb2 contains bb1
self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb1, bb2)) self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb1, bb2))
self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb2, bb1)) self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb2, bb1))

View file

@ -26,169 +26,296 @@ license:
""" """
import colorsys
import copy import copy
import unittest import math
import numpy as np import numpy as np
from build123d.geometry import Color import pytest
from OCP.Quantity import Quantity_ColorRGBA from OCP.Quantity import Quantity_ColorRGBA
from build123d.geometry import Color
class TestColor(unittest.TestCase): # Overloads
# name + alpha overload @pytest.mark.parametrize(
def test_name1(self): "color, expected",
c = Color("blue") [
np.testing.assert_allclose(tuple(c), (0, 0, 1, 1), 1e-5) pytest.param(Color("blue"), (0, 0, 1, 1), id="name"),
pytest.param(Color("blue", alpha=0.5), (0, 0, 1, 0.5), id="name + kw alpha"),
pytest.param(Color("blue", 0.5), (0, 0, 1, 0.5), id="name + alpha"),
],
)
def test_overload_name(color, expected):
np.testing.assert_allclose(tuple(color), expected, 1e-5)
def test_name2(self):
c = Color("blue", alpha=0.5)
np.testing.assert_allclose(tuple(c), (0, 0, 1, 0.5), 1e-5)
def test_name3(self): @pytest.mark.parametrize(
c = Color("blue", 0.5) "color, expected",
np.testing.assert_allclose(tuple(c), (0, 0, 1, 0.5), 1e-5) [
pytest.param(Color(0.0, 1.0, 0.0), (0, 1, 0, 1), id="rgb"),
pytest.param(Color(1.0, 1.0, 0.0, 0.5), (1, 1, 0, 0.5), id="rgba"),
pytest.param(
Color(1.0, 1.0, 0.0, alpha=0.5), (1, 1, 0, 0.5), id="rgb + kw alpha"
),
pytest.param(
Color(red=0.1, green=0.2, blue=0.3, alpha=0.5),
(0.1, 0.2, 0.3, 0.5),
id="kw rgba",
),
],
)
def test_overload_rgba(color, expected):
np.testing.assert_allclose(tuple(color), expected, 1e-5)
# red + green + blue + alpha overload
def test_rgb0(self):
c = Color(0.0, 1.0, 0.0)
np.testing.assert_allclose(tuple(c), (0, 1, 0, 1), 1e-5)
def test_rgba1(self): @pytest.mark.parametrize(
c = Color(1.0, 1.0, 0.0, 0.5) "color, expected",
self.assertEqual(c.wrapped.GetRGB().Red(), 1.0) [
self.assertEqual(c.wrapped.GetRGB().Green(), 1.0) pytest.param(
self.assertEqual(c.wrapped.GetRGB().Blue(), 0.0) Color(0x996692), (0x99 / 0xFF, 0x66 / 0xFF, 0x92 / 0xFF, 1), id="color_code"
self.assertEqual(c.wrapped.Alpha(), 0.5) ),
pytest.param(
Color(0x006692, 0x80),
(0, 0x66 / 0xFF, 0x92 / 0xFF, 0x80 / 0xFF),
id="color_code + alpha",
),
pytest.param(
Color(0x006692, alpha=0x80),
(0, 102 / 255, 146 / 255, 128 / 255),
id="color_code + kw alpha",
),
pytest.param(
Color(color_code=0x996692, alpha=0xCC),
(153 / 255, 102 / 255, 146 / 255, 204 / 255),
id="kw color_code + alpha",
),
],
)
def test_overload_hex(color, expected):
np.testing.assert_allclose(tuple(color), expected, 1e-5)
def test_rgba2(self):
c = Color(1.0, 1.0, 0.0, alpha=0.5)
np.testing.assert_allclose(tuple(c), (1, 1, 0, 0.5), 1e-5)
def test_rgba3(self): @pytest.mark.parametrize(
c = Color(red=0.1, green=0.2, blue=0.3, alpha=0.5) "color, expected",
np.testing.assert_allclose(tuple(c), (0.1, 0.2, 0.3, 0.5), 1e-5) [
pytest.param(Color((0.1,)), (0.1, 1.0, 1.0, 1.0), id="tuple r"),
pytest.param(Color((0.1, 0.2)), (0.1, 0.2, 1.0, 1.0), id="tuple rg"),
pytest.param(Color((0.1, 0.2, 0.3)), (0.1, 0.2, 0.3, 1.0), id="tuple rgb"),
pytest.param(
Color((0.1, 0.2, 0.3, 0.4)), (0.1, 0.2, 0.3, 0.4), id="tuple rbga"
),
pytest.param(Color((0.1, 0.2, 0.3, 0.4)), (0.1, 0.2, 0.3, 0.4), id="kw tuple"),
],
)
def test_overload_tuple(color, expected):
np.testing.assert_allclose(tuple(color), expected, 1e-5)
# hex (int) + alpha overload
def test_hex(self):
c = Color(0x996692)
np.testing.assert_allclose(
tuple(c), (0x99 / 0xFF, 0x66 / 0xFF, 0x92 / 0xFF, 1), 5
)
c = Color(0x006692, 0x80) # ColorLikes
np.testing.assert_allclose( @pytest.mark.parametrize(
tuple(c), (0, 0x66 / 0xFF, 0x92 / 0xFF, 0x80 / 0xFF), 5 "color_like",
) [
pytest.param(Quantity_ColorRGBA(1, 0, 0, 1), id="Quantity_ColorRGBA"),
pytest.param("red", id="name str"),
pytest.param("red ", id="name str whitespace"),
pytest.param(("red",), id="tuple name str"),
pytest.param(("red", 1), id="tuple name str + alpha"),
pytest.param("#ff0000", id="hex str rgb 24bit"),
pytest.param(" #ff0000 ", id="hex str rgb 24bit whitespace"),
pytest.param(("#ff0000",), id="tuple hex str rgb 24bit"),
pytest.param(("#ff0000", 1), id="tuple hex str rgb 24bit + alpha"),
pytest.param("#ff0000ff", id="hex str rgba 24bit"),
pytest.param(" #ff0000ff ", id="hex str rgba 24bit whitespace"),
pytest.param(("#ff0000ff",), id="tuple hex str rgba 24bit"),
pytest.param(
("#ff0000ff", 0.6), id="tuple hex str rgba 24bit + alpha (not used)"
),
pytest.param("#f00", id="hex str rgb 12bit"),
pytest.param(" #f00 ", id="hex str rgb 12bit whitespace"),
pytest.param(("#f00",), id="tuple hex str rgb 12bit"),
pytest.param(("#f00", 1), id="tuple hex str rgb 12bit + alpha"),
pytest.param("#f00f", id="hex str rgba 12bit"),
pytest.param(" #f00f ", id="hex str rgba 12bit whitespace"),
pytest.param(("#f00f",), id="tuple hex str rgba 12bit"),
pytest.param(("#f00f", 0.6), id="tuple hex str rgba 12bit + alpha (not used)"),
pytest.param(0xFF0000, id="hex int"),
pytest.param((0xFF0000), id="tuple hex int"),
pytest.param((0xFF0000, 0xFF), id="tuple hex int + alpha"),
pytest.param((1, 0, 0), id="tuple rgb int"),
pytest.param((1, 0, 0, 1), id="tuple rgba int"),
pytest.param((1.0, 0.0, 0.0), id="tuple rgb float"),
pytest.param((1.0, 0.0, 0.0, 1.0), id="tuple rgba float"),
],
)
def test_color_likes(color_like):
expected = (1, 0, 0, 1)
np.testing.assert_allclose(tuple(Color(color_like)), expected, 1e-5)
np.testing.assert_allclose(tuple(Color(color_like=color_like)), expected, 1e-5)
c = Color(0x006692, alpha=0x80)
np.testing.assert_allclose(tuple(c), (0, 102 / 255, 146 / 255, 128 / 255), 1e-5)
c = Color(color_code=0x996692, alpha=0xCC) @pytest.mark.parametrize(
np.testing.assert_allclose( "color_like, expected",
tuple(c), (153 / 255, 102 / 255, 146 / 255, 204 / 255), 5 [
) pytest.param(Color(), (1, 1, 1, 1), id="empty Color()"),
pytest.param(1.0, (1, 1, 1, 1), id="r float"),
pytest.param((1.0,), (1, 1, 1, 1), id="tuple r float"),
pytest.param((1.0, 0.0), (1, 0, 1, 1), id="tuple rg float"),
],
)
def test_color_likes_incomplete(color_like, expected):
np.testing.assert_allclose(tuple(Color(color_like)), expected, 1e-5)
np.testing.assert_allclose(tuple(Color(color_like=color_like)), expected, 1e-5)
c = Color(0.0, 0.0, 1.0, 1.0)
np.testing.assert_allclose(tuple(c), (0, 0, 1, 1), 1e-5)
c = Color(0, 0, 1, 1) @pytest.mark.parametrize(
np.testing.assert_allclose(tuple(c), (0, 0, 1, 1), 1e-5) "color_like",
[
pytest.param(Quantity_ColorRGBA(1, 0, 0, 0.6), id="Quantity_ColorRGBA"),
pytest.param(("red", 0.6), id="tuple name str + alpha"),
pytest.param(("#ff0000", 0.6), id="tuple hex str rgb 24bit + alpha"),
pytest.param(("#ff000099"), id="tuple hex str rgba 24bit"),
pytest.param(("#f00", 0.6), id="tuple hex str rgb 12bit + alpha"),
pytest.param(("#f009"), id="tuple hex str rgba 12bit"),
pytest.param((0xFF0000, 153), id="tuple hex int + alpha int"),
pytest.param((1.0, 0.0, 0.0, 0.6), id="tuple rbga float"),
],
)
def test_color_likes_alpha(color_like):
expected = (1, 0, 0, 0.6)
np.testing.assert_allclose(tuple(Color(color_like)), expected, 1e-5)
np.testing.assert_allclose(tuple(Color(color_like=color_like)), expected, 1e-5)
# Methods
def test_to_tuple(self):
c = Color("blue", alpha=0.5)
np.testing.assert_allclose(tuple(c), (0, 0, 1, 0.5), 1e-5)
def test_copy(self): # Exceptions
c = Color(0.1, 0.2, 0.3, alpha=0.4) @pytest.mark.parametrize(
c_copy = copy.copy(c) "name",
np.testing.assert_allclose(tuple(c_copy), (0.1, 0.2, 0.3, 0.4), 1e-5) [
pytest.param("build123d", id="invalid color name"),
pytest.param("#ffg", id="invalid rgb 12bit"),
pytest.param("#fffg", id="invalid rgba 12bit"),
pytest.param("#fffgg", id="invalid rgb 24bit"),
pytest.param("#fff00gg", id="invalid rgba 24bit"),
pytest.param("#ff", id="short rgb 12bit"),
pytest.param("#fffff", id="short rgb 24bit"),
pytest.param("#fffffff", id="short rgba 24bit"),
pytest.param("#fffffffff", id="long rgba 24bit"),
],
)
def test_exceptions_color_name(name):
with pytest.raises(Exception):
Color(name)
def test_str_repr(self):
c = Color(1, 0, 0)
self.assertEqual(str(c), "Color: (1.0, 0.0, 0.0, 1.0) is 'RED'")
self.assertEqual(repr(c), "Color(1.0, 0.0, 0.0, 1.0)")
c = Color(1, .5, 0) @pytest.mark.parametrize(
self.assertEqual(str(c), "Color: (1.0, 0.5, 0.0, 1.0) near 'DARKGOLDENROD1'") "color_type",
self.assertEqual(repr(c), "Color(1.0, 0.5, 0.0, 1.0)") [
pytest.param(
(
dict(
{"name": "red", "alpha": 1},
)
),
id="dict arg",
),
pytest.param(("red", "blue"), id="str + str"),
pytest.param((1.0, "blue"), id="float + str order"),
pytest.param((1, "blue"), id="int + str order"),
],
)
def test_exceptions_color_type(color_type):
with pytest.raises(Exception):
Color(*color_type)
def test_tuple(self):
c = Color((0.1,))
np.testing.assert_allclose(tuple(c), (0.1, 1.0, 1.0, 1.0), 1e-5)
c = Color((0.1, 0.2))
np.testing.assert_allclose(tuple(c), (0.1, 0.2, 1.0, 1.0), 1e-5)
c = Color((0.1, 0.2, 0.3))
np.testing.assert_allclose(tuple(c), (0.1, 0.2, 0.3, 1.0), 1e-5)
c = Color((0.1, 0.2, 0.3, 0.4))
np.testing.assert_allclose(tuple(c), (0.1, 0.2, 0.3, 0.4), 1e-5)
c = Color(color_like=(0.1, 0.2, 0.3, 0.4))
np.testing.assert_allclose(tuple(c), (0.1, 0.2, 0.3, 0.4), 1e-5)
# color_like overload # Methods
def test_color_like(self): def test_rgba_wrapped():
red_color_likes = [ c = Color(1.0, 1.0, 0.0, 0.5)
Quantity_ColorRGBA(1, 0, 0, 1), assert c.wrapped.GetRGB().Red() == 1.0
"red", assert c.wrapped.GetRGB().Green() == 1.0
"red ", assert c.wrapped.GetRGB().Blue() == 0.0
("red",), assert c.wrapped.Alpha() == 0.5
("red", 1),
"#ff0000",
" #ff0000 ",
("#ff0000",),
("#ff0000", 1),
0xff0000,
(0xff0000),
(0xff0000, 0xff),
(1, 0, 0),
(1, 0, 0, 1),
(1., 0., 0.),
(1., 0., 0., 1.)
]
expected = (1, 0, 0, 1)
for cl in red_color_likes:
np.testing.assert_allclose(tuple(Color(cl)), expected, 1e-5)
np.testing.assert_allclose(tuple(Color(color_like=cl)), expected, 1e-5)
incomplete_color_likes = [
(Color(), (1, 1, 1, 1)),
(1., (1, 1, 1, 1)),
((1.,), (1, 1, 1, 1)),
((1., 0.), (1, 0, 1, 1)),
]
for cl, expected in incomplete_color_likes:
np.testing.assert_allclose(tuple(Color(cl)), expected, 1e-5)
np.testing.assert_allclose(tuple(Color(color_like=cl)), expected, 1e-5)
alpha_color_likes = [ def test_copy():
Quantity_ColorRGBA(1, 0, 0, 0.6), c = Color(0.1, 0.2, 0.3, alpha=0.4)
("red", 0.6), c_copy = copy.copy(c)
("#ff0000", 0.6), np.testing.assert_allclose(tuple(c_copy), (0.1, 0.2, 0.3, 0.4), rtol=1e-5)
(0xff0000, 153),
(1., 0., 0., 0.6)
]
expected = (1, 0, 0, 0.6)
for cl in alpha_color_likes:
np.testing.assert_allclose(tuple(Color(cl)), expected, 1e-5)
np.testing.assert_allclose(tuple(Color(color_like=cl)), expected, 1e-5)
# Exceptions
def test_bad_color_name(self):
with self.assertRaises(ValueError):
Color("build123d")
def test_bad_color_type(self): def test_str_repr_is():
with self.assertRaises(TypeError): c = Color(1, 0, 0)
Color(dict({"name": "red", "alpha": 1})) assert str(c) == "Color: (1.0, 0.0, 0.0, 1.0) is 'RED'"
assert repr(c) == "Color(1.0, 0.0, 0.0, 1.0)"
with self.assertRaises(TypeError):
Color("red", "blue")
with self.assertRaises(TypeError): def test_str_repr_near():
Color(1., "blue") c = Color(1, 0.5, 0)
assert str(c) == "Color: (1.0, 0.5, 0.0, 1.0) near 'DARKGOLDENROD1'"
assert repr(c) == "Color(1.0, 0.5, 0.0, 1.0)"
with self.assertRaises(TypeError):
Color(1, "blue")
if __name__ == "__main__": class TestColorCategoricalSet:
unittest.main() def test_returns_expected_number_of_colors(self):
colors = Color.categorical_set(5)
assert len(colors) == 5
assert all(isinstance(c, Color) for c in colors)
def test_colors_are_evenly_spaced_in_hue(self):
count = 8
colors = Color.categorical_set(count)
hues = [colorsys.rgb_to_hls(*tuple(c)[:3])[0] for c in colors]
diffs = [(hues[(i + 1) % count] - hues[i]) % 1.0 for i in range(count)]
avg_diff = sum(diffs) / len(diffs)
assert all(math.isclose(d, avg_diff, rel_tol=1e-2) for d in diffs)
def test_starting_hue_as_float(self):
(r, g, b, _) = tuple(Color.categorical_set(1, starting_hue=0.25)[0])
h = colorsys.rgb_to_hls(r, g, b)[0]
assert math.isclose(h, 0.25, rel_tol=0.05)
def test_starting_hue_as_int_hex(self):
# Blue (0x0000FF) should be valid and return a Color
c = Color.categorical_set(1, starting_hue=0x0000FF)[0]
assert isinstance(c, Color)
def test_starting_hue_invalid_type(self):
with pytest.raises(TypeError):
Color.categorical_set(3, starting_hue="invalid")
def test_starting_hue_out_of_range(self):
with pytest.raises(ValueError):
Color.categorical_set(3, starting_hue=1.5)
with pytest.raises(ValueError):
Color.categorical_set(3, starting_hue=-0.1)
def test_starting_hue_negative_int(self):
with pytest.raises(ValueError):
Color.categorical_set(3, starting_hue=-1)
def test_constant_alpha_applied(self):
colors = Color.categorical_set(3, alpha=0.7)
for c in colors:
(_, _, _, a) = tuple(c)
assert math.isclose(a, 0.7, rel_tol=1e-6)
def test_iterable_alpha_applied(self):
alphas = (0.1, 0.5, 0.9)
colors = Color.categorical_set(3, alpha=alphas)
for a, c in zip(alphas, colors):
(_, _, _, returned_alpha) = tuple(c)
assert math.isclose(a, returned_alpha, rel_tol=1e-6)
def test_iterable_alpha_length_mismatch(self):
with pytest.raises(ValueError):
Color.categorical_set(4, alpha=[0.5, 0.7])
def test_hues_wrap_around(self):
colors = Color.categorical_set(10, starting_hue=0.95)
hues = [colorsys.rgb_to_hls(*tuple(c)[:3])[0] for c in colors]
assert all(0.0 <= h <= 1.0 for h in hues)
def test_alpha_defaults_to_one(self):
colors = Color.categorical_set(4)
for c in colors:
(_, _, _, a) = tuple(c)
assert math.isclose(a, 1.0, rel_tol=1e-6)

View file

@ -130,8 +130,8 @@ class TestFace(unittest.TestCase):
distance=1, distance2=2, vertices=[vertex], edge=other_edge distance=1, distance2=2, vertices=[vertex], edge=other_edge
) )
def test_make_rect(self): def test_plane_as_face(self):
test_face = Face.make_plane() test_face = Face(Plane.XY)
self.assertAlmostEqual(test_face.normal_at(), (0, 0, 1), 5) self.assertAlmostEqual(test_face.normal_at(), (0, 0, 1), 5)
def test_length_width(self): def test_length_width(self):

View file

@ -152,6 +152,7 @@ def test_shape_0d(obj, target, expected):
run_test(obj, target, expected) run_test(obj, target, expected)
# 1d Shapes
ed1 = Line((0, 0), (5, 0)).edge() ed1 = Line((0, 0), (5, 0)).edge()
ed2 = Line((0, -1), (5, 1)).edge() ed2 = Line((0, -1), (5, 1)).edge()
ed3 = Line((0, 0, 5), (5, 0, 5)).edge() ed3 = Line((0, 0, 5), (5, 0, 5)).edge()
@ -220,6 +221,195 @@ def test_shape_1d(obj, target, expected):
run_test(obj, target, expected) run_test(obj, target, expected)
# 2d Shapes
fc1 = Rectangle(5, 5).face()
fc2 = Pos(Z=5) * Rectangle(5, 5).face()
fc3 = Rot(Y=90) * Rectangle(5, 5).face()
fc4 = Rot(Z=45) * Rectangle(5, 5).face()
fc5 = Pos(2.5, 2.5, 2.5) * Rot(0, 90) * Rectangle(5, 5).face()
fc6 = Pos(2.5, 2.5) * Rot(0, 90, 45, Extrinsic.XYZ) * Rectangle(5, 5).face()
fc7 = (Rot(90) * Cylinder(2, 4)).faces().filter_by(GeomType.CYLINDER)[0]
fc11 = Rectangle(4, 4).face()
fc22 = sweep(Rot(90) * CenterArc((0, 0), 2, 0, 180), Line((0, 2), (0, -2)))
sh1 = Shell([Pos(-4) * fc11, fc22])
sh2 = Pos(Z=1) * sh1
sh3 = Shell([Pos(-4) * fc11, fc22, Pos(2, 0, -2) * Rot(0, 90) * fc11])
sh4 = Shell([Pos(-4) * fc11, fc22, Pos(4) * fc11])
sh5 = Pos(Z=1) * Shell([Pos(-2, 0, -2) * Rot(0, -90) * fc11, fc22, Pos(2, 0, -2) * Rot(0, 90) * fc11])
shape_2d_matrix = [
Case(fc1, vl2, None, "non-coincident", None),
Case(fc1, vl1, [Vertex], "coincident", None),
Case(fc1, lc2, None, "non-coincident", None),
Case(fc1, lc1, [Vertex], "coincident", None),
Case(fc2, ax1, None, "parallel/skew", None),
Case(fc3, ax1, [Vertex], "intersecting", None),
Case(fc1, ax1, [Edge], "collinear", None),
Case(fc1, pl3, None, "parallel/skew", None),
Case(fc1, pl1, [Edge], "intersecting", None),
Case(fc1, pl2, [Face], "collinear", None),
Case(fc7, pl1, [Edge, Edge], "multi intersect", None),
Case(fc1, vt2, None, "non-coincident", None),
Case(fc1, vt1, [Vertex], "coincident", None),
Case(fc1, ed3, None, "parallel/skew", None),
Case(Pos(1) * fc3, ed1, [Vertex], "intersecting", None),
Case(fc1, ed1, [Edge], "collinear", None),
Case(Pos(1.1) * fc3, ed4, [Vertex, Vertex], "multi intersect", None),
Case(fc1, wi6, None, "parallel/skew", None),
Case(Pos(1) * fc3, wi4, [Vertex], "intersecting", None),
Case(fc1, wi1, [Edge, Edge], "2 collinear", None),
Case(Rot(90) * fc4, wi5, [Vertex, Vertex], "multi intersect", None),
Case(Rot(90) * fc4, wi2, [Vertex, Edge], "intersect + collinear", None),
Case(fc1, fc2, None, "parallel/skew", None),
Case(fc1, fc3, [Edge], "intersecting", None),
Case(fc1, fc4, [Face], "coplanar", None),
Case(fc1, fc5, [Edge], "intersecting edge", None),
Case(fc1, fc6, [Vertex], "intersecting vertex", None),
Case(fc1, fc7, [Edge, Edge], "multi-intersecting", None),
Case(fc7, Pos(Y=2) * fc7, [Face], "cyl intersecting", None),
Case(sh2, fc1, None, "parallel/skew", None),
Case(Pos(Z=1) * sh3, fc1, [Edge], "intersecting", None),
Case(sh1, fc1, [Face, Edge], "coplanar + intersecting", None),
Case(sh4, fc1, [Face, Face], "2 coplanar", None),
Case(sh5, fc1, [Edge, Edge], "2 intersecting", None),
Case(fc1, [fc4, Pos(2, 2) * fc1], [Face], "multi to_intersect, intersecting", None),
Case(fc1, [ed1, Pos(2.5, 2.5) * fc1], [Edge], "multi to_intersect, intersecting", None),
Case(fc7, [wi5, fc1], [Vertex], "multi to_intersect, intersecting", None),
]
@pytest.mark.parametrize("obj, target, expected", make_params(shape_2d_matrix))
def test_shape_2d(obj, target, expected):
run_test(obj, target, expected)
# 3d Shapes
sl1 = Box(2, 2, 2).solid()
sl2 = Pos(Z=5) * Box(2, 2, 2).solid()
sl3 = Cylinder(2, 1).solid() - Cylinder(1.5, 1).solid()
wi7 = Wire([l1 := sl3.faces().sort_by(Axis.Z)[-1].edges()[0].trim(.3, .4),
l2 := l1.trim(2, 3),
RadiusArc(l1 @ 1, l2 @ 0, 1, short_sagitta=False)
])
shape_3d_matrix = [
Case(sl2, vl1, None, "non-coincident", None),
Case(Pos(2) * sl1, vl1, [Vertex], "contained", None),
Case(Pos(1, 1, -1) * sl1, vl1, [Vertex], "coincident", None),
Case(sl2, lc1, None, "non-coincident", None),
Case(Pos(2) * sl1, lc1, [Vertex], "contained", None),
Case(Pos(1, 1, -1) * sl1, lc1, [Vertex], "coincident", None),
Case(sl2, ax1, None, "non-coincident", None),
Case(sl1, ax1, [Edge], "intersecting", None),
Case(Pos(1, 1, 1) * sl1, ax2, [Edge], "coincident", None),
Case(sl1, pl3, None, "non-coincident", None),
Case(sl1, pl2, [Face], "intersecting", None),
Case(sl2, vt1, None, "non-coincident", None),
Case(Pos(2) * sl1, vt1, [Vertex], "contained", None),
Case(Pos(1, 1, -1) * sl1, vt1, [Vertex], "coincident", None),
Case(sl1, ed3, None, "non-coincident", None),
Case(sl1, ed1, [Edge], "intersecting", None),
Case(sl1, Pos(0, 1, 1) * ed1, [Edge], "edge collinear", "duplicate edges, BRepAlgoAPI_Common and _Section both return edge"),
Case(sl1, Pos(1, 1, 1) * ed1, [Vertex], "corner coincident", None),
Case(Pos(2.1, 1) * sl1, ed4, [Edge, Edge], "multi-intersect", None),
Case(Pos(2, .5, -1) * sl1, wi6, None, "non-coincident", None),
Case(Pos(2, .5, 1) * sl1, wi6, [Edge, Edge], "multi-intersecting", None),
Case(sl3, wi7, [Edge, Edge], "multi-coincident, is_equal check", None),
Case(sl2, fc1, None, "non-coincident", None),
Case(sl1, fc1, [Face], "intersecting", None),
Case(Pos(3.5, 0, 1) * sl1, fc1, [Edge], "edge collinear", None),
Case(Pos(3.5, 3.5) * sl1, fc1, [Vertex], "corner coincident", None),
Case(Pos(.9) * sl1, fc7, [Face, Face], "multi-intersecting", None),
Case(sl2, sh1, None, "non-coincident", None),
Case(Pos(-2) * sl1, sh1, [Face, Face], "multi-intersecting", None),
Case(sl1, sl2, None, "non-coincident", None),
Case(sl1, Pos(1, 1, 1) * sl1, [Solid], "intersecting", None),
Case(sl1, Pos(2, 2, 1) * sl1, [Edge], "edge collinear", None),
Case(sl1, Pos(2, 2, 2) * sl1, [Vertex], "corner coincident", None),
Case(sl1, Pos(.45) * sl3, [Solid, Solid], "multi-intersect", None),
Case(Pos(1.5, 1.5) * sl1, [sl3, Pos(.5, .5) * sl1], [Solid], "multi to_intersect, intersecting", None),
Case(Pos(1.5, 1.5) * sl1, [sl3, Pos(Z=.5) * fc1], [Face], "multi to_intersect, intersecting", None),
]
@pytest.mark.parametrize("obj, target, expected", make_params(shape_3d_matrix))
def test_shape_3d(obj, target, expected):
run_test(obj, target, expected)
# Compound Shapes
cp1 = Compound(GridLocations(5, 0, 2, 1) * Vertex())
cp2 = Compound(GridLocations(5, 0, 2, 1) * Line((0, -1), (0, 1)))
cp3 = Compound(GridLocations(5, 0, 2, 1) * Rectangle(2, 2))
cp4 = Compound(GridLocations(5, 0, 2, 1) * Box(2, 2, 2))
cv1 = Curve() + [ed1, ed2, ed3]
sk1 = Sketch() + [fc1, fc2, fc3]
pt1 = Part() + [sl1, sl2, sl3]
shape_compound_matrix = [
Case(cp1, vl1, None, "non-coincident", None),
Case(Pos(-.5) * cp1, vl1, [Vertex], "intersecting", None),
Case(cp2, lc1, None, "non-coincident", None),
Case(Pos(-.5) * cp2, lc1, [Vertex], "intersecting", None),
Case(Pos(Z=1) * cp3, ax1, None, "non-coincident", None),
Case(cp3, ax1, [Edge, Edge], "intersecting", None),
Case(Pos(Z=3) * cp4, pl2, None, "non-coincident", None),
Case(cp4, pl2, [Face, Face], "intersecting", None),
Case(cp1, vt1, None, "non-coincident", None),
Case(Pos(-.5) * cp1, vt1, [Vertex], "intersecting", None),
Case(Pos(Z=1) * cp2, ed1, None, "non-coincident", None),
Case(cp2, ed1, [Vertex], "intersecting", None),
Case(Pos(Z=1) * cp3, fc1, None, "non-coincident", None),
Case(cp3, fc1, [Face, Face], "intersecting", None),
Case(Pos(Z=5) * cp4, sl1, None, "non-coincident", None),
Case(Pos(2) * cp4, sl1, [Solid], "intersecting", None),
Case(cp1, Pos(Z=1) * cp1, None, "non-coincident", None),
Case(cp1, cp2, [Vertex, Vertex], "intersecting", None),
Case(cp2, cp3, [Edge, Edge], "intersecting", None),
Case(cp3, cp4, [Face, Face], "intersecting", None),
Case(cp1, Compound(children=cp1.get_type(Vertex)), [Vertex, Vertex], "mixed child type", None),
Case(cp4, Compound(children=cp3.get_type(Face)), [Face, Face], "mixed child type", None),
Case(cp2, [cp3, cp4], [Edge, Edge], "multi to_intersect, intersecting", None),
Case(cv1, cp3, [Edge, Edge], "intersecting", "duplicate edges, BRepAlgoAPI_Common and _Section both return edge"),
Case(sk1, cp3, [Face, Face], "intersecting", None),
Case(pt1, cp3, [Face, Face], "intersecting", None),
]
@pytest.mark.parametrize("obj, target, expected", make_params(shape_compound_matrix))
def test_shape_compound(obj, target, expected):
run_test(obj, target, expected)
# FreeCAD issue example # FreeCAD issue example
c1 = CenterArc((0, 0), 10, 0, 360).edge() c1 = CenterArc((0, 0), 10, 0, 360).edge()
c2 = CenterArc((19, 0), 10, 0, 360).edge() c2 = CenterArc((19, 0), 10, 0, 360).edge()
@ -240,7 +430,7 @@ freecad_matrix = [
Case(vert, e1, [Vertex], "vertical, ellipse, tangent", None), Case(vert, e1, [Vertex], "vertical, ellipse, tangent", None),
Case(horz, e1, [Vertex], "horizontal, ellipse, tangent", None), Case(horz, e1, [Vertex], "horizontal, ellipse, tangent", None),
Case(c1, c2, [Vertex, Vertex], "circle, skew, intersect", "Should return 2 Vertices"), Case(c1, c2, [Vertex, Vertex], "circle, skew, intersect", None),
Case(c1, horz, [Vertex], "circle, horiz, tangent", None), Case(c1, horz, [Vertex], "circle, horiz, tangent", None),
Case(c2, horz, [Vertex], "circle, horiz, tangent", None), Case(c2, horz, [Vertex], "circle, horiz, tangent", None),
Case(c1, vert, [Vertex], "circle, vert, tangent", None), Case(c1, vert, [Vertex], "circle, vert, tangent", None),
@ -263,11 +453,11 @@ w1 = Wire.make_circle(0.5)
f1 = Face(Wire.make_circle(0.5)) f1 = Face(Wire.make_circle(0.5))
issues_matrix = [ issues_matrix = [
Case(t, t, [Face, Face], "issue #1015", "Returns Compound"), Case(t, t, [Face, Face], "issue #1015", None),
Case(l, s, [Edge], "issue #945", "Edge.intersect only takes 1D"), Case(l, s, [Edge], "issue #945", None),
Case(a, b, [Edge], "issue #918", "Returns empty Compound"), Case(a, b, [Edge], "issue #918", None),
Case(e1, w1, [Vertex, Vertex], "issue #697"), Case(e1, w1, [Vertex, Vertex], "issue #697", None),
Case(e1, f1, [Edge], "issue #697", "Edge.intersect only takes 1D"), Case(e1, f1, [Edge], "issue #697", None),
] ]
@pytest.mark.parametrize("obj, target, expected", make_params(issues_matrix)) @pytest.mark.parametrize("obj, target, expected", make_params(issues_matrix))
@ -279,6 +469,9 @@ def test_issues(obj, target, expected):
exception_matrix = [ exception_matrix = [
Case(vt1, Color(), None, "Unsupported type", None), Case(vt1, Color(), None, "Unsupported type", None),
Case(ed1, Color(), None, "Unsupported type", None), Case(ed1, Color(), None, "Unsupported type", None),
Case(fc1, Color(), None, "Unsupported type", None),
Case(sl1, Color(), None, "Unsupported type", None),
Case(cp1, Color(), None, "Unsupported type", None),
] ]
@pytest.mark.skip @pytest.mark.skip

View file

@ -28,6 +28,7 @@ license:
import math import math
import unittest import unittest
from unittest.mock import patch
from build123d.build_enums import ( from build123d.build_enums import (
CenterOf, CenterOf,
@ -106,13 +107,73 @@ class TestMixin1D(unittest.TestCase):
5, 5,
) )
def test_positions(self): def test_positions_with_distances(self):
e = Edge.make_line((0, 0, 0), (1, 1, 1)) e = Edge.make_line((0, 0, 0), (1, 1, 1))
distances = [i / 4 for i in range(3)] distances = [i / 4 for i in range(3)]
pts = e.positions(distances) pts = e.positions(distances)
for i, position in enumerate(pts): for i, position in enumerate(pts):
self.assertAlmostEqual(position, (i / 4, i / 4, i / 4), 5) self.assertAlmostEqual(position, (i / 4, i / 4, i / 4), 5)
def test_positions_deflection_line(self):
"""Deflection sampling on a straight line should yield exactly 2 points."""
e = Edge.make_line((0, 0, 0), (10, 0, 0))
pts = e.positions(deflection=0.1)
self.assertEqual(len(pts), 2)
self.assertAlmostEqual(pts[0], (0, 0, 0), 7)
self.assertAlmostEqual(pts[1], (10, 0, 0), 7)
def test_positions_deflection_circle(self):
"""Deflection on a C2 curve (circle) should produce multiple points."""
radius = 5
e = Edge.make_circle(radius)
pts = e.positions(deflection=0.1)
# Should produce more than just two points
self.assertGreater(len(pts), 2)
# Endpoints should match curve endpoints
first, last = pts[0], pts[-1]
curve = e.geom_adaptor()
p0 = Vector(curve.Value(curve.FirstParameter()))
p1 = Vector(curve.Value(curve.LastParameter()))
self.assertAlmostEqual(first, p0, 7)
self.assertAlmostEqual(last, p1, 7)
def test_positions_deflection_resolution(self):
"""Smaller deflection tolerance should produce more points."""
e = Edge.make_circle(10)
pts_coarse = e.positions(deflection=0.5)
pts_fine = e.positions(deflection=0.05)
self.assertGreater(len(pts_fine), len(pts_coarse))
def test_positions_deflection_C0_curve(self):
"""C0 spline should use QuasiUniformDeflection and still succeed."""
e = Polyline((0, 0), (1, 2), (2, 0))._to_bspline() # C0
pts = e.positions(deflection=0.1)
self.assertGreater(len(pts), 2)
def test_positions_missing_arguments(self):
e = Edge.make_line((0, 0, 0), (1, 0, 0))
with self.assertRaises(ValueError):
e.positions()
def test_positions_deflection_failure(self):
e = Edge.make_circle(1.0)
with patch("build123d.topology.one_d.GCPnts_UniformDeflection") as MockDefl:
instance = MockDefl.return_value
instance.IsDone.return_value = False
instance.NbPoints.return_value = 0
with self.assertRaises(RuntimeError):
e.positions(deflection=0.1)
def test_tangent_at(self): def test_tangent_at(self):
self.assertAlmostEqual( self.assertAlmostEqual(
Edge.make_circle(1, start_angle=0, end_angle=90).tangent_at(1.0), Edge.make_circle(1, start_angle=0, end_angle=90).tangent_at(1.0),

View file

@ -229,13 +229,15 @@ class TestOrientedBoundBox(unittest.TestCase):
obb = OrientedBoundBox(rect) obb = OrientedBoundBox(rect)
corners = obb.corners corners = obb.corners
poly = Polygon(*corners, align=None) poly = Polygon(*corners, align=None)
self.assertAlmostEqual(rect.intersect(poly).area, rect.area, 5) area = sum(f.area for f in rect.intersect(poly).faces())
self.assertAlmostEqual(area, rect.area, 5)
for face in Box(1, 2, 3).faces(): for face in Box(1, 2, 3).faces():
obb = OrientedBoundBox(face) obb = OrientedBoundBox(face)
corners = obb.corners corners = obb.corners
poly = Polygon(*corners, align=None) poly = Polygon(*corners, align=None)
self.assertAlmostEqual(face.intersect(poly).area, face.area, 5) area = sum(f.area for f in face.intersect(poly).faces())
self.assertAlmostEqual(area, face.area, 5)
def test_line_corners(self): def test_line_corners(self):
""" """

View file

@ -172,10 +172,11 @@ class TestShape(unittest.TestCase):
self.assertEqual(len(top), 2) self.assertEqual(len(top), 2)
self.assertAlmostEqual(top[0].length, 3, 5) self.assertAlmostEqual(top[0].length, 3, 5)
def test_split_return_none(self): def test_split_invalid_keep(self):
shape = Box(1, 1, 1) - Pos((0, 0, -0.25)) * Box(1, 0.5, 0.5) with self.assertRaises(ValueError):
split_shape = shape.split(Plane.XY, keep=Keep.INSIDE) Box(1, 1, 1).split(Plane.XY, keep=Keep.INSIDE)
self.assertIsNone(split_shape) with self.assertRaises(ValueError):
Box(1, 1, 1).split(Plane.XY, keep=Keep.OUTSIDE)
def test_split_by_perimeter(self): def test_split_by_perimeter(self):
# Test 0 - extract a spherical cap # Test 0 - extract a spherical cap
@ -299,7 +300,8 @@ class TestShape(unittest.TestCase):
predicted_location = Location(offset) * Rotation(*rotation) predicted_location = Location(offset) * Rotation(*rotation)
located_shape = Solid.make_box(1, 1, 1).locate(predicted_location) located_shape = Solid.make_box(1, 1, 1).locate(predicted_location)
intersect = shape.intersect(located_shape) intersect = shape.intersect(located_shape)
self.assertAlmostEqual(intersect.volume, 1, 5) volume = sum(s.volume for s in intersect.solids())
self.assertAlmostEqual(volume, 1, 5)
def test_position_and_orientation(self): def test_position_and_orientation(self):
box = Solid.make_box(1, 1, 1).locate(Location((1, 2, 3), (10, 20, 30))) box = Solid.make_box(1, 1, 1).locate(Location((1, 2, 3), (10, 20, 30)))
@ -475,7 +477,7 @@ class TestShape(unittest.TestCase):
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5) self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
self.assertListEqual(edges, []) self.assertListEqual(edges, [])
verts, edges = Vertex(1, 2, 0)._ocp_section(Face.make_plane(Plane.XY)) verts, edges = Vertex(1, 2, 0)._ocp_section(Face(Plane.XY))
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5) self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
self.assertListEqual(edges, []) self.assertListEqual(edges, [])
@ -493,7 +495,7 @@ class TestShape(unittest.TestCase):
self.assertEqual(len(edges1), 1) self.assertEqual(len(edges1), 1)
self.assertAlmostEqual(edges1[0].length, 20, 5) self.assertAlmostEqual(edges1[0].length, 20, 5)
vertices2, edges2 = cylinder._ocp_section(Face.make_plane(pln)) vertices2, edges2 = cylinder._ocp_section(Face(pln))
self.assertEqual(len(vertices2), 1) self.assertEqual(len(vertices2), 1)
self.assertEqual(len(edges2), 1) self.assertEqual(len(edges2), 1)
self.assertAlmostEqual(Vector(vertices2[0]), (5, 0, 0), 5) self.assertAlmostEqual(Vector(vertices2[0]), (5, 0, 0), 5)
@ -588,7 +590,7 @@ class TestShape(unittest.TestCase):
empty.distance_to_with_closest_points(Vector(1, 1, 1)) empty.distance_to_with_closest_points(Vector(1, 1, 1))
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
empty.distance_to(Vector(1, 1, 1)) empty.distance_to(Vector(1, 1, 1))
with self.assertRaises(ValueError): with self.assertRaises(AttributeError):
box.intersect(empty_loc) box.intersect(empty_loc)
self.assertEqual(empty._ocp_section(Vertex(1, 1, 1)), ([], [])) self.assertEqual(empty._ocp_section(Vertex(1, 1, 1)), ([], []))
self.assertEqual(empty.faces_intersected_by_axis(Axis.Z), ShapeList()) self.assertEqual(empty.faces_intersected_by_axis(Axis.Z), ShapeList())

View file

@ -153,7 +153,9 @@ class TestSolid(unittest.TestCase):
self.assertAlmostEqual(twist.volume, 1, 5) self.assertAlmostEqual(twist.volume, 1, 5)
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45) top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
bottom = twist.faces().sort_by(Axis.Z)[0] bottom = twist.faces().sort_by(Axis.Z)[0]
self.assertAlmostEqual(top.translate((0, 0, -1)).intersect(bottom).area, 1, 5) intersect = top.translate((0, 0, -1)).intersect(bottom)
area = sum(f.area for f in intersect.faces())
self.assertAlmostEqual(area, 1, 5)
# Wire # Wire
base = Wire.make_rect(1, 1) base = Wire.make_rect(1, 1)
twist = Solid.extrude_linear_with_rotation( twist = Solid.extrude_linear_with_rotation(
@ -162,7 +164,9 @@ class TestSolid(unittest.TestCase):
self.assertAlmostEqual(twist.volume, 1, 5) self.assertAlmostEqual(twist.volume, 1, 5)
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45) top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
bottom = twist.faces().sort_by(Axis.Z)[0] bottom = twist.faces().sort_by(Axis.Z)[0]
self.assertAlmostEqual(top.translate((0, 0, -1)).intersect(bottom).area, 1, 5) intersect = top.translate((0, 0, -1)).intersect(bottom)
area = sum(f.area for f in intersect.faces())
self.assertAlmostEqual(area, 1, 5)
def test_make_loft(self): def test_make_loft(self):
loft = Solid.make_loft( loft = Solid.make_loft(

View file

@ -231,7 +231,8 @@ class DimensionLineTestCase(unittest.TestCase):
], ],
draft=metric, draft=metric,
) )
self.assertGreater(hole.intersect(d_line).area, 0) area = sum(f.area for f in hole.intersect(d_line).faces())
self.assertGreater(area, 0)
def test_outside_arrows(self): def test_outside_arrows(self):
d_line = DimensionLine([(0, 0, 0), (15, 0, 0)], draft=metric) d_line = DimensionLine([(0, 0, 0), (15, 0, 0)], draft=metric)

View file

@ -1,3 +1,4 @@
from io import BytesIO
from os import fsdecode, fsencode from os import fsdecode, fsencode
from typing import Union, Iterable from typing import Union, Iterable
import math import math
@ -194,7 +195,9 @@ class ExportersTestCase(unittest.TestCase):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"format", (Path, fsencode, fsdecode), ids=["path", "bytes", "str"] "format",
(Path, fsencode, fsdecode),
ids=["path", "bytes", "str"],
) )
@pytest.mark.parametrize("Exporter", (ExportSVG, ExportDXF)) @pytest.mark.parametrize("Exporter", (ExportSVG, ExportDXF))
def test_pathlike_exporters(tmp_path, format, Exporter): def test_pathlike_exporters(tmp_path, format, Exporter):
@ -205,5 +208,14 @@ def test_pathlike_exporters(tmp_path, format, Exporter):
exporter.write(path) exporter.write(path)
@pytest.mark.parametrize("Exporter", (ExportSVG, ExportDXF))
def test_exporters_in_memory(Exporter):
buffer = BytesIO()
sketch = ExportersTestCase.create_test_sketch()
exporter = Exporter()
exporter.add_shape(sketch)
exporter.write(buffer)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -206,10 +206,11 @@ def test_pathlike_exporters(tmp_path, format, exporter):
exporter(box, path) exporter(box, path)
def test_export_brep_in_memory(): @pytest.mark.parametrize("exporter", (export_step, export_brep))
def test_exporters_in_memory(exporter):
buffer = io.BytesIO() buffer = io.BytesIO()
box = Box(1, 1, 1).locate(Pos(-1, -2, -3)) box = Box(1, 1, 1).locate(Pos(-1, -2, -3))
export_brep(box, buffer) exporter(box, buffer)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,4 +1,5 @@
import unittest, uuid import unittest, uuid
from io import BytesIO
from packaging.specifiers import SpecifierSet from packaging.specifiers import SpecifierSet
from pathlib import Path from pathlib import Path
from os import fsdecode, fsencode from os import fsdecode, fsencode
@ -209,6 +210,7 @@ class TestHollowImport(unittest.TestCase):
importer = Mesher() importer = Mesher()
stl = importer.read("test.stl") stl = importer.read("test.stl")
self.assertTrue(stl[0].is_valid) self.assertTrue(stl[0].is_valid)
self.assertAlmostEqual(test_shape.volume, stl[0].volume, 0)
class TestImportDegenerateTriangles(unittest.TestCase): class TestImportDegenerateTriangles(unittest.TestCase):
@ -237,5 +239,13 @@ def test_pathlike_mesher(tmp_path, format):
importer.read(path) importer.read(path)
@pytest.mark.parametrize("file_type", ("3mf", "stl"))
def test_in_memory_mesher(file_type):
stream = BytesIO()
exporter = Mesher()
exporter.add_shape(Solid.make_box(1, 1, 1))
exporter.write_stream(stream, file_type)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()