Compare commits
No commits in common. "dev" and "v0.10.0" have entirely different histories.
5
.github/actions/setup/action.yml
vendored
|
|
@ -3,9 +3,6 @@ inputs:
|
|||
python-version: # id of input
|
||||
description: 'Python version'
|
||||
required: true
|
||||
optional-dependencies:
|
||||
description: 'Optional build123d dependencies'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
|
@ -18,4 +15,4 @@ runs:
|
|||
- name: Install Requirements
|
||||
shell: bash
|
||||
run: |
|
||||
uv pip install .[${{ inputs.optional-dependencies }}]
|
||||
uv pip install .[development]
|
||||
|
|
|
|||
1
.github/workflows/benchmark.yml
vendored
|
|
@ -20,7 +20,6 @@ jobs:
|
|||
- uses: ./.github/actions/setup/
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
optional-dependencies: "development"
|
||||
- name: benchmark
|
||||
run: |
|
||||
python -m pytest --benchmark-only --benchmark-autosave
|
||||
|
|
|
|||
1
.github/workflows/coverage.yml
vendored
|
|
@ -10,7 +10,6 @@ jobs:
|
|||
uses: ./.github/actions/setup/
|
||||
with:
|
||||
python-version: "3.10"
|
||||
optional-dependencies: "development"
|
||||
- name: Run tests and collect coverage
|
||||
run: pytest --cov=build123d
|
||||
- name: Upload coverage to Codecov
|
||||
|
|
|
|||
2
.github/workflows/lint.yml
vendored
|
|
@ -12,6 +12,6 @@ jobs:
|
|||
- uses: ./.github/actions/setup
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
optional-dependencies: "development"
|
||||
|
||||
- name: lint
|
||||
run: pylint --rcfile=.pylintrc --fail-under=9.5 src/build123d
|
||||
|
|
|
|||
1
.github/workflows/mypy.yml
vendored
|
|
@ -20,7 +20,6 @@ jobs:
|
|||
uses: ./.github/actions/setup
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
optional-dependencies: "development,stubs"
|
||||
|
||||
- name: Typecheck
|
||||
run: |
|
||||
|
|
|
|||
1
.github/workflows/test.yml
vendored
|
|
@ -21,7 +21,6 @@ jobs:
|
|||
- uses: ./.github/actions/setup/
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
optional-dependencies: "development"
|
||||
- name: test
|
||||
run: |
|
||||
python -m pytest -n auto --benchmark-disable
|
||||
|
|
|
|||
3
.gitignore
vendored
|
|
@ -41,6 +41,3 @@ venv.bak/
|
|||
|
||||
# Profiling debris.
|
||||
prof/
|
||||
|
||||
# MacOS cruft
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -1,34 +1,13 @@
|
|||
# Contributing
|
||||
|
||||
When writing code for inclusion in build123d please add docstrings and
|
||||
When writing code for inclusion in build123d please add docs and
|
||||
tests, ensure they build and pass, and ensure that `pylint` and `mypy`
|
||||
are happy with your code.
|
||||
|
||||
## Setup
|
||||
|
||||
Ensure `pip` is installed and [up-to-date](https://pip.pypa.io/en/stable/installation/#upgrading-pip).
|
||||
Clone the build123d repo and install in editable mode:
|
||||
|
||||
```
|
||||
git clone https://github.com/gumyr/build123d.git
|
||||
cd build123d
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
Install development and docs dependencies:
|
||||
|
||||
```
|
||||
pip install -e ".[development]"
|
||||
pip install -e ".[docs]"
|
||||
```
|
||||
|
||||
## Before submitting a PR
|
||||
|
||||
- Install `pip` following their [documentation](https://pip.pypa.io/en/stable/installation/).
|
||||
- Install development dependencies: `pip install -e .[development]`
|
||||
- Install docs dependencies: `pip install -e .[docs]`
|
||||
- Install `build123d` in editable mode from current dir: `pip install -e .`
|
||||
- Run tests with: `python -m pytest -n auto`
|
||||
- Check added files' style with: `pylint <path/to/file.py>`
|
||||
- Build docs with: `cd docs && make html`
|
||||
- Check added files' style with: `pylint <path/to/file.py>`
|
||||
- Check added files' type annotations with: `mypy <path/to/file.py>`
|
||||
- Run black formatter against files' changed: `black <path/to/file.py>`
|
||||
|
||||
To verify documentation changes build docs with:
|
||||
- Linux/macOS: `./docs/make html`
|
||||
- Windows: `./docs/make.bat html`
|
||||
- Run black formatter against files' changed: `black --config pyproject.toml <path/to/file.py>` (where the pyproject.toml is from this project's repository)
|
||||
|
|
|
|||
236
README.md
|
|
@ -1,6 +1,6 @@
|
|||
<h1 align="center">
|
||||
<p align="center">
|
||||
<img alt="build123d logo" src="https://github.com/gumyr/build123d/raw/dev/docs/assets/build123d_logo/logo-banner.svg">
|
||||
</h1>
|
||||
</p>
|
||||
|
||||
[](https://build123d.readthedocs.io/en/latest/?badge=latest)
|
||||
[](https://github.com/gumyr/build123d/actions/workflows/test.yml)
|
||||
|
|
@ -16,241 +16,65 @@
|
|||
[](https://pepy.tech/project/build123d)
|
||||
[](https://pepy.tech/project/build123d)
|
||||
[](https://pypi.org/project/build123d/)
|
||||
[](https://zenodo.org/badge/latestdoi/510925389)
|
||||
[](https://doi.org/10.5281/zenodo.14872322)
|
||||
|
||||
[Documentation](https://build123d.readthedocs.io/en/latest/index.html) |
|
||||
[Cheat Sheet](https://build123d.readthedocs.io/en/latest/cheat_sheet.html) |
|
||||
[Discord](https://discord.com/invite/Bj9AQPsCfx) |
|
||||
[Discussions](https://github.com/gumyr/build123d/discussions) |
|
||||
[Issues](https://github.com/gumyr/build123d/issues ) |
|
||||
[Contributing](#contributing)
|
||||
|
||||
build123d is a Python-based, parametric [boundary representation (BREP)][BREP] modeling framework for 2D and 3D CAD. Built on the [Open Cascade] geometric kernel, it provides a clean, fully Pythonic interface for creating precise models suitable for 3D printing, CNC machining, laser cutting, and other manufacturing processes.
|
||||
|
||||
<div align="left">
|
||||
<img style="height:200px" alt="bracket" src="https://github.com/gumyr/build123d/raw/dev/docs/assets/examples/toy_truck.png">
|
||||
<img style="height:200px" alt="key cap" src="https://github.com/gumyr/build123d/raw/dev/docs/assets/examples/key_cap.png">
|
||||
<img style="height:200px" alt="hangar" src="https://github.com/gumyr/build123d/raw/dev/docs/assets/ttt/ttt-23-02-02-sm_hanger_object.png">
|
||||
</div>
|
||||
|
||||
## Features
|
||||
Build123d is a Python-based, parametric [boundary representation (BREP)][BREP] modeling framework for 2D and 3D CAD. Built on the [Open Cascade] geometric kernel, it provides a clean, fully Pythonic interface for creating precise models suitable for 3D printing, CNC machining, laser cutting, and other manufacturing processes. Models can be exported to popular CAD tools such as [FreeCAD] and SolidWorks.
|
||||
|
||||
Designed for modern, maintainable CAD-as-code, build123d combines clear architecture with expressive, algebraic modeling. It offers:
|
||||
- Minimal or no internal state depending on mode,
|
||||
- Explicit 1D, 2D, and 3D geometry classes with well-defined operations,
|
||||
- Extensibility through subclassing and functional composition—no monkey patching,
|
||||
- Standards-compliant code (PEP 8, mypy, pylint) with rich pylance type hints,
|
||||
- Deep Python integration—selectors as lists, locations as iterables, and natural conversions (`Solid(shell)`, `tuple(Vector)`),
|
||||
- Operator-driven modeling (`obj += sub_obj`, `Plane.XZ * Pos(X=5) * Rectangle(1, 1)`) for algebraic, readable, and composable design logic,
|
||||
- Export formats to popular CAD tools such as [FreeCAD] and SolidWorks.
|
||||
- Deep Python integration—selectors as lists, locations as iterables, and natural conversions (Solid(shell), tuple(Vector)),
|
||||
- Operator-driven modeling (obj += sub_obj, Plane.XZ * Pos(X=5) * Rectangle(1, 1)) for algebraic, readable, and composable design logic.
|
||||
|
||||
## Usage
|
||||
The result is a framework that feels native to Python while providing the full power of OpenCascade geometry underneath.
|
||||
|
||||
Although wildcard imports are generally bad practice, build123d scripts are usually self contained and importing the large number of objects and methods into the namespace is common:
|
||||
The documentation for **build123d** can be found at [readthedocs](https://build123d.readthedocs.io/en/latest/index.html).
|
||||
|
||||
```py
|
||||
from build123d import *
|
||||
```
|
||||
There is a [***Discord***](https://discord.com/invite/Bj9AQPsCfx) server (shared with [CadQuery]) where you can ask for help in the build123d channel.
|
||||
|
||||
### Constructing a 1D Shape
|
||||
|
||||
Edges, Wires (multiple connected Edges), and Curves (a Compound of Edges and Wires) are the 1D Shapes available in build123d. A single Edge can be created from a Line object with two vector-like positions:
|
||||
|
||||
```py
|
||||
line = Line((0, -3), (6, -3))
|
||||
```
|
||||
|
||||
Additional Edges and Wires may be added to (or subtracted from) the initial line. These objects can reference coordinates along another line through the position (`@`) and tangent (`%`) operators to specify input Vectors:
|
||||
|
||||
```py
|
||||
line += JernArc(line @ 1, line % 1, radius=3, arc_size=180)
|
||||
line += PolarLine(line @ 1, 6, direction=line % 1)
|
||||
```
|
||||
|
||||
<div align="left">
|
||||
<img style="max-height:150px" alt="create 1d" src="https://github.com/gumyr/build123d/raw/dev/docs/assets/readme/create_1d.png">
|
||||
</div>
|
||||
|
||||
### Upgrading to 2D and 3D
|
||||
|
||||
Faces, Shells (multiple connected Faces), and Sketches (a Compound of Faces and Shells) are the 2D Shapes available in build123d. The previous line is sufficiently defined to close the Wire and create a Face with `make_hull`:
|
||||
|
||||
```py
|
||||
sketch = make_hull(line)
|
||||
```
|
||||
|
||||
A Circle face is translated with `Pos`, a Location object like `Rot` for transforming Shapes, and subtracted from the sketch. This sketch face is then extruded into a Solid part:
|
||||
|
||||
```py
|
||||
sketch -= Pos(6, 0, 0) * Circle(2)
|
||||
part = extrude(sketch, amount= 2)
|
||||
```
|
||||
|
||||
<div align="left">
|
||||
<img style="max-height:150px" alt="upgrade 2D" src="https://github.com/gumyr/build123d/raw/dev/docs/assets/readme/upgrade_2d.png">
|
||||
</div>
|
||||
|
||||
### Adding to and modifying part
|
||||
|
||||
Solids and Parts (a Compound of Solids) are the 1D Shapes available in build123d. A second part can be created from an additional Face. Planes can also be used for positioning and orienting Shape objects. Many objects offer an affordance for alignment relative to the object origin:
|
||||
|
||||
```py
|
||||
plate_sketch = Plane.YZ * RectangleRounded(16, 6, 1.5, align=(Align.CENTER, Align.MIN))
|
||||
plate = extrude(plate_sketch, amount=-2)
|
||||
```
|
||||
|
||||
Shape topology can be extracted from Shapes with selectors which return ShapeLists. ShapeLists offer methods for sorting, grouping, and filtering Shapes by Shape properties, such as finding a Face by area and selecting position along an Axis and specifying a target with a list slice. A Plane is created from the specified Face to locate an iterable of Locations to place multiple objects on the second part before it is added to the main part:
|
||||
|
||||
```py
|
||||
plate_face = plate.faces().group_by(Face.area)[-1].sort_by(Axis.X)[-1]
|
||||
plate -= Plane(plate_face) * GridLocations(13, 3, 2, 2) * CounterSinkHole(.5, 1, 2)
|
||||
|
||||
part += plate
|
||||
```
|
||||
|
||||
ShapeList selectors and operators offer powerful methods for specifying Shape features through properties such as length/area/volume, orientation relative to an Axis or Plane, and geometry type:
|
||||
|
||||
```py
|
||||
part = fillet(part.edges().filter_by(lambda e: e.length == 2).filter_by(Axis.Z), 1)
|
||||
bore = part.faces().filter_by(GeomType.CYLINDER).filter_by(lambda f: f.radius == 2)
|
||||
part = chamfer(bore.edges(), .2)
|
||||
```
|
||||
|
||||
<div align="left">
|
||||
<img style="max-height:150px" alt="modify part" src="https://github.com/gumyr/build123d/raw/dev/docs/assets/readme/add_part.png">
|
||||
</div>
|
||||
|
||||
### Builder Mode
|
||||
|
||||
The previous construction is through the **Algebra Mode** interface, which follows a stateless paradigm where each object is explicitly tracked and mutated by algebraic operators.
|
||||
|
||||
**Builder Mode** is an alternative build123d interface where state is tracked and structured in a design history-like way where each dimension is distinct. Operations are aware pending faces and edges from Build contexts and location transformations are applied to all child objects in Build and Locations contexts. While each Build context tracks state, operations like `extrude` can still optionally take explicit Shape input instead of implicitly using pending Shapes. Builder mode also introduces the `mode` affordance to objects to specify how new Shapes are combined with the context:
|
||||
|
||||
```py
|
||||
with BuildPart() as part_context:
|
||||
with BuildSketch() as sketch:
|
||||
with BuildLine() as line:
|
||||
l1 = Line((0, -3), (6, -3))
|
||||
l2 = JernArc(l1 @ 1, l1 % 1, radius=3, arc_size=180)
|
||||
l3 = PolarLine(l2 @ 1, 6, direction=l2 % 1)
|
||||
l4 = Line(l1 @ 0, l3 @ 1)
|
||||
make_face()
|
||||
|
||||
with Locations((6, 0, 0)):
|
||||
Circle(2, mode=Mode.SUBTRACT)
|
||||
|
||||
extrude(amount=2)
|
||||
|
||||
with BuildSketch(Plane.YZ) as plate_sketch:
|
||||
RectangleRounded(16, 6, 1.5, align=(Align.CENTER, Align.MIN))
|
||||
|
||||
plate = extrude(amount=-2)
|
||||
|
||||
with Locations(plate.faces().group_by(Face.area)[-1].sort_by(Axis.X)[-1]):
|
||||
with GridLocations(13, 3, 2, 2):
|
||||
CounterSinkHole(.5, 1)
|
||||
|
||||
fillet(edges().filter_by(lambda e: e.length == 2).filter_by(Axis.Z), 1)
|
||||
bore = faces().filter_by(GeomType.CYLINDER).filter_by(lambda f: f.radius == 2)
|
||||
chamfer(bore.edges(), .2)
|
||||
```
|
||||
|
||||
### Extending objects
|
||||
|
||||
New objects may be created for parametric reusability from base object classes:
|
||||
|
||||
```py
|
||||
class Punch(BaseSketchObject):
|
||||
def __init__(
|
||||
self,
|
||||
radius: float,
|
||||
size: float,
|
||||
blobs: float,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
with BuildSketch() as punch:
|
||||
if blobs == 1:
|
||||
Circle(size)
|
||||
else:
|
||||
with PolarLocations(radius, blobs):
|
||||
Circle(size)
|
||||
|
||||
if len(faces()) > 1:
|
||||
raise RuntimeError("radius is too large for number and size of blobs")
|
||||
|
||||
add(Face(faces()[0].outer_wire()), mode=Mode.REPLACE)
|
||||
|
||||
super().__init__(obj=punch.sketch, mode=mode)
|
||||
|
||||
tape = Rectangle(20, 5)
|
||||
for i, location in enumerate(GridLocations(5, 0, 4, 1)):
|
||||
tape -= location * Punch(.8, 1, i + 1)
|
||||
```
|
||||
|
||||
<div align="left">
|
||||
<img style="max-height:150px" alt="extend" src="https://github.com/gumyr/build123d/raw/dev/docs/assets/readme/extend.png">
|
||||
</div>
|
||||
|
||||
### Data interchange
|
||||
|
||||
build123d can import and export a number data formats for interchange with 2D and 3D design tools, 3D printing slicers, and traditional CAM:
|
||||
|
||||
```py
|
||||
svg = import_svg("spade.svg")
|
||||
step = import_step("nema-17-bracket.step")
|
||||
|
||||
export_stl(part, "bracket.stl")
|
||||
export_step(part_context.part, "bracket.step")
|
||||
```
|
||||
|
||||
### Further reading
|
||||
|
||||
More [Examples](https://build123d.readthedocs.io/en/latest/introductory_examples.html) and [Tutorials](https://build123d.readthedocs.io/en/latest/tutorials.html) are found in the documentation.
|
||||
|
||||
## Installation
|
||||
|
||||
For additional installation options see [Installation](https://build123d.readthedocs.io/en/latest/installation.html)
|
||||
|
||||
### Current release
|
||||
|
||||
Installing build123d from `pip` is recommended for most users:
|
||||
The recommended method for most users to install **build123d** is:
|
||||
|
||||
```
|
||||
pip install build123d
|
||||
```
|
||||
|
||||
If you receive errors about conflicting dependencies, retry the installation after upgrading pip to the latest version:
|
||||
To get the latest non-released version of **build123d** one can install from GitHub using one of the following two commands:
|
||||
|
||||
Linux/MacOS:
|
||||
|
||||
```
|
||||
pip install --upgrade pip
|
||||
python3 -m pip install git+https://github.com/gumyr/build123d
|
||||
```
|
||||
|
||||
### Pre-release
|
||||
|
||||
build123d is under active development and up-to-date features are found in the
|
||||
development branch:
|
||||
Windows:
|
||||
|
||||
```
|
||||
pip install git+https://github.com/gumyr/build123d
|
||||
python -m pip install git+https://github.com/gumyr/build123d
|
||||
```
|
||||
|
||||
### Viewers
|
||||
If you receive errors about conflicting dependencies, you can retry the installation after having upgraded pip to the latest version with the following command:
|
||||
```
|
||||
python3 -m pip install --upgrade pip
|
||||
```
|
||||
|
||||
build123d is best used with a viewer. The most popular viewer is [ocp_vscode](https://github.com/bernhard-42/vscode-ocp-cad-viewer), a Python package with a standalone viewer and VS Code extension. Other [Editors & Viewers](https://build123d.readthedocs.io/en/latest/external.html#external) are found in the documentation.
|
||||
Development install:
|
||||
|
||||
## Contributing
|
||||
```
|
||||
git clone https://github.com/gumyr/build123d.git
|
||||
cd build123d
|
||||
python3 -m pip install -e .
|
||||
```
|
||||
|
||||
build123d is a rapidly growing project and we welcome all contributions. Whether you want to share ideas, report bugs, or implement new features, your contribution is welcome! Please see our [CONTRIBUTING.md](CONTRIBUTING.md) file to get started.
|
||||
Further installation instructions are available (e.g. Poetry) see the [installation section on readthedocs](https://build123d.readthedocs.io/en/latest/installation.html).
|
||||
|
||||
## Attribution
|
||||
Attribution:
|
||||
|
||||
build123d is derived from portions of [CadQuery], but is extensively refactored and restructured into an independent framework over [Open Cascade].
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [Apache License 2.0](LICENSE).
|
||||
Build123d was originally derived from portions of the [CadQuery] codebase but has since been extensively refactored and restructured into an independent system.
|
||||
|
||||
[BREP]: https://en.wikipedia.org/wiki/Boundary_representation
|
||||
[CadQuery]: https://cadquery.readthedocs.io/en/latest/index.html
|
||||
[FreeCAD]: https://www.freecad.org/
|
||||
[Open Cascade]: https://dev.opencascade.org/
|
||||
[Open Cascade]: https://dev.opencascade.org/
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ build123d of a piece of angle iron:
|
|||
|
||||
**build123d Approach**
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
# Builder mode
|
||||
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)
|
||||
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
# Algebra mode
|
||||
profile = Rectangle(3 * CM, 4 * MM, align=Align.MIN)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ python context manager.
|
|||
...
|
||||
)
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
# build123d API
|
||||
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
|
||||
an instance variable as with standard python programming for direct use.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildSketch() as plan:
|
||||
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
|
||||
features to these points without knowing their numeric values.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildLine() as outline:
|
||||
...
|
||||
|
|
@ -81,7 +81,6 @@ by the last operation and fillets them. Such a selection would be quite difficul
|
|||
otherwise.
|
||||
|
||||
.. literalinclude:: ../examples/intersecting_pipes.py
|
||||
:language: build123d
|
||||
:lines: 30, 39-49
|
||||
|
||||
|
||||
|
|
@ -105,7 +104,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
|
||||
a fully custom selection:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
top = rail.faces().filter_by(Axis.Z)[-1]
|
||||
...
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ Objects and arithmetic
|
|||
|
||||
:math:`B^2 := \lbrace` ``Sketch``, ``Rectangle``, ``Circle``, ``Ellipse``, ``Rectangle``, ``Polygon``, ``RegularPolygon``, ``Text``, ``Trapezoid``, ``SlotArc``, ``SlotCenterPoint``, ``SlotCenterToCenter``, ``SlotOverall`` :math:`\rbrace`
|
||||
|
||||
:math:`B^1 := \lbrace` ``Curve``, ``Bezier``, ``FilletPolyline``, ``PolarLine``, ``Polyline``, ``Spline``, ``Helix``, ``CenterArc``, ``EllipticalCenterArc``, ``ParabolicCenterArc``, ``HyperbolicCenterArc``, ``RadiusArc``, ``SagittaArc``, ``TangentArc``, ``ThreePointArc``, ``JernArc`` :math:`\rbrace`
|
||||
:math:`B^1 := \lbrace` ``Curve``, ``Bezier``, ``FilletPolyline``, ``PolarLine``, ``Polyline``, ``Spline``, ``Helix``, ``CenterArc``, ``EllipticalCenterArc``, ``RadiusArc``, ``SagittaArc``, ``TangentArc``, ``ThreePointArc``, ``JernArc`` :math:`\rbrace`
|
||||
|
||||
with :math:`B^3 \subset C^3, B^2 \subset C^2` and :math:`B^1 \subset C^1`
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
already fused. Overall it takes on an M1 Mac 4.76 sec.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
diam = 80
|
||||
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
|
||||
then call ``fuse`` (``+``) once with all objects and ``clean`` once. Overall it takes 0.19 sec.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
r = Rectangle(2, 2)
|
||||
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
|
||||
an empty ``Part``, ``Sketch`` or ``Curve``:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
polygons = Sketch() + [
|
||||
loc * RegularPolygon(radius=5, side_count=5)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ Here we'll assign labels to all of the components that will be part of the box
|
|||
assembly:
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Add labels]
|
||||
:end-before: [Create assembly]
|
||||
|
||||
|
|
@ -37,7 +36,6 @@ Creation of the assembly is done by simply creating a :class:`~topology.Compound
|
|||
appropriate ``parent`` and ``children`` attributes as shown here:
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Create assembly]
|
||||
:end-before: [Display assembly]
|
||||
|
||||
|
|
@ -45,7 +43,6 @@ To display the topology of an assembly :class:`~topology.Compound`, the :meth:`~
|
|||
method can be used as follows:
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Display assembly]
|
||||
:end-before: [Add to the assembly by assigning the parent attribute of an object]
|
||||
|
||||
|
|
@ -62,7 +59,6 @@ which results in:
|
|||
To add to an assembly :class:`~topology.Compound` one can change either ``children`` or ``parent`` attributes.
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
: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]
|
||||
|
||||
|
|
@ -184,7 +180,7 @@ Compare this to assembly3_volume which only results in the volume of the top lev
|
|||
assembly2 = Compound(label='Assembly2', children=[assembly1, Box(1, 1, 1)])
|
||||
assembly3 = Compound(label='Assembly3', children=[assembly2, Box(1, 1, 1)])
|
||||
total_volume = sum(part.volume for part in assembly3.solids()) # 3
|
||||
assembly3_volume = assembly3.volume # 1
|
||||
assembly3_volume = assembly3.volume # 1
|
||||
|
||||
******
|
||||
pack
|
||||
|
|
@ -273,6 +269,6 @@ If you place the arranged objects into a ``Compound``, you can easily determine
|
|||
# [bounding box]
|
||||
print(Compound(xy_pack).bounding_box())
|
||||
# bbox: 0.0 <= x <= 159.0, 0.0 <= y <= 129.0, -54.0 <= z <= 100.0
|
||||
|
||||
|
||||
print(Compound(z_pack).bounding_box())
|
||||
# bbox: 0.0 <= x <= 159.0, 0.0 <= y <= 129.0, 0.0 <= z <= 100.0
|
||||
|
|
|
|||
|
|
@ -2,79 +2,79 @@
|
|||
<svg width="100.09mm" height="100.09mm" viewBox="-1.0009 -1.0009 2.0018 2.0018" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="scale(1,-1)" stroke-linecap="round">
|
||||
<g fill="none" stroke="rgb(0,0,0)" stroke-width="0.0018">
|
||||
<path d="M 0.933013,0.25 A 0.5,0.5 -89.99999999999996 0,1 0.25,0.933013 A 0.5,0.5 0.0 0,1 -0.25,0.933013 A 0.5,0.5 0.0 0,1 -0.933013,0.25 A 0.5,0.5 0.0 0,1 -0.933013,-0.25 A 0.5,0.5 0.0 0,1 -0.25,-0.933013 A 0.5,0.5 0.0 0,1 0.25,-0.933013 A 0.5,0.5 180.0 0,1 0.933013,-0.25 A 0.5,0.5 -30.000000000000266 0,1 0.933013,0.25 M -0.2599,-0.927144 A 0.49,0.49 0.0 0,0 -0.927144,-0.2599 A 0.5,0.5 0.0 0,1 -0.5,-0.5 A 0.5,0.5 0.0 0,1 -0.2599,-0.927144 M 0.49,-0.4999 A 0.49,0.49 0.011694209715779905 1,0 -0.49,-0.4999 A 0.5,0.5 0.0 0,1 -0.25,-0.433013 A 0.5,0.5 0.0 0,1 0.25,-0.433013 A 0.5,0.5 0.0 0,1 0.49,-0.4999 M 0.927144,-0.2599 A 0.49,0.49 119.34058157502298 0,0 0.2599,-0.927144 A 0.5,0.5 -58.681163150047965 0,1 0.5,-0.5 A 0.5,0.5 0.0 0,1 0.927144,-0.2599 M -0.2599,-0.427144 A 0.49,0.49 0.0 1,0 -0.2599,0.427144 A 0.5,0.5 0.0 0,1 -0.2599,-0.427144 M 0.49,-0.0 A 0.49,0.49 0.0 0,0 -0.49,0.0 A 0.49,0.49 0.0 0,0 0.49,0.0 M -0.2599,0.927144 A 0.5,0.5 0.0 0,1 -0.5,0.5 A 0.5,0.5 0.0 0,1 -0.927144,0.2599 A 0.49,0.49 0.0 0,0 -0.2599,0.927144 M 0.49,0.4999 A 0.5,0.5 0.0 0,1 0.25,0.433013 A 0.5,0.5 0.0 0,1 -0.25,0.433013 A 0.5,0.5 0.0 0,1 -0.49,0.4999 A 0.49,0.49 -179.98830579028402 1,0 0.49,0.4999 M 0.2599,-0.427144 A 0.5,0.5 -58.6811631500469 0,1 0.2599,0.427144 A 0.49,0.49 119.34058157502298 1,0 0.2599,-0.427144 M 0.2599,0.927144 A 0.49,0.49 119.34058157502298 0,0 0.927144,0.2599 A 0.5,0.5 0.0 0,1 0.5,0.5 A 0.5,0.5 -58.681163150048015 0,1 0.2599,0.927144" />
|
||||
<path d="M 0.69104,0.767124 L 0.697432,0.767124 L 0.697432,0.80978 L 0.711924,0.767124 L 0.717905,0.767124 L 0.732466,0.809063 L 0.732466,0.767124 L 0.738857,0.767124 L 0.738857,0.817231 L 0.729937,0.817231 L 0.717939,0.782368 Q 0.716128,0.77707 0.715273,0.774336 Q 0.714521,0.776797 0.712881,0.781753 L 0.701021,0.817231 L 0.69104,0.817231 L 0.69104,0.767124" />
|
||||
<path d="M 0.750684,0.767124 L 0.757314,0.767124 L 0.757314,0.817231 L 0.750684,0.817231 L 0.750684,0.767124" />
|
||||
<path d="M 0.768936,0.767124 L 0.775293,0.767124 L 0.775293,0.806499 L 0.801611,0.767124 L 0.808413,0.767124 L 0.808413,0.817231 L 0.802056,0.817231 L 0.802056,0.777891 L 0.775737,0.817231 L 0.768936,0.817231 L 0.768936,0.767124" />
|
||||
<path d="M 0.69104,0.686631 L 0.697432,0.686631 L 0.697432,0.729287 L 0.711924,0.686631 L 0.717905,0.686631 L 0.732466,0.728569 L 0.732466,0.686631 L 0.738857,0.686631 L 0.738857,0.736738 L 0.729937,0.736738 L 0.717939,0.701875 Q 0.716128,0.696577 0.715273,0.693843 Q 0.714521,0.696304 0.712881,0.70126 L 0.701021,0.736738 L 0.69104,0.736738 L 0.69104,0.686631" />
|
||||
<path d="M 0.750684,0.686631 L 0.757314,0.686631 L 0.757314,0.736738 L 0.750684,0.736738 L 0.750684,0.686631" />
|
||||
<path d="M 0.768936,0.686631 L 0.775293,0.686631 L 0.775293,0.726006 L 0.801611,0.686631 L 0.808413,0.686631 L 0.808413,0.736738 L 0.802056,0.736738 L 0.802056,0.697397 L 0.775737,0.736738 L 0.768936,0.736738 L 0.768936,0.686631" />
|
||||
<path d="M -0.096113,0.777771 L -0.102744,0.779446 C -0.104556,0.764184 -0.125824,0.762037 -0.131711,0.776182 C -0.13568,0.786337 -0.133959,0.795742 -0.12655,0.804397 C -0.119142,0.80932 -0.105948,0.808231 -0.103496,0.796707 L -0.096968,0.798245 C -0.101373,0.821099 -0.14289,0.816329 -0.14041,0.787307 C -0.140336,0.777059 -0.136878,0.769397 -0.130037,0.764321 C -0.119652,0.757557 -0.098714,0.760757 -0.096113,0.777771" />
|
||||
<path d="M -0.087808,0.761877 L -0.050415,0.761877 L -0.050415,0.767791 L -0.081177,0.767791 L -0.081177,0.784846 L -0.053457,0.784846 L -0.053457,0.790725 L -0.081177,0.790725 L -0.081177,0.806072 L -0.051577,0.806072 L -0.051577,0.811985 L -0.087808,0.811985 L -0.087808,0.761877" />
|
||||
<path d="M -0.041323,0.761877 L -0.034966,0.761877 L -0.034966,0.801252 L -0.008647,0.761877 L -0.001846,0.761877 L -0.001846,0.811985 L -0.008203,0.811985 L -0.008203,0.772644 L -0.034521,0.811985 L -0.041323,0.811985 L -0.041323,0.761877" />
|
||||
<path d="M 0.022046,0.761877 L 0.028677,0.761877 L 0.028677,0.806072 L 0.045254,0.806072 L 0.045254,0.811985 L 0.005537,0.811985 L 0.005537,0.806072 L 0.022046,0.806072 L 0.022046,0.761877" />
|
||||
<path d="M 0.052192,0.761877 L 0.089585,0.761877 L 0.089585,0.767791 L 0.058823,0.767791 L 0.058823,0.784846 L 0.086543,0.784846 L 0.086543,0.790725 L 0.058823,0.790725 L 0.058823,0.806072 L 0.088423,0.806072 L 0.088423,0.811985 L 0.052192,0.811985 L 0.052192,0.761877" />
|
||||
<path d="M 0.098848,0.761877 L 0.105479,0.761877 L 0.105479,0.784128 L 0.113169,0.784128 Q 0.115732,0.784128 0.11686,0.783889 Q 0.120468,0.784363 0.128037,0.772302 L 0.134668,0.761877 L 0.143008,0.761877 L 0.134292,0.775515 Q 0.131694,0.779514 0.128789,0.78218 Q 0.127422,0.783411 0.12479,0.784675 C 0.145154,0.786192 0.143243,0.814847 0.121064,0.811985 L 0.098848,0.811985 L 0.098848,0.761877 M 0.105479,0.789871 L 0.105479,0.806448 L 0.121338,0.806448 C 0.1357,0.807482 0.136366,0.78863 0.119731,0.789871 L 0.105479,0.789871" />
|
||||
<path d="M -0.05896,0.691877 L -0.052568,0.691877 L -0.052568,0.734534 L -0.038076,0.691877 L -0.032095,0.691877 L -0.017534,0.733816 L -0.017534,0.691877 L -0.011143,0.691877 L -0.011143,0.741985 L -0.020063,0.741985 L -0.032061,0.707122 Q -0.033872,0.701824 -0.034727,0.699089 Q -0.035479,0.70155 -0.037119,0.706506 L -0.048979,0.741985 L -0.05896,0.741985 L -0.05896,0.691877" />
|
||||
<path d="M 0.000684,0.691877 L 0.007314,0.691877 L 0.007314,0.741985 L 0.000684,0.741985 L 0.000684,0.691877" />
|
||||
<path d="M 0.018936,0.691877 L 0.025293,0.691877 L 0.025293,0.731252 L 0.051611,0.691877 L 0.058413,0.691877 L 0.058413,0.741985 L 0.052056,0.741985 L 0.052056,0.702644 L 0.025737,0.741985 L 0.018936,0.741985 L 0.018936,0.691877" />
|
||||
<path d="M -0.820649,0.761877 L -0.814258,0.761877 L -0.814258,0.804534 L -0.799766,0.761877 L -0.793784,0.761877 L -0.779224,0.803816 L -0.779224,0.761877 L -0.772832,0.761877 L -0.772832,0.811985 L -0.781753,0.811985 L -0.79375,0.777122 Q -0.795562,0.771824 -0.796416,0.769089 Q -0.797168,0.77155 -0.798809,0.776506 L -0.810669,0.811985 L -0.820649,0.811985 L -0.820649,0.761877" />
|
||||
<path d="M -0.767637,0.761877 L -0.760596,0.761877 L -0.755093,0.777053 L -0.734141,0.777053 L -0.728296,0.761877 L -0.720742,0.761877 L -0.74125,0.811985 L -0.748394,0.811985 L -0.767637,0.761877 M -0.753179,0.782454 L -0.747676,0.797151 Q -0.745933,0.801902 -0.744976,0.806721 Q -0.743813,0.802654 -0.741421,0.796331 L -0.736191,0.782454 L -0.753179,0.782454" />
|
||||
<path d="M -0.720537,0.761877 L -0.712573,0.761877 L -0.699995,0.779343 Q -0.699277,0.780369 -0.697603,0.783103 Q -0.696475,0.781292 -0.695415,0.779753 L -0.682803,0.761877 L -0.6746,0.761877 L -0.693569,0.788367 L -0.675967,0.811985 L -0.683179,0.811985 L -0.693262,0.798689 Q -0.695552,0.795715 -0.697227,0.792981 Q -0.698423,0.795134 -0.70126,0.799133 L -0.710352,0.811985 L -0.718247,0.811985 L -0.701157,0.787991 L -0.720537,0.761877" />
|
||||
<path d="M -0.80896,0.691877 L -0.802568,0.691877 L -0.802568,0.734534 L -0.788076,0.691877 L -0.782095,0.691877 L -0.767534,0.733816 L -0.767534,0.691877 L -0.761143,0.691877 L -0.761143,0.741985 L -0.770063,0.741985 L -0.782061,0.707122 Q -0.783872,0.701824 -0.784727,0.699089 Q -0.785479,0.70155 -0.787119,0.706506 L -0.798979,0.741985 L -0.80896,0.741985 L -0.80896,0.691877" />
|
||||
<path d="M -0.749316,0.691877 L -0.742686,0.691877 L -0.742686,0.741985 L -0.749316,0.741985 L -0.749316,0.691877" />
|
||||
<path d="M -0.731064,0.691877 L -0.724707,0.691877 L -0.724707,0.731252 L -0.698389,0.691877 L -0.691587,0.691877 L -0.691587,0.741985 L -0.697944,0.741985 L -0.697944,0.702644 L -0.724263,0.741985 L -0.731064,0.741985 L -0.731064,0.691877" />
|
||||
<path d="M 0.69104,0.011877 L 0.697432,0.011877 L 0.697432,0.054534 L 0.711924,0.011877 L 0.717905,0.011877 L 0.732466,0.053816 L 0.732466,0.011877 L 0.738857,0.011877 L 0.738857,0.061985 L 0.729937,0.061985 L 0.717939,0.027122 Q 0.716128,0.021824 0.715273,0.019089 Q 0.714521,0.02155 0.712881,0.026506 L 0.701021,0.061985 L 0.69104,0.061985 L 0.69104,0.011877" />
|
||||
<path d="M 0.750684,0.011877 L 0.757314,0.011877 L 0.757314,0.061985 L 0.750684,0.061985 L 0.750684,0.011877" />
|
||||
<path d="M 0.768936,0.011877 L 0.775293,0.011877 L 0.775293,0.051252 L 0.801611,0.011877 L 0.808413,0.011877 L 0.808413,0.061985 L 0.802056,0.061985 L 0.802056,0.022644 L 0.775737,0.061985 L 0.768936,0.061985 L 0.768936,0.011877" />
|
||||
<path d="M 0.653887,-0.042229 L 0.647256,-0.040554 C 0.645444,-0.055816 0.624176,-0.057963 0.618289,-0.043818 C 0.61432,-0.033663 0.616041,-0.024258 0.62345,-0.015603 C 0.630858,-0.01068 0.644052,-0.011769 0.646504,-0.023293 L 0.653032,-0.021755 C 0.648627,0.001099 0.60711,-0.003671 0.60959,-0.032693 C 0.609664,-0.042941 0.613122,-0.050603 0.619963,-0.055679 C 0.630348,-0.062443 0.651286,-0.059243 0.653887,-0.042229" />
|
||||
<path d="M 0.662192,-0.058123 L 0.699585,-0.058123 L 0.699585,-0.052209 L 0.668823,-0.052209 L 0.668823,-0.035154 L 0.696543,-0.035154 L 0.696543,-0.029275 L 0.668823,-0.029275 L 0.668823,-0.013928 L 0.698423,-0.013928 L 0.698423,-0.008015 L 0.662192,-0.008015 L 0.662192,-0.058123" />
|
||||
<path d="M 0.708677,-0.058123 L 0.715034,-0.058123 L 0.715034,-0.018748 L 0.741353,-0.058123 L 0.748154,-0.058123 L 0.748154,-0.008015 L 0.741797,-0.008015 L 0.741797,-0.047356 L 0.715479,-0.008015 L 0.708677,-0.008015 L 0.708677,-0.058123" />
|
||||
<path d="M 0.772046,-0.058123 L 0.778677,-0.058123 L 0.778677,-0.013928 L 0.795254,-0.013928 L 0.795254,-0.008015 L 0.755537,-0.008015 L 0.755537,-0.013928 L 0.772046,-0.013928 L 0.772046,-0.058123" />
|
||||
<path d="M 0.802192,-0.058123 L 0.839585,-0.058123 L 0.839585,-0.052209 L 0.808823,-0.052209 L 0.808823,-0.035154 L 0.836543,-0.035154 L 0.836543,-0.029275 L 0.808823,-0.029275 L 0.808823,-0.013928 L 0.838423,-0.013928 L 0.838423,-0.008015 L 0.802192,-0.008015 L 0.802192,-0.058123" />
|
||||
<path d="M 0.848848,-0.058123 L 0.855479,-0.058123 L 0.855479,-0.035872 L 0.863169,-0.035872 Q 0.865732,-0.035872 0.86686,-0.036111 Q 0.870468,-0.035637 0.878037,-0.047698 L 0.884668,-0.058123 L 0.893008,-0.058123 L 0.884292,-0.044485 Q 0.881694,-0.040486 0.878789,-0.03782 Q 0.877422,-0.036589 0.87479,-0.035325 C 0.895154,-0.033808 0.893243,-0.005153 0.871064,-0.008015 L 0.848848,-0.008015 L 0.848848,-0.058123 M 0.855479,-0.030129 L 0.855479,-0.013552 L 0.871338,-0.013552 C 0.8857,-0.012518 0.886366,-0.03137 0.869731,-0.030129 L 0.855479,-0.030129" />
|
||||
<path d="M -0.096113,0.033018 L -0.102744,0.034692 C -0.104556,0.019431 -0.125824,0.017284 -0.131711,0.031428 C -0.13568,0.041583 -0.133959,0.050988 -0.12655,0.059644 C -0.119142,0.064567 -0.105948,0.063477 -0.103496,0.051953 L -0.096968,0.053491 C -0.101373,0.076345 -0.14289,0.071576 -0.14041,0.042554 C -0.140336,0.032306 -0.136878,0.024644 -0.130037,0.019568 C -0.119652,0.012804 -0.098714,0.016004 -0.096113,0.033018" />
|
||||
<path d="M -0.087808,0.017124 L -0.050415,0.017124 L -0.050415,0.023037 L -0.081177,0.023037 L -0.081177,0.040093 L -0.053457,0.040093 L -0.053457,0.045972 L -0.081177,0.045972 L -0.081177,0.061318 L -0.051577,0.061318 L -0.051577,0.067231 L -0.087808,0.067231 L -0.087808,0.017124" />
|
||||
<path d="M -0.041323,0.017124 L -0.034966,0.017124 L -0.034966,0.056499 L -0.008647,0.017124 L -0.001846,0.017124 L -0.001846,0.067231 L -0.008203,0.067231 L -0.008203,0.027891 L -0.034521,0.067231 L -0.041323,0.067231 L -0.041323,0.017124" />
|
||||
<path d="M 0.022046,0.017124 L 0.028677,0.017124 L 0.028677,0.061318 L 0.045254,0.061318 L 0.045254,0.067231 L 0.005537,0.067231 L 0.005537,0.061318 L 0.022046,0.061318 L 0.022046,0.017124" />
|
||||
<path d="M 0.052192,0.017124 L 0.089585,0.017124 L 0.089585,0.023037 L 0.058823,0.023037 L 0.058823,0.040093 L 0.086543,0.040093 L 0.086543,0.045972 L 0.058823,0.045972 L 0.058823,0.061318 L 0.088423,0.061318 L 0.088423,0.067231 L 0.052192,0.067231 L 0.052192,0.017124" />
|
||||
<path d="M 0.098848,0.017124 L 0.105479,0.017124 L 0.105479,0.039375 L 0.113169,0.039375 Q 0.115732,0.039375 0.11686,0.039136 Q 0.120468,0.039609 0.128037,0.027549 L 0.134668,0.017124 L 0.143008,0.017124 L 0.134292,0.030762 Q 0.131694,0.034761 0.128789,0.037427 Q 0.127422,0.038657 0.12479,0.039922 C 0.145154,0.041439 0.143243,0.070094 0.121064,0.067231 L 0.098848,0.067231 L 0.098848,0.017124 M 0.105479,0.045117 L 0.105479,0.061694 L 0.121338,0.061694 C 0.1357,0.062729 0.136366,0.043876 0.119731,0.045117 L 0.105479,0.045117" />
|
||||
<path d="M -0.096113,-0.047476 L -0.102744,-0.045801 C -0.104556,-0.061062 -0.125824,-0.063209 -0.131711,-0.049065 C -0.13568,-0.03891 -0.133959,-0.029505 -0.12655,-0.02085 C -0.119142,-0.015927 -0.105948,-0.017016 -0.103496,-0.02854 L -0.096968,-0.027002 C -0.101373,-0.004148 -0.14289,-0.008917 -0.14041,-0.037939 C -0.140336,-0.048188 -0.136878,-0.05585 -0.130037,-0.060925 C -0.119652,-0.067689 -0.098714,-0.064489 -0.096113,-0.047476" />
|
||||
<path d="M -0.087808,-0.063369 L -0.050415,-0.063369 L -0.050415,-0.057456 L -0.081177,-0.057456 L -0.081177,-0.0404 L -0.053457,-0.0404 L -0.053457,-0.034521 L -0.081177,-0.034521 L -0.081177,-0.019175 L -0.051577,-0.019175 L -0.051577,-0.013262 L -0.087808,-0.013262 L -0.087808,-0.063369" />
|
||||
<path d="M -0.041323,-0.063369 L -0.034966,-0.063369 L -0.034966,-0.023994 L -0.008647,-0.063369 L -0.001846,-0.063369 L -0.001846,-0.013262 L -0.008203,-0.013262 L -0.008203,-0.052603 L -0.034521,-0.013262 L -0.041323,-0.013262 L -0.041323,-0.063369" />
|
||||
<path d="M 0.022046,-0.063369 L 0.028677,-0.063369 L 0.028677,-0.019175 L 0.045254,-0.019175 L 0.045254,-0.013262 L 0.005537,-0.013262 L 0.005537,-0.019175 L 0.022046,-0.019175 L 0.022046,-0.063369" />
|
||||
<path d="M 0.052192,-0.063369 L 0.089585,-0.063369 L 0.089585,-0.057456 L 0.058823,-0.057456 L 0.058823,-0.0404 L 0.086543,-0.0404 L 0.086543,-0.034521 L 0.058823,-0.034521 L 0.058823,-0.019175 L 0.088423,-0.019175 L 0.088423,-0.013262 L 0.052192,-0.013262 L 0.052192,-0.063369" />
|
||||
<path d="M 0.098848,-0.063369 L 0.105479,-0.063369 L 0.105479,-0.041118 L 0.113169,-0.041118 Q 0.115732,-0.041118 0.11686,-0.041357 Q 0.120468,-0.040884 0.128037,-0.052944 L 0.134668,-0.063369 L 0.143008,-0.063369 L 0.134292,-0.049731 Q 0.131694,-0.045732 0.128789,-0.043066 Q 0.127422,-0.041836 0.12479,-0.040571 C 0.145154,-0.039055 0.143243,-0.010399 0.121064,-0.013262 L 0.098848,-0.013262 L 0.098848,-0.063369 M 0.105479,-0.035376 L 0.105479,-0.018799 L 0.121338,-0.018799 C 0.1357,-0.017765 0.136366,-0.036617 0.119731,-0.035376 L 0.105479,-0.035376" />
|
||||
<path d="M -0.820649,0.011877 L -0.814258,0.011877 L -0.814258,0.054534 L -0.799766,0.011877 L -0.793784,0.011877 L -0.779224,0.053816 L -0.779224,0.011877 L -0.772832,0.011877 L -0.772832,0.061985 L -0.781753,0.061985 L -0.79375,0.027122 Q -0.795562,0.021824 -0.796416,0.019089 Q -0.797168,0.02155 -0.798809,0.026506 L -0.810669,0.061985 L -0.820649,0.061985 L -0.820649,0.011877" />
|
||||
<path d="M -0.767637,0.011877 L -0.760596,0.011877 L -0.755093,0.027053 L -0.734141,0.027053 L -0.728296,0.011877 L -0.720742,0.011877 L -0.74125,0.061985 L -0.748394,0.061985 L -0.767637,0.011877 M -0.753179,0.032454 L -0.747676,0.047151 Q -0.745933,0.051902 -0.744976,0.056721 Q -0.743813,0.052654 -0.741421,0.046331 L -0.736191,0.032454 L -0.753179,0.032454" />
|
||||
<path d="M -0.720537,0.011877 L -0.712573,0.011877 L -0.699995,0.029343 Q -0.699277,0.030369 -0.697603,0.033103 Q -0.696475,0.031292 -0.695415,0.029753 L -0.682803,0.011877 L -0.6746,0.011877 L -0.693569,0.038367 L -0.675967,0.061985 L -0.683179,0.061985 L -0.693262,0.048689 Q -0.695552,0.045715 -0.697227,0.042981 Q -0.698423,0.045134 -0.70126,0.049133 L -0.710352,0.061985 L -0.718247,0.061985 L -0.701157,0.037991 L -0.720537,0.011877" />
|
||||
<path d="M -0.846113,-0.042229 L -0.852744,-0.040554 C -0.854556,-0.055816 -0.875824,-0.057963 -0.881711,-0.043818 C -0.88568,-0.033663 -0.883959,-0.024258 -0.87655,-0.015603 C -0.869142,-0.01068 -0.855948,-0.011769 -0.853496,-0.023293 L -0.846968,-0.021755 C -0.851373,0.001099 -0.89289,-0.003671 -0.89041,-0.032693 C -0.890336,-0.042941 -0.886878,-0.050603 -0.880037,-0.055679 C -0.869652,-0.062443 -0.848714,-0.059243 -0.846113,-0.042229" />
|
||||
<path d="M -0.837808,-0.058123 L -0.800415,-0.058123 L -0.800415,-0.052209 L -0.831177,-0.052209 L -0.831177,-0.035154 L -0.803457,-0.035154 L -0.803457,-0.029275 L -0.831177,-0.029275 L -0.831177,-0.013928 L -0.801577,-0.013928 L -0.801577,-0.008015 L -0.837808,-0.008015 L -0.837808,-0.058123" />
|
||||
<path d="M -0.791323,-0.058123 L -0.784966,-0.058123 L -0.784966,-0.018748 L -0.758647,-0.058123 L -0.751846,-0.058123 L -0.751846,-0.008015 L -0.758203,-0.008015 L -0.758203,-0.047356 L -0.784521,-0.008015 L -0.791323,-0.008015 L -0.791323,-0.058123" />
|
||||
<path d="M -0.727954,-0.058123 L -0.721323,-0.058123 L -0.721323,-0.013928 L -0.704746,-0.013928 L -0.704746,-0.008015 L -0.744463,-0.008015 L -0.744463,-0.013928 L -0.727954,-0.013928 L -0.727954,-0.058123" />
|
||||
<path d="M -0.697808,-0.058123 L -0.660415,-0.058123 L -0.660415,-0.052209 L -0.691177,-0.052209 L -0.691177,-0.035154 L -0.663457,-0.035154 L -0.663457,-0.029275 L -0.691177,-0.029275 L -0.691177,-0.013928 L -0.661577,-0.013928 L -0.661577,-0.008015 L -0.697808,-0.008015 L -0.697808,-0.058123" />
|
||||
<path d="M -0.651152,-0.058123 L -0.644521,-0.058123 L -0.644521,-0.035872 L -0.636831,-0.035872 Q -0.634268,-0.035872 -0.63314,-0.036111 Q -0.629532,-0.035637 -0.621963,-0.047698 L -0.615332,-0.058123 L -0.606992,-0.058123 L -0.615708,-0.044485 Q -0.618306,-0.040486 -0.621211,-0.03782 Q -0.622578,-0.036589 -0.62521,-0.035325 C -0.604846,-0.033808 -0.606757,-0.005153 -0.628936,-0.008015 L -0.651152,-0.008015 L -0.651152,-0.058123 M -0.644521,-0.030129 L -0.644521,-0.013552 L -0.628662,-0.013552 C -0.6143,-0.012518 -0.613634,-0.03137 -0.630269,-0.030129 L -0.644521,-0.030129" />
|
||||
<path d="M 0.69104,-0.738123 L 0.697432,-0.738123 L 0.697432,-0.695466 L 0.711924,-0.738123 L 0.717905,-0.738123 L 0.732466,-0.696184 L 0.732466,-0.738123 L 0.738857,-0.738123 L 0.738857,-0.688015 L 0.729937,-0.688015 L 0.717939,-0.722878 Q 0.716128,-0.728176 0.715273,-0.730911 Q 0.714521,-0.72845 0.712881,-0.723494 L 0.701021,-0.688015 L 0.69104,-0.688015 L 0.69104,-0.738123" />
|
||||
<path d="M 0.750684,-0.738123 L 0.757314,-0.738123 L 0.757314,-0.688015 L 0.750684,-0.688015 L 0.750684,-0.738123" />
|
||||
<path d="M 0.768936,-0.738123 L 0.775293,-0.738123 L 0.775293,-0.698748 L 0.801611,-0.738123 L 0.808413,-0.738123 L 0.808413,-0.688015 L 0.802056,-0.688015 L 0.802056,-0.727356 L 0.775737,-0.688015 L 0.768936,-0.688015 L 0.768936,-0.738123" />
|
||||
<path d="M 0.679351,-0.808123 L 0.685742,-0.808123 L 0.685742,-0.765466 L 0.700234,-0.808123 L 0.706216,-0.808123 L 0.720776,-0.766184 L 0.720776,-0.808123 L 0.727168,-0.808123 L 0.727168,-0.758015 L 0.718247,-0.758015 L 0.70625,-0.792878 Q 0.704438,-0.798176 0.703584,-0.800911 Q 0.702832,-0.79845 0.701191,-0.793494 L 0.689331,-0.758015 L 0.679351,-0.758015 L 0.679351,-0.808123" />
|
||||
<path d="M 0.732363,-0.808123 L 0.739404,-0.808123 L 0.744907,-0.792947 L 0.765859,-0.792947 L 0.771704,-0.808123 L 0.779258,-0.808123 L 0.75875,-0.758015 L 0.751606,-0.758015 L 0.732363,-0.808123 M 0.746821,-0.787546 L 0.752324,-0.772849 Q 0.754067,-0.768098 0.755024,-0.763279 Q 0.756187,-0.767346 0.758579,-0.773669 L 0.763809,-0.787546 L 0.746821,-0.787546" />
|
||||
<path d="M 0.779463,-0.808123 L 0.787427,-0.808123 L 0.800005,-0.790657 Q 0.800723,-0.789631 0.802397,-0.786897 Q 0.803525,-0.788708 0.804585,-0.790247 L 0.817197,-0.808123 L 0.8254,-0.808123 L 0.806431,-0.781633 L 0.824033,-0.758015 L 0.816821,-0.758015 L 0.806738,-0.771311 Q 0.804448,-0.774285 0.802773,-0.777019 Q 0.801577,-0.774866 0.79874,-0.770867 L 0.789648,-0.758015 L 0.781753,-0.758015 L 0.798843,-0.782009 L 0.779463,-0.808123" />
|
||||
<path d="M -0.096113,-0.722229 L -0.102744,-0.720554 C -0.104556,-0.735816 -0.125824,-0.737963 -0.131711,-0.723818 C -0.13568,-0.713663 -0.133959,-0.704258 -0.12655,-0.695603 C -0.119142,-0.69068 -0.105948,-0.691769 -0.103496,-0.703293 L -0.096968,-0.701755 C -0.101373,-0.678901 -0.14289,-0.683671 -0.14041,-0.712693 C -0.140336,-0.722941 -0.136878,-0.730603 -0.130037,-0.735679 C -0.119652,-0.742443 -0.098714,-0.739243 -0.096113,-0.722229" />
|
||||
<path d="M -0.087808,-0.738123 L -0.050415,-0.738123 L -0.050415,-0.732209 L -0.081177,-0.732209 L -0.081177,-0.715154 L -0.053457,-0.715154 L -0.053457,-0.709275 L -0.081177,-0.709275 L -0.081177,-0.693928 L -0.051577,-0.693928 L -0.051577,-0.688015 L -0.087808,-0.688015 L -0.087808,-0.738123" />
|
||||
<path d="M -0.041323,-0.738123 L -0.034966,-0.738123 L -0.034966,-0.698748 L -0.008647,-0.738123 L -0.001846,-0.738123 L -0.001846,-0.688015 L -0.008203,-0.688015 L -0.008203,-0.727356 L -0.034521,-0.688015 L -0.041323,-0.688015 L -0.041323,-0.738123" />
|
||||
<path d="M 0.022046,-0.738123 L 0.028677,-0.738123 L 0.028677,-0.693928 L 0.045254,-0.693928 L 0.045254,-0.688015 L 0.005537,-0.688015 L 0.005537,-0.693928 L 0.022046,-0.693928 L 0.022046,-0.738123" />
|
||||
<path d="M 0.052192,-0.738123 L 0.089585,-0.738123 L 0.089585,-0.732209 L 0.058823,-0.732209 L 0.058823,-0.715154 L 0.086543,-0.715154 L 0.086543,-0.709275 L 0.058823,-0.709275 L 0.058823,-0.693928 L 0.088423,-0.693928 L 0.088423,-0.688015 L 0.052192,-0.688015 L 0.052192,-0.738123" />
|
||||
<path d="M 0.098848,-0.738123 L 0.105479,-0.738123 L 0.105479,-0.715872 L 0.113169,-0.715872 Q 0.115732,-0.715872 0.11686,-0.716111 Q 0.120468,-0.715637 0.128037,-0.727698 L 0.134668,-0.738123 L 0.143008,-0.738123 L 0.134292,-0.724485 Q 0.131694,-0.720486 0.128789,-0.71782 Q 0.127422,-0.716589 0.12479,-0.715325 C 0.145154,-0.713808 0.143243,-0.685153 0.121064,-0.688015 L 0.098848,-0.688015 L 0.098848,-0.738123 M 0.105479,-0.710129 L 0.105479,-0.693552 L 0.121338,-0.693552 C 0.1357,-0.692518 0.136366,-0.71137 0.119731,-0.710129 L 0.105479,-0.710129" />
|
||||
<path d="M -0.070649,-0.808123 L -0.064258,-0.808123 L -0.064258,-0.765466 L -0.049766,-0.808123 L -0.043784,-0.808123 L -0.029224,-0.766184 L -0.029224,-0.808123 L -0.022832,-0.808123 L -0.022832,-0.758015 L -0.031753,-0.758015 L -0.04375,-0.792878 Q -0.045562,-0.798176 -0.046416,-0.800911 Q -0.047168,-0.79845 -0.048809,-0.793494 L -0.060669,-0.758015 L -0.070649,-0.758015 L -0.070649,-0.808123" />
|
||||
<path d="M -0.017637,-0.808123 L -0.010596,-0.808123 L -0.005093,-0.792947 L 0.015859,-0.792947 L 0.021704,-0.808123 L 0.029258,-0.808123 L 0.00875,-0.758015 L 0.001606,-0.758015 L -0.017637,-0.808123 M -0.003179,-0.787546 L 0.002324,-0.772849 Q 0.004067,-0.768098 0.005024,-0.763279 Q 0.006187,-0.767346 0.008579,-0.773669 L 0.013809,-0.787546 L -0.003179,-0.787546" />
|
||||
<path d="M 0.029463,-0.808123 L 0.037427,-0.808123 L 0.050005,-0.790657 Q 0.050723,-0.789631 0.052397,-0.786897 Q 0.053525,-0.788708 0.054585,-0.790247 L 0.067197,-0.808123 L 0.0754,-0.808123 L 0.056431,-0.781633 L 0.074033,-0.758015 L 0.066821,-0.758015 L 0.056738,-0.771311 Q 0.054448,-0.774285 0.052773,-0.777019 Q 0.051577,-0.774866 0.04874,-0.770867 L 0.039648,-0.758015 L 0.031753,-0.758015 L 0.048843,-0.782009 L 0.029463,-0.808123" />
|
||||
<path d="M -0.820649,-0.732876 L -0.814258,-0.732876 L -0.814258,-0.69022 L -0.799766,-0.732876 L -0.793784,-0.732876 L -0.779224,-0.690937 L -0.779224,-0.732876 L -0.772832,-0.732876 L -0.772832,-0.682769 L -0.781753,-0.682769 L -0.79375,-0.717632 Q -0.795562,-0.72293 -0.796416,-0.725664 Q -0.797168,-0.723203 -0.798809,-0.718247 L -0.810669,-0.682769 L -0.820649,-0.682769 L -0.820649,-0.732876" />
|
||||
<path d="M -0.767637,-0.732876 L -0.760596,-0.732876 L -0.755093,-0.7177 L -0.734141,-0.7177 L -0.728296,-0.732876 L -0.720742,-0.732876 L -0.74125,-0.682769 L -0.748394,-0.682769 L -0.767637,-0.732876 M -0.753179,-0.7123 L -0.747676,-0.697603 Q -0.745933,-0.692852 -0.744976,-0.688032 Q -0.743813,-0.6921 -0.741421,-0.698423 L -0.736191,-0.7123 L -0.753179,-0.7123" />
|
||||
<path d="M -0.720537,-0.732876 L -0.712573,-0.732876 L -0.699995,-0.71541 Q -0.699277,-0.714385 -0.697603,-0.71165 Q -0.696475,-0.713462 -0.695415,-0.715 L -0.682803,-0.732876 L -0.6746,-0.732876 L -0.693569,-0.706387 L -0.675967,-0.682769 L -0.683179,-0.682769 L -0.693262,-0.696064 Q -0.695552,-0.699038 -0.697227,-0.701772 Q -0.698423,-0.699619 -0.70126,-0.69562 L -0.710352,-0.682769 L -0.718247,-0.682769 L -0.701157,-0.706763 L -0.720537,-0.732876" />
|
||||
<path d="M -0.820649,-0.813369 L -0.814258,-0.813369 L -0.814258,-0.770713 L -0.799766,-0.813369 L -0.793784,-0.813369 L -0.779224,-0.771431 L -0.779224,-0.813369 L -0.772832,-0.813369 L -0.772832,-0.763262 L -0.781753,-0.763262 L -0.79375,-0.798125 Q -0.795562,-0.803423 -0.796416,-0.806157 Q -0.797168,-0.803696 -0.798809,-0.79874 L -0.810669,-0.763262 L -0.820649,-0.763262 L -0.820649,-0.813369" />
|
||||
<path d="M -0.767637,-0.813369 L -0.760596,-0.813369 L -0.755093,-0.798193 L -0.734141,-0.798193 L -0.728296,-0.813369 L -0.720742,-0.813369 L -0.74125,-0.763262 L -0.748394,-0.763262 L -0.767637,-0.813369 M -0.753179,-0.792793 L -0.747676,-0.778096 Q -0.745933,-0.773345 -0.744976,-0.768525 Q -0.743813,-0.772593 -0.741421,-0.778916 L -0.736191,-0.792793 L -0.753179,-0.792793" />
|
||||
<path d="M -0.720537,-0.813369 L -0.712573,-0.813369 L -0.699995,-0.795903 Q -0.699277,-0.794878 -0.697603,-0.792144 Q -0.696475,-0.793955 -0.695415,-0.795493 L -0.682803,-0.813369 L -0.6746,-0.813369 L -0.693569,-0.78688 L -0.675967,-0.763262 L -0.683179,-0.763262 L -0.693262,-0.776558 Q -0.695552,-0.779531 -0.697227,-0.782266 Q -0.698423,-0.780112 -0.70126,-0.776113 L -0.710352,-0.763262 L -0.718247,-0.763262 L -0.701157,-0.787256 L -0.720537,-0.813369" />
|
||||
<path d="M 0.933013,0.25 A 0.5,0.5 -89.99999999999996 0,1 0.25,0.933013 A 0.5,0.5 0.0 0,1 -0.25,0.933013 A 0.5,0.5 0.0 0,1 -0.933013,0.25 A 0.5,0.5 0.0 0,1 -0.933013,-0.25 A 0.5,0.5 0.0 0,1 -0.25,-0.933013 A 0.5,0.5 0.0 0,1 0.25,-0.933013 A 0.5,0.5 180.0 0,1 0.933013,-0.25 A 0.5,0.5 -30.000000000000266 0,1 0.933013,0.25 M -0.2599,-0.927144 A 0.49,0.49 0.0 0,0 -0.927144,-0.2599 A 0.5,0.5 0.0 0,1 -0.5,-0.5 A 0.5,0.5 0.0 0,1 -0.2599,-0.927144 M 0.49,-0.4999 A 0.49,0.49 0.011694209715779905 1,0 -0.49,-0.4999 A 0.5,0.5 0.0 0,1 -0.25,-0.433013 A 0.5,0.5 0.0 0,1 0.25,-0.433013 A 0.5,0.5 0.0 0,1 0.49,-0.4999 M 0.927144,-0.2599 A 0.49,0.49 119.34058157502295 0,0 0.2599,-0.927144 A 0.5,0.5 -58.681163150047965 0,1 0.5,-0.5 A 0.5,0.5 0.0 0,1 0.927144,-0.2599 M -0.2599,-0.427144 A 0.49,0.49 0.0 1,0 -0.2599,0.427144 A 0.5,0.5 0.0 0,1 -0.2599,-0.427144 M -0.2599,0.927144 A 0.5,0.5 0.0 0,1 -0.5,0.5 A 0.5,0.5 0.0 0,1 -0.927144,0.2599 A 0.49,0.49 0.0 0,0 -0.2599,0.927144 M 0.49,-0.0 A 0.49,0.49 0.0 0,0 -0.49,0.0 A 0.49,0.49 0.0 0,0 0.49,0.0 M 0.2599,-0.427144 A 0.5,0.5 -58.681163150047965 0,1 0.2599,0.427144 A 0.49,0.49 119.34058157502298 1,0 0.2599,-0.427144 M 0.49,0.4999 A 0.5,0.5 0.0 0,1 0.25,0.433013 A 0.5,0.5 0.0 0,1 -0.25,0.433013 A 0.5,0.5 0.0 0,1 -0.49,0.4999 A 0.49,0.49 -179.98830579028402 1,0 0.49,0.4999 M 0.2599,0.927144 A 0.49,0.49 119.34058157502295 0,0 0.927144,0.2599 A 0.5,0.5 0.0 0,1 0.5,0.5 A 0.5,0.5 -58.681163150048015 0,1 0.2599,0.927144" />
|
||||
<path d="M 0.729735,0.805825 L 0.729735,0.774115 Q 0.73068,0.76652 0.723365,0.767045 L 0.723365,0.765715 L 0.742965,0.765715 L 0.742965,0.767045 Q 0.73607,0.766765 0.736875,0.773345 L 0.736875,0.804425 Q 0.736,0.810935 0.742965,0.810725 L 0.742965,0.812055 L 0.729035,0.812055 L 0.713565,0.776705 L 0.697395,0.812055 L 0.683535,0.812055 L 0.683535,0.810725 Q 0.6912,0.811145 0.690185,0.804425 L 0.690185,0.776005 Q 0.69113,0.766415 0.683395,0.767045 L 0.683395,0.765715 L 0.699845,0.765715 L 0.699845,0.767045 Q 0.692495,0.766415 0.693265,0.776005 L 0.693265,0.804215 L 0.710835,0.765715 L 0.711815,0.765715 L 0.729735,0.805825" />
|
||||
<path d="M 0.751855,0.804425 L 0.751855,0.773345 Q 0.75287,0.766415 0.745065,0.767045 L 0.745065,0.765715 L 0.765855,0.765715 L 0.765855,0.767045 Q 0.758085,0.766485 0.758995,0.773345 L 0.758995,0.804425 Q 0.758015,0.811215 0.765855,0.810725 L 0.765855,0.812055 L 0.745065,0.812055 L 0.745065,0.810725 Q 0.75294,0.811145 0.751855,0.804425" />
|
||||
<path d="M 0.816605,0.810725 L 0.816605,0.812055 L 0.800155,0.812055 L 0.800155,0.810725 Q 0.807645,0.811215 0.806875,0.801765 L 0.806875,0.778175 L 0.779925,0.812055 L 0.767955,0.812055 L 0.767955,0.810725 Q 0.77149,0.81125 0.774745,0.806875 L 0.774745,0.776005 Q 0.77569,0.766415 0.767955,0.767045 L 0.767955,0.765715 L 0.784405,0.765715 L 0.784405,0.767045 Q 0.777055,0.766415 0.777825,0.776005 L 0.777825,0.803445 L 0.808765,0.764945 L 0.809955,0.764945 L 0.809955,0.801765 Q 0.810049,0.812075 0.816605,0.810725" />
|
||||
<path d="M 0.729735,0.728825 L 0.729735,0.697115 Q 0.73068,0.68952 0.723365,0.690045 L 0.723365,0.688715 L 0.742965,0.688715 L 0.742965,0.690045 Q 0.73607,0.689765 0.736875,0.696345 L 0.736875,0.727425 Q 0.736,0.733935 0.742965,0.733725 L 0.742965,0.735055 L 0.729035,0.735055 L 0.713565,0.699705 L 0.697395,0.735055 L 0.683535,0.735055 L 0.683535,0.733725 Q 0.6912,0.734145 0.690185,0.727425 L 0.690185,0.699005 Q 0.69113,0.689415 0.683395,0.690045 L 0.683395,0.688715 L 0.699845,0.688715 L 0.699845,0.690045 Q 0.692495,0.689415 0.693265,0.699005 L 0.693265,0.727215 L 0.710835,0.688715 L 0.711815,0.688715 L 0.729735,0.728825" />
|
||||
<path d="M 0.751855,0.727425 L 0.751855,0.696345 Q 0.75287,0.689415 0.745065,0.690045 L 0.745065,0.688715 L 0.765855,0.688715 L 0.765855,0.690045 Q 0.758085,0.689485 0.758995,0.696345 L 0.758995,0.727425 Q 0.758015,0.734215 0.765855,0.733725 L 0.765855,0.735055 L 0.745065,0.735055 L 0.745065,0.733725 Q 0.75294,0.734145 0.751855,0.727425" />
|
||||
<path d="M 0.816605,0.733725 L 0.816605,0.735055 L 0.800155,0.735055 L 0.800155,0.733725 Q 0.807645,0.734215 0.806875,0.724765 L 0.806875,0.701175 L 0.779925,0.735055 L 0.767955,0.735055 L 0.767955,0.733725 Q 0.77149,0.73425 0.774745,0.729875 L 0.774745,0.699005 Q 0.77569,0.689415 0.767955,0.690045 L 0.767955,0.688715 L 0.784405,0.688715 L 0.784405,0.690045 Q 0.777055,0.689415 0.777825,0.699005 L 0.777825,0.726445 L 0.808765,0.687945 L 0.809955,0.687945 L 0.809955,0.724765 Q 0.810049,0.735075 0.816605,0.733725" />
|
||||
<path d="M -0.095165,0.80915 C -0.096145,0.80838 -0.097125,0.80761 -0.098105,0.80684 C -0.101546,0.808546 -0.121245,0.812147 -0.127785,0.802325 C -0.136661,0.794207 -0.136733,0.775291 -0.128065,0.76736 C -0.119764,0.757929 -0.09957,0.75884 -0.092155,0.76974 L -0.093415,0.771 C -0.10351,0.76152 -0.118145,0.760015 -0.12481,0.774955 C -0.127678,0.784641 -0.126698,0.793216 -0.12187,0.80068 C -0.114562,0.810002 -0.097513,0.808008 -0.094675,0.79333 L -0.093065,0.79333 L -0.093695,0.80915 L -0.095165,0.80915" />
|
||||
<path d="M -0.075775,0.76743 L -0.075775,0.78472 L -0.064995,0.78472 Q -0.057505,0.7857 -0.057295,0.778 L -0.055685,0.778 L -0.055685,0.79424 L -0.057295,0.79424 Q -0.057295,0.786995 -0.064995,0.78759 L -0.075775,0.78759 L -0.075775,0.80313 Q -0.07602,0.80579 -0.073465,0.80551 L -0.064015,0.80551 Q -0.053655,0.806525 -0.053375,0.79816 L -0.051625,0.79816 L -0.051835,0.80817 L -0.089005,0.80817 L -0.089005,0.80684 Q -0.08211,0.80712 -0.082915,0.80054 L -0.082915,0.76946 Q -0.08211,0.76288 -0.089005,0.76316 L -0.089005,0.76183 L -0.051205,0.76183 L -0.048055,0.77366 L -0.050015,0.77366 Q -0.053375,0.76337 -0.066535,0.76442 L -0.068425,0.76442 Q -0.07714,0.763895 -0.075775,0.76743" />
|
||||
<path d="M 0.002345,0.80684 L 0.002345,0.80817 L -0.014105,0.80817 L -0.014105,0.80684 Q -0.006615,0.80733 -0.007385,0.79788 L -0.007385,0.77429 L -0.034335,0.80817 L -0.046305,0.80817 L -0.046305,0.80684 Q -0.04277,0.807365 -0.039515,0.80299 L -0.039515,0.77212 Q -0.03857,0.76253 -0.046305,0.76316 L -0.046305,0.76183 L -0.029855,0.76183 L -0.029855,0.76316 Q -0.037205,0.76253 -0.036435,0.77212 L -0.036435,0.79956 L -0.005495,0.76106 L -0.004305,0.76106 L -0.004305,0.79788 Q -0.004211,0.80819 0.002345,0.80684" />
|
||||
<path d="M 0.021035,0.80523 L 0.021035,0.77023 Q 0.022085,0.762495 0.014455,0.76316 L 0.014455,0.76183 L 0.034895,0.76183 L 0.034895,0.76316 Q 0.027195,0.76267 0.028175,0.76946 L 0.028175,0.80523 L 0.031955,0.80523 Q 0.04228,0.80656 0.043085,0.79627 L 0.044765,0.79627 L 0.044345,0.80817 L 0.004865,0.80817 L 0.004445,0.79627 L 0.006125,0.79627 Q 0.00707,0.80649 0.017255,0.80523 L 0.021035,0.80523" />
|
||||
<path d="M 0.060095,0.76743 L 0.060095,0.78472 L 0.070875,0.78472 Q 0.078365,0.7857 0.078575,0.778 L 0.080185,0.778 L 0.080185,0.79424 L 0.078575,0.79424 Q 0.078575,0.786995 0.070875,0.78759 L 0.060095,0.78759 L 0.060095,0.80313 Q 0.05985,0.80579 0.062405,0.80551 L 0.071855,0.80551 Q 0.082215,0.806525 0.082495,0.79816 L 0.084245,0.79816 L 0.084035,0.80817 L 0.046865,0.80817 L 0.046865,0.80684 Q 0.05376,0.80712 0.052955,0.80054 L 0.052955,0.76946 Q 0.05376,0.76288 0.046865,0.76316 L 0.046865,0.76183 L 0.084665,0.76183 L 0.087815,0.77366 L 0.085855,0.77366 Q 0.082495,0.76337 0.069335,0.76442 L 0.067445,0.76442 Q 0.05873,0.763895 0.060095,0.76743" />
|
||||
<path d="M 0.108885,0.80817 L 0.089565,0.80817 L 0.089565,0.80684 Q 0.09646,0.80698 0.095515,0.80054 L 0.095515,0.77023 Q 0.09646,0.762635 0.089565,0.76316 L 0.089565,0.76183 L 0.108955,0.76183 L 0.108955,0.76316 Q 0.101745,0.76281 0.102655,0.76946 L 0.102655,0.78325 L 0.106575,0.78339 L 0.123235,0.76183 L 0.134505,0.76183 L 0.134505,0.76316 L 0.128415,0.76645 L 0.113995,0.78416 Q 0.114135,0.78416 0.114345,0.78423 Q 0.131438,0.788934 0.124915,0.80215 Q 0.118129,0.808903 0.108885,0.80817 M 0.102655,0.78584 L 0.102655,0.80306 C 0.101909,0.807537 0.113894,0.805293 0.11592,0.803235 C 0.117997,0.801672 0.119035,0.799257 0.119035,0.79599 C 0.119222,0.788827 0.113762,0.785443 0.102655,0.78584" />
|
||||
<path d="M -0.020265,0.732325 L -0.020265,0.700615 Q -0.01932,0.69302 -0.026635,0.693545 L -0.026635,0.692215 L -0.007035,0.692215 L -0.007035,0.693545 Q -0.01393,0.693265 -0.013125,0.699845 L -0.013125,0.730925 Q -0.014,0.737435 -0.007035,0.737225 L -0.007035,0.738555 L -0.020965,0.738555 L -0.036435,0.703205 L -0.052605,0.738555 L -0.066465,0.738555 L -0.066465,0.737225 Q -0.0588,0.737645 -0.059815,0.730925 L -0.059815,0.702505 Q -0.05887,0.692915 -0.066605,0.693545 L -0.066605,0.692215 L -0.050155,0.692215 L -0.050155,0.693545 Q -0.057505,0.692915 -0.056735,0.702505 L -0.056735,0.730715 L -0.039165,0.692215 L -0.038185,0.692215 L -0.020265,0.732325" />
|
||||
<path d="M 0.001855,0.730925 L 0.001855,0.699845 Q 0.00287,0.692915 -0.004935,0.693545 L -0.004935,0.692215 L 0.015855,0.692215 L 0.015855,0.693545 Q 0.008085,0.692985 0.008995,0.699845 L 0.008995,0.730925 Q 0.008015,0.737715 0.015855,0.737225 L 0.015855,0.738555 L -0.004935,0.738555 L -0.004935,0.737225 Q 0.00294,0.737645 0.001855,0.730925" />
|
||||
<path d="M 0.066605,0.737225 L 0.066605,0.738555 L 0.050155,0.738555 L 0.050155,0.737225 Q 0.057645,0.737715 0.056875,0.728265 L 0.056875,0.704675 L 0.029925,0.738555 L 0.017955,0.738555 L 0.017955,0.737225 Q 0.02149,0.73775 0.024745,0.733375 L 0.024745,0.702505 Q 0.02569,0.692915 0.017955,0.693545 L 0.017955,0.692215 L 0.034405,0.692215 L 0.034405,0.693545 Q 0.027055,0.692915 0.027825,0.702505 L 0.027825,0.729945 L 0.058765,0.691445 L 0.059955,0.691445 L 0.059955,0.728265 Q 0.060049,0.738575 0.066605,0.737225" />
|
||||
<path d="M -0.783845,0.80152 L -0.783845,0.76981 Q -0.7829,0.762215 -0.790215,0.76274 L -0.790215,0.76141 L -0.770615,0.76141 L -0.770615,0.76274 Q -0.77751,0.76246 -0.776705,0.76904 L -0.776705,0.80012 Q -0.77758,0.80663 -0.770615,0.80642 L -0.770615,0.80775 L -0.784545,0.80775 L -0.800015,0.7724 L -0.816185,0.80775 L -0.830045,0.80775 L -0.830045,0.80642 Q -0.82238,0.80684 -0.823395,0.80012 L -0.823395,0.7717 Q -0.82245,0.76211 -0.830185,0.76274 L -0.830185,0.76141 L -0.813735,0.76141 L -0.813735,0.76274 Q -0.821085,0.76211 -0.820315,0.7717 L -0.820315,0.79991 L -0.802745,0.76141 L -0.801765,0.76141 L -0.783845,0.80152" />
|
||||
<path d="M -0.720145,0.76141 L -0.720145,0.76274 Q -0.723995,0.762075 -0.726445,0.76883 L -0.743875,0.80859 L -0.745275,0.80859 L -0.759835,0.77422 Q -0.764805,0.76099 -0.768515,0.76274 L -0.768515,0.76141 L -0.754655,0.76141 L -0.754655,0.76274 Q -0.761273,0.762962 -0.758855,0.76834 L -0.755635,0.77653 L -0.737295,0.77653 L -0.734425,0.76981 Q -0.730194,0.762295 -0.737995,0.76274 L -0.737995,0.76141 L -0.720145,0.76141 M -0.754445,0.7794 L -0.746395,0.79865 L -0.738275,0.7794 L -0.754445,0.7794" />
|
||||
<path d="M -0.670375,0.80775 L -0.687035,0.80775 L -0.687035,0.80642 Q -0.678392,0.807372 -0.684935,0.79935 L -0.692845,0.78969 L -0.695785,0.79382 Q -0.706309,0.80787 -0.698375,0.80635 L -0.696415,0.80642 L -0.696415,0.80775 L -0.717555,0.80775 L -0.717555,0.80642 Q -0.71262,0.807995 -0.704885,0.79543 L -0.697255,0.78423 L -0.708245,0.77072 Q -0.71507,0.76141 -0.718395,0.76274 L -0.718395,0.76141 L -0.702085,0.76141 L -0.702085,0.76274 Q -0.711864,0.761632 -0.703765,0.77121 L -0.695435,0.78157 L -0.688785,0.77177 Q -0.68077,0.761704 -0.690605,0.76274 L -0.690605,0.76141 L -0.669815,0.76141 L -0.669815,0.76274 Q -0.67447,0.76267 -0.677585,0.76792 L -0.691025,0.7871 L -0.680805,0.79984 Q -0.67573,0.80698 -0.670375,0.80642 L -0.670375,0.80775" />
|
||||
<path d="M -0.770265,0.732325 L -0.770265,0.700615 Q -0.76932,0.69302 -0.776635,0.693545 L -0.776635,0.692215 L -0.757035,0.692215 L -0.757035,0.693545 Q -0.76393,0.693265 -0.763125,0.699845 L -0.763125,0.730925 Q -0.764,0.737435 -0.757035,0.737225 L -0.757035,0.738555 L -0.770965,0.738555 L -0.786435,0.703205 L -0.802605,0.738555 L -0.816465,0.738555 L -0.816465,0.737225 Q -0.8088,0.737645 -0.809815,0.730925 L -0.809815,0.702505 Q -0.80887,0.692915 -0.816605,0.693545 L -0.816605,0.692215 L -0.800155,0.692215 L -0.800155,0.693545 Q -0.807505,0.692915 -0.806735,0.702505 L -0.806735,0.730715 L -0.789165,0.692215 L -0.788185,0.692215 L -0.770265,0.732325" />
|
||||
<path d="M -0.748145,0.730925 L -0.748145,0.699845 Q -0.74713,0.692915 -0.754935,0.693545 L -0.754935,0.692215 L -0.734145,0.692215 L -0.734145,0.693545 Q -0.741915,0.692985 -0.741005,0.699845 L -0.741005,0.730925 Q -0.741985,0.737715 -0.734145,0.737225 L -0.734145,0.738555 L -0.754935,0.738555 L -0.754935,0.737225 Q -0.74706,0.737645 -0.748145,0.730925" />
|
||||
<path d="M -0.683395,0.737225 L -0.683395,0.738555 L -0.699845,0.738555 L -0.699845,0.737225 Q -0.692355,0.737715 -0.693125,0.728265 L -0.693125,0.704675 L -0.720075,0.738555 L -0.732045,0.738555 L -0.732045,0.737225 Q -0.72851,0.73775 -0.725255,0.733375 L -0.725255,0.702505 Q -0.72431,0.692915 -0.732045,0.693545 L -0.732045,0.692215 L -0.715595,0.692215 L -0.715595,0.693545 Q -0.722945,0.692915 -0.722175,0.702505 L -0.722175,0.729945 L -0.691235,0.691445 L -0.690045,0.691445 L -0.690045,0.728265 Q -0.689951,0.738575 -0.683395,0.737225" />
|
||||
<path d="M 0.729735,0.052325 L 0.729735,0.020615 Q 0.73068,0.01302 0.723365,0.013545 L 0.723365,0.012215 L 0.742965,0.012215 L 0.742965,0.013545 Q 0.73607,0.013265 0.736875,0.019845 L 0.736875,0.050925 Q 0.736,0.057435 0.742965,0.057225 L 0.742965,0.058555 L 0.729035,0.058555 L 0.713565,0.023205 L 0.697395,0.058555 L 0.683535,0.058555 L 0.683535,0.057225 Q 0.6912,0.057645 0.690185,0.050925 L 0.690185,0.022505 Q 0.69113,0.012915 0.683395,0.013545 L 0.683395,0.012215 L 0.699845,0.012215 L 0.699845,0.013545 Q 0.692495,0.012915 0.693265,0.022505 L 0.693265,0.050715 L 0.710835,0.012215 L 0.711815,0.012215 L 0.729735,0.052325" />
|
||||
<path d="M 0.751855,0.050925 L 0.751855,0.019845 Q 0.75287,0.012915 0.745065,0.013545 L 0.745065,0.012215 L 0.765855,0.012215 L 0.765855,0.013545 Q 0.758085,0.012985 0.758995,0.019845 L 0.758995,0.050925 Q 0.758015,0.057715 0.765855,0.057225 L 0.765855,0.058555 L 0.745065,0.058555 L 0.745065,0.057225 Q 0.75294,0.057645 0.751855,0.050925" />
|
||||
<path d="M 0.816605,0.057225 L 0.816605,0.058555 L 0.800155,0.058555 L 0.800155,0.057225 Q 0.807645,0.057715 0.806875,0.048265 L 0.806875,0.024675 L 0.779925,0.058555 L 0.767955,0.058555 L 0.767955,0.057225 Q 0.77149,0.05775 0.774745,0.053375 L 0.774745,0.022505 Q 0.77569,0.012915 0.767955,0.013545 L 0.767955,0.012215 L 0.784405,0.012215 L 0.784405,0.013545 Q 0.777055,0.012915 0.777825,0.022505 L 0.777825,0.049945 L 0.808765,0.011445 L 0.809955,0.011445 L 0.809955,0.048265 Q 0.810049,0.058575 0.816605,0.057225" />
|
||||
<path d="M 0.654835,-0.01085 C 0.653855,-0.01162 0.652875,-0.01239 0.651895,-0.01316 C 0.648454,-0.011454 0.628755,-0.007853 0.622215,-0.017675 C 0.613339,-0.025793 0.613267,-0.044709 0.621935,-0.05264 C 0.630236,-0.062071 0.65043,-0.06116 0.657845,-0.05026 L 0.656585,-0.049 C 0.64649,-0.05848 0.631855,-0.059985 0.62519,-0.045045 C 0.622322,-0.035359 0.623302,-0.026784 0.62813,-0.01932 C 0.635438,-0.009998 0.652487,-0.011992 0.655325,-0.02667 L 0.656935,-0.02667 L 0.656305,-0.01085 L 0.654835,-0.01085" />
|
||||
<path d="M 0.674225,-0.05257 L 0.674225,-0.03528 L 0.685005,-0.03528 Q 0.692495,-0.0343 0.692705,-0.042 L 0.694315,-0.042 L 0.694315,-0.02576 L 0.692705,-0.02576 Q 0.692705,-0.033005 0.685005,-0.03241 L 0.674225,-0.03241 L 0.674225,-0.01687 Q 0.67398,-0.01421 0.676535,-0.01449 L 0.685985,-0.01449 Q 0.696345,-0.013475 0.696625,-0.02184 L 0.698375,-0.02184 L 0.698165,-0.01183 L 0.660995,-0.01183 L 0.660995,-0.01316 Q 0.66789,-0.01288 0.667085,-0.01946 L 0.667085,-0.05054 Q 0.66789,-0.05712 0.660995,-0.05684 L 0.660995,-0.05817 L 0.698795,-0.05817 L 0.701945,-0.04634 L 0.699985,-0.04634 Q 0.696625,-0.05663 0.683465,-0.05558 L 0.681575,-0.05558 Q 0.67286,-0.056105 0.674225,-0.05257" />
|
||||
<path d="M 0.752345,-0.01316 L 0.752345,-0.01183 L 0.735895,-0.01183 L 0.735895,-0.01316 Q 0.743385,-0.01267 0.742615,-0.02212 L 0.742615,-0.04571 L 0.715665,-0.01183 L 0.703695,-0.01183 L 0.703695,-0.01316 Q 0.70723,-0.012635 0.710485,-0.01701 L 0.710485,-0.04788 Q 0.71143,-0.05747 0.703695,-0.05684 L 0.703695,-0.05817 L 0.720145,-0.05817 L 0.720145,-0.05684 Q 0.712795,-0.05747 0.713565,-0.04788 L 0.713565,-0.02044 L 0.744505,-0.05894 L 0.745695,-0.05894 L 0.745695,-0.02212 Q 0.745789,-0.01181 0.752345,-0.01316" />
|
||||
<path d="M 0.771035,-0.01477 L 0.771035,-0.04977 Q 0.772085,-0.057505 0.764455,-0.05684 L 0.764455,-0.05817 L 0.784895,-0.05817 L 0.784895,-0.05684 Q 0.777195,-0.05733 0.778175,-0.05054 L 0.778175,-0.01477 L 0.781955,-0.01477 Q 0.79228,-0.01344 0.793085,-0.02373 L 0.794765,-0.02373 L 0.794345,-0.01183 L 0.754865,-0.01183 L 0.754445,-0.02373 L 0.756125,-0.02373 Q 0.75707,-0.01351 0.767255,-0.01477 L 0.771035,-0.01477" />
|
||||
<path d="M 0.810095,-0.05257 L 0.810095,-0.03528 L 0.820875,-0.03528 Q 0.828365,-0.0343 0.828575,-0.042 L 0.830185,-0.042 L 0.830185,-0.02576 L 0.828575,-0.02576 Q 0.828575,-0.033005 0.820875,-0.03241 L 0.810095,-0.03241 L 0.810095,-0.01687 Q 0.80985,-0.01421 0.812405,-0.01449 L 0.821855,-0.01449 Q 0.832215,-0.013475 0.832495,-0.02184 L 0.834245,-0.02184 L 0.834035,-0.01183 L 0.796865,-0.01183 L 0.796865,-0.01316 Q 0.80376,-0.01288 0.802955,-0.01946 L 0.802955,-0.05054 Q 0.80376,-0.05712 0.796865,-0.05684 L 0.796865,-0.05817 L 0.834665,-0.05817 L 0.837815,-0.04634 L 0.835855,-0.04634 Q 0.832495,-0.05663 0.819335,-0.05558 L 0.817445,-0.05558 Q 0.80873,-0.056105 0.810095,-0.05257" />
|
||||
<path d="M 0.858885,-0.01183 L 0.839565,-0.01183 L 0.839565,-0.01316 Q 0.84646,-0.01302 0.845515,-0.01946 L 0.845515,-0.04977 Q 0.84646,-0.057365 0.839565,-0.05684 L 0.839565,-0.05817 L 0.858955,-0.05817 L 0.858955,-0.05684 Q 0.851745,-0.05719 0.852655,-0.05054 L 0.852655,-0.03675 L 0.856575,-0.03661 L 0.873235,-0.05817 L 0.884505,-0.05817 L 0.884505,-0.05684 L 0.878415,-0.05355 L 0.863995,-0.03584 Q 0.864135,-0.03584 0.864345,-0.03577 Q 0.881438,-0.031066 0.874915,-0.01785 Q 0.868129,-0.011097 0.858885,-0.01183 M 0.852655,-0.03416 L 0.852655,-0.01694 C 0.851909,-0.012463 0.863894,-0.014707 0.86592,-0.016765 C 0.867997,-0.018328 0.869035,-0.020743 0.869035,-0.02401 C 0.869222,-0.031173 0.863762,-0.034557 0.852655,-0.03416" />
|
||||
<path d="M -0.095165,0.06265 C -0.096145,0.06188 -0.097125,0.06111 -0.098105,0.06034 C -0.101546,0.062046 -0.121245,0.065647 -0.127785,0.055825 C -0.136661,0.047707 -0.136733,0.028791 -0.128065,0.02086 C -0.119764,0.011429 -0.09957,0.01234 -0.092155,0.02324 L -0.093415,0.0245 C -0.10351,0.01502 -0.118145,0.013515 -0.12481,0.028455 C -0.127678,0.038141 -0.126698,0.046716 -0.12187,0.05418 C -0.114562,0.063502 -0.097513,0.061508 -0.094675,0.04683 L -0.093065,0.04683 L -0.093695,0.06265 L -0.095165,0.06265" />
|
||||
<path d="M -0.075775,0.02093 L -0.075775,0.03822 L -0.064995,0.03822 Q -0.057505,0.0392 -0.057295,0.0315 L -0.055685,0.0315 L -0.055685,0.04774 L -0.057295,0.04774 Q -0.057295,0.040495 -0.064995,0.04109 L -0.075775,0.04109 L -0.075775,0.05663 Q -0.07602,0.05929 -0.073465,0.05901 L -0.064015,0.05901 Q -0.053655,0.060025 -0.053375,0.05166 L -0.051625,0.05166 L -0.051835,0.06167 L -0.089005,0.06167 L -0.089005,0.06034 Q -0.08211,0.06062 -0.082915,0.05404 L -0.082915,0.02296 Q -0.08211,0.01638 -0.089005,0.01666 L -0.089005,0.01533 L -0.051205,0.01533 L -0.048055,0.02716 L -0.050015,0.02716 Q -0.053375,0.01687 -0.066535,0.01792 L -0.068425,0.01792 Q -0.07714,0.017395 -0.075775,0.02093" />
|
||||
<path d="M 0.002345,0.06034 L 0.002345,0.06167 L -0.014105,0.06167 L -0.014105,0.06034 Q -0.006615,0.06083 -0.007385,0.05138 L -0.007385,0.02779 L -0.034335,0.06167 L -0.046305,0.06167 L -0.046305,0.06034 Q -0.04277,0.060865 -0.039515,0.05649 L -0.039515,0.02562 Q -0.03857,0.01603 -0.046305,0.01666 L -0.046305,0.01533 L -0.029855,0.01533 L -0.029855,0.01666 Q -0.037205,0.01603 -0.036435,0.02562 L -0.036435,0.05306 L -0.005495,0.01456 L -0.004305,0.01456 L -0.004305,0.05138 Q -0.004211,0.06169 0.002345,0.06034" />
|
||||
<path d="M 0.021035,0.05873 L 0.021035,0.02373 Q 0.022085,0.015995 0.014455,0.01666 L 0.014455,0.01533 L 0.034895,0.01533 L 0.034895,0.01666 Q 0.027195,0.01617 0.028175,0.02296 L 0.028175,0.05873 L 0.031955,0.05873 Q 0.04228,0.06006 0.043085,0.04977 L 0.044765,0.04977 L 0.044345,0.06167 L 0.004865,0.06167 L 0.004445,0.04977 L 0.006125,0.04977 Q 0.00707,0.05999 0.017255,0.05873 L 0.021035,0.05873" />
|
||||
<path d="M 0.060095,0.02093 L 0.060095,0.03822 L 0.070875,0.03822 Q 0.078365,0.0392 0.078575,0.0315 L 0.080185,0.0315 L 0.080185,0.04774 L 0.078575,0.04774 Q 0.078575,0.040495 0.070875,0.04109 L 0.060095,0.04109 L 0.060095,0.05663 Q 0.05985,0.05929 0.062405,0.05901 L 0.071855,0.05901 Q 0.082215,0.060025 0.082495,0.05166 L 0.084245,0.05166 L 0.084035,0.06167 L 0.046865,0.06167 L 0.046865,0.06034 Q 0.05376,0.06062 0.052955,0.05404 L 0.052955,0.02296 Q 0.05376,0.01638 0.046865,0.01666 L 0.046865,0.01533 L 0.084665,0.01533 L 0.087815,0.02716 L 0.085855,0.02716 Q 0.082495,0.01687 0.069335,0.01792 L 0.067445,0.01792 Q 0.05873,0.017395 0.060095,0.02093" />
|
||||
<path d="M 0.108885,0.06167 L 0.089565,0.06167 L 0.089565,0.06034 Q 0.09646,0.06048 0.095515,0.05404 L 0.095515,0.02373 Q 0.09646,0.016135 0.089565,0.01666 L 0.089565,0.01533 L 0.108955,0.01533 L 0.108955,0.01666 Q 0.101745,0.01631 0.102655,0.02296 L 0.102655,0.03675 L 0.106575,0.03689 L 0.123235,0.01533 L 0.134505,0.01533 L 0.134505,0.01666 L 0.128415,0.01995 L 0.113995,0.03766 Q 0.114135,0.03766 0.114345,0.03773 Q 0.131438,0.042434 0.124915,0.05565 Q 0.118129,0.062403 0.108885,0.06167 M 0.102655,0.03934 L 0.102655,0.05656 C 0.101909,0.061037 0.113894,0.058793 0.11592,0.056735 C 0.117997,0.055172 0.119035,0.052757 0.119035,0.04949 C 0.119222,0.042327 0.113762,0.038943 0.102655,0.03934" />
|
||||
<path d="M -0.095165,-0.01435 C -0.096145,-0.01512 -0.097125,-0.01589 -0.098105,-0.01666 C -0.101546,-0.014954 -0.121245,-0.011353 -0.127785,-0.021175 C -0.136661,-0.029293 -0.136733,-0.048209 -0.128065,-0.05614 C -0.119764,-0.065571 -0.09957,-0.06466 -0.092155,-0.05376 L -0.093415,-0.0525 C -0.10351,-0.06198 -0.118145,-0.063485 -0.12481,-0.048545 C -0.127678,-0.038859 -0.126698,-0.030284 -0.12187,-0.02282 C -0.114562,-0.013498 -0.097513,-0.015492 -0.094675,-0.03017 L -0.093065,-0.03017 L -0.093695,-0.01435 L -0.095165,-0.01435" />
|
||||
<path d="M -0.075775,-0.05607 L -0.075775,-0.03878 L -0.064995,-0.03878 Q -0.057505,-0.0378 -0.057295,-0.0455 L -0.055685,-0.0455 L -0.055685,-0.02926 L -0.057295,-0.02926 Q -0.057295,-0.036505 -0.064995,-0.03591 L -0.075775,-0.03591 L -0.075775,-0.02037 Q -0.07602,-0.01771 -0.073465,-0.01799 L -0.064015,-0.01799 Q -0.053655,-0.016975 -0.053375,-0.02534 L -0.051625,-0.02534 L -0.051835,-0.01533 L -0.089005,-0.01533 L -0.089005,-0.01666 Q -0.08211,-0.01638 -0.082915,-0.02296 L -0.082915,-0.05404 Q -0.08211,-0.06062 -0.089005,-0.06034 L -0.089005,-0.06167 L -0.051205,-0.06167 L -0.048055,-0.04984 L -0.050015,-0.04984 Q -0.053375,-0.06013 -0.066535,-0.05908 L -0.068425,-0.05908 Q -0.07714,-0.059605 -0.075775,-0.05607" />
|
||||
<path d="M 0.002345,-0.01666 L 0.002345,-0.01533 L -0.014105,-0.01533 L -0.014105,-0.01666 Q -0.006615,-0.01617 -0.007385,-0.02562 L -0.007385,-0.04921 L -0.034335,-0.01533 L -0.046305,-0.01533 L -0.046305,-0.01666 Q -0.04277,-0.016135 -0.039515,-0.02051 L -0.039515,-0.05138 Q -0.03857,-0.06097 -0.046305,-0.06034 L -0.046305,-0.06167 L -0.029855,-0.06167 L -0.029855,-0.06034 Q -0.037205,-0.06097 -0.036435,-0.05138 L -0.036435,-0.02394 L -0.005495,-0.06244 L -0.004305,-0.06244 L -0.004305,-0.02562 Q -0.004211,-0.01531 0.002345,-0.01666" />
|
||||
<path d="M 0.021035,-0.01827 L 0.021035,-0.05327 Q 0.022085,-0.061005 0.014455,-0.06034 L 0.014455,-0.06167 L 0.034895,-0.06167 L 0.034895,-0.06034 Q 0.027195,-0.06083 0.028175,-0.05404 L 0.028175,-0.01827 L 0.031955,-0.01827 Q 0.04228,-0.01694 0.043085,-0.02723 L 0.044765,-0.02723 L 0.044345,-0.01533 L 0.004865,-0.01533 L 0.004445,-0.02723 L 0.006125,-0.02723 Q 0.00707,-0.01701 0.017255,-0.01827 L 0.021035,-0.01827" />
|
||||
<path d="M 0.060095,-0.05607 L 0.060095,-0.03878 L 0.070875,-0.03878 Q 0.078365,-0.0378 0.078575,-0.0455 L 0.080185,-0.0455 L 0.080185,-0.02926 L 0.078575,-0.02926 Q 0.078575,-0.036505 0.070875,-0.03591 L 0.060095,-0.03591 L 0.060095,-0.02037 Q 0.05985,-0.01771 0.062405,-0.01799 L 0.071855,-0.01799 Q 0.082215,-0.016975 0.082495,-0.02534 L 0.084245,-0.02534 L 0.084035,-0.01533 L 0.046865,-0.01533 L 0.046865,-0.01666 Q 0.05376,-0.01638 0.052955,-0.02296 L 0.052955,-0.05404 Q 0.05376,-0.06062 0.046865,-0.06034 L 0.046865,-0.06167 L 0.084665,-0.06167 L 0.087815,-0.04984 L 0.085855,-0.04984 Q 0.082495,-0.06013 0.069335,-0.05908 L 0.067445,-0.05908 Q 0.05873,-0.059605 0.060095,-0.05607" />
|
||||
<path d="M 0.108885,-0.01533 L 0.089565,-0.01533 L 0.089565,-0.01666 Q 0.09646,-0.01652 0.095515,-0.02296 L 0.095515,-0.05327 Q 0.09646,-0.060865 0.089565,-0.06034 L 0.089565,-0.06167 L 0.108955,-0.06167 L 0.108955,-0.06034 Q 0.101745,-0.06069 0.102655,-0.05404 L 0.102655,-0.04025 L 0.106575,-0.04011 L 0.123235,-0.06167 L 0.134505,-0.06167 L 0.134505,-0.06034 L 0.128415,-0.05705 L 0.113995,-0.03934 Q 0.114135,-0.03934 0.114345,-0.03927 Q 0.131438,-0.034566 0.124915,-0.02135 Q 0.118129,-0.014597 0.108885,-0.01533 M 0.102655,-0.03766 L 0.102655,-0.02044 C 0.101909,-0.015963 0.113894,-0.018207 0.11592,-0.020265 C 0.117997,-0.021828 0.119035,-0.024243 0.119035,-0.02751 C 0.119222,-0.034673 0.113762,-0.038057 0.102655,-0.03766" />
|
||||
<path d="M -0.783845,0.05152 L -0.783845,0.01981 Q -0.7829,0.012215 -0.790215,0.01274 L -0.790215,0.01141 L -0.770615,0.01141 L -0.770615,0.01274 Q -0.77751,0.01246 -0.776705,0.01904 L -0.776705,0.05012 Q -0.77758,0.05663 -0.770615,0.05642 L -0.770615,0.05775 L -0.784545,0.05775 L -0.800015,0.0224 L -0.816185,0.05775 L -0.830045,0.05775 L -0.830045,0.05642 Q -0.82238,0.05684 -0.823395,0.05012 L -0.823395,0.0217 Q -0.82245,0.01211 -0.830185,0.01274 L -0.830185,0.01141 L -0.813735,0.01141 L -0.813735,0.01274 Q -0.821085,0.01211 -0.820315,0.0217 L -0.820315,0.04991 L -0.802745,0.01141 L -0.801765,0.01141 L -0.783845,0.05152" />
|
||||
<path d="M -0.720145,0.01141 L -0.720145,0.01274 Q -0.723995,0.012075 -0.726445,0.01883 L -0.743875,0.05859 L -0.745275,0.05859 L -0.759835,0.02422 Q -0.764805,0.01099 -0.768515,0.01274 L -0.768515,0.01141 L -0.754655,0.01141 L -0.754655,0.01274 Q -0.761273,0.012962 -0.758855,0.01834 L -0.755635,0.02653 L -0.737295,0.02653 L -0.734425,0.01981 Q -0.730194,0.012295 -0.737995,0.01274 L -0.737995,0.01141 L -0.720145,0.01141 M -0.754445,0.0294 L -0.746395,0.04865 L -0.738275,0.0294 L -0.754445,0.0294" />
|
||||
<path d="M -0.670375,0.05775 L -0.687035,0.05775 L -0.687035,0.05642 Q -0.678392,0.057372 -0.684935,0.04935 L -0.692845,0.03969 L -0.695785,0.04382 Q -0.706309,0.05787 -0.698375,0.05635 L -0.696415,0.05642 L -0.696415,0.05775 L -0.717555,0.05775 L -0.717555,0.05642 Q -0.71262,0.057995 -0.704885,0.04543 L -0.697255,0.03423 L -0.708245,0.02072 Q -0.71507,0.01141 -0.718395,0.01274 L -0.718395,0.01141 L -0.702085,0.01141 L -0.702085,0.01274 Q -0.711864,0.011632 -0.703765,0.02121 L -0.695435,0.03157 L -0.688785,0.02177 Q -0.68077,0.011704 -0.690605,0.01274 L -0.690605,0.01141 L -0.669815,0.01141 L -0.669815,0.01274 Q -0.67447,0.01267 -0.677585,0.01792 L -0.691025,0.0371 L -0.680805,0.04984 Q -0.67573,0.05698 -0.670375,0.05642 L -0.670375,0.05775" />
|
||||
<path d="M -0.845165,-0.01085 C -0.846145,-0.01162 -0.847125,-0.01239 -0.848105,-0.01316 C -0.851546,-0.011454 -0.871245,-0.007853 -0.877785,-0.017675 C -0.886661,-0.025793 -0.886733,-0.044709 -0.878065,-0.05264 C -0.869764,-0.062071 -0.84957,-0.06116 -0.842155,-0.05026 L -0.843415,-0.049 C -0.85351,-0.05848 -0.868145,-0.059985 -0.87481,-0.045045 C -0.877678,-0.035359 -0.876698,-0.026784 -0.87187,-0.01932 C -0.864562,-0.009998 -0.847513,-0.011992 -0.844675,-0.02667 L -0.843065,-0.02667 L -0.843695,-0.01085 L -0.845165,-0.01085" />
|
||||
<path d="M -0.825775,-0.05257 L -0.825775,-0.03528 L -0.814995,-0.03528 Q -0.807505,-0.0343 -0.807295,-0.042 L -0.805685,-0.042 L -0.805685,-0.02576 L -0.807295,-0.02576 Q -0.807295,-0.033005 -0.814995,-0.03241 L -0.825775,-0.03241 L -0.825775,-0.01687 Q -0.82602,-0.01421 -0.823465,-0.01449 L -0.814015,-0.01449 Q -0.803655,-0.013475 -0.803375,-0.02184 L -0.801625,-0.02184 L -0.801835,-0.01183 L -0.839005,-0.01183 L -0.839005,-0.01316 Q -0.83211,-0.01288 -0.832915,-0.01946 L -0.832915,-0.05054 Q -0.83211,-0.05712 -0.839005,-0.05684 L -0.839005,-0.05817 L -0.801205,-0.05817 L -0.798055,-0.04634 L -0.800015,-0.04634 Q -0.803375,-0.05663 -0.816535,-0.05558 L -0.818425,-0.05558 Q -0.82714,-0.056105 -0.825775,-0.05257" />
|
||||
<path d="M -0.747655,-0.01316 L -0.747655,-0.01183 L -0.764105,-0.01183 L -0.764105,-0.01316 Q -0.756615,-0.01267 -0.757385,-0.02212 L -0.757385,-0.04571 L -0.784335,-0.01183 L -0.796305,-0.01183 L -0.796305,-0.01316 Q -0.79277,-0.012635 -0.789515,-0.01701 L -0.789515,-0.04788 Q -0.78857,-0.05747 -0.796305,-0.05684 L -0.796305,-0.05817 L -0.779855,-0.05817 L -0.779855,-0.05684 Q -0.787205,-0.05747 -0.786435,-0.04788 L -0.786435,-0.02044 L -0.755495,-0.05894 L -0.754305,-0.05894 L -0.754305,-0.02212 Q -0.754211,-0.01181 -0.747655,-0.01316" />
|
||||
<path d="M -0.728965,-0.01477 L -0.728965,-0.04977 Q -0.727915,-0.057505 -0.735545,-0.05684 L -0.735545,-0.05817 L -0.715105,-0.05817 L -0.715105,-0.05684 Q -0.722805,-0.05733 -0.721825,-0.05054 L -0.721825,-0.01477 L -0.718045,-0.01477 Q -0.70772,-0.01344 -0.706915,-0.02373 L -0.705235,-0.02373 L -0.705655,-0.01183 L -0.745135,-0.01183 L -0.745555,-0.02373 L -0.743875,-0.02373 Q -0.74293,-0.01351 -0.732745,-0.01477 L -0.728965,-0.01477" />
|
||||
<path d="M -0.689905,-0.05257 L -0.689905,-0.03528 L -0.679125,-0.03528 Q -0.671635,-0.0343 -0.671425,-0.042 L -0.669815,-0.042 L -0.669815,-0.02576 L -0.671425,-0.02576 Q -0.671425,-0.033005 -0.679125,-0.03241 L -0.689905,-0.03241 L -0.689905,-0.01687 Q -0.69015,-0.01421 -0.687595,-0.01449 L -0.678145,-0.01449 Q -0.667785,-0.013475 -0.667505,-0.02184 L -0.665755,-0.02184 L -0.665965,-0.01183 L -0.703135,-0.01183 L -0.703135,-0.01316 Q -0.69624,-0.01288 -0.697045,-0.01946 L -0.697045,-0.05054 Q -0.69624,-0.05712 -0.703135,-0.05684 L -0.703135,-0.05817 L -0.665335,-0.05817 L -0.662185,-0.04634 L -0.664145,-0.04634 Q -0.667505,-0.05663 -0.680665,-0.05558 L -0.682555,-0.05558 Q -0.69127,-0.056105 -0.689905,-0.05257" />
|
||||
<path d="M -0.641115,-0.01183 L -0.660435,-0.01183 L -0.660435,-0.01316 Q -0.65354,-0.01302 -0.654485,-0.01946 L -0.654485,-0.04977 Q -0.65354,-0.057365 -0.660435,-0.05684 L -0.660435,-0.05817 L -0.641045,-0.05817 L -0.641045,-0.05684 Q -0.648255,-0.05719 -0.647345,-0.05054 L -0.647345,-0.03675 L -0.643425,-0.03661 L -0.626765,-0.05817 L -0.615495,-0.05817 L -0.615495,-0.05684 L -0.621585,-0.05355 L -0.636005,-0.03584 Q -0.635865,-0.03584 -0.635655,-0.03577 Q -0.618562,-0.031066 -0.625085,-0.01785 Q -0.631871,-0.011097 -0.641115,-0.01183 M -0.647345,-0.03416 L -0.647345,-0.01694 C -0.648091,-0.012463 -0.636106,-0.014707 -0.63408,-0.016765 C -0.632003,-0.018328 -0.630965,-0.020743 -0.630965,-0.02401 C -0.630778,-0.031173 -0.636238,-0.034557 -0.647345,-0.03416" />
|
||||
<path d="M 0.729735,-0.697675 L 0.729735,-0.729385 Q 0.73068,-0.73698 0.723365,-0.736455 L 0.723365,-0.737785 L 0.742965,-0.737785 L 0.742965,-0.736455 Q 0.73607,-0.736735 0.736875,-0.730155 L 0.736875,-0.699075 Q 0.736,-0.692565 0.742965,-0.692775 L 0.742965,-0.691445 L 0.729035,-0.691445 L 0.713565,-0.726795 L 0.697395,-0.691445 L 0.683535,-0.691445 L 0.683535,-0.692775 Q 0.6912,-0.692355 0.690185,-0.699075 L 0.690185,-0.727495 Q 0.69113,-0.737085 0.683395,-0.736455 L 0.683395,-0.737785 L 0.699845,-0.737785 L 0.699845,-0.736455 Q 0.692495,-0.737085 0.693265,-0.727495 L 0.693265,-0.699285 L 0.710835,-0.737785 L 0.711815,-0.737785 L 0.729735,-0.697675" />
|
||||
<path d="M 0.751855,-0.699075 L 0.751855,-0.730155 Q 0.75287,-0.737085 0.745065,-0.736455 L 0.745065,-0.737785 L 0.765855,-0.737785 L 0.765855,-0.736455 Q 0.758085,-0.737015 0.758995,-0.730155 L 0.758995,-0.699075 Q 0.758015,-0.692285 0.765855,-0.692775 L 0.765855,-0.691445 L 0.745065,-0.691445 L 0.745065,-0.692775 Q 0.75294,-0.692355 0.751855,-0.699075" />
|
||||
<path d="M 0.816605,-0.692775 L 0.816605,-0.691445 L 0.800155,-0.691445 L 0.800155,-0.692775 Q 0.807645,-0.692285 0.806875,-0.701735 L 0.806875,-0.725325 L 0.779925,-0.691445 L 0.767955,-0.691445 L 0.767955,-0.692775 Q 0.77149,-0.69225 0.774745,-0.696625 L 0.774745,-0.727495 Q 0.77569,-0.737085 0.767955,-0.736455 L 0.767955,-0.737785 L 0.784405,-0.737785 L 0.784405,-0.736455 Q 0.777055,-0.737085 0.777825,-0.727495 L 0.777825,-0.700055 L 0.808765,-0.738555 L 0.809955,-0.738555 L 0.809955,-0.701735 Q 0.810049,-0.691425 0.816605,-0.692775" />
|
||||
<path d="M 0.716155,-0.76848 L 0.716155,-0.80019 Q 0.7171,-0.807785 0.709785,-0.80726 L 0.709785,-0.80859 L 0.729385,-0.80859 L 0.729385,-0.80726 Q 0.72249,-0.80754 0.723295,-0.80096 L 0.723295,-0.76988 Q 0.72242,-0.76337 0.729385,-0.76358 L 0.729385,-0.76225 L 0.715455,-0.76225 L 0.699985,-0.7976 L 0.683815,-0.76225 L 0.669955,-0.76225 L 0.669955,-0.76358 Q 0.67762,-0.76316 0.676605,-0.76988 L 0.676605,-0.7983 Q 0.67755,-0.80789 0.669815,-0.80726 L 0.669815,-0.80859 L 0.686265,-0.80859 L 0.686265,-0.80726 Q 0.678915,-0.80789 0.679685,-0.7983 L 0.679685,-0.77009 L 0.697255,-0.80859 L 0.698235,-0.80859 L 0.716155,-0.76848" />
|
||||
<path d="M 0.779855,-0.80859 L 0.779855,-0.80726 Q 0.776005,-0.807925 0.773555,-0.80117 L 0.756125,-0.76141 L 0.754725,-0.76141 L 0.740165,-0.79578 Q 0.735195,-0.80901 0.731485,-0.80726 L 0.731485,-0.80859 L 0.745345,-0.80859 L 0.745345,-0.80726 Q 0.738727,-0.807038 0.741145,-0.80166 L 0.744365,-0.79347 L 0.762705,-0.79347 L 0.765575,-0.80019 Q 0.769806,-0.807705 0.762005,-0.80726 L 0.762005,-0.80859 L 0.779855,-0.80859 M 0.745555,-0.7906 L 0.753605,-0.77135 L 0.761725,-0.7906 L 0.745555,-0.7906" />
|
||||
<path d="M 0.829625,-0.76225 L 0.812965,-0.76225 L 0.812965,-0.76358 Q 0.821608,-0.762628 0.815065,-0.77065 L 0.807155,-0.78031 L 0.804215,-0.77618 Q 0.793691,-0.76213 0.801625,-0.76365 L 0.803585,-0.76358 L 0.803585,-0.76225 L 0.782445,-0.76225 L 0.782445,-0.76358 Q 0.78738,-0.762005 0.795115,-0.77457 L 0.802745,-0.78577 L 0.791755,-0.79928 Q 0.78493,-0.80859 0.781605,-0.80726 L 0.781605,-0.80859 L 0.797915,-0.80859 L 0.797915,-0.80726 Q 0.788136,-0.808368 0.796235,-0.79879 L 0.804565,-0.78843 L 0.811215,-0.79823 Q 0.81923,-0.808296 0.809395,-0.80726 L 0.809395,-0.80859 L 0.830185,-0.80859 L 0.830185,-0.80726 Q 0.82553,-0.80733 0.822415,-0.80208 L 0.808975,-0.7829 L 0.819195,-0.77016 Q 0.82427,-0.76302 0.829625,-0.76358 L 0.829625,-0.76225" />
|
||||
<path d="M -0.033845,-0.69848 L -0.033845,-0.73019 Q -0.0329,-0.737785 -0.040215,-0.73726 L -0.040215,-0.73859 L -0.020615,-0.73859 L -0.020615,-0.73726 Q -0.02751,-0.73754 -0.026705,-0.73096 L -0.026705,-0.69988 Q -0.02758,-0.69337 -0.020615,-0.69358 L -0.020615,-0.69225 L -0.034545,-0.69225 L -0.050015,-0.7276 L -0.066185,-0.69225 L -0.080045,-0.69225 L -0.080045,-0.69358 Q -0.07238,-0.69316 -0.073395,-0.69988 L -0.073395,-0.7283 Q -0.07245,-0.73789 -0.080185,-0.73726 L -0.080185,-0.73859 L -0.063735,-0.73859 L -0.063735,-0.73726 Q -0.071085,-0.73789 -0.070315,-0.7283 L -0.070315,-0.70009 L -0.052745,-0.73859 L -0.051765,-0.73859 L -0.033845,-0.69848" />
|
||||
<path d="M 0.029855,-0.73859 L 0.029855,-0.73726 Q 0.026005,-0.737925 0.023555,-0.73117 L 0.006125,-0.69141 L 0.004725,-0.69141 L -0.009835,-0.72578 Q -0.014805,-0.73901 -0.018515,-0.73726 L -0.018515,-0.73859 L -0.004655,-0.73859 L -0.004655,-0.73726 Q -0.011273,-0.737038 -0.008855,-0.73166 L -0.005635,-0.72347 L 0.012705,-0.72347 L 0.015575,-0.73019 Q 0.019806,-0.737705 0.012005,-0.73726 L 0.012005,-0.73859 L 0.029855,-0.73859 M -0.004445,-0.7206 L 0.003605,-0.70135 L 0.011725,-0.7206 L -0.004445,-0.7206" />
|
||||
<path d="M 0.079625,-0.69225 L 0.062965,-0.69225 L 0.062965,-0.69358 Q 0.071608,-0.692628 0.065065,-0.70065 L 0.057155,-0.71031 L 0.054215,-0.70618 Q 0.043691,-0.69213 0.051625,-0.69365 L 0.053585,-0.69358 L 0.053585,-0.69225 L 0.032445,-0.69225 L 0.032445,-0.69358 Q 0.03738,-0.692005 0.045115,-0.70457 L 0.052745,-0.71577 L 0.041755,-0.72928 Q 0.03493,-0.73859 0.031605,-0.73726 L 0.031605,-0.73859 L 0.047915,-0.73859 L 0.047915,-0.73726 Q 0.038136,-0.738368 0.046235,-0.72879 L 0.054565,-0.71843 L 0.061215,-0.72823 Q 0.06923,-0.738296 0.059395,-0.73726 L 0.059395,-0.73859 L 0.080185,-0.73859 L 0.080185,-0.73726 Q 0.07553,-0.73733 0.072415,-0.73208 L 0.058975,-0.7129 L 0.069195,-0.70016 Q 0.07427,-0.69302 0.079625,-0.69358 L 0.079625,-0.69225" />
|
||||
<path d="M -0.095165,-0.76085 C -0.096145,-0.76162 -0.097125,-0.76239 -0.098105,-0.76316 C -0.101546,-0.761454 -0.121245,-0.757853 -0.127785,-0.767675 C -0.136661,-0.775793 -0.136733,-0.794709 -0.128065,-0.80264 C -0.119764,-0.812071 -0.09957,-0.81116 -0.092155,-0.80026 L -0.093415,-0.799 C -0.10351,-0.80848 -0.118145,-0.809985 -0.12481,-0.795045 C -0.127678,-0.785359 -0.126698,-0.776784 -0.12187,-0.76932 C -0.114562,-0.759998 -0.097513,-0.761992 -0.094675,-0.77667 L -0.093065,-0.77667 L -0.093695,-0.76085 L -0.095165,-0.76085" />
|
||||
<path d="M -0.075775,-0.80257 L -0.075775,-0.78528 L -0.064995,-0.78528 Q -0.057505,-0.7843 -0.057295,-0.792 L -0.055685,-0.792 L -0.055685,-0.77576 L -0.057295,-0.77576 Q -0.057295,-0.783005 -0.064995,-0.78241 L -0.075775,-0.78241 L -0.075775,-0.76687 Q -0.07602,-0.76421 -0.073465,-0.76449 L -0.064015,-0.76449 Q -0.053655,-0.763475 -0.053375,-0.77184 L -0.051625,-0.77184 L -0.051835,-0.76183 L -0.089005,-0.76183 L -0.089005,-0.76316 Q -0.08211,-0.76288 -0.082915,-0.76946 L -0.082915,-0.80054 Q -0.08211,-0.80712 -0.089005,-0.80684 L -0.089005,-0.80817 L -0.051205,-0.80817 L -0.048055,-0.79634 L -0.050015,-0.79634 Q -0.053375,-0.80663 -0.066535,-0.80558 L -0.068425,-0.80558 Q -0.07714,-0.806105 -0.075775,-0.80257" />
|
||||
<path d="M 0.002345,-0.76316 L 0.002345,-0.76183 L -0.014105,-0.76183 L -0.014105,-0.76316 Q -0.006615,-0.76267 -0.007385,-0.77212 L -0.007385,-0.79571 L -0.034335,-0.76183 L -0.046305,-0.76183 L -0.046305,-0.76316 Q -0.04277,-0.762635 -0.039515,-0.76701 L -0.039515,-0.79788 Q -0.03857,-0.80747 -0.046305,-0.80684 L -0.046305,-0.80817 L -0.029855,-0.80817 L -0.029855,-0.80684 Q -0.037205,-0.80747 -0.036435,-0.79788 L -0.036435,-0.77044 L -0.005495,-0.80894 L -0.004305,-0.80894 L -0.004305,-0.77212 Q -0.004211,-0.76181 0.002345,-0.76316" />
|
||||
<path d="M 0.021035,-0.76477 L 0.021035,-0.79977 Q 0.022085,-0.807505 0.014455,-0.80684 L 0.014455,-0.80817 L 0.034895,-0.80817 L 0.034895,-0.80684 Q 0.027195,-0.80733 0.028175,-0.80054 L 0.028175,-0.76477 L 0.031955,-0.76477 Q 0.04228,-0.76344 0.043085,-0.77373 L 0.044765,-0.77373 L 0.044345,-0.76183 L 0.004865,-0.76183 L 0.004445,-0.77373 L 0.006125,-0.77373 Q 0.00707,-0.76351 0.017255,-0.76477 L 0.021035,-0.76477" />
|
||||
<path d="M 0.060095,-0.80257 L 0.060095,-0.78528 L 0.070875,-0.78528 Q 0.078365,-0.7843 0.078575,-0.792 L 0.080185,-0.792 L 0.080185,-0.77576 L 0.078575,-0.77576 Q 0.078575,-0.783005 0.070875,-0.78241 L 0.060095,-0.78241 L 0.060095,-0.76687 Q 0.05985,-0.76421 0.062405,-0.76449 L 0.071855,-0.76449 Q 0.082215,-0.763475 0.082495,-0.77184 L 0.084245,-0.77184 L 0.084035,-0.76183 L 0.046865,-0.76183 L 0.046865,-0.76316 Q 0.05376,-0.76288 0.052955,-0.76946 L 0.052955,-0.80054 Q 0.05376,-0.80712 0.046865,-0.80684 L 0.046865,-0.80817 L 0.084665,-0.80817 L 0.087815,-0.79634 L 0.085855,-0.79634 Q 0.082495,-0.80663 0.069335,-0.80558 L 0.067445,-0.80558 Q 0.05873,-0.806105 0.060095,-0.80257" />
|
||||
<path d="M 0.108885,-0.76183 L 0.089565,-0.76183 L 0.089565,-0.76316 Q 0.09646,-0.76302 0.095515,-0.76946 L 0.095515,-0.79977 Q 0.09646,-0.807365 0.089565,-0.80684 L 0.089565,-0.80817 L 0.108955,-0.80817 L 0.108955,-0.80684 Q 0.101745,-0.80719 0.102655,-0.80054 L 0.102655,-0.78675 L 0.106575,-0.78661 L 0.123235,-0.80817 L 0.134505,-0.80817 L 0.134505,-0.80684 L 0.128415,-0.80355 L 0.113995,-0.78584 Q 0.114135,-0.78584 0.114345,-0.78577 Q 0.131438,-0.781066 0.124915,-0.76785 Q 0.118129,-0.761097 0.108885,-0.76183 M 0.102655,-0.78416 L 0.102655,-0.76694 C 0.101909,-0.762463 0.113894,-0.764707 0.11592,-0.766765 C 0.117997,-0.768328 0.119035,-0.770743 0.119035,-0.77401 C 0.119222,-0.781173 0.113762,-0.784557 0.102655,-0.78416" />
|
||||
<path d="M -0.783845,-0.69498 L -0.783845,-0.72669 Q -0.7829,-0.734285 -0.790215,-0.73376 L -0.790215,-0.73509 L -0.770615,-0.73509 L -0.770615,-0.73376 Q -0.77751,-0.73404 -0.776705,-0.72746 L -0.776705,-0.69638 Q -0.77758,-0.68987 -0.770615,-0.69008 L -0.770615,-0.68875 L -0.784545,-0.68875 L -0.800015,-0.7241 L -0.816185,-0.68875 L -0.830045,-0.68875 L -0.830045,-0.69008 Q -0.82238,-0.68966 -0.823395,-0.69638 L -0.823395,-0.7248 Q -0.82245,-0.73439 -0.830185,-0.73376 L -0.830185,-0.73509 L -0.813735,-0.73509 L -0.813735,-0.73376 Q -0.821085,-0.73439 -0.820315,-0.7248 L -0.820315,-0.69659 L -0.802745,-0.73509 L -0.801765,-0.73509 L -0.783845,-0.69498" />
|
||||
<path d="M -0.720145,-0.73509 L -0.720145,-0.73376 Q -0.723995,-0.734425 -0.726445,-0.72767 L -0.743875,-0.68791 L -0.745275,-0.68791 L -0.759835,-0.72228 Q -0.764805,-0.73551 -0.768515,-0.73376 L -0.768515,-0.73509 L -0.754655,-0.73509 L -0.754655,-0.73376 Q -0.761273,-0.733538 -0.758855,-0.72816 L -0.755635,-0.71997 L -0.737295,-0.71997 L -0.734425,-0.72669 Q -0.730194,-0.734205 -0.737995,-0.73376 L -0.737995,-0.73509 L -0.720145,-0.73509 M -0.754445,-0.7171 L -0.746395,-0.69785 L -0.738275,-0.7171 L -0.754445,-0.7171" />
|
||||
<path d="M -0.670375,-0.68875 L -0.687035,-0.68875 L -0.687035,-0.69008 Q -0.678392,-0.689128 -0.684935,-0.69715 L -0.692845,-0.70681 L -0.695785,-0.70268 Q -0.706309,-0.68863 -0.698375,-0.69015 L -0.696415,-0.69008 L -0.696415,-0.68875 L -0.717555,-0.68875 L -0.717555,-0.69008 Q -0.71262,-0.688505 -0.704885,-0.70107 L -0.697255,-0.71227 L -0.708245,-0.72578 Q -0.71507,-0.73509 -0.718395,-0.73376 L -0.718395,-0.73509 L -0.702085,-0.73509 L -0.702085,-0.73376 Q -0.711864,-0.734868 -0.703765,-0.72529 L -0.695435,-0.71493 L -0.688785,-0.72473 Q -0.68077,-0.734796 -0.690605,-0.73376 L -0.690605,-0.73509 L -0.669815,-0.73509 L -0.669815,-0.73376 Q -0.67447,-0.73383 -0.677585,-0.72858 L -0.691025,-0.7094 L -0.680805,-0.69666 Q -0.67573,-0.68952 -0.670375,-0.69008 L -0.670375,-0.68875" />
|
||||
<path d="M -0.783845,-0.77198 L -0.783845,-0.80369 Q -0.7829,-0.811285 -0.790215,-0.81076 L -0.790215,-0.81209 L -0.770615,-0.81209 L -0.770615,-0.81076 Q -0.77751,-0.81104 -0.776705,-0.80446 L -0.776705,-0.77338 Q -0.77758,-0.76687 -0.770615,-0.76708 L -0.770615,-0.76575 L -0.784545,-0.76575 L -0.800015,-0.8011 L -0.816185,-0.76575 L -0.830045,-0.76575 L -0.830045,-0.76708 Q -0.82238,-0.76666 -0.823395,-0.77338 L -0.823395,-0.8018 Q -0.82245,-0.81139 -0.830185,-0.81076 L -0.830185,-0.81209 L -0.813735,-0.81209 L -0.813735,-0.81076 Q -0.821085,-0.81139 -0.820315,-0.8018 L -0.820315,-0.77359 L -0.802745,-0.81209 L -0.801765,-0.81209 L -0.783845,-0.77198" />
|
||||
<path d="M -0.720145,-0.81209 L -0.720145,-0.81076 Q -0.723995,-0.811425 -0.726445,-0.80467 L -0.743875,-0.76491 L -0.745275,-0.76491 L -0.759835,-0.79928 Q -0.764805,-0.81251 -0.768515,-0.81076 L -0.768515,-0.81209 L -0.754655,-0.81209 L -0.754655,-0.81076 Q -0.761273,-0.810538 -0.758855,-0.80516 L -0.755635,-0.79697 L -0.737295,-0.79697 L -0.734425,-0.80369 Q -0.730194,-0.811205 -0.737995,-0.81076 L -0.737995,-0.81209 L -0.720145,-0.81209 M -0.754445,-0.7941 L -0.746395,-0.77485 L -0.738275,-0.7941 L -0.754445,-0.7941" />
|
||||
<path d="M -0.670375,-0.76575 L -0.687035,-0.76575 L -0.687035,-0.76708 Q -0.678392,-0.766128 -0.684935,-0.77415 L -0.692845,-0.78381 L -0.695785,-0.77968 Q -0.706309,-0.76563 -0.698375,-0.76715 L -0.696415,-0.76708 L -0.696415,-0.76575 L -0.717555,-0.76575 L -0.717555,-0.76708 Q -0.71262,-0.765505 -0.704885,-0.77807 L -0.697255,-0.78927 L -0.708245,-0.80278 Q -0.71507,-0.81209 -0.718395,-0.81076 L -0.718395,-0.81209 L -0.702085,-0.81209 L -0.702085,-0.81076 Q -0.711864,-0.811868 -0.703765,-0.80229 L -0.695435,-0.79193 L -0.688785,-0.80173 Q -0.68077,-0.811796 -0.690605,-0.81076 L -0.690605,-0.81209 L -0.669815,-0.81209 L -0.669815,-0.81076 Q -0.67447,-0.81083 -0.677585,-0.80558 L -0.691025,-0.7864 L -0.680805,-0.77366 Q -0.67573,-0.76652 -0.670375,-0.76708 L -0.670375,-0.76575" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 19 KiB |
|
|
@ -1,57 +0,0 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<svg width="100.089999mm" height="89.739501mm" viewBox="-21.153999 -19.756955 42.291249 37.91783" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="scale(1,-1)" stroke-linecap="round">
|
||||
<g fill="none" stroke="rgb(0,0,0)" stroke-width="0.03802789924227124" id="Visible">
|
||||
<line x1="16.711371" y1="17.161275" x2="-0.916091" y2="19.737941" />
|
||||
<line x1="-10.364508" y1="14.930779" x2="-0.916091" y2="19.737941" />
|
||||
<line x1="7.262954" y1="12.354113" x2="-10.364508" y2="14.930779" />
|
||||
<line x1="7.262954" y1="12.354113" x2="16.711371" y2="17.161275" />
|
||||
<line x1="16.711371" y1="17.161275" x2="21.118236" y2="11.706626" />
|
||||
<line x1="-21.134985" y1="6.949528" x2="-10.364508" y2="14.930779" />
|
||||
<line x1="21.118236" y1="-7.535307" x2="21.118236" y2="11.706626" />
|
||||
<line x1="-21.134985" y1="-15.178695" x2="-21.134985" y2="6.949528" />
|
||||
<line x1="-0.863404" y1="3.986362" x2="7.262954" y2="12.354113" />
|
||||
<line x1="-0.863404" y1="3.986362" x2="-21.134985" y2="6.949528" />
|
||||
<line x1="7.262954" y1="12.354113" x2="11.669819" y2="6.899463" />
|
||||
<line x1="11.669819" y1="6.899463" x2="21.118236" y2="11.706626" />
|
||||
<line x1="21.118236" y1="-7.535307" x2="16.711371" y2="-11.701624" />
|
||||
<line x1="11.669819" y1="-12.34247" x2="21.118236" y2="-7.535307" />
|
||||
<line x1="11.669819" y1="-12.34247" x2="11.669819" y2="6.899463" />
|
||||
<line x1="7.262954" y1="-16.508787" x2="16.711371" y2="-11.701624" />
|
||||
<line x1="-0.863404" y1="-18.141861" x2="-0.863404" y2="3.986362" />
|
||||
<line x1="-0.863404" y1="-18.141861" x2="-21.134985" y2="-15.178695" />
|
||||
<line x1="11.669819" y1="6.899463" x2="-0.863404" y2="3.986362" />
|
||||
<line x1="11.669819" y1="-12.34247" x2="7.262954" y2="-16.508787" />
|
||||
<line x1="-0.863404" y1="-18.141861" x2="11.669819" y2="-12.34247" />
|
||||
<line x1="7.262954" y1="-16.508787" x2="-0.863404" y2="-18.141861" />
|
||||
</g>
|
||||
<g fill="none" stroke="rgb(99,99,99)" stroke-width="0.03802789924227124" id="Hidden" stroke-dasharray="0.000966 0.114567">
|
||||
<line x1="-5.322957" y1="15.571624" x2="1.446013" y2="16.129248" />
|
||||
<line x1="1.446013" y1="16.129248" x2="-0.916091" y2="19.737941" />
|
||||
<line x1="-0.916091" y1="19.737941" x2="-5.322957" y2="15.571624" />
|
||||
<line x1="1.446013" y1="-3.112684" x2="1.446013" y2="16.129248" />
|
||||
<line x1="-5.322957" y1="-3.670309" x2="1.446013" y2="-3.112684" />
|
||||
<line x1="-5.322957" y1="-3.670309" x2="-5.322957" y2="15.571624" />
|
||||
<line x1="19.073475" y1="13.552583" x2="16.711371" y2="17.161275" />
|
||||
<line x1="19.073475" y1="13.552583" x2="1.446013" y2="16.129248" />
|
||||
<line x1="-10.364508" y1="14.930779" x2="-14.771374" y2="10.764462" />
|
||||
<line x1="-14.771374" y1="10.764462" x2="-5.322957" y2="15.571624" />
|
||||
<line x1="19.073475" y1="-5.68935" x2="19.073475" y2="13.552583" />
|
||||
<line x1="19.073475" y1="-5.68935" x2="1.446013" y2="-3.112684" />
|
||||
<line x1="-0.916091" y1="-9.124958" x2="-5.322957" y2="-3.670309" />
|
||||
<line x1="1.446013" y1="-3.112684" x2="-0.916091" y2="-9.124958" />
|
||||
<line x1="-14.771374" y1="-8.477471" x2="-5.322957" y2="-3.670309" />
|
||||
<line x1="-14.771374" y1="-8.477471" x2="-14.771374" y2="10.764462" />
|
||||
<line x1="21.118236" y1="11.706626" x2="19.073475" y2="13.552583" />
|
||||
<line x1="-14.771374" y1="10.764462" x2="-21.134985" y2="6.949528" />
|
||||
<line x1="19.073475" y1="-5.68935" x2="21.118236" y2="-7.535307" />
|
||||
<line x1="16.711371" y1="-11.701624" x2="-0.916091" y2="-9.124958" />
|
||||
<line x1="16.711371" y1="-11.701624" x2="19.073475" y2="-5.68935" />
|
||||
<line x1="-10.364508" y1="-13.932121" x2="-14.771374" y2="-8.477471" />
|
||||
<line x1="-10.364508" y1="-13.932121" x2="-0.916091" y2="-9.124958" />
|
||||
<line x1="-14.771374" y1="-8.477471" x2="-21.134985" y2="-15.178695" />
|
||||
<line x1="7.262954" y1="-16.508787" x2="-10.364508" y2="-13.932121" />
|
||||
<line x1="-21.134985" y1="-15.178695" x2="-10.364508" y2="-13.932121" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
|
@ -1,95 +0,0 @@
|
|||
import os
|
||||
from copy import copy
|
||||
|
||||
from build123d import *
|
||||
from ocp_vscode import *
|
||||
|
||||
working_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
with BuildPart() as part_context:
|
||||
with BuildSketch() as sketch:
|
||||
with BuildLine() as line:
|
||||
l1 = Line((0, -3), (6, -3))
|
||||
l2 = JernArc(l1 @ 1, l1 % 1, radius=3, arc_size=180)
|
||||
l3 = PolarLine(l2 @ 1, 6, direction=l2 % 1)
|
||||
l4 = Line(l1 @ 0, l3 @ 1)
|
||||
make_face()
|
||||
|
||||
with Locations((6, 0, 0)):
|
||||
Circle(2, mode=Mode.SUBTRACT)
|
||||
|
||||
extrude(amount=2)
|
||||
|
||||
with BuildSketch(Plane.YZ) as plate_sketch:
|
||||
RectangleRounded(16, 6, 1.5, align=(Align.CENTER, Align.MIN))
|
||||
|
||||
plate = extrude(amount=-2)
|
||||
|
||||
with Locations(plate.faces().group_by(Face.area)[-1].sort_by(Axis.X)[-1]):
|
||||
with GridLocations(13, 3, 2, 2):
|
||||
CounterSinkHole(.5, 1)
|
||||
|
||||
fillet(edges().filter_by(lambda e: e.length == 2).filter_by(Axis.Z), 1)
|
||||
bore = faces().filter_by(GeomType.CYLINDER).filter_by(lambda f: f.radius == 2)
|
||||
chamfer(bore.edges(), .2)
|
||||
|
||||
line = Line((0, -3), (6, -3))
|
||||
line += JernArc(line @ 1, line % 1, radius=3, arc_size=180)
|
||||
line += PolarLine(line @ 1, 6, direction=line % 1)
|
||||
|
||||
sketch = make_hull(line.edges())
|
||||
sketch -= Pos(6, 0, 0) * Circle(2)
|
||||
part = extrude(sketch, amount= 2)
|
||||
part_before = copy(part)
|
||||
|
||||
plate_sketch = Plane.YZ * RectangleRounded(16, 6, 1.5, align=(Align.CENTER, Align.MIN))
|
||||
plate = extrude(plate_sketch, amount=-2)
|
||||
plate_face = plate.faces().group_by(Face.area)[-1].sort_by(Axis.X)[-1]
|
||||
plate -= Plane(plate_face) * GridLocations(13, 3, 2, 2) * CounterSinkHole(.5, 1, 2)
|
||||
|
||||
part += plate
|
||||
part_before2 = copy(part)
|
||||
|
||||
part = fillet(part.edges().filter_by(lambda e: e.length == 2).filter_by(Axis.Z), 1)
|
||||
bore = part.faces().filter_by(GeomType.CYLINDER).filter_by(lambda f: f.radius == 2)
|
||||
part = chamfer(bore.edges(), .2)
|
||||
|
||||
class Punch(BaseSketchObject):
|
||||
def __init__(
|
||||
self,
|
||||
radius: float,
|
||||
size: float,
|
||||
blobs: float,
|
||||
rotation: float = 0,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
with BuildSketch() as punch:
|
||||
if blobs == 1:
|
||||
Circle(size)
|
||||
else:
|
||||
with PolarLocations(radius, blobs):
|
||||
Circle(size)
|
||||
|
||||
if len(faces()) > 1:
|
||||
raise RuntimeError("radius is too large for number and size of blobs")
|
||||
|
||||
add(Face(faces()[0].outer_wire()), mode=Mode.REPLACE)
|
||||
|
||||
super().__init__(obj=punch.sketch, rotation=rotation, mode=mode)
|
||||
|
||||
tape = Rectangle(20, 5)
|
||||
for i, location in enumerate(GridLocations(5, 0, 4, 1)):
|
||||
tape -= location * Punch(.8, 1, i + 1)
|
||||
|
||||
set_defaults(reset_camera=Camera.RESET)
|
||||
show(line)
|
||||
save_screenshot(os.path.join(working_path, "create_1d.png"))
|
||||
|
||||
show(sketch, Pos(10, 10) * part_before)
|
||||
save_screenshot(os.path.join(working_path, "upgrade_2d.png"))
|
||||
|
||||
show(plate, Pos(12, 12) * part_before2, Pos(24, 24) * part)
|
||||
save_screenshot(os.path.join(working_path, "add_part.png"))
|
||||
|
||||
show(tape)
|
||||
save_screenshot(os.path.join(working_path, "extend.png"))
|
||||
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 168 KiB |
|
|
@ -21,8 +21,8 @@ with BuildSketch(Location((0, -r1, y3))) as sk_body:
|
|||
m3 = IntersectingLine(m2 @ 1, m2 % 1, c1)
|
||||
m4 = Line(m3 @ 1, (r1, r1))
|
||||
m5 = JernArc(m4 @ 1, m4 % 1, r1, -90)
|
||||
mirror(about=Plane.YZ)
|
||||
make_face()
|
||||
m6 = Line(m5 @ 1, m1 @ 0)
|
||||
mirror(make_face(l.line), Plane.YZ)
|
||||
fillet(sk_body.vertices().group_by(Axis.Y)[1], 12)
|
||||
with Locations((x1 / 2, y_tot - 10), (-x1 / 2, y_tot - 10)):
|
||||
Circle(r2, mode=Mode.SUBTRACT)
|
||||
|
|
|
|||
|
|
@ -47,17 +47,16 @@ with BuildPart() as ppp109:
|
|||
split(bisect_by=Plane.YZ)
|
||||
extrude(amount=6)
|
||||
f = ppp109.faces().filter_by(Axis((0, 0, 0), (-1, 0, 1)))[0]
|
||||
extrude(f, until=Until.NEXT)
|
||||
fillet(ppp109.edges().filter_by(Axis.Y).sort_by(Axis.Z)[2], 16)
|
||||
# extrude(f, amount=10)
|
||||
# fillet(ppp109.edges(Select.NEW), 16)
|
||||
# extrude(f, until=Until.NEXT) # throws a warning
|
||||
extrude(f, amount=10)
|
||||
fillet(ppp109.edge(Select.NEW), 16)
|
||||
|
||||
|
||||
show(ppp109)
|
||||
|
||||
got_mass = ppp109.part.volume * densb
|
||||
got_mass = ppp109.part.volume*densb
|
||||
want_mass = 307.23
|
||||
tolerance = 1
|
||||
delta = abs(got_mass - want_mass)
|
||||
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=}'
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
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"}
|
||||
|
|
@ -15,7 +15,6 @@ Basic Functionality
|
|||
The following is a simple BuildLine example:
|
||||
|
||||
.. literalinclude:: objects_1d.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 1]
|
||||
:end-before: [Ex. 1]
|
||||
|
||||
|
|
@ -51,7 +50,6 @@ 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:
|
||||
|
||||
.. literalinclude:: objects_1d.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 2]
|
||||
:end-before: [Ex. 2]
|
||||
|
||||
|
|
@ -65,7 +63,6 @@ This example can be improved on further by calculating the mid-point
|
|||
of the arc as follows:
|
||||
|
||||
.. literalinclude:: objects_1d.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 3]
|
||||
:end-before: [Ex. 3]
|
||||
|
||||
|
|
@ -76,7 +73,6 @@ To make the design even more parametric, the height of the arc can be calculated
|
|||
from ``l1`` as follows:
|
||||
|
||||
.. literalinclude:: objects_1d.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 4]
|
||||
:end-before: [Ex. 4]
|
||||
|
||||
|
|
@ -91,7 +87,6 @@ The other operator that is commonly used within BuildLine is ``%`` the tangent a
|
|||
operator. Here is another example:
|
||||
|
||||
.. literalinclude:: objects_1d.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 5]
|
||||
:end-before: [Ex. 5]
|
||||
|
||||
|
|
@ -129,7 +124,6 @@ Here is an example of using BuildLine to create an object that otherwise might b
|
|||
difficult to create:
|
||||
|
||||
.. literalinclude:: objects_1d.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 6]
|
||||
:end-before: [Ex. 6]
|
||||
|
||||
|
|
@ -161,7 +155,6 @@ The other primary reasons to use BuildLine is to create paths for BuildPart
|
|||
define a path:
|
||||
|
||||
.. literalinclude:: objects_1d.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 7]
|
||||
:end-before: [Ex. 7]
|
||||
|
||||
|
|
@ -191,7 +184,6 @@ to global coordinates. Sometimes it's convenient to work on another plane, espec
|
|||
creating paths for BuildPart ``Sweep`` operations.
|
||||
|
||||
.. literalinclude:: objects_1d.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 8]
|
||||
:end-before: [Ex. 8]
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ Basic Functionality
|
|||
The following is a simple BuildPart example:
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 2]
|
||||
:end-before: [Ex. 2]
|
||||
|
||||
|
|
@ -53,7 +52,6 @@ This tea cup example uses implicit parameters - note the :func:`~operations_gene
|
|||
operation on the last line:
|
||||
|
||||
.. literalinclude:: ../examples/tea_cup.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
:emphasize-lines: 52
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ Basic Functionality
|
|||
The following is a simple BuildSketch example:
|
||||
|
||||
.. literalinclude:: objects_2d.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 13]
|
||||
:end-before: [Ex. 13]
|
||||
|
||||
|
|
@ -62,7 +61,6 @@ As an example, let's build the following simple control box with a display on an
|
|||
Here is the code:
|
||||
|
||||
.. literalinclude:: objects_2d.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 14]
|
||||
:end-before: [Ex. 14]
|
||||
:emphasize-lines: 14-25
|
||||
|
|
@ -90,14 +88,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
|
||||
above, one would use:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
show_object(display.sketch_local, name="sketch on Plane.XY")
|
||||
|
||||
while the sketches as applied to their target workplanes is accessible through
|
||||
the ``sketch`` property, as follows:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
show_object(display.sketch, name="sketch on target workplane(s)")
|
||||
|
||||
|
|
@ -108,7 +106,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
|
||||
follows:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
reoriented_face = plane.to_local_coords(face)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ Cheat Sheet
|
|||
| :class:`~objects_curve.CenterArc`
|
||||
| :class:`~objects_curve.DoubleTangentArc`
|
||||
| :class:`~objects_curve.EllipticalCenterArc`
|
||||
| :class:`~objects_curve.ParabolicCenterArc`
|
||||
| :class:`~objects_curve.HyperbolicCenterArc`
|
||||
| :class:`~objects_curve.FilletPolyline`
|
||||
| :class:`~objects_curve.Helix`
|
||||
| :class:`~objects_curve.IntersectingLine`
|
||||
|
|
@ -65,7 +63,6 @@ Cheat Sheet
|
|||
|
||||
| :class:`~objects_part.Box`
|
||||
| :class:`~objects_part.Cone`
|
||||
| :class:`~objects_part.ConvexPolyhedron`
|
||||
| :class:`~objects_part.CounterBoreHole`
|
||||
| :class:`~objects_part.CounterSinkHole`
|
||||
| :class:`~objects_part.Cylinder`
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ extensions = [
|
|||
"sphinx_design",
|
||||
"sphinx_copybutton",
|
||||
"hoverxref.extension",
|
||||
"build123d_lexer"
|
||||
]
|
||||
|
||||
# Napoleon settings
|
||||
|
|
@ -100,7 +99,6 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
|||
#
|
||||
# html_theme = "alabaster"
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
pygments_style = "colorful"
|
||||
|
||||
# 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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
location in memory, as follows:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
plane = Plane.XY.offset(1)
|
||||
print(f"{plane=}")
|
||||
|
|
|
|||
|
|
@ -29,11 +29,6 @@ Most of the examples show the builder and algebra modes.
|
|||
:link: examples-bicycle_tire
|
||||
:link-type: ref
|
||||
|
||||
.. grid-item-card:: Bracelet |Algebra|
|
||||
:img-top: assets/examples/bracelet.png
|
||||
:link: examples-bracelet
|
||||
:link-type: ref
|
||||
|
||||
.. grid-item-card:: Canadian Flag Blowing in The Wind |Builder| |Algebra|
|
||||
:img-top: assets/examples/example_canadian_flag_01.png
|
||||
:link: examples-canadian_flag
|
||||
|
|
@ -169,7 +164,6 @@ modify it by replacing chimney with a BREP version.
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/benchy.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -190,40 +184,6 @@ surface.
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/bicycle_tire.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
||||
.. _examples-bracelet:
|
||||
|
||||
Bracelet
|
||||
--------------------------------
|
||||
.. image:: assets/examples/bracelet.png
|
||||
:align: center
|
||||
|
||||
Doubly-curved bracelet with an embossed label
|
||||
|
||||
This model is a good “stress test” for OCCT because most of the final boundary
|
||||
surfaces are *freeform* (not analytic planes/cylinders/spheres). The geometry
|
||||
is assembled from:
|
||||
|
||||
- a swept center section (using a curved solid end-face as the sweep profile)
|
||||
- two freeform “tip caps” built as Gordon surfaces (network of curves)
|
||||
- an optional embossed text label projected onto a curved solid
|
||||
- alignment holes for splitting/printing/assembly
|
||||
|
||||
Key techniques demonstrated:
|
||||
|
||||
- using location_at/position_at/tangent (%) to extract local frames & tangents
|
||||
- projecting curves onto a non-planar surface to create “true” 3D guide curves
|
||||
- Gordon surfaces to build high-quality doubly-curved skins
|
||||
- projecting faces (text) onto a complex solid and thickening them
|
||||
|
||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/bracelet.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -244,14 +204,12 @@ The builder mode example also generates the SVG file `logo.svg`.
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/build123d_logo.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/build123d_logo_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -270,7 +228,6 @@ using the `draft` operation to add appropriate draft angles for mold release.
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/cast_bearing_unit.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -300,14 +257,12 @@ This example also demonstrates building complex lines that snap to existing feat
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/canadian_flag.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/canadian_flag_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -338,14 +293,12 @@ This example demonstrates placing holes around a part.
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/circuit_board.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/circuit_board_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -360,14 +313,12 @@ Clock Face
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/clock.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/clock_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -389,7 +340,6 @@ Fast Grid Holes
|
|||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/fast_grid_holes.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -417,14 +367,12 @@ Handle
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/handle.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/handle_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -440,14 +388,12 @@ Heat Exchanger
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/heat_exchanger.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/heat_exchanger_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -466,14 +412,12 @@ Key Cap
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/key_cap.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/key_cap_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -500,7 +444,6 @@ YouTube channel. There are two key features:
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/maker_coin.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -519,14 +462,12 @@ the top and bottom by type, and shelling.
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/loft.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/loft_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -547,7 +488,6 @@ to aid 3D printing.
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/pegboard_j_hook.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -555,7 +495,6 @@ to aid 3D printing.
|
|||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/pegboard_j_hook_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -582,7 +521,6 @@ embodying ideals of symmetry and balance.
|
|||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/platonic_solids.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -601,7 +539,6 @@ imported as code from an SVG file and modified to the code found here.
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/playing_cards.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -621,7 +558,6 @@ are used to position all of objects.
|
|||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/stud_wall.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -635,14 +571,12 @@ Tea Cup
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/tea_cup.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/tea_cup_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -676,7 +610,6 @@ Toy Truck
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/toy_truck.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -697,14 +630,12 @@ Vase
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/vase.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/vase_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -746,13 +677,11 @@ selecting edges by position range and type for the application of fillets
|
|||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||
|
||||
.. literalinclude:: ../examples/boxes_on_faces.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||
|
||||
.. literalinclude:: ../examples/boxes_on_faces_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
|
|
|||
|
|
@ -115,13 +115,6 @@ A gear generation framework that allows easy creation of a wide range of gears a
|
|||
|
||||
See `gggears <https://github.com/GarryBGoode/gggears>`_
|
||||
|
||||
bd_vslot
|
||||
=================
|
||||
|
||||
A library of V-Slot linear rail components, including V-Slot rails.
|
||||
|
||||
See: `bd_vslot <https://bd-vslot.readthedocs.io>`_
|
||||
|
||||
*****
|
||||
Tools
|
||||
*****
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Methods and functions specific to exporting and importing build123d objects are
|
|||
|
||||
For example:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as box_builder:
|
||||
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 this example:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
view_port_origin=(-100, -50, 30)
|
||||
visible, hidden = part.project_to_viewport(view_port_origin)
|
||||
|
|
@ -222,7 +222,7 @@ more complex API than the simple Shape exporters.
|
|||
|
||||
For example:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
# Create the shapes and assign attributes
|
||||
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:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
importer = Mesher()
|
||||
cone, cyl = importer.read("example.3mf")
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ expressive, algebraic modeling. It offers:
|
|||
* Operator-driven modeling (``obj += sub_obj``, ``Plane.XZ * Pos(X=5) * Rectangle(1, 1)``)
|
||||
for algebraic, readable, and composable design logic
|
||||
|
||||
.. code-block:: build123d
|
||||
The result is a framework that feels native to Python while providing the full power of
|
||||
OpenCascade geometry underneath.
|
||||
|
||||
|
||||
With build123d, intricate parametric models can be created in just a few lines of readable
|
||||
|
|
@ -59,10 +60,9 @@ Python code—as demonstrated by the tea cup example below.
|
|||
|
||||
.. dropdown:: Teacup Example
|
||||
|
||||
.. literalinclude:: ../examples/tea_cup.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
.. literalinclude:: ../examples/tea_cup.py
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
||||
.. raw:: html
|
||||
|
||||
|
|
|
|||
|
|
@ -36,14 +36,12 @@ Just about the simplest possible example, a rectangular :class:`~objects_part.Bo
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 1]
|
||||
:end-before: [Ex. 1]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 1]
|
||||
:end-before: [Ex. 1]
|
||||
|
||||
|
|
@ -65,7 +63,6 @@ A rectangular box, but with a hole added.
|
|||
from the :class:`~objects_part.Box`.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 2]
|
||||
:end-before: [Ex. 2]
|
||||
|
||||
|
|
@ -76,7 +73,6 @@ A rectangular box, but with a hole added.
|
|||
from the :class:`~objects_part.Box`.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 2]
|
||||
:end-before: [Ex. 2]
|
||||
|
||||
|
|
@ -94,11 +90,10 @@ Build a prismatic solid using extrusion.
|
|||
* **Builder mode**
|
||||
|
||||
This time we can first create a 2D :class:`~build_sketch.BuildSketch` adding a
|
||||
:class:`~objects_sketch.Circle` and a subtracted :class:`~objects_sketch.Rectangle`
|
||||
:class:`~objects_sketch.Circle` and a subtracted :class:`~objects_sketch.Rectangle``
|
||||
and then use :class:`~build_part.BuildPart`'s :meth:`~operations_part.extrude` feature.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 3]
|
||||
:end-before: [Ex. 3]
|
||||
|
||||
|
|
@ -108,7 +103,6 @@ Build a prismatic solid using extrusion.
|
|||
:class:`~objects_sketch.Rectangle`` and then use the :meth:`~operations_part.extrude` operation for parts.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 3]
|
||||
:end-before: [Ex. 3]
|
||||
|
||||
|
|
@ -132,7 +126,6 @@ variables for the line segments, but it will be useful in a later example.
|
|||
from :class:`~build_line.BuildLine` into a closed Face.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 4]
|
||||
:end-before: [Ex. 4]
|
||||
|
||||
|
|
@ -145,7 +138,6 @@ variables for the line segments, but it will be useful in a later example.
|
|||
segments into a Face.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 4]
|
||||
:end-before: [Ex. 4]
|
||||
|
||||
|
|
@ -166,7 +158,6 @@ Note that to build a closed face it requires line segments that form a closed sh
|
|||
at one (or multiple) places.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 5]
|
||||
:end-before: [Ex. 5]
|
||||
|
||||
|
|
@ -177,7 +168,6 @@ Note that to build a closed face it requires line segments that form a closed sh
|
|||
(with :class:`geometry.Rot`) would rotate the object.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 5]
|
||||
:end-before: [Ex. 5]
|
||||
|
||||
|
|
@ -198,7 +188,6 @@ Sometimes you need to create a number of features at various
|
|||
You can use a list of points to construct multiple objects at once.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 6]
|
||||
:end-before: [Ex. 6]
|
||||
|
||||
|
|
@ -211,7 +200,6 @@ 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`).
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 6]
|
||||
:end-before: [Ex. 6]
|
||||
|
||||
|
|
@ -230,7 +218,6 @@ Sometimes you need to create a number of features at various
|
|||
you would like.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 7]
|
||||
:end-before: [Ex. 7]
|
||||
|
||||
|
|
@ -240,7 +227,6 @@ Sometimes you need to create a number of features at various
|
|||
for each location via loops or list comprehensions.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 7]
|
||||
:end-before: [Ex. 7]
|
||||
|
||||
|
|
@ -261,14 +247,12 @@ create the final profile.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 8]
|
||||
:end-before: [Ex. 8]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 8]
|
||||
:end-before: [Ex. 8]
|
||||
|
||||
|
|
@ -289,14 +273,12 @@ edges, you could simply pass in ``ex9.edges()``.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 9]
|
||||
:end-before: [Ex. 9]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 9]
|
||||
:end-before: [Ex. 9]
|
||||
|
||||
|
|
@ -321,7 +303,6 @@ be the highest z-dimension group.
|
|||
makes use of :class:`~objects_part.Hole` which automatically cuts through the entire part.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 10]
|
||||
:end-before: [Ex. 10]
|
||||
|
||||
|
|
@ -333,7 +314,6 @@ 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.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 10]
|
||||
:end-before: [Ex. 10]
|
||||
|
||||
|
|
@ -359,7 +339,6 @@ be the highest z-dimension group.
|
|||
cut these from the parent.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 11]
|
||||
:end-before: [Ex. 11]
|
||||
|
||||
|
|
@ -376,7 +355,6 @@ be the highest z-dimension group.
|
|||
parent.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 11]
|
||||
:end-before: [Ex. 11]
|
||||
|
||||
|
|
@ -398,21 +376,19 @@ edge that needs a complex profile.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 12]
|
||||
:end-before: [Ex. 12]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 12]
|
||||
:end-before: [Ex. 12]
|
||||
|
||||
|
||||
.. _ex 13:
|
||||
|
||||
13. CounterBoreHoles, CounterSinkHoles, and PolarLocations
|
||||
13. CounterBoreHoles, CounterSinkHoles and PolarLocations
|
||||
-------------------------------------------------------------
|
||||
|
||||
Counter-sink and counter-bore holes are useful for creating recessed areas for fasteners.
|
||||
|
|
@ -425,7 +401,6 @@ 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`.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 13]
|
||||
:end-before: [Ex. 13]
|
||||
|
||||
|
|
@ -435,7 +410,6 @@ Counter-sink and counter-bore holes are useful for creating recessed areas for f
|
|||
onto this plane.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 13]
|
||||
:end-before: [Ex. 13]
|
||||
|
||||
|
|
@ -443,7 +417,7 @@ Counter-sink and counter-bore holes are useful for creating recessed areas for f
|
|||
|
||||
.. _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
|
||||
|
|
@ -463,10 +437,9 @@ path, please see example 37 for a way to make this placement easier.
|
|||
|
||||
The :meth:`~operations_generic.sweep` method takes any pending faces and sweeps them through the provided
|
||||
path (in this case the path is taken from the pending edges from ``ex14_ln``).
|
||||
:meth:`~operations_part.revolve` requires a single connected wire.
|
||||
:meth:`~operations_part.revolve` requires a single connected wire.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 14]
|
||||
:end-before: [Ex. 14]
|
||||
|
||||
|
|
@ -476,7 +449,6 @@ path, please see example 37 for a way to make this placement easier.
|
|||
path (in this case the path is taken from ``ex14_ln``).
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 14]
|
||||
:end-before: [Ex. 14]
|
||||
|
||||
|
|
@ -499,7 +471,6 @@ Additionally the '@' operator is used to simplify the line segment commands.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 15]
|
||||
:end-before: [Ex. 15]
|
||||
|
||||
|
|
@ -508,7 +479,6 @@ Additionally the '@' operator is used to simplify the line segment commands.
|
|||
Combine lines via the pattern ``Curve() + [l1, l2, l3, l4, l5]``
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 15]
|
||||
:end-before: [Ex. 15]
|
||||
|
||||
|
|
@ -526,14 +496,12 @@ The ``Plane.offset()`` method shifts the plane in the normal direction (positive
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 16]
|
||||
:end-before: [Ex. 16]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 16]
|
||||
:end-before: [Ex. 16]
|
||||
|
||||
|
|
@ -552,14 +520,12 @@ Here we select the farthest face in the Y-direction and turn it into a :class:`~
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 17]
|
||||
:end-before: [Ex. 17]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 17]
|
||||
:end-before: [Ex. 17]
|
||||
|
||||
|
|
@ -580,7 +546,6 @@ with a negative distance.
|
|||
We then use ``Mode.SUBTRACT`` to cut it out from the main body.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 18]
|
||||
:end-before: [Ex. 18]
|
||||
|
||||
|
|
@ -589,7 +554,6 @@ with a negative distance.
|
|||
We then use ``-=`` to cut it out from the main body.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 18]
|
||||
:end-before: [Ex. 18]
|
||||
|
||||
|
|
@ -614,7 +578,6 @@ this custom Axis.
|
|||
:class:`~build_common.Locations` then the part would be offset from the workplane by the vertex z-position.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 19]
|
||||
:end-before: [Ex. 19]
|
||||
|
||||
|
|
@ -625,7 +588,6 @@ this custom Axis.
|
|||
:class:`~geometry.Pos` then the part would be offset from the workplane by the vertex z-position.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 19]
|
||||
:end-before: [Ex. 19]
|
||||
|
||||
|
|
@ -644,14 +606,12 @@ negative x-direction. The resulting Plane is offset from the original position.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 20]
|
||||
:end-before: [Ex. 20]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 20]
|
||||
:end-before: [Ex. 20]
|
||||
|
||||
|
|
@ -670,14 +630,12 @@ positioning another cylinder perpendicular and halfway along the first.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 21]
|
||||
:end-before: [Ex. 21]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 21]
|
||||
:end-before: [Ex. 21]
|
||||
|
||||
|
|
@ -698,7 +656,6 @@ example.
|
|||
Use the :meth:`~geometry.Plane.rotated` method to rotate the workplane.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 22]
|
||||
:end-before: [Ex. 22]
|
||||
|
||||
|
|
@ -707,7 +664,6 @@ example.
|
|||
Use the operator ``*`` to relocate the plane (post-multiplication!).
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 22]
|
||||
:end-before: [Ex. 22]
|
||||
|
||||
|
|
@ -734,14 +690,12 @@ It is highly recommended to view your sketch before you attempt to call revolve.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 23]
|
||||
:end-before: [Ex. 23]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 23]
|
||||
:end-before: [Ex. 23]
|
||||
|
||||
|
|
@ -762,14 +716,12 @@ Loft can behave unexpectedly when the input faces are not parallel to each other
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 24]
|
||||
:end-before: [Ex. 24]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 24]
|
||||
:end-before: [Ex. 24]
|
||||
|
||||
|
|
@ -787,7 +739,6 @@ 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`.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 25]
|
||||
:end-before: [Ex. 25]
|
||||
|
||||
|
|
@ -796,7 +747,6 @@ 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`.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 25]
|
||||
:end-before: [Ex. 25]
|
||||
|
||||
|
|
@ -822,14 +772,12 @@ Note that self intersecting edges and/or faces can break both 2D and 3D offsets.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 26]
|
||||
:end-before: [Ex. 26]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 26]
|
||||
:end-before: [Ex. 26]
|
||||
|
||||
|
|
@ -848,14 +796,12 @@ a face and offset half the width of the box.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 27]
|
||||
:end-before: [Ex. 27]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 27]
|
||||
:end-before: [Ex. 27]
|
||||
|
||||
|
|
@ -874,7 +820,6 @@ a face and offset half the width of the box.
|
|||
use the faces of this object to cut holes in a sphere.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 28]
|
||||
:end-before: [Ex. 28]
|
||||
|
||||
|
|
@ -883,7 +828,6 @@ 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.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 28]
|
||||
:end-before: [Ex. 28]
|
||||
|
||||
|
|
@ -905,14 +849,12 @@ the bottle opening.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 29]
|
||||
:end-before: [Ex. 29]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 29]
|
||||
:end-before: [Ex. 29]
|
||||
|
||||
|
|
@ -932,14 +874,12 @@ create a closed line that is made into a face and extruded.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 30]
|
||||
:end-before: [Ex. 30]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 30]
|
||||
:end-before: [Ex. 30]
|
||||
|
||||
|
|
@ -959,14 +899,12 @@ rotates any "children" groups by default.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 31]
|
||||
:end-before: [Ex. 31]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 31]
|
||||
:end-before: [Ex. 31]
|
||||
|
||||
|
|
@ -989,14 +927,12 @@ separate calls to :meth:`~operations_part.extrude`.
|
|||
adding these faces until the for-loop.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 32]
|
||||
:end-before: [Ex. 32]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 32]
|
||||
:end-before: [Ex. 32]
|
||||
|
||||
|
|
@ -1018,7 +954,6 @@ progressively modify the size of each square.
|
|||
The function returns a :class:`~build_sketch.BuildSketch`.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 33]
|
||||
:end-before: [Ex. 33]
|
||||
|
||||
|
|
@ -1027,7 +962,6 @@ progressively modify the size of each square.
|
|||
The function returns a ``Sketch`` object.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 33]
|
||||
:end-before: [Ex. 33]
|
||||
|
||||
|
|
@ -1049,7 +983,6 @@ progressively modify the size of each square.
|
|||
the 2nd "World" text on the top of the "Hello" text.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 34]
|
||||
:end-before: [Ex. 34]
|
||||
|
||||
|
|
@ -1060,7 +993,6 @@ progressively modify the size of each square.
|
|||
the ``topf`` variable to select the same face and deboss (indented) the text "World".
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 34]
|
||||
:end-before: [Ex. 34]
|
||||
|
||||
|
|
@ -1080,7 +1012,6 @@ progressively modify the size of each square.
|
|||
arc for two instances of :class:`~objects_sketch.SlotArc`.
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 35]
|
||||
:end-before: [Ex. 35]
|
||||
|
||||
|
|
@ -1090,7 +1021,6 @@ progressively modify the size of each square.
|
|||
a :class:`~objects_curve.RadiusArc` to create an arc for two instances of :class:`~operations_sketch.SlotArc`.
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 35]
|
||||
:end-before: [Ex. 35]
|
||||
|
||||
|
|
@ -1111,14 +1041,11 @@ with ``Until.NEXT`` or ``Until.LAST``.
|
|||
* **Builder mode**
|
||||
|
||||
.. literalinclude:: general_examples.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 36]
|
||||
:end-before: [Ex. 36]
|
||||
|
||||
* **Algebra mode**
|
||||
|
||||
.. literalinclude:: general_examples_algebra.py
|
||||
:language: build123d
|
||||
:start-after: [Ex. 36]
|
||||
:end-before: [Ex. 36]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
:class:`~geometry.Location`) - as follows:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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
|
||||
repositioning another part relative to ``self`` which stay fixed - as follows:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
pipe.joints["outlet"].connect_to(flange_outlet.joints["pipe"])
|
||||
|
||||
|
|
@ -70,7 +70,6 @@ flanges are attached to the ends of a curved pipe:
|
|||
.. image:: assets/rigid_joints_pipe.png
|
||||
|
||||
.. literalinclude:: rigid_joints_pipe.py
|
||||
:language: build123d
|
||||
:emphasize-lines: 19-20, 23-24
|
||||
|
||||
Note how the locations of the joints are determined by the :meth:`~topology.Mixin1D.location_at` method
|
||||
|
|
@ -133,7 +132,6 @@ Component moves along a single axis as with a sliding latch shown here:
|
|||
The code to generate these components follows:
|
||||
|
||||
.. literalinclude:: slide_latch.py
|
||||
:language: build123d
|
||||
:emphasize-lines: 30, 52, 55
|
||||
|
||||
.. image:: assets/joint-latch.png
|
||||
|
|
@ -195,7 +193,6 @@ is found within a rod end as shown here:
|
|||
.. image:: assets/rod_end.png
|
||||
|
||||
.. literalinclude:: rod_end.py
|
||||
:language: build123d
|
||||
: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
|
||||
|
|
|
|||
|
|
@ -12,26 +12,26 @@ Object arithmetic
|
|||
|
||||
- Creating a box and a cylinder centered at ``(0, 0, 0)``
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
b = Box(1, 2, 3)
|
||||
c = Cylinder(0.2, 5)
|
||||
|
||||
- Fusing a box and a cylinder
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
r = Box(1, 2, 3) + Cylinder(0.2, 5)
|
||||
|
||||
- Cutting a cylinder from a box
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
r = Box(1, 2, 3) - Cylinder(0.2, 5)
|
||||
|
||||
- Intersecting a box and a cylinder
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
plane * 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``
|
||||
(the location is relative to the local coordinate system of the plane).
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
plane * location * alg_compound
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ Examples:
|
|||
|
||||
- Box on the ``XY`` plane, centered at `(0, 0, 0)` (both forms are equivalent):
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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):
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
Plane.XY * Pos(0, 1, 0) * Box(1, 2, 3)
|
||||
|
||||
|
|
@ -96,21 +96,21 @@ Examples:
|
|||
|
||||
- Box on plane ``Plane.XZ``:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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.,
|
||||
using the x-, y- and z-axis of the ``XZ`` plane:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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
|
||||
by the angles `(X=0, Y=100, Z=45)` around ``Plane.XZ`` axes:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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
|
||||
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:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
b = Plane.XZ * Rot(X=30) * Box(1, 2, 3) + Plane.YZ * Pos(X=-1) * Cylinder(0.2, 5)
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ Example Workflow
|
|||
|
||||
Here is an example of using a Builder to create a simple part:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
from build123d import *
|
||||
|
||||
|
|
@ -117,21 +117,21 @@ class for further processing.
|
|||
One can access the objects created by these builders by referencing the appropriate
|
||||
instance variable. For example:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as my_part:
|
||||
...
|
||||
|
||||
show_object(my_part.part)
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildSketch() as my_sketch:
|
||||
...
|
||||
|
||||
show_object(my_sketch.sketch)
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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
|
||||
objects or operations that impact that builder like this:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as part_builder:
|
||||
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
|
||||
information - as follows:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as part_builder:
|
||||
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.
|
||||
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart(Plane.XY) as example:
|
||||
... # 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
|
||||
rotated boxes.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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
|
||||
more locations are active within a scope. For example:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart():
|
||||
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
|
||||
nested. It's easy for a user to retrieve the global locations:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with Locations(Plane.XY, Plane.XZ):
|
||||
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:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
def fillet(
|
||||
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
|
||||
vertices must be provided followed by a fillet radius with or without the keyword as follows:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as pipes:
|
||||
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
|
||||
``Mode`` Enum class as follows:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
class Mode(Enum):
|
||||
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
|
||||
will be created at the origin of the given workplane such that:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as pipes:
|
||||
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
|
||||
objects as follows:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as pipes:
|
||||
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
|
||||
there was one. Here is an example:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
height, width, thickness, f_rad = 60, 80, 20, 10
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@
|
|||
Location arithmetic for algebra mode
|
||||
======================================
|
||||
|
||||
|
||||
Position a shape relative to the XY plane
|
||||
---------------------------------------------
|
||||
|
||||
For the following use the helper function:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
def location_symbol(location: Location, scale: float = 1) -> Compound:
|
||||
return Compound.make_triad(axes_scale=scale).locate(location)
|
||||
|
|
@ -18,131 +19,134 @@ For the following use the helper function:
|
|||
circle = Circle(scale * .8).edge()
|
||||
return (triad + circle).locate(plane.location)
|
||||
|
||||
|
||||
1. **Positioning at a location**
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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(location_symbol(loc), name="location")
|
||||
show_object(face, name="face")
|
||||
show_object(location_symbol(loc), name="location")
|
||||
|
||||
.. image:: assets/location-example-01.png
|
||||
.. image:: assets/location-example-01.png
|
||||
|
||||
2) **Positioning on a plane**
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
plane = Plane.XZ
|
||||
plane = Plane.XZ
|
||||
|
||||
face = plane * Rectangle(1, 2)
|
||||
face = plane * Rectangle(1, 2)
|
||||
|
||||
show_object(face, name="face")
|
||||
show_object(plane_symbol(plane), name="plane")
|
||||
show_object(face, name="face")
|
||||
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
|
||||
------------------------------------
|
||||
|
||||
1. **Position an object on a plane relative to the plane**
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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(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 = 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 = loc * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
|
||||
|
||||
show_object(face, name="face")
|
||||
show_object(location_symbol(loc), name="location")
|
||||
show_object(box, name="box")
|
||||
show_object(face, name="face")
|
||||
show_object(location_symbol(loc), name="location")
|
||||
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
|
||||
``z``-axis of the underlying location ``loc``.
|
||||
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``.
|
||||
|
||||
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**
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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(location_symbol(loc), name="location")
|
||||
show_object(box, name="box")
|
||||
show_object(face, name="face")
|
||||
show_object(location_symbol(loc), name="location")
|
||||
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
|
||||
(and not of the z-axis of the world).
|
||||
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).
|
||||
|
||||
More general:
|
||||
More general:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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(location_symbol(loc), name="location")
|
||||
show_object(box, name="box")
|
||||
show_object(face, name="face")
|
||||
show_object(location_symbol(loc), name="location")
|
||||
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**
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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(location_symbol(loc), name="location")
|
||||
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(face, name="face")
|
||||
show_object(location_symbol(loc), name="location")
|
||||
show_object(box, name="box")
|
||||
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**
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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(location_symbol(loc), name="location")
|
||||
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(face, name="face")
|
||||
show_object(location_symbol(loc), name="location")
|
||||
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")
|
||||
|
||||
.. 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)``
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ construction process. The following tools are commonly used to specify locations
|
|||
|
||||
Example:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with Locations((10, 20, 30)):
|
||||
Box(5, 5, 5)
|
||||
|
|
@ -42,7 +42,7 @@ an existing one.
|
|||
|
||||
Example:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
rotated_box = Rotation(45, 0, 0) * box
|
||||
|
||||
|
|
@ -55,13 +55,13 @@ Position
|
|||
^^^^^^^^
|
||||
- **Absolute Position:** Set the position directly.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
shape.position = (x, y, z)
|
||||
|
||||
- **Relative Position:** Adjust the position incrementally.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
shape.position += (x, y, z)
|
||||
shape.position -= (x, y, z)
|
||||
|
|
@ -71,13 +71,13 @@ Orientation
|
|||
^^^^^^^^^^^
|
||||
- **Absolute Orientation:** Set the orientation directly.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
shape.orientation = (X, Y, Z)
|
||||
|
||||
- **Relative Orientation:** Adjust the orientation incrementally.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
shape.orientation += (X, Y, Z)
|
||||
shape.orientation -= (X, Y, Z)
|
||||
|
|
@ -86,25 +86,25 @@ Movement Methods
|
|||
^^^^^^^^^^^^^^^^
|
||||
- **Relative Move:**
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
shape.move(Location)
|
||||
|
||||
- **Relative Move of Copy:**
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
relocated_shape = shape.moved(Location)
|
||||
|
||||
- **Absolute Move:**
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
shape.locate(Location)
|
||||
|
||||
- **Absolute Move of Copy:**
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
relocated_shape = shape.translate(x, y, z)
|
||||
|
||||
- **Rotation:** Rotate a shape around a specified axis by a given angle.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
rotated_shape = shape.rotate(Axis, angle_in_degrees)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
are positioned with the ``*`` operator and shown in these examples:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as disk:
|
||||
with BuildSketch():
|
||||
|
|
@ -18,7 +18,7 @@ are positioned with the ``*`` operator and shown in these examples:
|
|||
Circle(d, mode=Mode.SUBTRACT)
|
||||
extrude(amount=c)
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
sketch = Circle(a) - Pos(b, 0.0) * Rectangle(c, c) - Pos(0.0, b) * Circle(d)
|
||||
disk = extrude(sketch, c)
|
||||
|
|
@ -36,7 +36,7 @@ right or left of each Axis. The following diagram shows how this alignment works
|
|||
|
||||
For example:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildSketch():
|
||||
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 -
|
||||
as shown here:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildSketch():
|
||||
Circle(1, align=Align.MIN)
|
||||
|
|
@ -118,20 +118,6 @@ The following objects all can be used in BuildLine contexts. Note that
|
|||
+++
|
||||
Elliptical arc defined by center, radii & angles
|
||||
|
||||
.. grid-item-card:: :class:`~objects_curve.ParabolicCenterArc`
|
||||
|
||||
.. image:: assets/parabolic_center_arc_example.svg
|
||||
|
||||
+++
|
||||
Parabolic arc defined by vertex, focal length & angles
|
||||
|
||||
.. grid-item-card:: :class:`~objects_curve.HyperbolicCenterArc`
|
||||
|
||||
.. image:: assets/hyperbolic_center_arc_example.svg
|
||||
|
||||
+++
|
||||
Hyperbolic arc defined by center, radii & angles
|
||||
|
||||
.. grid-item-card:: :class:`~objects_curve.FilletPolyline`
|
||||
|
||||
.. image:: assets/filletpolyline_example.svg
|
||||
|
|
@ -255,8 +241,6 @@ Reference
|
|||
.. autoclass:: CenterArc
|
||||
.. autoclass:: DoubleTangentArc
|
||||
.. autoclass:: EllipticalCenterArc
|
||||
.. autoclass:: ParabolicCenterArc
|
||||
.. autoclass:: HyperbolicCenterArc
|
||||
.. autoclass:: FilletPolyline
|
||||
.. autoclass:: Helix
|
||||
.. autoclass:: IntersectingLine
|
||||
|
|
@ -455,13 +439,6 @@ Reference
|
|||
+++
|
||||
Cone defined by radii and height
|
||||
|
||||
.. grid-item-card:: :class:`~objects_part.ConvexPolyhedron`
|
||||
|
||||
.. image:: assets/convex_polyhedron_example.svg
|
||||
|
||||
+++
|
||||
Convex Polyhedron defined by points
|
||||
|
||||
.. grid-item-card:: :class:`~objects_part.CounterBoreHole`
|
||||
|
||||
.. image:: assets/counter_bore_hole_example.svg
|
||||
|
|
@ -519,7 +496,6 @@ Reference
|
|||
.. autoclass:: BasePartObject
|
||||
.. autoclass:: Box
|
||||
.. autoclass:: Cone
|
||||
.. autoclass:: ConvexPolyhedron
|
||||
.. autoclass:: CounterBoreHole
|
||||
.. autoclass:: CounterSinkHole
|
||||
.. autoclass:: Cylinder
|
||||
|
|
@ -543,7 +519,6 @@ 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>`):
|
||||
|
||||
.. literalinclude:: ../examples/playing_cards.py
|
||||
:language: build123d
|
||||
:start-after: [Club]
|
||||
:end-before: [Club]
|
||||
|
||||
|
|
|
|||
|
|
@ -125,22 +125,6 @@ svg.add_shape(elliptical_center_arc.line)
|
|||
svg.add_shape(dot.moved(Location(Vector((0, 0)))))
|
||||
svg.write("assets/elliptical_center_arc_example.svg")
|
||||
|
||||
with BuildLine() as parabolic_center_arc:
|
||||
ParabolicCenterArc((0, 0), 0.5, 60, 0)
|
||||
s = 100 / max(*parabolic_center_arc.line.bounding_box().size)
|
||||
svg = ExportSVG(scale=s)
|
||||
svg.add_shape(parabolic_center_arc.line)
|
||||
svg.add_shape(dot.moved(Location(Vector((0, 0)))))
|
||||
svg.write("assets/parabolic_center_arc_example.svg")
|
||||
|
||||
with BuildLine() as hyperbolic_center_arc:
|
||||
HyperbolicCenterArc((0, 0), 0.5, 1, 45, 90)
|
||||
s = 100 / max(*hyperbolic_center_arc.line.bounding_box().size)
|
||||
svg = ExportSVG(scale=s)
|
||||
svg.add_shape(hyperbolic_center_arc.line)
|
||||
svg.add_shape(dot.moved(Location(Vector((0, 0)))))
|
||||
svg.write("assets/hyperbolic_center_arc_example.svg")
|
||||
|
||||
with BuildLine() as helix:
|
||||
Helix(1, 3, 1)
|
||||
scene = Compound(helix.line) + Compound.make_triad(0.5)
|
||||
|
|
|
|||
|
|
@ -268,9 +268,9 @@ with BuildSketch() as align:
|
|||
Text("MAX", font="FreeSerif", font_size=0.07)
|
||||
# Bottom Center: (CENTER, MAX)
|
||||
with Locations((0.0, -0.75 + 0.07 / 2)):
|
||||
Text("CENTER", font="FreeSerif", font_size=0.07)
|
||||
with Locations((0.0, -0.75 - 0.07 / 2)):
|
||||
Text("MAX", font="FreeSerif", font_size=0.07)
|
||||
with Locations((0.0, -0.75 - 0.07 / 2)):
|
||||
Text("CENTER", font="FreeSerif", font_size=0.07)
|
||||
# Bottom Left: (MAx, MAX)
|
||||
with Locations((-0.75, -0.75)):
|
||||
Text("MAX\nMAX", font="FreeSerif", font_size=0.07)
|
||||
|
|
|
|||
|
|
@ -77,14 +77,3 @@ with BuildPart() as example_9:
|
|||
Wedge(1, 1, 1, 0, 0, 0.5, 0.5)
|
||||
# [Ex. 9]
|
||||
write_svg("wedge_example")
|
||||
|
||||
# [Ex. 10]
|
||||
with BuildPart() as example_10:
|
||||
Box(30, 20, 20)
|
||||
Box(20, 30, 20)
|
||||
Box(20, 20, 30)
|
||||
with Locations((-10, 0, 0)):
|
||||
Box(40, 23, 23)
|
||||
ConvexPolyhedron(example_10.vertices())
|
||||
# [Ex. 10]
|
||||
write_svg("convex_polyhedron_example")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as cylinder:
|
||||
with BuildSketch():
|
||||
Circle(radius)
|
||||
extrude(amount=height)
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
cylinder = extrude(Circle(radius), amount=height)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
sorts and filters. Here is an example of a custom filters:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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
|
||||
selectors as shown in this example:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
obj = Box(1, 1, 1) - Cylinder(0.2, 1)
|
||||
faces_with_holes = obj.faces().filter_by(lambda f: f.inner_wires())
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
Technical Drawing Tutorial
|
||||
##########################
|
||||
|
||||
This example demonstrates how to generate a standard technical drawing of a 3D part
|
||||
using `build123d`. It creates orthographic and isometric views of a Nema 23 stepper
|
||||
This example demonstrates how to generate a standard technical drawing of a 3D part
|
||||
using `build123d`. It creates orthographic and isometric views of a Nema 23 stepper
|
||||
motor and exports the result as an SVG file suitable for printing or inspection.
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
A technical drawing represents a 3D object in 2D using a series of standardized views.
|
||||
A technical drawing represents a 3D object in 2D using a series of standardized views.
|
||||
These include:
|
||||
|
||||
- **Plan (Top View)** – as seen from directly above (Z-axis down)
|
||||
|
|
@ -24,8 +24,8 @@ Each view is aligned to a position on the page and optionally scaled or annotate
|
|||
How It Works
|
||||
------------
|
||||
|
||||
The script uses the `project_to_viewport` method to project the 3D part geometry into 2D.
|
||||
A helper function, `project_to_2d`, sets up the viewport (camera origin and up direction)
|
||||
The script uses the `project_to_viewport` method to project the 3D part geometry into 2D.
|
||||
A helper function, `project_to_2d`, sets up the viewport (camera origin and up direction)
|
||||
and places the result onto a virtual drawing sheet.
|
||||
|
||||
The steps involved are:
|
||||
|
|
@ -34,7 +34,7 @@ The steps involved are:
|
|||
2. Define a `TechnicalDrawing` border and title block using A4 page size.
|
||||
3. Generate each of the standard views and apply transformations to place them.
|
||||
4. Add dimensions using `ExtensionLine` and labels using `Text`.
|
||||
5. Export the drawing using `ExportSVG`, separating visible and hidden edges by layer
|
||||
5. Export the drawing using `ExportSVG`, separating visible and hidden edges by layer
|
||||
and style.
|
||||
|
||||
Result
|
||||
|
|
@ -59,7 +59,7 @@ Code
|
|||
----
|
||||
|
||||
.. literalinclude:: technical_drawing.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:start-after: [code]
|
||||
:end-before: [end]
|
||||
|
||||
|
|
|
|||
|
|
@ -162,13 +162,9 @@ vis, _ = project_to_2d(
|
|||
)
|
||||
visible_lines.extend(vis)
|
||||
side_bbox = Curve(vis).bounding_box()
|
||||
shaft_top_corner = vis.edges().sort_by(Axis.Y)[-1].vertices().sort_by(Axis.X)[-1]
|
||||
body_bottom_corner = (side_bbox.max.X, side_bbox.min.Y)
|
||||
perimeter = Pos(*side_bbox.center()) * Rectangle(side_bbox.size.X, side_bbox.size.Y)
|
||||
d4 = ExtensionLine(
|
||||
border=(shaft_top_corner, body_bottom_corner),
|
||||
offset=-(side_bbox.max.X - shaft_top_corner.X) - 1 * CM, # offset to outside view.
|
||||
measurement_direction=(0, 1, 0),
|
||||
draft=drafting_options,
|
||||
border=perimeter.edges().sort_by(Axis.X)[-1], offset=1 * CM, draft=drafting_options
|
||||
)
|
||||
l3 = Text("Side Elevation", 6)
|
||||
l3.position = vis.group_by(Axis.Y)[0].sort_by(Edge.length)[-1].center() + (0, -5 * MM)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
belong to then select the edges as shown here:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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``
|
||||
objects as follows (first from CadQuery to build123d):
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
import build123d as b3d
|
||||
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:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
import build123d as b3d
|
||||
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
|
||||
on the workplane / coordinate system provided. For example:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildSketch(Plane.XZ) as vertical_sketch:
|
||||
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``
|
||||
of a 3D part but is simulated here by rotating a ``Plane``):
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildSketch(Plane.YZ.rotated((123, 45, 6))) as custom_plane:
|
||||
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
|
||||
is a common issue:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildSketch() as sketch:
|
||||
with BuildLine(Plane.XZ):
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Overview
|
|||
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.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
# In 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!
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
# In 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
|
||||
``Select.ALL``. Specifying ``Select.ALL`` for the selector is not required.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as part:
|
||||
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``.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as part:
|
||||
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
|
||||
available for a ``ShapeList`` of edges!
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as part:
|
||||
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
|
||||
edges are reused?
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as part:
|
||||
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
|
||||
``Select.NEW``.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as part:
|
||||
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
|
||||
Algebra Mode where ``Select.NEW`` is unavailable
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
box = Box(5, 5, 1)
|
||||
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
|
||||
comparing the object before the operation to the "combined" object.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
box = Box(5, 5, 1)
|
||||
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
|
||||
subclass of ``list``, so any list slice can be used.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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
|
||||
return a new list of all edges in the previous list.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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
|
||||
each face can be checked against a vector direction and filtered accordingly.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
part.faces().filter_by(lambda f: f.normal_at() == Vector(0, 0, 1))
|
||||
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ operations, and are sometimes necessary e.g. before sorting or filtering by radi
|
|||
.. dropdown:: Setup
|
||||
|
||||
.. literalinclude:: examples/filter_geomtype.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 3, 8-13
|
||||
|
||||
.. literalinclude:: examples/filter_geomtype.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 15
|
||||
|
||||
.. 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
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 17
|
||||
|
||||
.. 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
|
||||
|
||||
.. literalinclude:: examples/filter_all_edges_circle.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 3, 8-41
|
||||
|
||||
.. literalinclude:: examples/filter_all_edges_circle.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 43-47
|
||||
|
||||
.. figure:: ../assets/topology_selection/filter_all_edges_circle.png
|
||||
|
|
@ -74,14 +74,14 @@ Plane will select faces parallel to the plane.
|
|||
|
||||
.. dropdown:: Setup
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
from build123d import *
|
||||
|
||||
with BuildPart() as part:
|
||||
Box(1, 1, 1)
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
part.faces().filter_by(Axis.Z)
|
||||
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
|
||||
to 0. The result is faces parallel to the axis or perpendicular to the plane.
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
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)
|
||||
|
|
@ -122,11 +122,11 @@ and then filtering for the specific inner wire by radius.
|
|||
.. dropdown:: Setup
|
||||
|
||||
.. literalinclude:: examples/filter_inner_wire_count.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 4, 9-16
|
||||
|
||||
.. literalinclude:: examples/filter_inner_wire_count.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 18-21
|
||||
|
||||
.. 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.
|
||||
|
||||
.. literalinclude:: examples/filter_inner_wire_count.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 25-32
|
||||
|
||||
.. figure:: ../assets/topology_selection/filter_inner_wire_count_linear.png
|
||||
|
|
@ -163,11 +163,11 @@ any line edges.
|
|||
.. dropdown:: Setup
|
||||
|
||||
.. literalinclude:: examples/filter_nested.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 4, 9-22
|
||||
|
||||
.. literalinclude:: examples/filter_nested.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 26-32
|
||||
|
||||
.. 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.
|
||||
|
||||
.. literalinclude:: examples/filter_shape_properties.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 3-4, 8-22
|
||||
|
||||
.. figure:: ../assets/topology_selection/filter_shape_properties.png
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ result knowing how many edges to expect.
|
|||
.. dropdown:: Setup
|
||||
|
||||
.. literalinclude:: examples/group_axis.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 4, 9-17
|
||||
|
||||
.. 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.
|
||||
|
||||
.. literalinclude:: examples/group_axis.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 21-22
|
||||
|
||||
.. figure:: ../assets/topology_selection/group_axis_with.png
|
||||
|
|
@ -46,11 +46,11 @@ with the largest hole.
|
|||
.. dropdown:: Setup
|
||||
|
||||
.. literalinclude:: examples/group_hole_area.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 4, 9-17
|
||||
|
||||
.. literalinclude:: examples/group_hole_area.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 21-24
|
||||
|
||||
.. 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
|
||||
|
||||
.. literalinclude:: examples/group_properties_with_keys.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 4, 9-26
|
||||
|
||||
.. literalinclude:: examples/group_properties_with_keys.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 30, 31
|
||||
|
||||
.. 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
|
||||
|
||||
.. literalinclude:: examples/group_properties_with_keys.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 35-43
|
||||
|
||||
.. literalinclude:: examples/group_properties_with_keys.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 47-50
|
||||
|
||||
.. 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
|
||||
value! ``filter_by`` with a lambda expression can be used as well:
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
radius_groups = part.edges().filter_by(GeomType.CIRCLE)
|
||||
bearing_edges = radius_groups.filter_by(lambda e: e.radius == 8)
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ be used with``group_by``.
|
|||
.. dropdown:: Setup
|
||||
|
||||
.. literalinclude:: examples/sort_sortby.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 3, 8-13
|
||||
|
||||
.. literalinclude:: examples/sort_sortby.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 19-22
|
||||
|
||||
.. figure:: ../assets/topology_selection/sort_sortby_length.png
|
||||
|
|
@ -36,7 +36,7 @@ be used with``group_by``.
|
|||
|
|
||||
|
||||
.. literalinclude:: examples/sort_sortby.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 24-27
|
||||
|
||||
.. figure:: ../assets/topology_selection/sort_sortby_distance.png
|
||||
|
|
@ -57,11 +57,11 @@ the order is random.
|
|||
.. dropdown:: Setup
|
||||
|
||||
.. literalinclude:: examples/sort_along_wire.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 3, 8-12
|
||||
|
||||
.. literalinclude:: examples/sort_along_wire.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 14-15
|
||||
|
||||
.. 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.
|
||||
|
||||
.. literalinclude:: examples/sort_along_wire.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 26-28
|
||||
|
||||
.. figure:: ../assets/topology_selection/sort_along_wire.png
|
||||
|
|
@ -94,11 +94,11 @@ edge can be found sorting along y-axis.
|
|||
.. dropdown:: Setup
|
||||
|
||||
.. literalinclude:: examples/sort_axis.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 4, 9-18
|
||||
|
||||
.. literalinclude:: examples/sort_axis.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 22-24
|
||||
|
||||
.. 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
|
||||
|
||||
.. literalinclude:: examples/sort_distance_from.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 2-5, 9-13
|
||||
|
||||
.. literalinclude:: examples/sort_distance_from.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 15-16
|
||||
|
||||
.. 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.
|
||||
|
||||
.. literalinclude:: examples/sort_distance_from.py
|
||||
:language: build123d
|
||||
:language: python
|
||||
:lines: 19-20
|
||||
|
||||
.. figure:: ../assets/topology_selection/sort_distance_from_largest.png
|
||||
|
|
|
|||
|
|
@ -98,7 +98,6 @@ Party Pack 01-01 Bearing Bracket
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-ppp0101.py
|
||||
:language: build123d
|
||||
|
||||
|
||||
.. _ttt-ppp0102:
|
||||
|
|
@ -115,7 +114,6 @@ Party Pack 01-02 Post Cap
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-ppp0102.py
|
||||
:language: build123d
|
||||
|
||||
.. _ttt-ppp0103:
|
||||
|
||||
|
|
@ -131,7 +129,6 @@ Party Pack 01-03 C Clamp Base
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-ppp0103.py
|
||||
:language: build123d
|
||||
|
||||
.. _ttt-ppp0104:
|
||||
|
||||
|
|
@ -147,7 +144,6 @@ Party Pack 01-04 Angle Bracket
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-ppp0104.py
|
||||
:language: build123d
|
||||
|
||||
.. _ttt-ppp0105:
|
||||
|
||||
|
|
@ -163,7 +159,6 @@ Party Pack 01-05 Paste Sleeve
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-ppp0105.py
|
||||
:language: build123d
|
||||
|
||||
.. _ttt-ppp0106:
|
||||
|
||||
|
|
@ -179,7 +174,6 @@ Party Pack 01-06 Bearing Jig
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-ppp0106.py
|
||||
:language: build123d
|
||||
|
||||
.. _ttt-ppp0107:
|
||||
|
||||
|
|
@ -195,7 +189,6 @@ Party Pack 01-07 Flanged Hub
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-ppp0107.py
|
||||
:language: build123d
|
||||
|
||||
.. _ttt-ppp0108:
|
||||
|
||||
|
|
@ -211,7 +204,6 @@ Party Pack 01-08 Tie Plate
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-ppp0108.py
|
||||
:language: build123d
|
||||
|
||||
.. _ttt-ppp0109:
|
||||
|
||||
|
|
@ -227,7 +219,6 @@ Party Pack 01-09 Corner Tie
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-ppp0109.py
|
||||
:language: build123d
|
||||
|
||||
.. _ttt-ppp0110:
|
||||
|
||||
|
|
@ -243,7 +234,6 @@ Party Pack 01-10 Light Cap
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-ppp0110.py
|
||||
:language: build123d
|
||||
|
||||
.. _ttt-23-02-02-sm_hanger:
|
||||
|
||||
|
|
@ -259,7 +249,6 @@ Party Pack 01-10 Light Cap
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-23-02-02-sm_hanger.py
|
||||
:language: build123d
|
||||
|
||||
.. _ttt-23-t-24:
|
||||
|
||||
|
|
@ -276,7 +265,6 @@ Party Pack 01-10 Light Cap
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-23-t-24-curved_support.py
|
||||
:language: build123d
|
||||
|
||||
.. _ttt-24-spo-06:
|
||||
|
||||
|
|
@ -293,4 +281,3 @@ Party Pack 01-10 Light Cap
|
|||
.. dropdown:: Reference Implementation
|
||||
|
||||
.. literalinclude:: assets/ttt/ttt-24-SPO-06-Buffer_Stand.py
|
||||
:language: build123d
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
Designing a Part in build123d
|
||||
#############################
|
||||
|
||||
Designing a part with build123d involves a systematic approach that leverages the power
|
||||
of 2D profiles, extrusions, and revolutions. Where possible, always work in the lowest
|
||||
Designing a part with build123d involves a systematic approach that leverages the power
|
||||
of 2D profiles, extrusions, and revolutions. Where possible, always work in the lowest
|
||||
possible dimension, 1D lines before 2D sketches before 3D parts. The following guide will
|
||||
get you started:
|
||||
|
||||
|
|
@ -18,8 +18,8 @@ get you started:
|
|||
Step 1. Examine the Part in All Three Orientations
|
||||
**************************************************
|
||||
|
||||
Start by visualizing the part from the front, top, and side views. Identify any symmetries
|
||||
in these orientations, as symmetries can simplify the design by reducing the number of
|
||||
Start by visualizing the part from the front, top, and side views. Identify any symmetries
|
||||
in these orientations, as symmetries can simplify the design by reducing the number of
|
||||
unique features you need to model.
|
||||
|
||||
*In the following view of the bracket one can see two planes of symmetry
|
||||
|
|
@ -31,8 +31,8 @@ so we'll only need to design one quarter of it.*
|
|||
Step 2. Identify Rotational Symmetries
|
||||
**************************************
|
||||
|
||||
Look for structures that could be created through the rotation of a 2D shape. For instance,
|
||||
cylindrical or spherical features are often the result of revolving a profile around an axis.
|
||||
Look for structures that could be created through the rotation of a 2D shape. For instance,
|
||||
cylindrical or spherical features are often the result of revolving a profile around an axis.
|
||||
Identify the axis of rotation and make a note of it.
|
||||
|
||||
*There are no rotational structures in the example bracket.*
|
||||
|
|
@ -40,17 +40,17 @@ Identify the axis of rotation and make a note of it.
|
|||
Step 3. Select a Convenient Origin
|
||||
**********************************
|
||||
|
||||
Choose an origin point that minimizes the need to move or transform components later in the
|
||||
design process. Ideally, the origin should be placed at a natural center of symmetry or a
|
||||
Choose an origin point that minimizes the need to move or transform components later in the
|
||||
design process. Ideally, the origin should be placed at a natural center of symmetry or a
|
||||
critical reference point on the part.
|
||||
|
||||
*The planes of symmetry for the bracket was identified in step 1, making it logical to
|
||||
place the origin at the intersection of these planes on the bracket's front face. Additionally,
|
||||
we'll define the coordinate system we'll be working in: Plane.XY (the default), where
|
||||
the origin is set at the global (0,0,0) position. In this system, the x-axis aligns with
|
||||
the front of the bracket, and the z-axis corresponds to its width. It’s important to note
|
||||
*The planes of symmetry for the bracket was identified in step 1, making it logical to
|
||||
place the origin at the intersection of these planes on the bracket's front face. Additionally,
|
||||
we'll define the coordinate system we'll be working in: Plane.XY (the default), where
|
||||
the origin is set at the global (0,0,0) position. In this system, the x-axis aligns with
|
||||
the front of the bracket, and the z-axis corresponds to its width. It’s important to note
|
||||
that all coordinate systems/planes in build123d adhere to the*
|
||||
`right-hand rule <https://en.wikipedia.org/wiki/Right-hand_rule>`_ *meaning the y-axis is
|
||||
`right-hand rule <https://en.wikipedia.org/wiki/Right-hand_rule>`_ *meaning the y-axis is
|
||||
automatically determined by this convention.*
|
||||
|
||||
.. image:: assets/bracket_with_origin.png
|
||||
|
|
@ -58,18 +58,18 @@ automatically determined by this convention.*
|
|||
|
||||
Step 4. Create 2D Profiles
|
||||
**************************
|
||||
Design the 2D profiles of your part in the appropriate orientation(s). These profiles are
|
||||
the foundation of the part's geometry and can often represent cross-sections of the part.
|
||||
Design the 2D profiles of your part in the appropriate orientation(s). These profiles are
|
||||
the foundation of the part's geometry and can often represent cross-sections of the part.
|
||||
Mirror parts of profiles across any axes of symmetry identified earlier.
|
||||
|
||||
*The 2D profile of the bracket is as follows:*
|
||||
|
||||
.. image:: assets/bracket_sketch.png
|
||||
:align: center
|
||||
|
||||
|
||||
*The build123d code to generate this profile is as follows:*
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildSketch() as sketch:
|
||||
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
|
||||
a part by extruding it as shown in this code:*
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart() as bracket:
|
||||
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
|
||||
code block, captures just these edges:*
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
corners = bracket.edges().filter_by(Axis.X).group_by(Axis.Y)[-1]
|
||||
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
|
||||
the two circular holes:*
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with Locations(bracket.faces().sort_by(Axis.X)[-1]):
|
||||
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
|
||||
the front of the bracket and extruding the sketch through the part.*
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
with BuildSketch(bracket.faces().sort_by(Axis.Y)[0]):
|
||||
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:*
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
thickness = 3 * 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:*
|
||||
|
||||
.. code-block:: build123d
|
||||
.. code-block:: python
|
||||
|
||||
from build123d import *
|
||||
from ocp_vscode import show_all
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ Before getting to the CAD operations, this selector script needs to import the b
|
|||
environment.
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [import]
|
||||
:end-before: [Hinge Class]
|
||||
|
||||
|
|
@ -33,7 +32,6 @@ tutorial is the joints and not the CAD operations to create objects, this code i
|
|||
described in detail.
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Hinge Class]
|
||||
:end-before: [Create the Joints]
|
||||
|
||||
|
|
@ -64,7 +62,6 @@ The first joint to add is a :class:`~topology.RigidJoint` that is used to fix th
|
|||
or lid.
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Create the Joints]
|
||||
:end-before: [Hinge Axis]
|
||||
|
||||
|
|
@ -81,7 +78,6 @@ The second joint to add is either a :class:`~topology.RigidJoint` (on the inner
|
|||
(on the outer leaf) that describes the hinge axis.
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Create the Joints]
|
||||
:end-before: [Fastener holes]
|
||||
:emphasize-lines: 10-24
|
||||
|
|
@ -100,7 +96,6 @@ The third set of joints to add are :class:`~topology.CylindricalJoint`'s that de
|
|||
screws used to attach the leaves move.
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Fastener holes]
|
||||
:end-before: [End Fastener holes]
|
||||
|
||||
|
|
@ -120,7 +115,6 @@ Step 3d: Call Super
|
|||
To finish off, the base class for the Hinge class is initialized:
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [End Fastener holes]
|
||||
:end-before: [Hinge Class]
|
||||
|
||||
|
|
@ -131,7 +125,6 @@ Now that the Hinge class is complete it can be used to instantiate the two hinge
|
|||
required to attach the box and lid together.
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Create instances of the two leaves of the hinge]
|
||||
:end-before: [Create the box with a RigidJoint to mount the hinge]
|
||||
|
||||
|
|
@ -146,7 +139,6 @@ the joint used to attach the outer hinge leaf.
|
|||
:align: center
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
: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]
|
||||
:emphasize-lines: 13-16
|
||||
|
|
@ -165,7 +157,6 @@ having to recreate or modify :class:`~topology.Joint`'s. Here is the box is move
|
|||
property.
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Demonstrate that objects with Joints can be moved and the joints follow]
|
||||
:end-before: [The lid with a RigidJoint for the hinge]
|
||||
|
||||
|
|
@ -179,7 +170,6 @@ Much like the box, the lid is created in a :class:`~build_part.BuildPart` contex
|
|||
:align: center
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [The lid with a RigidJoint for the hinge]
|
||||
:end-before: [A screw to attach the hinge to the box]
|
||||
:emphasize-lines: 6-9
|
||||
|
|
@ -201,7 +191,6 @@ screw.
|
|||
:align: center
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [A screw to attach the hinge to the box]
|
||||
:end-before: [End of screw creation]
|
||||
|
||||
|
|
@ -221,7 +210,6 @@ Step 7a: Hinge to Box
|
|||
To start, the outer hinge leaf will be connected to the box, as follows:
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Connect Box to Outer Hinge]
|
||||
:end-before: [Connect Box to Outer Hinge]
|
||||
|
||||
|
|
@ -239,7 +227,6 @@ Next, the hinge inner leaf is connected to the hinge outer leaf which is attache
|
|||
box.
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Connect Hinge Leaves]
|
||||
:end-before: [Connect Hinge Leaves]
|
||||
|
||||
|
|
@ -256,7 +243,6 @@ Step 7c: Lid to Hinge
|
|||
Now the ``lid`` is connected to the ``hinge_inner``:
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Connect Hinge to Lid]
|
||||
:end-before: [Connect Hinge to Lid]
|
||||
|
||||
|
|
@ -274,7 +260,6 @@ Step 7d: Screw to Hinge
|
|||
The last step in this example is to place a screw in one of the hinges:
|
||||
|
||||
.. literalinclude:: tutorial_joints.py
|
||||
:language: build123d
|
||||
:start-after: [Connect Screw to Hole]
|
||||
:end-before: [Connect Screw to Hole]
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ 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.
|
||||
|
||||
.. literalinclude:: ../examples/lego.py
|
||||
:language: build123d
|
||||
:lines: 30,31, 34-47
|
||||
|
||||
********************
|
||||
|
|
@ -32,7 +31,6 @@ 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``.
|
||||
|
||||
.. literalinclude:: ../examples/lego.py
|
||||
:language: build123d
|
||||
:lines: 49
|
||||
|
||||
**********************
|
||||
|
|
@ -45,7 +43,6 @@ 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:
|
||||
|
||||
.. literalinclude:: ../examples/lego.py
|
||||
:language: build123d
|
||||
:lines: 49-51
|
||||
:emphasize-lines: 3
|
||||
|
||||
|
|
@ -62,7 +59,6 @@ of the Lego block. The following step is going to refer to this rectangle, so it
|
|||
be assigned the identifier ``perimeter``.
|
||||
|
||||
.. literalinclude:: ../examples/lego.py
|
||||
:language: build123d
|
||||
:lines: 49-53
|
||||
:emphasize-lines: 5
|
||||
|
||||
|
|
@ -80,7 +76,6 @@ hollowed out. This will be done with the ``Offset`` operation which is going to
|
|||
create a new object from ``perimeter``.
|
||||
|
||||
.. literalinclude:: ../examples/lego.py
|
||||
:language: build123d
|
||||
:lines: 49-53,58-64
|
||||
:emphasize-lines: 7-12
|
||||
|
||||
|
|
@ -109,7 +104,6 @@ objects are in the scope of a location context (``GridLocations`` in this case)
|
|||
that defined multiple points, multiple rectangles are created.
|
||||
|
||||
.. literalinclude:: ../examples/lego.py
|
||||
:language: build123d
|
||||
:lines: 49-53,58-64,69-73
|
||||
:emphasize-lines: 13-17
|
||||
|
||||
|
|
@ -131,7 +125,6 @@ To convert the internal grid to ridges, the center needs to be removed. This wil
|
|||
with another ``Rectangle``.
|
||||
|
||||
.. literalinclude:: ../examples/lego.py
|
||||
:language: build123d
|
||||
:lines: 49-53,58-64,69-73,78-83
|
||||
:emphasize-lines: 18-23
|
||||
|
||||
|
|
@ -149,7 +142,6 @@ 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``.
|
||||
|
||||
.. literalinclude:: ../examples/lego.py
|
||||
:language: build123d
|
||||
:lines: 49-53,58-64,69-73,78-83,88-93
|
||||
:emphasize-lines: 24-29
|
||||
|
||||
|
|
@ -170,7 +162,6 @@ Now that the sketch is complete it needs to be extruded into the three dimension
|
|||
wall object.
|
||||
|
||||
.. literalinclude:: ../examples/lego.py
|
||||
:language: build123d
|
||||
:lines: 49-53,58-64,69-73,78-83,88-93,98-99
|
||||
:emphasize-lines: 30-31
|
||||
|
||||
|
|
@ -192,7 +183,6 @@ 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.
|
||||
|
||||
.. literalinclude:: ../examples/lego.py
|
||||
:language: build123d
|
||||
:lines: 49-53,58-64,69-73,78-83,88-93,98-99,110-118
|
||||
:emphasize-lines: 32-40
|
||||
|
||||
|
|
@ -221,7 +211,6 @@ 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.
|
||||
|
||||
.. literalinclude:: ../examples/lego.py
|
||||
:language: build123d
|
||||
:lines: 49-53,58-64,69-73,78-83,88-93,98-99,110-118,129-137
|
||||
:emphasize-lines: 41-49
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ this part:
|
|||
.. note::
|
||||
One can see any object in the following tutorial by using the ``ocp_vscode`` (or
|
||||
any other supported viewer) by using the ``show(object_to_be_viewed)`` command.
|
||||
Alternatively, the ``show_all()`` command will display all objects that have been
|
||||
Alternatively, the ``show_all()`` command will display all objects that have been
|
||||
assigned an identifier.
|
||||
|
||||
*************
|
||||
|
|
@ -22,7 +22,6 @@ Before getting to the CAD operations, this selector script needs to import the b
|
|||
environment.
|
||||
|
||||
.. literalinclude:: selector_example.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
:lines: 1-2
|
||||
|
|
@ -35,7 +34,6 @@ To start off, the part will be based on a cylinder so we'll use the :class:`~obj
|
|||
of :class:`~build_part.BuildPart`:
|
||||
|
||||
.. literalinclude:: selector_example.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
:lines: 1-5
|
||||
|
|
@ -52,7 +50,6 @@ 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:
|
||||
|
||||
.. literalinclude:: selector_example.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
:lines: 1-6
|
||||
|
|
@ -85,7 +82,6 @@ The object has a hexagonal hole in the top with a central cylinder which we'll d
|
|||
in the sketch.
|
||||
|
||||
.. literalinclude:: selector_example.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
:lines: 1-8
|
||||
|
|
@ -111,7 +107,6 @@ To create the hole we'll :func:`~operations_part.extrude` the sketch we just cre
|
|||
the :class:`~objects_part.Cylinder` and subtract it.
|
||||
|
||||
.. literalinclude:: selector_example.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
:lines: 1-9
|
||||
|
|
@ -133,7 +128,6 @@ Step 6: Fillet the top perimeter Edge
|
|||
The final step is to apply a fillet to the top perimeter.
|
||||
|
||||
.. literalinclude:: selector_example.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
:lines: 1-9,18-24,33-34
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ We model a single wing (half‑span), with an elliptic leading and trailing edge
|
|||
These two edges act as the *guides* for the Gordon surface.
|
||||
|
||||
.. literalinclude:: spitfire_wing_gordon.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [AirfoilSizes]
|
||||
|
||||
|
|
@ -46,7 +45,6 @@ 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.
|
||||
|
||||
.. literalinclude:: spitfire_wing_gordon.py
|
||||
:language: build123d
|
||||
:start-after: [AirfoilSizes]
|
||||
:end-before: [Airfoils]
|
||||
|
||||
|
|
@ -58,7 +56,6 @@ shifted so the leading edge fraction is aligned—then scale to the chord length
|
|||
from Step 2.
|
||||
|
||||
.. literalinclude:: spitfire_wing_gordon.py
|
||||
:language: build123d
|
||||
:start-after: [Airfoils]
|
||||
:end-before: [Profiles]
|
||||
|
||||
|
|
@ -71,7 +68,6 @@ profiles; the elliptic edges are the guides. We also add the wing tip section
|
|||
so the profile grid closes at the tip.
|
||||
|
||||
.. literalinclude:: spitfire_wing_gordon.py
|
||||
:language: build123d
|
||||
:start-after: [Profiles]
|
||||
:end-before: [Solid]
|
||||
|
||||
|
|
@ -86,7 +82,6 @@ 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.
|
||||
|
||||
.. literalinclude:: spitfire_wing_gordon.py
|
||||
:language: build123d
|
||||
:start-after: [Solid]
|
||||
:end-before: [End]
|
||||
|
||||
|
|
@ -107,6 +102,5 @@ Complete listing
|
|||
For convenience, here is the full script in one block:
|
||||
|
||||
.. literalinclude:: spitfire_wing_gordon.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [End]
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ a heart‑shaped token from a small set of non‑planar faces. We’ll create
|
|||
non‑planar surfaces, mirror them, add side faces, and assemble a closed shell
|
||||
into a solid.
|
||||
|
||||
As described in the `topology_` section, a BREP model consists of vertices, edges, faces,
|
||||
and other elements that define the boundary of an object. When creating objects with
|
||||
non-planar faces, it is often more convenient to explicitly create the boundary faces of
|
||||
As described in the `topology_` section, a BREP model consists of vertices, edges, faces,
|
||||
and other elements that define the boundary of an object. When creating objects with
|
||||
non-planar faces, it is often more convenient to explicitly create the boundary faces of
|
||||
the object. To illustrate this process, we will create the following game token:
|
||||
|
||||
.. raw:: html
|
||||
|
|
@ -20,16 +20,15 @@ the object. To illustrate this process, we will create the following game token:
|
|||
Useful :class:`~topology.Face` creation methods include
|
||||
:meth:`~topology.Face.make_surface`, :meth:`~topology.Face.make_bezier_surface`,
|
||||
and :meth:`~topology.Face.make_surface_from_array_of_points`. See the
|
||||
:doc:`tutorial_surface_modeling` overview for the full list.
|
||||
:doc:`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.
|
||||
|
||||
To create the perimeter, we'll define the perimeter edges. Since the heart is
|
||||
To create the perimeter, we'll define the perimeter edges. Since the heart is
|
||||
symmetric, we'll only create half of its surface here:
|
||||
|
||||
.. literalinclude:: heart_token.py
|
||||
:language: build123d
|
||||
:start-after: [Code]
|
||||
:end-before: [SurfaceEdges]
|
||||
|
||||
|
|
@ -43,14 +42,12 @@ of the heart and archs up off ``Plane.XY``.
|
|||
In preparation for creating the surface, we'll define a point on the surface:
|
||||
|
||||
.. literalinclude:: heart_token.py
|
||||
:language: build123d
|
||||
:start-after: [SurfaceEdges]
|
||||
:end-before: [SurfacePoint]
|
||||
|
||||
We will then use this point to create a non-planar ``Face``:
|
||||
|
||||
.. literalinclude:: heart_token.py
|
||||
:language: build123d
|
||||
:start-after: [SurfacePoint]
|
||||
:end-before: [Surface]
|
||||
|
||||
|
|
@ -58,23 +55,21 @@ We will then use this point to create a non-planar ``Face``:
|
|||
:align: center
|
||||
:alt: token perimeter
|
||||
|
||||
Note that the surface was raised up by 0.5 using an Algebra expression with Pos. Also,
|
||||
note that the ``-`` in front of ``Face`` simply flips the face normal so that the colored
|
||||
Note that the surface was raised up by 0.5 using an Algebra expression with Pos. Also,
|
||||
note that the ``-`` in front of ``Face`` simply flips the face normal so that the colored
|
||||
side is up, which isn't necessary but helps with viewing.
|
||||
|
||||
Now that one half of the top of the heart has been created, the remainder of the top
|
||||
Now that one half of the top of the heart has been created, the remainder of the top
|
||||
and bottom can be created by mirroring:
|
||||
|
||||
.. literalinclude:: heart_token.py
|
||||
:language: build123d
|
||||
:start-after: [Surface]
|
||||
:end-before: [Surfaces]
|
||||
|
||||
The sides of the heart are going to be created by extruding the outside of the perimeter
|
||||
The sides of the heart are going to be created by extruding the outside of the perimeter
|
||||
as follows:
|
||||
|
||||
.. literalinclude:: heart_token.py
|
||||
:language: build123d
|
||||
:start-after: [Surfaces]
|
||||
:end-before: [Sides]
|
||||
|
||||
|
|
@ -82,12 +77,11 @@ as follows:
|
|||
:align: center
|
||||
:alt: token sides
|
||||
|
||||
With the top, bottom, and sides, the complete boundary of the object is defined. We can
|
||||
now put them together, first into a :class:`~topology.Shell` and then into a
|
||||
With the top, bottom, and sides, the complete boundary of the object is defined. We can
|
||||
now put them together, first into a :class:`~topology.Shell` and then into a
|
||||
:class:`~topology.Solid`:
|
||||
|
||||
.. literalinclude:: heart_token.py
|
||||
:language: build123d
|
||||
:start-after: [Sides]
|
||||
:end-before: [Solid]
|
||||
|
||||
|
|
@ -96,37 +90,36 @@ now put them together, first into a :class:`~topology.Shell` and then into a
|
|||
:alt: token heart solid
|
||||
|
||||
.. note::
|
||||
When creating a Solid from a Shell, the Shell must be "water-tight," meaning it
|
||||
should have no holes. For objects with complex Edges, it's best practice to reuse
|
||||
Edges in adjoining Faces whenever possible to avoid slight mismatches that can
|
||||
When creating a Solid from a Shell, the Shell must be "water-tight," meaning it
|
||||
should have no holes. For objects with complex Edges, it's best practice to reuse
|
||||
Edges in adjoining Faces whenever possible to avoid slight mismatches that can
|
||||
create openings.
|
||||
|
||||
Finally, we'll create the frame around the heart as a simple extrusion of a planar
|
||||
Finally, we'll create the frame around the heart as a simple extrusion of a planar
|
||||
shape defined by the perimeter of the heart and merge all of the components together:
|
||||
|
||||
.. literalinclude:: heart_token.py
|
||||
:language: build123d
|
||||
:start-after: [Solid]
|
||||
:end-before: [End]
|
||||
|
||||
Note that an additional planar line is used to close ``l1`` and ``l3`` so a ``Face``
|
||||
can be created. The :func:`~operations_generic.offset` function defines the outside of
|
||||
Note that an additional planar line is used to close ``l1`` and ``l3`` so a ``Face``
|
||||
can be created. The :func:`~operations_generic.offset` function defines the outside of
|
||||
the frame as a constant distance from the heart itself.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
In this tutorial, we've explored surface modeling techniques to create a non-planar
|
||||
In this tutorial, we've explored surface modeling techniques to create a non-planar
|
||||
heart-shaped object using build123d. By utilizing methods from the :class:`~topology.Face`
|
||||
class, such as :meth:`~topology.Face.make_surface`, we constructed the perimeter and
|
||||
central point of the surface. We then assembled the complete boundary of the object
|
||||
by creating the top, bottom, and sides, and combined them into a :class:`~topology.Shell`
|
||||
and eventually a :class:`~topology.Solid`. Finally, we added a frame around the heart
|
||||
using the :func:`~operations_generic.offset` function to maintain a constant distance
|
||||
class, such as :meth:`~topology.Face.make_surface`, we constructed the perimeter and
|
||||
central point of the surface. We then assembled the complete boundary of the object
|
||||
by creating the top, bottom, and sides, and combined them into a :class:`~topology.Shell`
|
||||
and eventually a :class:`~topology.Solid`. Finally, we added a frame around the heart
|
||||
using the :func:`~operations_generic.offset` function to maintain a constant distance
|
||||
from the heart.
|
||||
|
||||
Next steps
|
||||
----------
|
||||
|
||||
Continue to :doc:`tutorial_spitfire_wing_gordon` for an advanced example using
|
||||
Continue to :doc:`tutorial_heart_token` for an advanced example using
|
||||
:meth:`~topology.Face.make_gordon_surface` to create a Supermarine Spitfire wing.
|
||||
|
|
|
|||
|
|
@ -3,28 +3,28 @@ Surface Modeling
|
|||
#################
|
||||
|
||||
|
||||
Surface modeling refers to the direct creation and manipulation of the skin of a 3D
|
||||
object—its bounding faces—rather than starting from volumetric primitives or solid
|
||||
Surface modeling refers to the direct creation and manipulation of the skin of a 3D
|
||||
object—its bounding faces—rather than starting from volumetric primitives or solid
|
||||
operations.
|
||||
|
||||
Instead of defining a shape by extruding or revolving a 2D profile to fill a volume,
|
||||
surface modeling focuses on building the individual curved or planar faces that together
|
||||
define the outer boundary of a part. This approach allows for precise control of complex
|
||||
freeform geometry such as aerodynamic surfaces, boat hulls, or organic transitions that
|
||||
Instead of defining a shape by extruding or revolving a 2D profile to fill a volume,
|
||||
surface modeling focuses on building the individual curved or planar faces that together
|
||||
define the outer boundary of a part. This approach allows for precise control of complex
|
||||
freeform geometry such as aerodynamic surfaces, boat hulls, or organic transitions that
|
||||
cannot easily be expressed with simple parametric solids.
|
||||
|
||||
In build123d, as in other CAD kernels based on BREP (Boundary Representation) modeling,
|
||||
all solids are ultimately defined by their boundaries: a hierarchy of faces, edges, and
|
||||
vertices. Each face represents a finite patch of a geometric surface (plane, cylinder,
|
||||
Bézier patch, etc.) bounded by one or more edge loops or wires. When adjacent faces share
|
||||
edges consistently and close into a continuous boundary, they form a manifold
|
||||
:class:`~topology.Shell`—the watertight surface of a volume. If this shell is properly
|
||||
In build123d, as in other CAD kernels based on BREP (Boundary Representation) modeling,
|
||||
all solids are ultimately defined by their boundaries: a hierarchy of faces, edges, and
|
||||
vertices. Each face represents a finite patch of a geometric surface (plane, cylinder,
|
||||
Bézier patch, etc.) bounded by one or more edge loops or wires. When adjacent faces share
|
||||
edges consistently and close into a continuous boundary, they form a manifold
|
||||
:class:`~topology.Shell`—the watertight surface of a volume. If this shell is properly
|
||||
oriented and encloses a finite region of space, the model becomes a solid.
|
||||
|
||||
Surface modeling therefore operates at the most fundamental level of BREP construction.
|
||||
Rather than relying on higher-level modeling operations to implicitly generate faces,
|
||||
it allows you to construct and connect those faces explicitly. This provides a path to
|
||||
build geometry that blends analytical and freeform shapes seamlessly, with full control
|
||||
Surface modeling therefore operates at the most fundamental level of BREP construction.
|
||||
Rather than relying on higher-level modeling operations to implicitly generate faces,
|
||||
it allows you to construct and connect those faces explicitly. This provides a path to
|
||||
build geometry that blends analytical and freeform shapes seamlessly, with full control
|
||||
over continuity, tangency, and curvature across boundaries.
|
||||
|
||||
This section provides:
|
||||
|
|
|
|||
|
|
@ -1,138 +0,0 @@
|
|||
"""
|
||||
Doubly-curved bracelet with an embossed label
|
||||
|
||||
name: bracelet.py
|
||||
by: Gumyr
|
||||
date: January 7, 2026
|
||||
|
||||
desc:
|
||||
This model is a good "stress test" for OCCT because most of the final boundary
|
||||
surfaces are *freeform* (not analytic planes/cylinders/spheres). The geometry
|
||||
is assembled from:
|
||||
- a swept center section (using a curved solid end-face as the sweep profile)
|
||||
- two freeform "tip caps" built as Gordon surfaces (network of curves)
|
||||
- an optional embossed text label projected onto a curved solid
|
||||
- alignment holes for splitting/printing/assembly
|
||||
|
||||
Key techniques demonstrated:
|
||||
- using location_at/position_at/tangent (%) to extract local frames & tangents
|
||||
- projecting curves onto a non-planar surface to create "true" 3D guide curves
|
||||
- Gordon surfaces to build high-quality doubly-curved skins
|
||||
- projecting faces (text) onto a complex solid and thickening them
|
||||
|
||||
"""
|
||||
|
||||
# [Code]
|
||||
from build123d import *
|
||||
from ocp_vscode import show
|
||||
|
||||
# Define input parameters
|
||||
# - radii: ellipse radii (X, Y) controlling the bracelet centerline shape
|
||||
# - width: bracelet width (along Z for the center sweep)
|
||||
# - thickness: bracelet thickness (radial thickness of the cross section)
|
||||
# - opening_angle: the missing angle that creates the wrist opening
|
||||
# - label_str: optional text to emboss on the outside surface
|
||||
# - Define input parameters
|
||||
radii, width, thickness, opening_angle, label_str = (45, 30), 25, 5, 80, "build123d"
|
||||
|
||||
# Step 1: Create an elliptical arc defining the *centerline* of the bracelet.
|
||||
# The arc is truncated to leave an opening (the "gap" where the bracelet goes on).
|
||||
# Angles are in degrees; 270° points downward, which keeps the opening centered at the bottom.
|
||||
center_arc = EllipticalCenterArc(
|
||||
(0, 0), *radii, 270 + opening_angle / 2, 270 - opening_angle / 2
|
||||
)
|
||||
|
||||
# Step 2: Create HALF of the end cross-section, positioned at the end of the arc.
|
||||
# We build only half so we can later mirror it to enforce symmetry and reduce
|
||||
# curve-network complexity when building the freeform tip.
|
||||
#
|
||||
# location_at(1) returns a local coordinate frame at the arc end (tangent-aware).
|
||||
# x_dir is chosen so the section’s local "X" is well-defined and stable.
|
||||
end_center_arc = center_arc.location_at(1, x_dir=(0, 0, 1))
|
||||
half_x_section = EllipticalCenterArc((0, 0), width / 2, thickness / 2, 90, 270).locate(
|
||||
end_center_arc
|
||||
)
|
||||
|
||||
# Step 3: Create a doubly-curved "tip edge" curve.
|
||||
# The tip edge must live in 3D and conform to the outside of the bracelet.
|
||||
# To do that, we:
|
||||
# 1) create a surface by extruding the center_arc into a sheet (a ribbon surface)
|
||||
# 2) build a planar arc in a local frame at the end of that surface
|
||||
# 3) project the planar arc onto the curved surface to get a true 3D curve
|
||||
#
|
||||
# The resulting tip_arc is a 3D edge that naturally matches the bracelet curvature.
|
||||
center_surface = -Face.extrude(center_arc, (0, 0, 2 * width)).moved(
|
||||
Location((0, 0, -width), (0, 0, 180))
|
||||
)
|
||||
tip_center_loc = -center_surface.location_at(center_arc @ 1, x_dir=(1, 0, 0))
|
||||
normal_at_tip_center = tip_center_loc.z_axis.direction
|
||||
|
||||
# A planar arc that would represent the outer boundary of the tip *if* the surface
|
||||
# were flat. We immediately project it to make it truly conformal in 3D.
|
||||
planar_tip_arc = CenterArc((0, 0), width / 2, 270, 180).locate(tip_center_loc).edge()
|
||||
tip_arc = planar_tip_arc.project_to_shape(center_surface, -normal_at_tip_center)[0]
|
||||
|
||||
# Step 4: Build the tip as a Gordon surface (a surface fit through a curve network).
|
||||
# Gordon surfaces are ideal when:
|
||||
# - you don’t have an obvious analytic surface
|
||||
# - curvature changes in two directions (doubly-curved "cap")
|
||||
# - you can define a consistent set of profile curves + guide curves
|
||||
#
|
||||
# Here:
|
||||
# - profiles define "across the tip" shape (section -> bulged spline -> mirrored section)
|
||||
# - guides define "along the tip" rails (start point -> projected 3D arc -> end point)
|
||||
#
|
||||
# Tangents are used to encourage smoothness where the tip joins the swept center section.
|
||||
profile = Spline(
|
||||
half_x_section @ 0,
|
||||
tip_arc @ 0.5,
|
||||
half_x_section @ 1,
|
||||
tangents=(center_arc % 1, -(center_arc % 1)),
|
||||
)
|
||||
tip_surface = Face.make_gordon_surface(
|
||||
profiles=[half_x_section, profile, half_x_section.mirror(Plane.XY)],
|
||||
guides=[half_x_section @ 0, tip_arc, half_x_section @ 1],
|
||||
)
|
||||
|
||||
# Step 5: Close the tip surface into a watertight Solid.
|
||||
# tip_surface is the outer "skin"; we create a side face from its boundary wire
|
||||
# and make a shell, then a solid.
|
||||
tip_side = Face(tip_surface.wire())
|
||||
tip = Solid(Shell([tip_side, tip_surface]))
|
||||
|
||||
# Step 6: Sweep the *flat end face* of the tip around the center arc.
|
||||
# This is the trick that makes the center section compatible with the freeform tip:
|
||||
# the sweep profile is the same face that bounds the tip, so the join is naturally aligned.
|
||||
center_section = sweep(tip_side, center_arc).solid()
|
||||
|
||||
# Step 7: Assemble the bracelet from the center and two mirrored tips.
|
||||
# Mirror across YZ to create the opposite end cap.
|
||||
bracelet = Solid() + [tip, center_section, tip.mirror(Plane.YZ)]
|
||||
|
||||
# Step 8: Add an embossed label.
|
||||
# This is often the hardest operation for OCCT in this model:
|
||||
# projecting text onto a doubly-curved surface can create many small faces/edges,
|
||||
# and thickening them adds even more boolean complexity.
|
||||
if label_str:
|
||||
label = Text(label_str, font_size=width * 0.8, align=Align.CENTER)
|
||||
|
||||
# Project the text onto the bracelet using a path-based placement along center_arc.
|
||||
# The parameter offsets the label so it sits centered along arc-length.
|
||||
p_labels = bracelet.project_faces(
|
||||
label, center_arc, 0.5 - 0.5 * (label.bounding_box().size.X) / center_arc.length
|
||||
)
|
||||
# Turn the projected faces into solids via thickening (embossing).
|
||||
embossed_label = [Solid.thicken(f, 0.5) for f in p_labels.faces()]
|
||||
bracelet += embossed_label
|
||||
|
||||
# Step 9: Add alignment holes to aid assembly after 3D printing in two halves.
|
||||
# These are placed at evenly spaced locations along the arc (including both ends).
|
||||
# A small clearance (+0.15) is included for typical FDM tolerances.
|
||||
alignment_holes = [
|
||||
Pos(p) * Cylinder(1.75 / 2 + 0.15, 8)
|
||||
for p in [center_arc.position_at(i / 4) for i in range(5)]
|
||||
]
|
||||
bracelet -= alignment_holes
|
||||
|
||||
show(bracelet)
|
||||
# [End]
|
||||
|
|
@ -68,7 +68,8 @@ with BuildPart() as ex26:
|
|||
with BuildSketch() as ex26_sk:
|
||||
with Locations((0, rev)):
|
||||
Circle(rad)
|
||||
revolve(axis=Axis.X, revolution_arc=180)
|
||||
revolve(axis=Axis.X, revolution_arc=90)
|
||||
mirror(about=Plane.XZ)
|
||||
with BuildSketch() as ex26_sk2:
|
||||
Rectangle(rad, rev)
|
||||
ex26_target = ex26.part
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ rad, rev = 3, 25
|
|||
|
||||
# Extrude last
|
||||
circle = Pos(0, rev) * Circle(rad)
|
||||
ex26_target = revolve(circle, Axis.X, revolution_arc=180)
|
||||
ex26_target = ex26_target
|
||||
ex26_target = revolve(circle, Axis.X, revolution_arc=90)
|
||||
ex26_target = ex26_target + mirror(ex26_target, Plane.XZ)
|
||||
|
||||
rect = Rectangle(rad, rev)
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ development = [
|
|||
"black",
|
||||
"mypy",
|
||||
"pylint",
|
||||
"pytest==8.4.2", # TODO: unpin on resolution of pytest-dev/pytest-xdist/issues/1273
|
||||
"pytest",
|
||||
"pytest-benchmark",
|
||||
"pytest-cov",
|
||||
"pytest-xdist",
|
||||
|
|
@ -95,7 +95,7 @@ all = [
|
|||
"build123d[ocp_vscode]",
|
||||
"build123d[development]",
|
||||
"build123d[docs]",
|
||||
"build123d[stubs]",
|
||||
# "build123d[stubs]", # excluded for now as mypy fails
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
|
|
|||
|
|
@ -88,8 +88,6 @@ __all__ = [
|
|||
"DoubleTangentArc",
|
||||
"EllipticalCenterArc",
|
||||
"EllipticalStartArc",
|
||||
"ParabolicCenterArc",
|
||||
"HyperbolicCenterArc",
|
||||
"FilletPolyline",
|
||||
"Helix",
|
||||
"IntersectingLine",
|
||||
|
|
@ -129,13 +127,12 @@ __all__ = [
|
|||
"Triangle",
|
||||
# 3D Part Objects
|
||||
"BasePartObject",
|
||||
"Box",
|
||||
"Cone",
|
||||
"ConvexPolyhedron",
|
||||
"CounterBoreHole",
|
||||
"CounterSinkHole",
|
||||
"Cylinder",
|
||||
"Hole",
|
||||
"Box",
|
||||
"Cone",
|
||||
"Cylinder",
|
||||
"Sphere",
|
||||
"Torus",
|
||||
"Wedge",
|
||||
|
|
|
|||
|
|
@ -466,7 +466,7 @@ class Builder(ABC, Generic[ShapeT]):
|
|||
elif mode == Mode.INTERSECT:
|
||||
if self._obj is None:
|
||||
raise RuntimeError("Nothing to intersect with")
|
||||
combined = self._obj.intersect(Compound(typed[self._shape]))
|
||||
combined = self._obj.intersect(*typed[self._shape])
|
||||
elif mode == Mode.REPLACE:
|
||||
combined = self._sub_class(list(typed[self._shape]))
|
||||
|
||||
|
|
|
|||
|
|
@ -453,7 +453,7 @@ class DimensionLine(BaseSketchObject):
|
|||
if self_intersection is None:
|
||||
self_intersection_area = 0.0
|
||||
else:
|
||||
self_intersection_area = sum(f.area for f in self_intersection.faces())
|
||||
self_intersection_area = self_intersection.area
|
||||
d_line += placed_label
|
||||
bbox_size = d_line.bounding_box().diagonal
|
||||
|
||||
|
|
@ -467,7 +467,7 @@ class DimensionLine(BaseSketchObject):
|
|||
if line_intersection is None:
|
||||
common_area = 0.0
|
||||
else:
|
||||
common_area = sum(f.area for f in line_intersection.faces())
|
||||
common_area = line_intersection.area
|
||||
common_area += self_intersection_area
|
||||
score = (d_line.area - 10 * common_area) / bbox_size
|
||||
d_lines[d_line] = score
|
||||
|
|
@ -504,8 +504,7 @@ class ExtensionLine(BaseSketchObject):
|
|||
label_angle (bool, optional): a flag indicating that instead of an extracted length
|
||||
value, the size of the circular arc extracted from the path should be displayed
|
||||
in degrees. Defaults to False.
|
||||
measurement_direction (VectorLike, optional): Vector line which to project the dimension
|
||||
against. Offset start point is the position of the start of border.
|
||||
project_line (Vector, optional): Vector line which to project dimension against.
|
||||
Defaults to None.
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
|
||||
|
|
@ -521,7 +520,7 @@ class ExtensionLine(BaseSketchObject):
|
|||
arrows: tuple[bool, bool] = (True, True),
|
||||
tolerance: float | tuple[float, float] | None = None,
|
||||
label_angle: bool = False,
|
||||
measurement_direction: VectorLike | None = None,
|
||||
project_line: VectorLike | None = None,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
# pylint: disable=too-many-locals
|
||||
|
|
@ -529,41 +528,24 @@ class ExtensionLine(BaseSketchObject):
|
|||
context = BuildSketch._get_context(self)
|
||||
if sketch is None and not (context is None or context.sketch is None):
|
||||
sketch = context.sketch
|
||||
if offset == 0:
|
||||
raise ValueError("A dimension line should be used if offset is 0")
|
||||
if project_line is not None:
|
||||
raise NotImplementedError("project_line is currently unsupported")
|
||||
|
||||
# Create a wire modelling the path of the dimension lines from a variety of input types
|
||||
object_to_measure = Draft._process_path(border)
|
||||
if object_to_measure.position_at(0) == object_to_measure.position_at(1):
|
||||
raise ValueError("Start and end points of border must be different.")
|
||||
|
||||
if measurement_direction is not None:
|
||||
if isinstance(measurement_direction, Iterable):
|
||||
measurement_direction = Vector(measurement_direction)
|
||||
measure_object_span = object_to_measure.position_at(
|
||||
1
|
||||
) - object_to_measure.position_at(0)
|
||||
extent_along_wire = measure_object_span.project_to_line(
|
||||
measurement_direction
|
||||
)
|
||||
object_to_dimension = Edge.make_line(
|
||||
object_to_measure.position_at(0),
|
||||
object_to_measure.position_at(0) + extent_along_wire,
|
||||
)
|
||||
else:
|
||||
object_to_dimension = object_to_measure
|
||||
|
||||
side_lut = {1: Side.RIGHT, -1: Side.LEFT}
|
||||
|
||||
dimension_path = object_to_dimension.offset_2d(
|
||||
if offset == 0:
|
||||
raise ValueError("A dimension line should be used if offset is 0")
|
||||
dimension_path = object_to_measure.offset_2d(
|
||||
distance=offset, side=side_lut[int(copysign(1, offset))], closed=False
|
||||
)
|
||||
dimension_label_str = (
|
||||
label
|
||||
if label is not None
|
||||
else draft._label_to_str(label, object_to_dimension, label_angle, tolerance)
|
||||
else draft._label_to_str(label, object_to_measure, label_angle, tolerance)
|
||||
)
|
||||
|
||||
extension_lines = [
|
||||
Edge.make_line(
|
||||
object_to_measure.position_at(e), dimension_path.position_at(e)
|
||||
|
|
|
|||
|
|
@ -34,10 +34,8 @@ import math
|
|||
import xml.etree.ElementTree as ET
|
||||
from copy import copy
|
||||
from enum import Enum, auto
|
||||
from io import BytesIO
|
||||
from os import PathLike, fsdecode
|
||||
from typing import Any, TypeAlias
|
||||
from typing import cast as tcast
|
||||
from warnings import warn
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
|
|
@ -49,7 +47,7 @@ from ezdxf.colors import RGB, aci2rgb
|
|||
from ezdxf.math import Vec2
|
||||
from OCP.BRepLib import BRepLib
|
||||
from OCP.BRepTools import BRepTools_WireExplorer
|
||||
from OCP.Geom import Geom_BezierCurve, Geom_BSplineCurve
|
||||
from OCP.Geom import Geom_BezierCurve
|
||||
from OCP.GeomConvert import GeomConvert
|
||||
from OCP.GeomConvert import GeomConvert_BSplineCurveToBezierCurve
|
||||
from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Vec, gp_XYZ
|
||||
|
|
@ -638,13 +636,13 @@ class ExportDXF(Export2D):
|
|||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def write(self, file_name: PathLike | str | bytes | BytesIO):
|
||||
def write(self, file_name: PathLike | str | bytes):
|
||||
"""write
|
||||
|
||||
Writes the DXF data to the specified file name.
|
||||
|
||||
Args:
|
||||
file_name (PathLike | str | bytes | BytesIO): The file name (including path) where
|
||||
file_name (PathLike | str | bytes): The file name (including path) where
|
||||
the DXF data will be written.
|
||||
"""
|
||||
# Reset the main CAD viewport of the model space to the
|
||||
|
|
@ -652,12 +650,7 @@ class ExportDXF(Export2D):
|
|||
# https://github.com/gumyr/build123d/issues/382 tracks
|
||||
# exposing viewport control to the user.
|
||||
zoom.extents(self._modelspace)
|
||||
|
||||
if not isinstance(file_name, BytesIO):
|
||||
file_name = fsdecode(file_name)
|
||||
self._document.saveas(file_name)
|
||||
else:
|
||||
self._document.write(file_name, fmt="bin")
|
||||
self._document.saveas(fsdecode(file_name))
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
|
|
@ -758,14 +751,14 @@ class ExportDXF(Export2D):
|
|||
|
||||
# Extract the relevant segment of the curve.
|
||||
spline = GeomConvert.SplitBSplineCurve_s(
|
||||
tcast(Geom_BSplineCurve, curve),
|
||||
curve,
|
||||
u1,
|
||||
u2,
|
||||
Export2D.PARAMETRIC_TOLERANCE,
|
||||
)
|
||||
|
||||
# need to apply the transform on the geometry level
|
||||
if not edge or edge.location is None:
|
||||
if edge.wrapped is None or edge.location is None:
|
||||
raise ValueError(f"Edge is empty {edge}.")
|
||||
t = edge.location.wrapped.Transformation()
|
||||
spline.Transform(t)
|
||||
|
|
@ -1137,7 +1130,7 @@ class ExportSVG(Export2D):
|
|||
)
|
||||
while explorer.More():
|
||||
topo_edge = explorer.Current()
|
||||
loose_edges.append(Edge(TopoDS.Edge_s(topo_edge)))
|
||||
loose_edges.append(Edge(topo_edge))
|
||||
explorer.Next()
|
||||
# print(f"{len(loose_edges)} loose edges")
|
||||
loose_edge_elements = [self._edge_element(edge) for edge in loose_edges]
|
||||
|
|
@ -1264,7 +1257,7 @@ class ExportSVG(Export2D):
|
|||
(u0, u1) = (lp, fp) if reverse else (fp, lp)
|
||||
start = self._path_point(curve.Value(u0))
|
||||
end = self._path_point(curve.Value(u1))
|
||||
radius = complex(radius, radius) # type: ignore[assignment]
|
||||
radius = complex(radius, radius)
|
||||
rotation = math.degrees(gp_Dir(1, 0, 0).AngleWithRef(x_axis, gp_Dir(0, 0, 1)))
|
||||
if curve.IsClosed():
|
||||
midway = self._path_point(curve.Value((u0 + u1) / 2))
|
||||
|
|
@ -1317,7 +1310,7 @@ class ExportSVG(Export2D):
|
|||
(u0, u1) = (lp, fp) if reverse else (fp, lp)
|
||||
start = self._path_point(curve.Value(u0))
|
||||
end = self._path_point(curve.Value(u1))
|
||||
radius = complex(major_radius, minor_radius) # type: ignore[assignment]
|
||||
radius = complex(major_radius, minor_radius)
|
||||
rotation = math.degrees(gp_Dir(1, 0, 0).AngleWithRef(x_axis, gp_Dir(0, 0, 1)))
|
||||
if curve.IsClosed():
|
||||
midway = self._path_point(curve.Value((u0 + u1) / 2))
|
||||
|
|
@ -1352,7 +1345,7 @@ class ExportSVG(Export2D):
|
|||
u2 = adaptor.LastParameter()
|
||||
|
||||
# Apply the shape location to the geometry.
|
||||
if not edge or edge.location is None:
|
||||
if edge.wrapped is None or edge.location is None:
|
||||
raise ValueError(f"Edge is empty {edge}.")
|
||||
t = edge.location.wrapped.Transformation()
|
||||
spline.Transform(t)
|
||||
|
|
@ -1362,7 +1355,7 @@ class ExportSVG(Export2D):
|
|||
# According to the OCCT 7.6.0 documentation,
|
||||
# "ParametricTolerance is not used."
|
||||
converter = GeomConvert_BSplineCurveToBezierCurve(
|
||||
tcast(Geom_BSplineCurve, spline), u1, u2, Export2D.PARAMETRIC_TOLERANCE
|
||||
spline, u1, u2, Export2D.PARAMETRIC_TOLERANCE
|
||||
)
|
||||
|
||||
def make_segment(bezier: Geom_BezierCurve, reverse: bool) -> PathSegment:
|
||||
|
|
@ -1418,7 +1411,7 @@ class ExportSVG(Export2D):
|
|||
}
|
||||
|
||||
def _edge_segments(self, edge: Edge, reverse: bool) -> list[PathSegment]:
|
||||
if not edge:
|
||||
if edge.wrapped is None:
|
||||
raise ValueError(f"Edge is empty {edge}.")
|
||||
edge_reversed = edge.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED
|
||||
geom_type = edge.geom_type
|
||||
|
|
@ -1504,13 +1497,13 @@ class ExportSVG(Export2D):
|
|||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def write(self, path: PathLike | str | bytes | BytesIO):
|
||||
def write(self, path: PathLike | str | bytes):
|
||||
"""write
|
||||
|
||||
Writes the SVG data to the specified file path.
|
||||
|
||||
Args:
|
||||
path (PathLike | str | bytes | BytesIO): The file path where the SVG data will be written.
|
||||
path (PathLike | str | bytes): The file path where the SVG data will be written.
|
||||
"""
|
||||
# pylint: disable=too-many-locals
|
||||
bb = self._bounds
|
||||
|
|
@ -1556,9 +1549,5 @@ class ExportSVG(Export2D):
|
|||
|
||||
xml = ET.ElementTree(svg)
|
||||
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=None)
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ def export_gltf(
|
|||
|
||||
messenger = Message.DefaultMessenger_s()
|
||||
for printer in messenger.Printers():
|
||||
printer.SetTraceLevel(Message_Gravity.Message_Fail)
|
||||
printer.SetTraceLevel(Message_Gravity(Message_Gravity.Message_Fail))
|
||||
|
||||
status = writer.Perform(doc, index_map, progress)
|
||||
|
||||
|
|
@ -262,7 +262,7 @@ def export_gltf(
|
|||
|
||||
def export_step(
|
||||
to_export: Shape,
|
||||
file_path: PathLike | str | bytes | BytesIO,
|
||||
file_path: PathLike | str | bytes,
|
||||
unit: Unit = Unit.MM,
|
||||
write_pcurves: bool = True,
|
||||
precision_mode: PrecisionMode = PrecisionMode.AVERAGE,
|
||||
|
|
@ -277,7 +277,7 @@ def export_step(
|
|||
|
||||
Args:
|
||||
to_export (Shape): object or assembly
|
||||
file_path (Union[PathLike, str, bytes, BytesIO]): step file path
|
||||
file_path (Union[PathLike, str, bytes]): step file path
|
||||
unit (Unit, optional): shape units. Defaults to Unit.MM.
|
||||
write_pcurves (bool, optional): write parametric curves to the STEP file.
|
||||
Defaults to True.
|
||||
|
|
@ -297,7 +297,7 @@ def export_step(
|
|||
# Disable writing OCCT info to console
|
||||
messenger = Message.DefaultMessenger_s()
|
||||
for printer in messenger.Printers():
|
||||
printer.SetTraceLevel(Message_Gravity.Message_Fail)
|
||||
printer.SetTraceLevel(Message_Gravity(Message_Gravity.Message_Fail))
|
||||
|
||||
session = XSControl_WorkSession()
|
||||
writer = STEPCAFControl_Writer(session, False)
|
||||
|
|
@ -326,13 +326,7 @@ def export_step(
|
|||
Interface_Static.SetIVal_s("write.precision.mode", precision_mode.value)
|
||||
writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs)
|
||||
|
||||
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
|
||||
|
||||
status = writer.Write(fspath(file_path)) == IFSelect_ReturnStatus.IFSelect_RetDone
|
||||
if not status:
|
||||
raise RuntimeError("Failed to write STEP file")
|
||||
|
||||
|
|
@ -352,7 +346,7 @@ def export_stl(
|
|||
|
||||
Args:
|
||||
to_export (Shape): object or assembly
|
||||
file_path (Union[PathLike, str, bytes]): The path and file name to write the STL output to.
|
||||
file_path (str): The path and file name to write the STL output to.
|
||||
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
|
||||
large meshes that can consume computing resources. Setting the value too high can
|
||||
|
|
@ -374,4 +368,7 @@ def export_stl(
|
|||
writer = StlAPI_Writer()
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -34,14 +34,13 @@ from __future__ import annotations
|
|||
# other pylint warning to temp remove:
|
||||
# too-many-arguments, too-many-locals, too-many-public-methods,
|
||||
# too-many-statements, too-many-instance-attributes, too-many-branches
|
||||
import colorsys
|
||||
import copy as copy_module
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import warnings
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from math import degrees, isclose, log10, pi, prod, radians
|
||||
from math import degrees, isclose, log10, pi, radians
|
||||
from typing import TYPE_CHECKING, Any, TypeAlias, overload
|
||||
|
||||
import numpy as np
|
||||
|
|
@ -72,7 +71,7 @@ from OCP.gp import (
|
|||
|
||||
# properties used to store mass calculation result
|
||||
from OCP.GProp import GProp_GProps
|
||||
from OCP.Quantity import Quantity_Color, Quantity_ColorRGBA, Quantity_TypeOfColor
|
||||
from OCP.Quantity import Quantity_Color, Quantity_ColorRGBA
|
||||
from OCP.TopAbs import TopAbs_ShapeEnum
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.TopoDS import TopoDS, TopoDS_Edge, TopoDS_Face, TopoDS_Shape, TopoDS_Vertex
|
||||
|
|
@ -443,35 +442,14 @@ class Vector:
|
|||
"""intersect vector with other &"""
|
||||
return self.intersect(other)
|
||||
|
||||
def __format__(self, spec) -> str:
|
||||
"""Format Vector"""
|
||||
|
||||
def trim_float(x: float, precision: int) -> float:
|
||||
return round(x, precision) if abs(x) > TOLERANCE else 0.0
|
||||
|
||||
last_char = spec[-1] if spec else None
|
||||
if last_char in ("f", "g"):
|
||||
if "." in spec:
|
||||
precision = int(spec[:-1].split(".")[-1])
|
||||
else:
|
||||
precision = 6 if last_char == "f" else 12
|
||||
|
||||
x = trim_float(self.X, precision)
|
||||
y = trim_float(self.Y, precision)
|
||||
z = trim_float(self.Z, precision)
|
||||
|
||||
return f"({x:{spec}}, {y:{spec}}, {z:{spec}})"
|
||||
|
||||
return str(tuple(self))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Represent Vector"""
|
||||
return f"{type(self).__name__}{self:.13g}"
|
||||
"""Display vector"""
|
||||
x = round(self.X, 13) if abs(self.X) > TOLERANCE else 0.0
|
||||
y = round(self.Y, 13) if abs(self.Y) > TOLERANCE else 0.0
|
||||
z = round(self.Z, 13) if abs(self.Z) > TOLERANCE else 0.0
|
||||
return f"Vector({x:.14g}, {y:.14g}, {z:.14g})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Display Vector"""
|
||||
x, y, z = format(self, ".6g")[1:-1].split(", ")
|
||||
return f"{type(self).__name__}: (X={x}, Y={y}, Z={z})"
|
||||
__str__ = __repr__
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""Vectors equal operator =="""
|
||||
|
|
@ -759,23 +737,14 @@ class Axis(metaclass=AxisMeta):
|
|||
)
|
||||
)
|
||||
|
||||
def __format__(self, spec) -> str:
|
||||
"""Format Axis"""
|
||||
last_char = spec[-1] if spec else None
|
||||
if last_char in ("f", "g"):
|
||||
return f"({self.position:{spec}}, {self.direction:{spec}})"
|
||||
|
||||
return f"({tuple(self.position)}, {tuple(self.direction)})"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Represent Axis"""
|
||||
return f"{type(self).__name__}{self:.{TOL_DIGITS}g}"
|
||||
"""Display self"""
|
||||
return f"({tuple(self.position)},{tuple(self.direction)})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Display Axis"""
|
||||
"""Display self"""
|
||||
return (
|
||||
f"{type(self).__name__}: "
|
||||
f"(position={self.position:.{TOL_DIGITS}g}, direction={self.direction:.{TOL_DIGITS}g})"
|
||||
f"{type(self).__name__}: ({tuple(self.position)},{tuple(self.direction)})"
|
||||
)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
|
|
@ -1031,16 +1000,6 @@ class BoundBox:
|
|||
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
|
||||
|
||||
@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
|
||||
def diagonal(self) -> float:
|
||||
"""body diagonal length (i.e. object maximum size)"""
|
||||
|
|
@ -1201,8 +1160,8 @@ class Color:
|
|||
|
||||
Args:
|
||||
color_like (ColorLike):
|
||||
name, ex: "red" or "#ff0000",
|
||||
name + alpha, ex: ("red", 0.5) or "#ff000080",
|
||||
name, ex: "red",
|
||||
name + alpha, ex: ("red", 0.5),
|
||||
rgb, ex: (1., 0., 0.),
|
||||
rgb + alpha, ex: (1., 0., 0., 0.5),
|
||||
hex, ex: 0xff0000,
|
||||
|
|
@ -1213,7 +1172,7 @@ class Color:
|
|||
|
||||
@overload
|
||||
def __init__(self, name: str, alpha: float = 1.0):
|
||||
"""Color from name or hexadecimal string
|
||||
"""Color from name
|
||||
|
||||
`CSS3 Color Names
|
||||
<https://en.wikipedia.org/wiki/Web_colors#Extended_colors>`
|
||||
|
|
@ -1221,16 +1180,14 @@ class Color:
|
|||
`OCCT Color Names
|
||||
<https://dev.opencascade.org/doc/refman/html/_quantity___name_of_color_8hxx.html>`_
|
||||
|
||||
Hexadecimal string may be RGB or RGBA format with leading "#"
|
||||
|
||||
Args:
|
||||
name (str): color, e.g. "blue" or "#0000ff""
|
||||
name (str): color, e.g. "blue"
|
||||
alpha (float, optional): 0.0 <= alpha <= 1.0. Defaults to 1.0
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self, red: float, green: float, blue: float, alpha: float = 1.0):
|
||||
"""Color from sRGB and Alpha values
|
||||
"""Color from RGBA and Alpha values
|
||||
|
||||
Args:
|
||||
red (float): 0.0 <= red <= 1.0
|
||||
|
|
@ -1280,27 +1237,6 @@ class Color:
|
|||
return
|
||||
case str():
|
||||
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():
|
||||
color_code, alpha = fill_defaults(args, (color_code, alpha))
|
||||
case float():
|
||||
|
|
@ -1342,10 +1278,7 @@ class Color:
|
|||
raise TypeError(f"Unsupported color definition: {color_format}")
|
||||
|
||||
if not self.wrapped:
|
||||
the_color = Quantity_Color(
|
||||
red, green, blue, Quantity_TypeOfColor.Quantity_TOC_sRGB
|
||||
)
|
||||
self.wrapped = Quantity_ColorRGBA(the_color, alpha)
|
||||
self.wrapped = Quantity_ColorRGBA(red, green, blue, alpha)
|
||||
|
||||
def __iter__(self):
|
||||
"""Initialize to beginning"""
|
||||
|
|
@ -1354,14 +1287,24 @@ class Color:
|
|||
|
||||
def __next__(self):
|
||||
"""return the next value"""
|
||||
r, g, b = self.wrapped.GetRGB().Values(Quantity_TypeOfColor.Quantity_TOC_sRGB)
|
||||
rgb_tuple = (r, g, b, self.wrapped.Alpha())
|
||||
rgb = self.wrapped.GetRGB()
|
||||
rgb_tuple = (rgb.Red(), rgb.Green(), rgb.Blue(), self.wrapped.Alpha())
|
||||
|
||||
if self.iter_index > 3:
|
||||
raise StopIteration
|
||||
value = rgb_tuple[self.iter_index]
|
||||
self.iter_index += 1
|
||||
return round(value, 7)
|
||||
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:
|
||||
"""Return copy of self"""
|
||||
|
|
@ -1373,88 +1316,21 @@ class Color:
|
|||
|
||||
def __str__(self) -> str:
|
||||
"""Generate string"""
|
||||
rgb = self.wrapped.GetRGB().Values(Quantity_TypeOfColor.Quantity_TOC_sRGB)
|
||||
rgb = self.wrapped.GetRGB()
|
||||
rgb = (rgb.Red(), rgb.Green(), rgb.Blue())
|
||||
try:
|
||||
name = webcolors.rgb_to_name([round(c * 255) for c in rgb])
|
||||
name = webcolors.rgb_to_name([int(c * 255) for c in rgb])
|
||||
qualifier = "is"
|
||||
except ValueError:
|
||||
# This still uses OCCT X11 colors instead of css3
|
||||
quantity_color_enum = self.wrapped.GetRGB().Name()
|
||||
name = Quantity_Color.StringName_s(quantity_color_enum)
|
||||
qualifier = "near"
|
||||
return f"{type(self).__name__}: {str(tuple(self))} {qualifier} {name.upper()!r}"
|
||||
return f"Color: {str(tuple(self))} {qualifier} {name.upper()!r}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Represent Color"""
|
||||
return f"{type(self).__name__}{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 sets—both 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.0–1.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
|
||||
"""Color repr"""
|
||||
return f"Color{str(tuple(self))}"
|
||||
|
||||
@staticmethod
|
||||
def _rgb_from_int(triplet: int) -> tuple[float, float, float]:
|
||||
|
|
@ -1464,6 +1340,7 @@ class Color:
|
|||
|
||||
@staticmethod
|
||||
def _rgb_from_str(name: str) -> tuple:
|
||||
name = name.strip()
|
||||
if "#" not in name:
|
||||
try:
|
||||
# Use css3 color names by default
|
||||
|
|
@ -1599,50 +1476,56 @@ class Location:
|
|||
|
||||
@overload
|
||||
def __init__(self):
|
||||
"""Location with no position or orientation"""
|
||||
"""Empty location with not rotation or translation with respect to the original location."""
|
||||
|
||||
@overload
|
||||
def __init__(self, location: Location):
|
||||
"""Location from Location"""
|
||||
"""Location with another given location."""
|
||||
|
||||
@overload
|
||||
def __init__(self, position: VectorLike, angle: float = 0):
|
||||
"""Location from position and rotation around z-axis by optional angle"""
|
||||
def __init__(self, translation: VectorLike, angle: float = 0):
|
||||
"""Location with translation with respect to the original location.
|
||||
If angle != 0 then the location includes a rotation around z-axis by angle"""
|
||||
|
||||
@overload
|
||||
def __init__(self, position: VectorLike, orientation: RotationLike | None = None):
|
||||
"""Location from position and optional orientation (see Rotation class)"""
|
||||
def __init__(self, translation: VectorLike, rotation: RotationLike | None = None):
|
||||
"""Location with translation with respect to the original location.
|
||||
If rotation is not None then the location includes the rotation (see also Rotation class)
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
position: VectorLike,
|
||||
orientation: RotationLike,
|
||||
translation: VectorLike,
|
||||
rotation: RotationLike,
|
||||
ordering: Extrinsic | Intrinsic,
|
||||
):
|
||||
"""Location from position and optional orientation (see Rotation class).
|
||||
Orientation determined by optional ordering, defaults to Intrinsic.XYZ
|
||||
"""Location with translation with respect to the original location.
|
||||
If rotation is not None then the location includes the rotation (see also Rotation class)
|
||||
ordering defaults to Intrinsic.XYZ, but can also be set to Extrinsic
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self, plane: Plane):
|
||||
"""Location from location of Plane."""
|
||||
"""Location corresponding to the location of the Plane."""
|
||||
|
||||
@overload
|
||||
def __init__(self, plane: Plane, plane_offset: VectorLike):
|
||||
"""Location from location of Plane translated by plane_offset"""
|
||||
"""Location corresponding to the angular location of the Plane with
|
||||
translation plane_offset."""
|
||||
|
||||
@overload
|
||||
def __init__(self, top_loc: TopLoc_Location):
|
||||
"""Location from low-level TopLoc_Location object"""
|
||||
"""Location wrapping the low-level TopLoc_Location object t"""
|
||||
|
||||
@overload
|
||||
def __init__(self, gp_trsf: gp_Trsf):
|
||||
"""Location from low-level gp_Trsf object"""
|
||||
"""Location wrapping the low-level gp_Trsf object t"""
|
||||
|
||||
@overload
|
||||
def __init__(self, position: VectorLike, direction: VectorLike, angle: float):
|
||||
"""Location from position and rotation around direction by angle"""
|
||||
def __init__(self, translation: VectorLike, direction: VectorLike, angle: float):
|
||||
"""Location with translation t and rotation around direction by angle
|
||||
with respect to the original location."""
|
||||
|
||||
def __init__(
|
||||
self, *args, **kwargs
|
||||
|
|
@ -1652,7 +1535,6 @@ class Location:
|
|||
|
||||
position = kwargs.pop("position", None)
|
||||
orientation = kwargs.pop("orientation", None)
|
||||
direction = kwargs.pop("direction", None)
|
||||
ordering = kwargs.pop("ordering", None)
|
||||
angle = kwargs.pop("angle", None)
|
||||
plane = kwargs.pop("plane", None)
|
||||
|
|
@ -1682,10 +1564,7 @@ class Location:
|
|||
elif isinstance(args[1], (int, float)):
|
||||
angle = args[1]
|
||||
if len(args) > 2:
|
||||
if isinstance(args[1], (Vector, Iterable)) and isinstance(
|
||||
args[2], (int, float)
|
||||
):
|
||||
direction = Vector(args[1])
|
||||
if isinstance(args[2], (int, float)) and orientation is not None:
|
||||
angle = args[2]
|
||||
elif isinstance(args[2], (Intrinsic, Extrinsic)):
|
||||
ordering = args[2]
|
||||
|
|
@ -1714,7 +1593,7 @@ class Location:
|
|||
elif angle is not None:
|
||||
axis = gp_Ax1(
|
||||
gp_Pnt(0, 0, 0),
|
||||
Vector(direction).to_dir() if direction else gp_Dir(0, 0, 1),
|
||||
Vector(orientation).to_dir() if orientation else gp_Dir(0, 0, 1),
|
||||
)
|
||||
trsf.SetRotation(axis, radians(angle))
|
||||
|
||||
|
|
@ -2032,24 +1911,29 @@ class Location:
|
|||
|
||||
return rv_trans, rv_rot
|
||||
|
||||
def __format__(self, spec) -> str:
|
||||
"""Format Location"""
|
||||
last_char = spec[-1] if spec else None
|
||||
if last_char in ("f", "g"):
|
||||
return f"({self.position:{spec}}, {self.orientation:{spec}})"
|
||||
def __repr__(self):
|
||||
"""To String
|
||||
|
||||
return f"({tuple(self.position)}, {tuple(self.orientation)})"
|
||||
Convert Location to String for display
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Represent Location"""
|
||||
return f"{type(self).__name__}{self:.{TOL_DIGITS}g}"
|
||||
Returns:
|
||||
Location as String
|
||||
"""
|
||||
position_str = ", ".join(f"{v:.2f}" for v in tuple(self)[0])
|
||||
orientation_str = ", ".join(f"{v:.2f}" for v in tuple(self)[1])
|
||||
return f"(p=({position_str}), o=({orientation_str}))"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Display Location"""
|
||||
return (
|
||||
f"{type(self).__name__}: "
|
||||
f"(position={self.position:.{TOL_DIGITS}g}, orientation={self.orientation:.{TOL_DIGITS}g})"
|
||||
)
|
||||
def __str__(self):
|
||||
"""To String
|
||||
|
||||
Convert Location to String for display
|
||||
|
||||
Returns:
|
||||
Location as String
|
||||
"""
|
||||
position_str = ", ".join(f"{v:.2f}" for v in tuple(self)[0])
|
||||
orientation_str = ", ".join(f"{v:.2f}" for v in tuple(self)[1])
|
||||
return f"Location: (position=({position_str}), orientation=({orientation_str}))"
|
||||
|
||||
@overload
|
||||
def intersect(self, vector: VectorLike) -> Vector | None:
|
||||
|
|
@ -2347,7 +2231,7 @@ class OrientedBoundBox:
|
|||
return self.wrapped.IsOut(point.to_pnt())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"OrientedBoundBox(center={self.center()!r}, size={self.size!r}, plane={self.plane!r})"
|
||||
return f"OrientedBoundBox(center={self.center()}, size={self.size}, plane={self.plane})"
|
||||
|
||||
|
||||
class Rotation(Location):
|
||||
|
|
@ -2970,24 +2854,18 @@ class Plane(metaclass=PlaneMeta):
|
|||
"""intersect plane with other &"""
|
||||
return self.intersect(other)
|
||||
|
||||
def __format__(self, spec) -> str:
|
||||
"""Format Plane"""
|
||||
last_char = spec[-1] if spec else None
|
||||
if last_char in ("f", "g"):
|
||||
return f"({self.origin:{spec}}, {self.x_dir:{spec}}, {self.z_dir:{spec}})"
|
||||
def __repr__(self):
|
||||
"""To String
|
||||
|
||||
return f"({tuple(self.origin)}, {tuple(self.x_dir)}, {tuple(self.z_dir)})"
|
||||
Convert Plane to String for display
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Represent Plane"""
|
||||
return f"{type(self).__name__}{self:.{TOL_DIGITS}g}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Display Plane"""
|
||||
return (
|
||||
f"{type(self).__name__}: "
|
||||
f"(origin={self.origin:.{TOL_DIGITS}g}, x_dir={self.x_dir:.{TOL_DIGITS}g}, z_dir={self.z_dir:.{TOL_DIGITS}g})"
|
||||
)
|
||||
Returns:
|
||||
Plane as String
|
||||
"""
|
||||
origin_str = ", ".join(f"{v:.2f}" for v in tuple(self._origin))
|
||||
x_dir_str = ", ".join(f"{v:.2f}" for v in tuple(self.x_dir))
|
||||
z_dir_str = ", ".join(f"{v:.2f}" for v in tuple(self.z_dir))
|
||||
return f"Plane(o=({origin_str}), x=({x_dir_str}), z=({z_dir_str}))"
|
||||
|
||||
def reverse(self) -> Plane:
|
||||
"""Reverse z direction of plane"""
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ license:
|
|||
# pylint: disable=no-name-in-module, import-error
|
||||
import copy as copy_module
|
||||
import ctypes
|
||||
from io import BytesIO
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
|
@ -106,23 +105,15 @@ from OCP.BRepGProp import BRepGProp
|
|||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCP.gp import gp_Pnt
|
||||
from OCP.GProp import GProp_GProps
|
||||
from OCP.Standard import Standard_TypeMismatch
|
||||
from OCP.TopAbs import TopAbs_ShapeEnum
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.TopoDS import TopoDS, TopoDS_Compound, TopoDS_Shell
|
||||
from OCP.TopoDS import TopoDS_Compound
|
||||
from lib3mf import Lib3MF
|
||||
|
||||
from build123d.build_enums import MeshType, Unit
|
||||
from build123d.geometry import TOLERANCE, Color
|
||||
from build123d.topology import (
|
||||
Compound,
|
||||
Shape,
|
||||
Shell,
|
||||
Solid,
|
||||
downcast,
|
||||
unwrap_topods_compound,
|
||||
)
|
||||
from build123d.topology import Compound, Shape, Shell, Solid, downcast
|
||||
|
||||
|
||||
class Mesher:
|
||||
|
|
@ -304,7 +295,7 @@ class Mesher:
|
|||
ocp_mesh_vertices.append(pnt)
|
||||
|
||||
# Store the triangles from the triangulated faces
|
||||
if not facet:
|
||||
if facet.wrapped is None:
|
||||
continue
|
||||
facet_reversed = facet.wrapped.Orientation() == ta.TopAbs_REVERSED
|
||||
order = [1, 3, 2] if facet_reversed else [1, 2, 3]
|
||||
|
|
@ -321,12 +312,12 @@ class Mesher:
|
|||
# Round off the vertices to avoid vertices within tolerance being
|
||||
# considered as different vertices
|
||||
digits = -int(round(math.log(TOLERANCE, 10), 1))
|
||||
|
||||
|
||||
# Create vertex to index mapping directly
|
||||
vertex_to_idx = {}
|
||||
next_idx = 0
|
||||
vert_table = {}
|
||||
|
||||
|
||||
# First pass - create mapping
|
||||
for i, (x, y, z) in enumerate(ocp_mesh_vertices):
|
||||
key = (round(x, digits), round(y, digits), round(z, digits))
|
||||
|
|
@ -334,16 +325,17 @@ class Mesher:
|
|||
vertex_to_idx[key] = next_idx
|
||||
next_idx += 1
|
||||
vert_table[i] = vertex_to_idx[key]
|
||||
|
||||
|
||||
# Create vertices array in one shot
|
||||
vertices_3mf = [
|
||||
Lib3MF.Position((ctypes.c_float * 3)(*v)) for v in vertex_to_idx.keys()
|
||||
Lib3MF.Position((ctypes.c_float * 3)(*v))
|
||||
for v in vertex_to_idx.keys()
|
||||
]
|
||||
|
||||
|
||||
# Pre-allocate triangles array and process in bulk
|
||||
c_uint3 = ctypes.c_uint * 3
|
||||
triangles_3mf = []
|
||||
|
||||
|
||||
# Process triangles in bulk
|
||||
for tri in triangles:
|
||||
# Map indices directly without list comprehension
|
||||
|
|
@ -351,13 +343,11 @@ class Mesher:
|
|||
mapped_a = vert_table[a]
|
||||
mapped_b = vert_table[b]
|
||||
mapped_c = vert_table[c]
|
||||
|
||||
|
||||
# Quick degenerate check without set creation
|
||||
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)
|
||||
|
||||
def _add_color(self, b3d_shape: Shape, mesh_3mf: Lib3MF.MeshObject):
|
||||
|
|
@ -474,9 +464,7 @@ class Mesher:
|
|||
# Convert to a list of gp_Pnt
|
||||
ocp_vertices = [gp_pnts[tri_indices[i]] for i in range(3)]
|
||||
# Create the triangular face using the polygon
|
||||
polygon_builder = BRepBuilderAPI_MakePolygon(
|
||||
ocp_vertices[0], ocp_vertices[1], ocp_vertices[2], Close=True
|
||||
)
|
||||
polygon_builder = BRepBuilderAPI_MakePolygon(*ocp_vertices, Close=True)
|
||||
face_builder = BRepBuilderAPI_MakeFace(polygon_builder.Wire())
|
||||
facet = face_builder.Face()
|
||||
facet_properties = GProp_GProps()
|
||||
|
|
@ -489,27 +477,19 @@ class Mesher:
|
|||
occ_sewed_shape = downcast(shell_builder.SewedShape())
|
||||
|
||||
if isinstance(occ_sewed_shape, TopoDS_Compound):
|
||||
bd_shells = []
|
||||
occ_shells = []
|
||||
explorer = TopExp_Explorer(occ_sewed_shape, TopAbs_ShapeEnum.TopAbs_SHELL)
|
||||
while explorer.More():
|
||||
# occ_shells.append(downcast(explorer.Current()))
|
||||
bd_shells.append(Shell(TopoDS.Shell_s(explorer.Current())))
|
||||
occ_shells.append(downcast(explorer.Current()))
|
||||
explorer.Next()
|
||||
else:
|
||||
assert isinstance(occ_sewed_shape, TopoDS_Shell)
|
||||
bd_shells = [Shell(occ_sewed_shape)]
|
||||
occ_shells = [occ_sewed_shape]
|
||||
|
||||
outer_shell = max(bd_shells, key=lambda s: math.prod(s.bounding_box().size))
|
||||
inner_shells = [s for s in bd_shells if s is not outer_shell]
|
||||
|
||||
# The the shell isn't water tight just return it else create a 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())
|
||||
# Create a solid if manifold
|
||||
shape_obj = Shell(occ_sewed_shape)
|
||||
if shape_obj.is_manifold:
|
||||
solid_builder = BRepBuilderAPI_MakeSolid(*occ_shells)
|
||||
shape_obj = Solid(solid_builder.Solid())
|
||||
|
||||
return shape_obj
|
||||
|
||||
|
|
@ -571,8 +551,3 @@ class Mesher:
|
|||
raise ValueError(f"Unknown file format {output_file_extension}")
|
||||
writer = self.model.QueryWriter(output_file_extension[1:])
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ from build123d.build_enums import (
|
|||
)
|
||||
from build123d.build_line import BuildLine
|
||||
from build123d.geometry import Axis, Plane, Vector, VectorLike, TOLERANCE
|
||||
from build123d.topology import Curve, Edge, Face, Vertex, Wire
|
||||
from build123d.topology import Edge, Face, Wire, Curve
|
||||
from build123d.topology.shape_core import ShapeList
|
||||
|
||||
|
||||
|
|
@ -741,117 +741,6 @@ class EllipticalCenterArc(BaseEdgeObject):
|
|||
super().__init__(curve, mode=mode)
|
||||
|
||||
|
||||
class ParabolicCenterArc(BaseEdgeObject):
|
||||
"""Line Object: Parabolic Center Arc
|
||||
|
||||
Create a parabolic arc defined by a vertex point and focal length (distance from focus to vertex).
|
||||
|
||||
Args:
|
||||
vertex (VectorLike): parabola vertex
|
||||
focal_length (float): focal length the parabola (distance from the vertex to focus along the x-axis of plane)
|
||||
start_angle (float, optional): arc start angle.
|
||||
Defaults to 0.0
|
||||
end_angle (float, optional): arc end angle.
|
||||
Defaults to 90.0
|
||||
rotation (float, optional): angle to rotate arc. Defaults to 0.0
|
||||
angular_direction (AngularDirection, optional): arc direction.
|
||||
Defaults to AngularDirection.COUNTER_CLOCKWISE
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
||||
"""
|
||||
|
||||
_applies_to = [BuildLine._tag]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vertex: VectorLike,
|
||||
focal_length: float,
|
||||
start_angle: float = 0.0,
|
||||
end_angle: float = 90.0,
|
||||
rotation: float = 0.0,
|
||||
angular_direction: AngularDirection = AngularDirection.COUNTER_CLOCKWISE,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context: BuildLine | None = BuildLine._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
vertex_pnt = WorkplaneList.localize(vertex)
|
||||
if context is None:
|
||||
parabola_workplane = Plane.XY
|
||||
else:
|
||||
parabola_workplane = copy_module.copy(
|
||||
WorkplaneList._get_context().workplanes[0]
|
||||
)
|
||||
parabola_workplane.origin = vertex_pnt
|
||||
curve = Edge.make_parabola(
|
||||
focal_length=focal_length,
|
||||
plane=parabola_workplane,
|
||||
start_angle=start_angle,
|
||||
end_angle=end_angle,
|
||||
angular_direction=angular_direction,
|
||||
).rotate(
|
||||
Axis(parabola_workplane.origin, parabola_workplane.z_dir.to_dir()), rotation
|
||||
)
|
||||
|
||||
super().__init__(curve, mode=mode)
|
||||
|
||||
|
||||
class HyperbolicCenterArc(BaseEdgeObject):
|
||||
"""Line Object: Hyperbolic Center Arc
|
||||
|
||||
Create a hyperbolic arc defined by a center point and focal length (distance from focus to vertex).
|
||||
|
||||
Args:
|
||||
center (VectorLike): hyperbola center
|
||||
x_radius (float): x radius of the ellipse (along the x-axis of plane)
|
||||
y_radius (float): y radius of the ellipse (along the y-axis of plane)
|
||||
start_angle (float, optional): arc start angle from x-axis.
|
||||
Defaults to 0.0
|
||||
end_angle (float, optional): arc end angle from x-axis.
|
||||
Defaults to 90.0
|
||||
rotation (float, optional): angle to rotate arc. Defaults to 0.0
|
||||
angular_direction (AngularDirection, optional): arc direction.
|
||||
Defaults to AngularDirection.COUNTER_CLOCKWISE
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
||||
"""
|
||||
|
||||
_applies_to = [BuildLine._tag]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
center: VectorLike,
|
||||
x_radius: float,
|
||||
y_radius: float,
|
||||
start_angle: float = 0.0,
|
||||
end_angle: float = 90.0,
|
||||
rotation: float = 0.0,
|
||||
angular_direction: AngularDirection = AngularDirection.COUNTER_CLOCKWISE,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context: BuildLine | None = BuildLine._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
center_pnt = WorkplaneList.localize(center)
|
||||
if context is None:
|
||||
hyperbola_workplane = Plane.XY
|
||||
else:
|
||||
hyperbola_workplane = copy_module.copy(
|
||||
WorkplaneList._get_context().workplanes[0]
|
||||
)
|
||||
hyperbola_workplane.origin = center_pnt
|
||||
curve = Edge.make_hyperbola(
|
||||
x_radius=x_radius,
|
||||
y_radius=y_radius,
|
||||
plane=hyperbola_workplane,
|
||||
start_angle=start_angle,
|
||||
end_angle=end_angle,
|
||||
angular_direction=angular_direction,
|
||||
).rotate(
|
||||
Axis(hyperbola_workplane.origin, hyperbola_workplane.z_dir.to_dir()), rotation
|
||||
)
|
||||
|
||||
super().__init__(curve, mode=mode)
|
||||
|
||||
|
||||
class Helix(BaseEdgeObject):
|
||||
"""Line Object: Helix
|
||||
|
||||
|
|
@ -898,22 +787,20 @@ class Helix(BaseEdgeObject):
|
|||
|
||||
class FilletPolyline(BaseLineObject):
|
||||
"""Line Object: Fillet Polyline
|
||||
|
||||
Create a sequence of straight lines defined by successive points that are filleted
|
||||
to a given radius.
|
||||
|
||||
Args:
|
||||
pts (VectorLike | Iterable[VectorLike]): sequence of two or more points
|
||||
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).
|
||||
|
||||
radius (float): fillet radius
|
||||
close (bool, optional): close end points with extra Edge and corner fillets.
|
||||
Defaults to False
|
||||
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
||||
|
||||
Raises:
|
||||
ValueError: Two or more points not provided
|
||||
ValueError: radius must be non-negative
|
||||
ValueError: radius must be positive
|
||||
"""
|
||||
|
||||
_applies_to = [BuildLine._tag]
|
||||
|
|
@ -921,49 +808,34 @@ class FilletPolyline(BaseLineObject):
|
|||
def __init__(
|
||||
self,
|
||||
*pts: VectorLike | Iterable[VectorLike],
|
||||
radius: float | Iterable[float],
|
||||
radius: float,
|
||||
close: bool = False,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
|
||||
context: BuildLine | None = BuildLine._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
points = flatten_sequence(*pts)
|
||||
|
||||
if len(points) < 2:
|
||||
raise ValueError("FilletPolyline requires two or more pts")
|
||||
|
||||
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")
|
||||
if radius <= 0:
|
||||
raise ValueError("radius must be positive")
|
||||
|
||||
lines_pts = WorkplaneList.localize(*points)
|
||||
# Create the polyline
|
||||
|
||||
# Create the polyline
|
||||
new_edges = [
|
||||
Edge.make_line(lines_pts[i], lines_pts[i + 1])
|
||||
for i in range(len(lines_pts) - 1)
|
||||
]
|
||||
|
||||
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))
|
||||
|
||||
wire_of_lines = Wire(new_edges)
|
||||
|
||||
# Create a list of vertices from wire_of_lines in the same order as
|
||||
# the original points so the resulting fillet edges are ordered
|
||||
ordered_vertices: list[Vertex] = []
|
||||
|
||||
ordered_vertices = []
|
||||
for pnts in lines_pts:
|
||||
distance = {
|
||||
v: (Vector(pnts) - Vector(*v)).length for v in wire_of_lines.vertices()
|
||||
|
|
@ -971,93 +843,40 @@ class FilletPolyline(BaseLineObject):
|
|||
ordered_vertices.append(sorted(distance.items(), key=lambda x: x[1])[0][0])
|
||||
|
||||
# Fillet the corners
|
||||
|
||||
# Create a map of vertices to edges containing that vertex
|
||||
vertex_to_edges = {
|
||||
v: [e for e in wire_of_lines.edges() if v in e.vertices()]
|
||||
for v in ordered_vertices
|
||||
}
|
||||
|
||||
# For each corner vertex create a new fillet Edge (or keep as vertex if radius is 0)
|
||||
fillets: list[None | Edge] = []
|
||||
|
||||
for i, (vertex, edges) in enumerate(vertex_to_edges.items()):
|
||||
# For each corner vertex create a new fillet Edge
|
||||
fillets = []
|
||||
for vertex, edges in vertex_to_edges.items():
|
||||
if len(edges) != 2:
|
||||
continue
|
||||
current_radius = radius_list[i - int(not close)]
|
||||
|
||||
if current_radius == 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])
|
||||
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(radius, [vertex])
|
||||
fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0])
|
||||
|
||||
# Create the Edges that join the fillets
|
||||
if close:
|
||||
interior_edges = []
|
||||
|
||||
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))
|
||||
|
||||
interior_edges = [
|
||||
Edge.make_line(fillets[i - 1] @ 1, fillets[i] @ 0)
|
||||
for i in range(len(fillets))
|
||||
]
|
||||
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
|
||||
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),
|
||||
]
|
||||
|
||||
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)
|
||||
new_wire = Wire(end_edges + interior_edges + fillets)
|
||||
|
||||
super().__init__(new_wire, mode=mode)
|
||||
|
||||
|
|
@ -1778,7 +1597,6 @@ class ArcArcTangentLine(BaseEdgeObject):
|
|||
Defaults to Keep.INSIDE
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
||||
"""
|
||||
|
||||
warnings.warn(
|
||||
"The 'ArcArcTangentLine' object is deprecated and will be removed in a future version.",
|
||||
DeprecationWarning,
|
||||
|
|
|
|||
|
|
@ -28,31 +28,13 @@ license:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from math import radians, tan
|
||||
from scipy.spatial import ConvexHull
|
||||
|
||||
from build123d.build_common import LocationList, validate_inputs
|
||||
from build123d.build_enums import Align, Mode
|
||||
from build123d.build_part import BuildPart
|
||||
from build123d.geometry import (
|
||||
Location,
|
||||
Plane,
|
||||
Rotation,
|
||||
RotationLike,
|
||||
Vector,
|
||||
VectorLike,
|
||||
)
|
||||
from build123d.topology import (
|
||||
Compound,
|
||||
Face,
|
||||
Part,
|
||||
ShapeList,
|
||||
Shell,
|
||||
Solid,
|
||||
Wire,
|
||||
tuplify,
|
||||
)
|
||||
from build123d.geometry import Location, Plane, Rotation, RotationLike
|
||||
from build123d.topology import Compound, Part, ShapeList, Solid, tuplify
|
||||
|
||||
|
||||
class BasePartObject(Part):
|
||||
|
|
@ -215,50 +197,6 @@ class Cone(BasePartObject):
|
|||
)
|
||||
|
||||
|
||||
class ConvexPolyhedron(BasePartObject):
|
||||
"""Part Object: ConvexPolyhedron
|
||||
|
||||
Create a convex solid from the convex hull of the provided points.
|
||||
|
||||
Args:
|
||||
points (Iterable[VectorLike]): vertices of the polyhedron
|
||||
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0)
|
||||
align (Align | tuple[Align, Align, Align] | None, optional): align MIN, CENTER,
|
||||
or MAX of object. Defaults to Align.NONE
|
||||
mode (Mode, optional): combine mode. Defaults to Mode.ADD
|
||||
"""
|
||||
|
||||
_applies_to = [BuildPart._tag]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
points: Iterable[VectorLike],
|
||||
rotation: RotationLike = (0, 0, 0),
|
||||
align: Align | tuple[Align, Align, Align] | None = Align.NONE,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context: BuildPart | None = BuildPart._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
pnts: list[tuple] = [tuple(Vector(p)) for p in points]
|
||||
|
||||
# Create a convex hull from the vertices
|
||||
convex_hull = ConvexHull(pnts).simplices.tolist()
|
||||
|
||||
# Create faces from the vertex indices
|
||||
polyhedron_faces = []
|
||||
for face_vertex_indices in convex_hull:
|
||||
corner_vertices = [pnts[i] for i in face_vertex_indices]
|
||||
polyhedron_faces.append(Face(Wire.make_polygon(corner_vertices)))
|
||||
|
||||
# Create the solid from the Faces
|
||||
polyhedron = Solid(Shell(polyhedron_faces)).clean()
|
||||
|
||||
super().__init__(
|
||||
part=polyhedron, rotation=rotation, align=tuplify(align, 3), mode=mode
|
||||
)
|
||||
|
||||
|
||||
class CounterBoreHole(BasePartObject):
|
||||
"""Part Operation: Counter Bore Hole
|
||||
|
||||
|
|
|
|||
|
|
@ -365,7 +365,7 @@ def chamfer(
|
|||
|
||||
if target._dim == 1:
|
||||
if isinstance(target, BaseLineObject):
|
||||
if not target:
|
||||
if target.wrapped is None:
|
||||
target = Wire([]) # empty wire
|
||||
else:
|
||||
target = Wire(target.wrapped)
|
||||
|
|
@ -465,7 +465,7 @@ def fillet(
|
|||
|
||||
if target._dim == 1:
|
||||
if isinstance(target, BaseLineObject):
|
||||
if not target:
|
||||
if target.wrapped is None:
|
||||
target = Wire([]) # empty wire
|
||||
else:
|
||||
target = Wire(target.wrapped)
|
||||
|
|
|
|||
|
|
@ -223,8 +223,8 @@ def extrude(
|
|||
|
||||
new_solids.append(
|
||||
Solid.extrude_until(
|
||||
face,
|
||||
target=target_object,
|
||||
section=face,
|
||||
target_object=target_object,
|
||||
direction=plane.z_dir * direction,
|
||||
until=until,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ from build123d.topology import (
|
|||
Wire,
|
||||
Sketch,
|
||||
topo_explore_connected_edges,
|
||||
topo_explore_common_vertex,
|
||||
)
|
||||
from build123d.geometry import Plane, Vector, TOLERANCE
|
||||
from build123d.build_common import flatten_sequence, validate_inputs
|
||||
|
|
|
|||
|
|
@ -58,13 +58,13 @@ import copy
|
|||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from collections.abc import Iterable, Iterator, Sequence
|
||||
from itertools import combinations
|
||||
from typing import Type, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
from collections.abc import Iterable, Iterator, Sequence
|
||||
|
||||
import OCP.TopAbs as ta
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Fuse, BRepAlgoAPI_Section
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse
|
||||
from OCP.Font import (
|
||||
Font_FA_Bold,
|
||||
Font_FA_BoldItalic,
|
||||
|
|
@ -107,6 +107,7 @@ from build123d.geometry import (
|
|||
VectorLike,
|
||||
logger,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
|
||||
from .one_d import Edge, Wire, Mixin1D
|
||||
from .shape_core import (
|
||||
|
|
@ -129,7 +130,7 @@ from .utils import (
|
|||
from .zero_d import Vertex
|
||||
|
||||
|
||||
class Compound(Mixin3D[TopoDS_Compound]):
|
||||
class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||
"""A Compound in build123d is a topological entity representing a collection of
|
||||
geometric shapes grouped together within a single structure. It serves as a
|
||||
container for organizing diverse shapes like edges, faces, or solids. This
|
||||
|
|
@ -141,6 +142,7 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
|
||||
order = 4.0
|
||||
|
||||
project_to_viewport = Mixin1D.project_to_viewport
|
||||
# ---- Constructor ----
|
||||
|
||||
def __init__(
|
||||
|
|
@ -164,7 +166,7 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
parent (Compound, optional): assembly parent. Defaults to None.
|
||||
children (Sequence[Shape], optional): assembly children. Defaults to None.
|
||||
"""
|
||||
topods_compound: TopoDS_Compound | None
|
||||
|
||||
if isinstance(obj, Iterable):
|
||||
topods_compound = _make_topods_compound_from_shapes(
|
||||
[s.wrapped for s in obj]
|
||||
|
|
@ -376,14 +378,8 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
)
|
||||
|
||||
text_flat = Compound(
|
||||
TopoDS.Compound_s(
|
||||
builder.Perform(
|
||||
font_i,
|
||||
NCollection_Utf8String(txt),
|
||||
gp_Ax3(),
|
||||
horiz_align,
|
||||
vert_align,
|
||||
)
|
||||
builder.Perform(
|
||||
font_i, NCollection_Utf8String(txt), gp_Ax3(), horiz_align, vert_align
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -459,7 +455,7 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
will be a Wire, otherwise a Shape.
|
||||
"""
|
||||
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
|
||||
if isinstance(sum1d, ShapeList):
|
||||
result1d: Curve | Wire = Curve(sum1d)
|
||||
|
|
@ -510,8 +506,6 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
def __and__(self, other: Shape | Iterable[Shape]) -> Compound:
|
||||
"""Intersect other to self `&` operator"""
|
||||
intersection = Shape.__and__(self, other)
|
||||
if intersection is None:
|
||||
return Compound()
|
||||
intersection = Compound(
|
||||
intersection if isinstance(intersection, list) else [intersection]
|
||||
)
|
||||
|
|
@ -523,7 +517,7 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
Check if empty.
|
||||
"""
|
||||
|
||||
return self._wrapped is not None and TopoDS_Iterator(self.wrapped).More()
|
||||
return TopoDS_Iterator(self.wrapped).More()
|
||||
|
||||
def __iter__(self) -> Iterator[Shape]:
|
||||
"""
|
||||
|
|
@ -540,7 +534,7 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
def __len__(self) -> int:
|
||||
"""Return the number of subshapes"""
|
||||
count = 0
|
||||
if self._wrapped is not None:
|
||||
if self.wrapped is not None:
|
||||
for _ in self:
|
||||
count += 1
|
||||
return count
|
||||
|
|
@ -599,7 +593,7 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
"""Return the Compound"""
|
||||
shape_list = self.compounds()
|
||||
entity_count = len(shape_list)
|
||||
if entity_count > 1:
|
||||
if entity_count != 1:
|
||||
warnings.warn(
|
||||
f"Found {entity_count} compounds, returning first",
|
||||
stacklevel=2,
|
||||
|
|
@ -608,7 +602,7 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
|
||||
def compounds(self) -> ShapeList[Compound]:
|
||||
"""compounds - all the compounds in this Shape"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return ShapeList()
|
||||
if isinstance(self.wrapped, TopoDS_Compound):
|
||||
# pylint: disable=not-an-iterable
|
||||
|
|
@ -657,7 +651,11 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
children[child_index_pair[1]]
|
||||
)
|
||||
if obj_intersection is not None:
|
||||
common_volume = sum(s.volume for s in obj_intersection.solids())
|
||||
common_volume = (
|
||||
0.0
|
||||
if isinstance(obj_intersection, list)
|
||||
else obj_intersection.volume
|
||||
)
|
||||
if common_volume > tolerance:
|
||||
return (
|
||||
True,
|
||||
|
|
@ -708,182 +706,11 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
while iterator.More():
|
||||
child = iterator.Value()
|
||||
if child.ShapeType() == type_map[obj_type]:
|
||||
results.append(obj_type(downcast(child))) # type: ignore
|
||||
results.append(obj_type(downcast(child)))
|
||||
iterator.Next()
|
||||
|
||||
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:
|
||||
"""Strip unnecessary Compound wrappers
|
||||
|
||||
|
|
@ -937,8 +764,8 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
parent.wrapped = _make_topods_compound_from_shapes(
|
||||
[c.wrapped for c in parent.children]
|
||||
)
|
||||
# else:
|
||||
# parent.wrapped = None
|
||||
else:
|
||||
parent.wrapped = None
|
||||
|
||||
def _post_detach_children(self, children):
|
||||
"""Method call before detaching `children`."""
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ def _as_gcc_arg(obj: Edge | Vector, constaint: Tangency) -> tuple[
|
|||
- Edge -> (QualifiedCurve, h2d, first, last, True)
|
||||
- Vector -> (CartesianPoint, None, None, None, False)
|
||||
"""
|
||||
if not obj:
|
||||
if obj.wrapped is None:
|
||||
raise TypeError("Can't create a qualified curve from empty edge")
|
||||
|
||||
if isinstance(obj.wrapped, TopoDS_Edge):
|
||||
|
|
|
|||
|
|
@ -100,8 +100,7 @@ from OCP.BRepFeat import BRepFeat_SplitShape
|
|||
from OCP.BRepGProp import BRepGProp, BRepGProp_Face
|
||||
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
|
||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace
|
||||
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
|
||||
from OCP.BRepTools import BRepTools
|
||||
from OCP.gce import gce_MakeLin
|
||||
from OCP.Geom import Geom_Line
|
||||
from OCP.GeomAPI import GeomAPI_ProjectPointOnSurf
|
||||
|
|
@ -163,7 +162,6 @@ if TYPE_CHECKING: # pragma: no cover
|
|||
Shapes = Literal["Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "Compound"]
|
||||
TrimmingTool = Union[Plane, "Shell", "Face"]
|
||||
TOPODS = TypeVar("TOPODS", bound=TopoDS_Shape)
|
||||
CalcFn = Callable[[TopoDS_Shape, GProp_GProps], None]
|
||||
|
||||
|
||||
class Shape(NodeMixin, Generic[TOPODS]):
|
||||
|
|
@ -198,7 +196,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
ta.TopAbs_COMPSOLID: "CompSolid",
|
||||
}
|
||||
|
||||
shape_properties_LUT: dict[TopAbs_ShapeEnum, CalcFn | None] = {
|
||||
shape_properties_LUT = {
|
||||
ta.TopAbs_VERTEX: None,
|
||||
ta.TopAbs_EDGE: BRepGProp.LinearProperties_s,
|
||||
ta.TopAbs_WIRE: BRepGProp.LinearProperties_s,
|
||||
|
|
@ -289,7 +287,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
color: ColorLike | 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
|
||||
)
|
||||
self.for_construction = False
|
||||
|
|
@ -306,18 +304,6 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
# 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
|
||||
@abstractmethod
|
||||
def _dim(self) -> int | None:
|
||||
|
|
@ -326,7 +312,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
@property
|
||||
def area(self) -> float:
|
||||
"""area -the surface area of all faces in this Shape"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return 0.0
|
||||
properties = GProp_GProps()
|
||||
BRepGProp.SurfaceProperties_s(self.wrapped, properties)
|
||||
|
|
@ -365,7 +351,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
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")
|
||||
|
||||
shape: TopAbs_ShapeEnum = shapetype(self.wrapped)
|
||||
|
|
@ -394,7 +380,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
bool: is the shape manifold or water tight
|
||||
"""
|
||||
# Extract one or more (if a Compound) shape from self
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return False
|
||||
shape_stack = get_top_level_topods_shapes(self.wrapped)
|
||||
|
||||
|
|
@ -445,12 +431,12 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
underlying shape with the potential to be given a location and an
|
||||
orientation.
|
||||
"""
|
||||
return self._wrapped is None or self.wrapped.IsNull()
|
||||
return self.wrapped is None or self.wrapped.IsNull()
|
||||
|
||||
@property
|
||||
def is_planar_face(self) -> bool:
|
||||
"""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
|
||||
surface = BRep_Tool.Surface_s(self.wrapped)
|
||||
is_face_planar = GeomLib_IsPlanarSurface(surface, TOLERANCE)
|
||||
|
|
@ -462,7 +448,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full
|
||||
description of what is checked.
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return True
|
||||
chk = BRepCheck_Analyzer(self.wrapped)
|
||||
chk.SetParallel(True)
|
||||
|
|
@ -488,7 +474,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
@property
|
||||
def location(self) -> 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")
|
||||
return Location(self.wrapped.Location())
|
||||
|
||||
|
|
@ -532,7 +518,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
- It is commonly used in structural analysis, mechanical simulations,
|
||||
and physics-based motion calculations.
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Can't calculate matrix for empty shape")
|
||||
properties = GProp_GProps()
|
||||
BRepGProp.VolumeProperties_s(self.wrapped, properties)
|
||||
|
|
@ -560,7 +546,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
@property
|
||||
def position(self) -> Vector:
|
||||
"""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")
|
||||
return self.location.position
|
||||
|
||||
|
|
@ -589,7 +575,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
(Vector(0, 1, 0), 1000.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")
|
||||
|
||||
properties = GProp_GProps()
|
||||
|
|
@ -629,7 +615,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
(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")
|
||||
|
||||
properties = GProp_GProps()
|
||||
|
|
@ -727,15 +713,17 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
address = node.address
|
||||
name = ""
|
||||
loc = (
|
||||
f"Center{node.position:.6g}"
|
||||
"Center" + str(tuple(node.position))
|
||||
if show_center
|
||||
else f"Position{node.position:.6g}"
|
||||
else "Position" + str(tuple(node.position))
|
||||
)
|
||||
else:
|
||||
address = id(node)
|
||||
name = node.__class__.__name__.ljust(9)
|
||||
loc = (
|
||||
f"Center{node.center():.6g}" if show_center else repr(node.location)
|
||||
"Center" + str(tuple(node.center()))
|
||||
if show_center
|
||||
else "Location" + repr(node.location)
|
||||
)
|
||||
result += f"{treestr}{name}at {address:#x}, {loc}\n"
|
||||
return result
|
||||
|
|
@ -797,13 +785,13 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if not obj:
|
||||
if obj.wrapped is None:
|
||||
return 0.0
|
||||
|
||||
properties = GProp_GProps()
|
||||
calc_function = Shape.shape_properties_LUT[shapetype(obj.wrapped)]
|
||||
|
||||
if calc_function is None:
|
||||
if not calc_function:
|
||||
raise NotImplementedError
|
||||
|
||||
calc_function(obj.wrapped, properties)
|
||||
|
|
@ -817,7 +805,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
],
|
||||
) -> ShapeList:
|
||||
"""Helper to extract entities of a specific type from a shape."""
|
||||
if not shape:
|
||||
if shape.wrapped is None:
|
||||
return ShapeList()
|
||||
shape_list = ShapeList(
|
||||
[shape.__class__.cast(i) for i in shape.entities(entity_type)]
|
||||
|
|
@ -837,9 +825,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
with a warning if count != 1."""
|
||||
shape_list = Shape.get_shape_list(shape, entity_type)
|
||||
entity_count = len(shape_list)
|
||||
if entity_count == 0:
|
||||
return None
|
||||
elif entity_count > 1:
|
||||
if entity_count != 1:
|
||||
warnings.warn(
|
||||
f"Found {entity_count} {entity_type.lower()}s, returning first",
|
||||
stacklevel=3,
|
||||
|
|
@ -873,7 +859,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
if not all(summand._dim == addend_dim for summand in summands):
|
||||
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:
|
||||
sum_shape = summands[0]
|
||||
else:
|
||||
|
|
@ -890,7 +876,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
"""intersect shape with self operator &"""
|
||||
others = other if isinstance(other, (list, tuple)) else [other]
|
||||
|
||||
if not self or (isinstance(other, Shape) and not other):
|
||||
if self.wrapped is None or (isinstance(other, Shape) and other.wrapped is None):
|
||||
raise ValueError("Cannot intersect shape with empty compound")
|
||||
new_shape = self.intersect(*others)
|
||||
|
||||
|
|
@ -962,7 +948,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
def __hash__(self) -> int:
|
||||
"""Return hash code"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return 0
|
||||
return hash(self.wrapped)
|
||||
|
||||
|
|
@ -980,7 +966,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
def __sub__(self, other: None | Shape | Iterable[Shape]) -> Self | ShapeList[Self]:
|
||||
"""cut shape from self operator -"""
|
||||
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Cannot subtract shape from empty compound")
|
||||
|
||||
# Convert `other` to list of base objects and filter out None values
|
||||
|
|
@ -1028,7 +1014,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
BoundBox: A box sized to contain this Shape
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return BoundBox(Bnd_Box())
|
||||
tolerance = TOLERANCE if tolerance is None else tolerance
|
||||
return BoundBox.from_topo_ds(self.wrapped, tolerance=tolerance, optimal=optimal)
|
||||
|
|
@ -1047,7 +1033,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Shape: Original object with extraneous internal edges removed
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return self
|
||||
upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True)
|
||||
upgrader.AllowInternalEdges(False)
|
||||
|
|
@ -1126,7 +1112,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self._wrapped is None or not other:
|
||||
if self.wrapped is None or other.wrapped is None:
|
||||
raise ValueError("Cannot calculate distance to or from an empty shape")
|
||||
|
||||
return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value()
|
||||
|
|
@ -1139,7 +1125,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
self, other: Shape | VectorLike
|
||||
) -> tuple[float, Vector, Vector]:
|
||||
"""Minimal distance between two shapes and the points on each shape"""
|
||||
if self._wrapped is None or (isinstance(other, Shape) and not other):
|
||||
if self.wrapped is None or (isinstance(other, Shape) and other.wrapped is None):
|
||||
raise ValueError("Cannot calculate distance to or from an empty shape")
|
||||
|
||||
if isinstance(other, Shape):
|
||||
|
|
@ -1169,14 +1155,14 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Cannot calculate distance to or from an empty shape")
|
||||
|
||||
dist_calc = BRepExtrema_DistShapeShape()
|
||||
dist_calc.LoadS1(self.wrapped)
|
||||
|
||||
for other_shape in others:
|
||||
if not other_shape:
|
||||
if other_shape.wrapped is None:
|
||||
raise ValueError("Cannot calculate distance to or from an empty shape")
|
||||
dist_calc.LoadS2(other_shape.wrapped)
|
||||
dist_calc.Perform()
|
||||
|
|
@ -1185,28 +1171,27 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
def edge(self) -> Edge | None:
|
||||
"""Return the Edge"""
|
||||
return Shape.get_single_shape(self, "Edge")
|
||||
return None
|
||||
|
||||
# Note all sub-classes have vertices and vertex methods
|
||||
|
||||
def edges(self) -> ShapeList[Edge]:
|
||||
"""edges - all the edges in this Shape - subclasses may override"""
|
||||
edge_list = Shape.get_shape_list(self, "Edge")
|
||||
return edge_list.filter_by(
|
||||
lambda e: BRep_Tool.Degenerated_s(e.wrapped), reverse=True
|
||||
)
|
||||
return ShapeList()
|
||||
|
||||
def entities(self, topo_type: Shapes) -> list[TopoDS_Shape]:
|
||||
"""Return all of the TopoDS sub entities of the given type"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return []
|
||||
return _topods_entities(self.wrapped, topo_type)
|
||||
|
||||
def face(self) -> Face | None:
|
||||
"""Return the Face"""
|
||||
return Shape.get_single_shape(self, "Face")
|
||||
return None
|
||||
|
||||
def faces(self) -> ShapeList[Face]:
|
||||
"""faces - all the faces in this Shape"""
|
||||
return Shape.get_shape_list(self, "Face")
|
||||
return ShapeList()
|
||||
|
||||
def faces_intersected_by_axis(
|
||||
self,
|
||||
|
|
@ -1224,7 +1209,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
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()
|
||||
|
||||
line = gce_MakeLin(axis.wrapped).Value()
|
||||
|
|
@ -1254,7 +1239,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
def fix(self) -> Self:
|
||||
"""fix - try to fix shape if not valid"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return self
|
||||
if not self.is_valid:
|
||||
shape_copy: Shape = copy.deepcopy(self, None)
|
||||
|
|
@ -1296,7 +1281,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
# self, child_type: Shapes, parent_type: Shapes
|
||||
# ) -> Dict[Shape, list[Shape]]:
|
||||
# """This function is very slow on M1 macs and is currently unused"""
|
||||
# if self._wrapped is None:
|
||||
# if self.wrapped is None:
|
||||
# return {}
|
||||
|
||||
# res = TopTools_IndexedDataMapOfShapeListOfShape()
|
||||
|
|
@ -1334,7 +1319,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
(e.g., edges, vertices) and other compounds, the method returns a list
|
||||
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(
|
||||
self.__class__.cast(s) for s in get_top_level_topods_shapes(self.wrapped)
|
||||
|
|
@ -1342,7 +1327,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
def intersect(
|
||||
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||
) -> None | ShapeList[Self]:
|
||||
) -> None | Self | ShapeList[Self]:
|
||||
"""Intersection of the arguments and this shape
|
||||
|
||||
Args:
|
||||
|
|
@ -1350,8 +1335,8 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
intersect with
|
||||
|
||||
Returns:
|
||||
None | ShapeList[Self]: Resulting ShapeList may contain different class
|
||||
than self
|
||||
Self | ShapeList[Self]: Resulting object may be of a different class than self
|
||||
or a ShapeList if multiple non-Compound object created
|
||||
"""
|
||||
|
||||
def _to_vertex(vec: Vector) -> Vertex:
|
||||
|
|
@ -1395,12 +1380,15 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
# Find the shape intersections
|
||||
intersect_op = BRepAlgoAPI_Common()
|
||||
intersections = self._bool_op((self,), objs, intersect_op)
|
||||
if isinstance(intersections, ShapeList):
|
||||
return intersections or None
|
||||
if isinstance(intersections, Shape) and not intersections.is_null:
|
||||
return ShapeList([intersections])
|
||||
return None
|
||||
shape_intersections = self._bool_op((self,), objs, intersect_op)
|
||||
if isinstance(shape_intersections, ShapeList) and not shape_intersections:
|
||||
return None
|
||||
if (
|
||||
not isinstance(shape_intersections, ShapeList)
|
||||
and shape_intersections.is_null
|
||||
):
|
||||
return None
|
||||
return shape_intersections
|
||||
|
||||
def is_equal(self, other: Shape) -> bool:
|
||||
"""Returns True if two shapes are equal, i.e. if they share the same
|
||||
|
|
@ -1413,7 +1401,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self._wrapped is None or not other:
|
||||
if self.wrapped is None or other.wrapped is None:
|
||||
return False
|
||||
return self.wrapped.IsEqual(other.wrapped)
|
||||
|
||||
|
|
@ -1428,7 +1416,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self._wrapped is None or not other:
|
||||
if self.wrapped is None or other.wrapped is None:
|
||||
return False
|
||||
return self.wrapped.IsSame(other.wrapped)
|
||||
|
||||
|
|
@ -1441,7 +1429,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Cannot locate an empty shape")
|
||||
if loc.wrapped is None:
|
||||
raise ValueError("Cannot locate a shape at an empty location")
|
||||
|
|
@ -1460,7 +1448,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Shape: copy of Shape at location
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Cannot locate an empty shape")
|
||||
if loc.wrapped is None:
|
||||
raise ValueError("Cannot locate a shape at an empty location")
|
||||
|
|
@ -1478,7 +1466,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Cannot mesh an empty shape")
|
||||
|
||||
if not BRepTools.Triangulation_s(self.wrapped, tolerance):
|
||||
|
|
@ -1499,7 +1487,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
if not mirror_plane:
|
||||
mirror_plane = Plane.XY
|
||||
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return self
|
||||
transformation = gp_Trsf()
|
||||
transformation.SetMirror(
|
||||
|
|
@ -1517,7 +1505,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Cannot move an empty shape")
|
||||
if loc.wrapped is None:
|
||||
raise ValueError("Cannot move a shape at an empty location")
|
||||
|
|
@ -1537,7 +1525,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
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")
|
||||
if loc.wrapped is None:
|
||||
raise ValueError("Cannot move a shape at an empty location")
|
||||
|
|
@ -1551,7 +1539,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
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(self)
|
||||
|
||||
|
|
@ -1653,7 +1641,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
- The radius of gyration is computed based on the shape’s mass properties.
|
||||
- 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")
|
||||
|
||||
properties = GProp_GProps()
|
||||
|
|
@ -1672,7 +1660,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Cannot relocate an empty shape")
|
||||
if loc.wrapped is None:
|
||||
raise ValueError("Cannot relocate a shape at an empty location")
|
||||
|
|
@ -1725,11 +1713,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
def shell(self) -> Shell | None:
|
||||
"""Return the Shell"""
|
||||
return Shape.get_single_shape(self, "Shell")
|
||||
return None
|
||||
|
||||
def shells(self) -> ShapeList[Shell]:
|
||||
"""shells - all the shells in this Shape"""
|
||||
return Shape.get_shape_list(self, "Shell")
|
||||
return ShapeList()
|
||||
|
||||
def show_topology(
|
||||
self,
|
||||
|
|
@ -1783,157 +1771,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
def solid(self) -> Solid | None:
|
||||
"""Return the Solid"""
|
||||
return Shape.get_single_shape(self, "Solid")
|
||||
return None
|
||||
|
||||
def solids(self) -> ShapeList[Solid]:
|
||||
"""solids - all the solids in this Shape"""
|
||||
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
|
||||
return ShapeList()
|
||||
|
||||
@overload
|
||||
def split_by_perimeter(
|
||||
|
|
@ -2013,7 +1855,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
"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")
|
||||
|
||||
# Process the perimeter
|
||||
|
|
@ -2021,7 +1863,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
raise ValueError("perimeter must be a closed Wire or Edge")
|
||||
perimeter_edges = TopTools_SequenceOfShape()
|
||||
for perimeter_edge in perimeter.edges():
|
||||
if not perimeter_edge:
|
||||
if perimeter_edge.wrapped is None:
|
||||
continue
|
||||
perimeter_edges.Append(perimeter_edge.wrapped)
|
||||
|
||||
|
|
@ -2029,7 +1871,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
lefts: list[Shell] = []
|
||||
rights: list[Shell] = []
|
||||
for target_shell in self.shells():
|
||||
if not target_shell:
|
||||
if target_shell.wrapped is None:
|
||||
continue
|
||||
constructor = BRepFeat_SplitShape(target_shell.wrapped)
|
||||
constructor.Add(perimeter_edges)
|
||||
|
|
@ -2058,7 +1900,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
self, tolerance: float, angular_tolerance: float = 0.1
|
||||
) -> tuple[list[Vector], list[tuple[int, int, int]]]:
|
||||
"""General triangulated approximation"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Cannot tessellate an empty shape")
|
||||
|
||||
self.mesh(tolerance, angular_tolerance)
|
||||
|
|
@ -2120,7 +1962,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Self: Approximated shape
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Cannot approximate an empty shape")
|
||||
|
||||
params = ShapeCustom_RestrictionParameters()
|
||||
|
|
@ -2157,7 +1999,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Shape: a copy of the object, but with geometry transformed
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return self
|
||||
new_shape = copy.deepcopy(self, None)
|
||||
transformed = downcast(
|
||||
|
|
@ -2180,7 +2022,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Shape: copy of transformed shape with all objects keeping their type
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return self
|
||||
new_shape = copy.deepcopy(self, None)
|
||||
transformed = downcast(
|
||||
|
|
@ -2236,11 +2078,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
def wire(self) -> Wire | None:
|
||||
"""Return the Wire"""
|
||||
return Shape.get_single_shape(self, "Wire")
|
||||
return None
|
||||
|
||||
def wires(self) -> ShapeList[Wire]:
|
||||
"""wires - all the wires in this Shape"""
|
||||
return Shape.get_shape_list(self, "Wire")
|
||||
return ShapeList()
|
||||
|
||||
def _apply_transform(self, transformation: gp_Trsf) -> Self:
|
||||
"""Private Apply Transform
|
||||
|
|
@ -2253,7 +2095,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Shape: copy of transformed Shape
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return self
|
||||
shape_copy: Shape = copy.deepcopy(self, None)
|
||||
transformed_shape = BRepBuilderAPI_Transform(
|
||||
|
|
@ -2284,11 +2126,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
args = list(args)
|
||||
tools = list(tools)
|
||||
# Find the highest order class from all the inputs Solid > Vertex
|
||||
order_dict = {
|
||||
type(s): type(s).order
|
||||
for s in [self] + args + tools
|
||||
if hasattr(type(s), "order")
|
||||
}
|
||||
order_dict = {type(s): type(s).order for s in [self] + args + tools}
|
||||
highest_order = sorted(order_dict.items(), key=lambda item: item[1])[-1]
|
||||
|
||||
# The base of the operation
|
||||
|
|
@ -2362,7 +2200,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
tuple[ShapeList[Vertex], ShapeList[Edge]]: section results
|
||||
"""
|
||||
if self._wrapped is None or not other:
|
||||
if self.wrapped is None or other.wrapped is None:
|
||||
return (ShapeList(), ShapeList())
|
||||
|
||||
section = BRepAlgoAPI_Section(self.wrapped, other.wrapped)
|
||||
|
|
@ -2396,14 +2234,6 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
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):
|
||||
"""Abstract base class that requires comparison methods"""
|
||||
|
|
@ -2871,16 +2701,15 @@ class ShapeList(list[T]):
|
|||
tol_digits,
|
||||
)
|
||||
|
||||
elif not group_by:
|
||||
raise ValueError("Cannot group by an empty object")
|
||||
elif hasattr(group_by, "wrapped"):
|
||||
if group_by.wrapped is None:
|
||||
raise ValueError("Cannot group by an empty object")
|
||||
|
||||
elif hasattr(group_by, "wrapped") and isinstance(
|
||||
group_by.wrapped, (TopoDS_Edge, TopoDS_Wire)
|
||||
):
|
||||
if isinstance(group_by.wrapped, (TopoDS_Edge, TopoDS_Wire)):
|
||||
|
||||
def key_f(obj):
|
||||
pnt1, _pnt2 = group_by.closest_points(obj.center())
|
||||
return round(group_by.param_at_point(pnt1), tol_digits)
|
||||
def key_f(obj):
|
||||
pnt1, _pnt2 = group_by.closest_points(obj.center())
|
||||
return round(group_by.param_at_point(pnt1), tol_digits)
|
||||
|
||||
elif isinstance(group_by, SortBy):
|
||||
if group_by == SortBy.LENGTH:
|
||||
|
|
@ -2986,22 +2815,22 @@ class ShapeList(list[T]):
|
|||
).position.Z,
|
||||
reverse=reverse,
|
||||
)
|
||||
elif not sort_by:
|
||||
raise ValueError("Cannot sort by an empty object")
|
||||
elif hasattr(sort_by, "wrapped") and isinstance(
|
||||
sort_by.wrapped, (TopoDS_Edge, TopoDS_Wire)
|
||||
):
|
||||
elif hasattr(sort_by, "wrapped"):
|
||||
if sort_by.wrapped is None:
|
||||
raise ValueError("Cannot sort by an empty object")
|
||||
|
||||
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)
|
||||
if isinstance(sort_by.wrapped, (TopoDS_Edge, TopoDS_Wire)):
|
||||
|
||||
# pylint: disable=unnecessary-lambda
|
||||
objects = sorted(
|
||||
self, key=lambda o: u_of_closest_center(o), reverse=reverse
|
||||
)
|
||||
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)
|
||||
|
||||
# pylint: disable=unnecessary-lambda
|
||||
objects = sorted(
|
||||
self, key=lambda o: u_of_closest_center(o), reverse=reverse
|
||||
)
|
||||
|
||||
elif isinstance(sort_by, SortBy):
|
||||
if sort_by == SortBy.LENGTH:
|
||||
|
|
@ -3168,45 +2997,6 @@ def _sew_topods_faces(faces: Iterable[TopoDS_Face]) -> TopoDS_Shape:
|
|||
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]:
|
||||
"""Return the TopoDS_Shapes of topo_type from this TopoDS_Shape"""
|
||||
out = {} # using dict to prevent duplicates
|
||||
|
|
|
|||
|
|
@ -54,13 +54,15 @@ license:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Sequence
|
||||
import platform
|
||||
import warnings
|
||||
from math import radians, cos, tan
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
from typing_extensions import Self
|
||||
from typing import Union, TYPE_CHECKING
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
import OCP.TopAbs as ta
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Cut, BRepAlgoAPI_Section
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeSolid
|
||||
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
|
||||
from OCP.BRepFeat import BRepFeat_MakeDPrism
|
||||
|
|
@ -84,23 +86,15 @@ from OCP.GProp import GProp_GProps
|
|||
from OCP.GeomAbs import GeomAbs_Intersection, GeomAbs_JoinType
|
||||
from OCP.LocOpe import LocOpe_DPrism
|
||||
from OCP.ShapeFix import ShapeFix_Solid
|
||||
from OCP.Standard import Standard_Failure, Standard_TypeMismatch
|
||||
from OCP.Standard import Standard_Failure
|
||||
from OCP.StdFail import StdFail_NotDone
|
||||
from OCP.TopExp import TopExp, TopExp_Explorer
|
||||
from OCP.TopExp import TopExp
|
||||
from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape
|
||||
from OCP.TopoDS import (
|
||||
TopoDS,
|
||||
TopoDS_Face,
|
||||
TopoDS_Shape,
|
||||
TopoDS_Shell,
|
||||
TopoDS_Solid,
|
||||
TopoDS_Wire,
|
||||
)
|
||||
from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape, TopoDS_Solid, TopoDS_Wire
|
||||
from OCP.gp import gp_Ax2, gp_Pnt
|
||||
from build123d.build_enums import CenterOf, GeomType, Keep, Kind, Transition, Until
|
||||
from build123d.build_enums import CenterOf, GeomType, Kind, Transition, Until
|
||||
from build123d.geometry import (
|
||||
DEG2RAD,
|
||||
TOLERANCE,
|
||||
Axis,
|
||||
BoundBox,
|
||||
Color,
|
||||
|
|
@ -110,20 +104,10 @@ from build123d.geometry import (
|
|||
Vector,
|
||||
VectorLike,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
|
||||
from .one_d import Edge, Wire, Mixin1D
|
||||
from .shape_core import (
|
||||
TOPODS,
|
||||
Shape,
|
||||
ShapeList,
|
||||
Joint,
|
||||
downcast,
|
||||
shapetype,
|
||||
_sew_topods_faces,
|
||||
get_top_level_topods_shapes,
|
||||
unwrap_topods_compound,
|
||||
)
|
||||
|
||||
from .shape_core import Shape, ShapeList, Joint, downcast, shapetype
|
||||
from .two_d import sort_wires_by_build_order, Mixin2D, Face, Shell
|
||||
from .utils import (
|
||||
_extrude_topods_shape,
|
||||
|
|
@ -135,14 +119,26 @@ from .zero_d import Vertex
|
|||
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from .composite import Compound # pylint: disable=R0801
|
||||
from .composite import Compound, Curve, Sketch, Part # pylint: disable=R0801
|
||||
|
||||
|
||||
class Mixin3D(Shape[TOPODS]):
|
||||
class Mixin3D(Shape):
|
||||
"""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
|
||||
|
||||
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 ----
|
||||
|
||||
@property
|
||||
|
|
@ -199,7 +195,6 @@ class Mixin3D(Shape[TOPODS]):
|
|||
if center_of == CenterOf.MASS:
|
||||
properties = GProp_GProps()
|
||||
calc_function = Shape.shape_properties_LUT[shapetype(self.wrapped)]
|
||||
assert calc_function is not None
|
||||
calc_function(self.wrapped, properties)
|
||||
middle = Vector(properties.CentreOfMass())
|
||||
elif center_of == CenterOf.BOUNDING_BOX:
|
||||
|
|
@ -425,132 +420,6 @@ class Mixin3D(Shape[TOPODS]):
|
|||
|
||||
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:
|
||||
"""Returns whether or not the point is inside a solid or compound
|
||||
object within the specified tolerance.
|
||||
|
|
@ -617,10 +486,8 @@ class Mixin3D(Shape[TOPODS]):
|
|||
try:
|
||||
new_shape = self.__class__(fillet_builder.Shape())
|
||||
if not new_shape.is_valid:
|
||||
# raise fillet_exception
|
||||
raise Standard_Failure
|
||||
# except fillet_exception:
|
||||
except (Standard_Failure, StdFail_NotDone):
|
||||
raise fillet_exception
|
||||
except fillet_exception:
|
||||
return __max_fillet(window_min, window_mid, current_iteration + 1)
|
||||
|
||||
# These numbers work, are they close enough? - if not try larger window
|
||||
|
|
@ -639,10 +506,10 @@ class Mixin3D(Shape[TOPODS]):
|
|||
|
||||
# Unfortunately, MacOS doesn't support the StdFail_NotDone exception so platform
|
||||
# specific exceptions are required.
|
||||
# if platform.system() == "Darwin":
|
||||
# fillet_exception = Standard_Failure
|
||||
# else:
|
||||
# fillet_exception = StdFail_NotDone
|
||||
if platform.system() == "Darwin":
|
||||
fillet_exception = Standard_Failure
|
||||
else:
|
||||
fillet_exception = StdFail_NotDone
|
||||
|
||||
max_radius = __max_fillet(0.0, 2 * self.bounding_box().diagonal, 0)
|
||||
|
||||
|
|
@ -714,35 +581,16 @@ class Mixin3D(Shape[TOPODS]):
|
|||
|
||||
return offset_solid
|
||||
|
||||
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
|
||||
def solid(self) -> Solid | None:
|
||||
"""Return the Solid"""
|
||||
return Shape.get_single_shape(self, "Solid")
|
||||
|
||||
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 solids(self) -> ShapeList[Solid]:
|
||||
"""solids - all the solids in this Shape"""
|
||||
return Shape.get_shape_list(self, "Solid")
|
||||
|
||||
|
||||
class Solid(Mixin3D[TopoDS_Solid]):
|
||||
class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||
"""A Solid in build123d represents a three-dimensional solid geometry
|
||||
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
|
||||
|
|
@ -920,17 +768,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
inner_comp = _make_topods_compound_from_shapes(inner_solids)
|
||||
|
||||
# subtract from the outer solid
|
||||
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)
|
||||
return Solid(BRepAlgoAPI_Cut(outer_solid, inner_comp).Shape())
|
||||
|
||||
@classmethod
|
||||
def extrude_taper(
|
||||
|
|
@ -971,7 +809,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
direction.length / cos(radians(taper)),
|
||||
radians(taper),
|
||||
)
|
||||
new_solid = Solid(TopoDS.Solid_s(prism_builder.Shape()))
|
||||
new_solid = Solid(prism_builder.Shape())
|
||||
else:
|
||||
# Determine the offset to get the taper
|
||||
offset_amt = -direction.length * tan(radians(taper))
|
||||
|
|
@ -1010,116 +848,110 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
@classmethod
|
||||
def extrude_until(
|
||||
cls,
|
||||
profile: Face,
|
||||
target: Compound | Solid,
|
||||
section: Face,
|
||||
target_object: Compound | Solid,
|
||||
direction: VectorLike,
|
||||
until: Until = Until.NEXT,
|
||||
) -> Solid:
|
||||
) -> Compound | Solid:
|
||||
"""extrude_until
|
||||
|
||||
Extrude `profile` in the provided `direction` until it encounters a
|
||||
bounding surface on the `target`. The termination surface is chosen
|
||||
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.
|
||||
Extrude section in provided direction until it encounters either the
|
||||
NEXT or LAST surface of target_object. Note that the bounding surface
|
||||
must be larger than the extruded face where they contact.
|
||||
|
||||
Args:
|
||||
profile (Face): The face to extrude.
|
||||
target (Union[Compound, Solid]): The object that limits the extrusion.
|
||||
direction (VectorLike): Extrusion direction.
|
||||
until (Until, optional): Surface selection mode controlling which
|
||||
intersection to stop at. Defaults to ``Until.NEXT``.
|
||||
section (Face): Face to extrude
|
||||
target_object (Union[Compound, Solid]): object to limit extrusion
|
||||
direction (VectorLike): extrusion direction
|
||||
until (Until, optional): surface to limit extrusion. Defaults to Until.NEXT.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provided profile does not intersect the target.
|
||||
ValueError: provided face does not intersect target_object
|
||||
|
||||
Returns:
|
||||
Solid: The extruded and limited solid.
|
||||
Union[Compound, Solid]: extruded Face
|
||||
"""
|
||||
direction = Vector(direction)
|
||||
if until in [Until.PREVIOUS, Until.FIRST]:
|
||||
direction *= -1
|
||||
until = Until.NEXT if until == Until.PREVIOUS else Until.LAST
|
||||
|
||||
# 1: Create extrusion of length the maximum distance between profile and target
|
||||
max_dimension = find_max_dimension([profile, target])
|
||||
extrusion = Solid.extrude(profile, direction * max_dimension)
|
||||
max_dimension = find_max_dimension([section, target_object])
|
||||
clipping_direction = (
|
||||
direction * max_dimension
|
||||
if until == Until.NEXT
|
||||
else -direction * max_dimension
|
||||
)
|
||||
direction_axis = Axis(section.center(), clipping_direction)
|
||||
# Create a linear extrusion to start
|
||||
extrusion = Solid.extrude(section, 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))
|
||||
# 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:
|
||||
raise RuntimeError(f"Invalid sewn shape {type(top_level_shape)}")
|
||||
faces += face.faces()
|
||||
|
||||
modified_target_surfaces = modified_target_surfaces.sort_by(
|
||||
lambda s: s.distance_to(profile)
|
||||
)
|
||||
limit = modified_target_surfaces[
|
||||
0 if until in [Until.NEXT, Until.PREVIOUS] else -1
|
||||
clip_faces = [
|
||||
f
|
||||
for f in faces
|
||||
if not (f.is_planar and f.normal_at().dot(direction) == 0.0)
|
||||
]
|
||||
keep: Literal[Keep.TOP, Keep.BOTTOM] = (
|
||||
Keep.TOP if until in [Until.NEXT, Until.PREVIOUS] else Keep.BOTTOM
|
||||
)
|
||||
if not clip_faces:
|
||||
raise ValueError("provided face does not intersect target_object")
|
||||
|
||||
# 4: Split the extrusion by the appropriate shell
|
||||
clipped_extrusion = extrusion.split(limit, keep=keep)
|
||||
# Create the objects that will clip the linear extrusion
|
||||
clipping_objects = [
|
||||
Solid.extrude(f, clipping_direction).fix() for f in clip_faces
|
||||
]
|
||||
clipping_objects = [o for o in clipping_objects if o.volume > 1e-9]
|
||||
|
||||
# 5: Return the appropriate type
|
||||
if clipped_extrusion is None:
|
||||
raise RuntimeError("Extrusion is None") # None isn't an option here
|
||||
elif isinstance(clipped_extrusion, Solid):
|
||||
return clipped_extrusion
|
||||
if until == Until.NEXT:
|
||||
trimmed_extrusion = extrusion.cut(target_object)
|
||||
if isinstance(trimmed_extrusion, ShapeList):
|
||||
closest_extrusion = trimmed_extrusion.sort_by(direction_axis)[0]
|
||||
else:
|
||||
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:
|
||||
# isinstance(clipped_extrusion, list):
|
||||
return ShapeList(clipped_extrusion).sort_by(
|
||||
Axis(profile.center(), direction)
|
||||
)[0]
|
||||
base_part = extrusion.intersect(target_object)
|
||||
if isinstance(base_part, ShapeList):
|
||||
extrusion_parts = base_part
|
||||
elif base_part is None:
|
||||
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
|
||||
def from_bounding_box(cls, bbox: BoundBox | OrientedBoundBox) -> Solid:
|
||||
|
|
@ -1150,14 +982,12 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Solid: Box
|
||||
"""
|
||||
return cls(
|
||||
TopoDS.Solid_s(
|
||||
BRepPrimAPI_MakeBox(
|
||||
plane.to_gp_ax2(),
|
||||
length,
|
||||
width,
|
||||
height,
|
||||
).Shape()
|
||||
)
|
||||
BRepPrimAPI_MakeBox(
|
||||
plane.to_gp_ax2(),
|
||||
length,
|
||||
width,
|
||||
height,
|
||||
).Shape()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -1184,15 +1014,13 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Solid: Full or partial cone
|
||||
"""
|
||||
return cls(
|
||||
TopoDS.Solid_s(
|
||||
BRepPrimAPI_MakeCone(
|
||||
plane.to_gp_ax2(),
|
||||
base_radius,
|
||||
top_radius,
|
||||
height,
|
||||
angle * DEG2RAD,
|
||||
).Shape()
|
||||
)
|
||||
BRepPrimAPI_MakeCone(
|
||||
plane.to_gp_ax2(),
|
||||
base_radius,
|
||||
top_radius,
|
||||
height,
|
||||
angle * DEG2RAD,
|
||||
).Shape()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -1217,14 +1045,12 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Solid: Full or partial cylinder
|
||||
"""
|
||||
return cls(
|
||||
TopoDS.Solid_s(
|
||||
BRepPrimAPI_MakeCylinder(
|
||||
plane.to_gp_ax2(),
|
||||
radius,
|
||||
height,
|
||||
angle * DEG2RAD,
|
||||
).Shape()
|
||||
)
|
||||
BRepPrimAPI_MakeCylinder(
|
||||
plane.to_gp_ax2(),
|
||||
radius,
|
||||
height,
|
||||
angle * DEG2RAD,
|
||||
).Shape()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -1245,7 +1071,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Returns:
|
||||
Solid: Lofted object
|
||||
"""
|
||||
return cls(TopoDS.Solid_s(_make_loft(objs, True, ruled)))
|
||||
return cls(_make_loft(objs, True, ruled))
|
||||
|
||||
@classmethod
|
||||
def make_sphere(
|
||||
|
|
@ -1271,15 +1097,13 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Solid: sphere
|
||||
"""
|
||||
return cls(
|
||||
TopoDS.Solid_s(
|
||||
BRepPrimAPI_MakeSphere(
|
||||
plane.to_gp_ax2(),
|
||||
radius,
|
||||
angle1 * DEG2RAD,
|
||||
angle2 * DEG2RAD,
|
||||
angle3 * DEG2RAD,
|
||||
).Shape()
|
||||
)
|
||||
BRepPrimAPI_MakeSphere(
|
||||
plane.to_gp_ax2(),
|
||||
radius,
|
||||
angle1 * DEG2RAD,
|
||||
angle2 * DEG2RAD,
|
||||
angle3 * DEG2RAD,
|
||||
).Shape()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -1307,16 +1131,14 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Solid: Full or partial torus
|
||||
"""
|
||||
return cls(
|
||||
TopoDS.Solid_s(
|
||||
BRepPrimAPI_MakeTorus(
|
||||
plane.to_gp_ax2(),
|
||||
major_radius,
|
||||
minor_radius,
|
||||
start_angle * DEG2RAD,
|
||||
end_angle * DEG2RAD,
|
||||
major_angle * DEG2RAD,
|
||||
).Shape()
|
||||
)
|
||||
BRepPrimAPI_MakeTorus(
|
||||
plane.to_gp_ax2(),
|
||||
major_radius,
|
||||
minor_radius,
|
||||
start_angle * DEG2RAD,
|
||||
end_angle * DEG2RAD,
|
||||
major_angle * DEG2RAD,
|
||||
).Shape()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -1347,18 +1169,16 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Solid: wedge
|
||||
"""
|
||||
return cls(
|
||||
TopoDS.Solid_s(
|
||||
BRepPrimAPI_MakeWedge(
|
||||
plane.to_gp_ax2(),
|
||||
delta_x,
|
||||
delta_y,
|
||||
delta_z,
|
||||
min_x,
|
||||
min_z,
|
||||
max_x,
|
||||
max_z,
|
||||
).Solid()
|
||||
)
|
||||
BRepPrimAPI_MakeWedge(
|
||||
plane.to_gp_ax2(),
|
||||
delta_x,
|
||||
delta_y,
|
||||
delta_z,
|
||||
min_x,
|
||||
min_z,
|
||||
max_x,
|
||||
max_z,
|
||||
).Solid()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -1396,7 +1216,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
True,
|
||||
)
|
||||
|
||||
return cls(TopoDS.Solid_s(revol_builder.Shape()))
|
||||
return cls(revol_builder.Shape())
|
||||
|
||||
@classmethod
|
||||
def sweep(
|
||||
|
|
@ -1449,7 +1269,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
outer_wire = section
|
||||
inner_wires = inner_wires if inner_wires else []
|
||||
|
||||
shapes: list[Mixin3D[TopoDS_Shape]] = []
|
||||
shapes = []
|
||||
for wire in [outer_wire] + inner_wires:
|
||||
builder = BRepOffsetAPI_MakePipeShell(Wire(path).wrapped)
|
||||
|
||||
|
|
@ -1544,7 +1364,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
if make_solid:
|
||||
builder.MakeSolid()
|
||||
|
||||
return cls(TopoDS.Solid_s(builder.Shape()))
|
||||
return cls(builder.Shape())
|
||||
|
||||
@classmethod
|
||||
def thicken(
|
||||
|
|
@ -1600,7 +1420,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
)
|
||||
offset_builder.MakeOffsetShape()
|
||||
try:
|
||||
result = Solid(TopoDS.Solid_s(offset_builder.Shape()))
|
||||
result = Solid(offset_builder.Shape())
|
||||
except StdFail_NotDone as err:
|
||||
raise RuntimeError("Error applying thicken to given surface") from err
|
||||
|
||||
|
|
@ -1647,7 +1467,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
|
||||
try:
|
||||
draft_angle_builder.Build()
|
||||
result = Solid(TopoDS.Solid_s(draft_angle_builder.Shape()))
|
||||
result = Solid(draft_angle_builder.Shape())
|
||||
except StdFail_NotDone as err:
|
||||
raise DraftAngleError(
|
||||
"Draft build failed on the given solid.",
|
||||
|
|
|
|||
|
|
@ -60,15 +60,13 @@ import sys
|
|||
import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterable, Sequence
|
||||
from math import degrees
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
||||
from typing import cast as tcast
|
||||
|
||||
import OCP.TopAbs as ta
|
||||
from OCP.BRep import BRep_Builder, BRep_Tool
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
|
||||
from OCP.BRepAlgo import BRepAlgo
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Section
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
|
||||
from OCP.BRepBuilderAPI import (
|
||||
BRepBuilderAPI_MakeEdge,
|
||||
BRepBuilderAPI_MakeFace,
|
||||
|
|
@ -105,7 +103,6 @@ from OCP.Standard import (
|
|||
Standard_ConstructionError,
|
||||
Standard_Failure,
|
||||
Standard_NoSuchObject,
|
||||
Standard_TypeMismatch,
|
||||
)
|
||||
from OCP.StdFail import StdFail_NotDone
|
||||
from OCP.TColgp import TColgp_Array1OfPnt, TColgp_HArray2OfPnt
|
||||
|
|
@ -142,12 +139,10 @@ from build123d.geometry import (
|
|||
|
||||
from .one_d import Edge, Mixin1D, Wire
|
||||
from .shape_core import (
|
||||
TOPODS,
|
||||
Shape,
|
||||
ShapeList,
|
||||
SkipClean,
|
||||
_sew_topods_faces,
|
||||
_topods_bool_op,
|
||||
_topods_entities,
|
||||
_topods_face_normal_at,
|
||||
downcast,
|
||||
|
|
@ -158,6 +153,7 @@ from .utils import (
|
|||
_extrude_topods_shape,
|
||||
_make_loft,
|
||||
_make_topods_face_from_wires,
|
||||
_topods_bool_op,
|
||||
find_max_dimension,
|
||||
)
|
||||
from .zero_d import Vertex
|
||||
|
|
@ -169,9 +165,17 @@ if TYPE_CHECKING: # pragma: no cover
|
|||
T = TypeVar("T", Edge, Wire, "Face")
|
||||
|
||||
|
||||
class Mixin2D(ABC, Shape[TOPODS]):
|
||||
class Mixin2D(ABC, Shape):
|
||||
"""Additional methods to add to Face and Shell class"""
|
||||
|
||||
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 ----
|
||||
|
||||
@property
|
||||
|
|
@ -209,23 +213,23 @@ class Mixin2D(ABC, Shape[TOPODS]):
|
|||
|
||||
def __neg__(self) -> Self:
|
||||
"""Reverse normal operator -"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Invalid Shape")
|
||||
new_surface = copy.deepcopy(self)
|
||||
new_surface.wrapped = tcast(TOPODS, downcast(self.wrapped.Complemented()))
|
||||
new_surface.wrapped = downcast(self.wrapped.Complemented())
|
||||
|
||||
# As the surface has been modified, the parent is no longer valid
|
||||
new_surface.topo_parent = None
|
||||
|
||||
return new_surface
|
||||
|
||||
# def face(self) -> Face | None:
|
||||
# """Return the Face"""
|
||||
# return Shape.get_single_shape(self, "Face")
|
||||
def face(self) -> Face | None:
|
||||
"""Return the Face"""
|
||||
return Shape.get_single_shape(self, "Face")
|
||||
|
||||
# def faces(self) -> ShapeList[Face]:
|
||||
# """faces - all the faces in this Shape"""
|
||||
# return Shape.get_shape_list(self, "Face")
|
||||
def faces(self) -> ShapeList[Face]:
|
||||
"""faces - all the faces in this Shape"""
|
||||
return Shape.get_shape_list(self, "Face")
|
||||
|
||||
def find_intersection_points(
|
||||
self, other: Axis, tolerance: float = TOLERANCE
|
||||
|
|
@ -240,7 +244,7 @@ class Mixin2D(ABC, Shape[TOPODS]):
|
|||
Returns:
|
||||
list[tuple[Vector, Vector]]: Point and normal of intersection
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return []
|
||||
|
||||
intersection_line = gce_MakeLin(other.wrapped).Value()
|
||||
|
|
@ -274,128 +278,6 @@ class Mixin2D(ABC, Shape[TOPODS]):
|
|||
|
||||
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
|
||||
def location_at(self, *args: Any, **kwargs: Any) -> Location:
|
||||
"""A location from a face or shell"""
|
||||
|
|
@ -405,32 +287,13 @@ class Mixin2D(ABC, Shape[TOPODS]):
|
|||
"""Return a copy of self moved along the normal by amount"""
|
||||
return copy.deepcopy(self).moved(Location(self.normal_at() * amount))
|
||||
|
||||
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
|
||||
def shell(self) -> Shell | None:
|
||||
"""Return the Shell"""
|
||||
return Shape.get_single_shape(self, "Shell")
|
||||
|
||||
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 shells(self) -> ShapeList[Shell]:
|
||||
"""shells - all the shells in this Shape"""
|
||||
return Shape.get_shape_list(self, "Shell")
|
||||
|
||||
def _wrap_edge(
|
||||
self,
|
||||
|
|
@ -487,7 +350,7 @@ class Mixin2D(ABC, Shape[TOPODS]):
|
|||
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")
|
||||
|
||||
# Initial setup
|
||||
|
|
@ -548,7 +411,7 @@ class Mixin2D(ABC, Shape[TOPODS]):
|
|||
raise RuntimeError(
|
||||
f"Length error of {length_error:.6f} exceeds tolerance {tolerance}"
|
||||
)
|
||||
if not wrapped_edge or not wrapped_edge.is_valid:
|
||||
if wrapped_edge.wrapped is None or not wrapped_edge.is_valid:
|
||||
raise RuntimeError("Wrapped edge is invalid")
|
||||
|
||||
if not snap_to_face:
|
||||
|
|
@ -571,7 +434,7 @@ class Mixin2D(ABC, Shape[TOPODS]):
|
|||
return projected_edge
|
||||
|
||||
|
||||
class Face(Mixin2D[TopoDS_Face]):
|
||||
class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||
"""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.
|
||||
These faces are integral components of complex structures, such as solids and
|
||||
|
|
@ -586,7 +449,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
obj: TopoDS_Face | Plane,
|
||||
obj: TopoDS_Face,
|
||||
label: str = "",
|
||||
color: Color | None = None,
|
||||
parent: Compound | None = None,
|
||||
|
|
@ -594,7 +457,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
"""Build a Face from an OCCT TopoDS_Shape/TopoDS_Face
|
||||
|
||||
Args:
|
||||
obj (TopoDS_Shape | Plane, optional): OCCT Face or Plane.
|
||||
obj (TopoDS_Shape, optional): OCCT Face.
|
||||
label (str, optional): Defaults to ''.
|
||||
color (Color, optional): Defaults to None.
|
||||
parent (Compound, optional): assembly parent. Defaults to None.
|
||||
|
|
@ -620,14 +483,11 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
"""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
obj: TopoDS_Face | Plane | None
|
||||
outer_wire, inner_wires, obj, label, color, parent = (None,) * 6
|
||||
|
||||
if args:
|
||||
l_a = len(args)
|
||||
if isinstance(args[0], Plane):
|
||||
obj = args[0]
|
||||
elif isinstance(args[0], TopoDS_Shape):
|
||||
if isinstance(args[0], TopoDS_Shape):
|
||||
obj, label, color, parent = args[:4] + (None,) * (4 - l_a)
|
||||
elif isinstance(args[0], Wire):
|
||||
outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * (
|
||||
|
|
@ -656,9 +516,6 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
color = kwargs.get("color", color)
|
||||
parent = kwargs.get("parent", parent)
|
||||
|
||||
if isinstance(obj, Plane):
|
||||
obj = BRepBuilderAPI_MakeFace(obj.wrapped).Face()
|
||||
|
||||
if outer_wire is not None:
|
||||
inner_topods_wires = (
|
||||
[w.wrapped for w in inner_wires] if inner_wires is not None else []
|
||||
|
|
@ -688,7 +545,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
float: The total surface area, including the area of holes. Returns 0.0 if
|
||||
the face is empty.
|
||||
"""
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
return 0.0
|
||||
|
||||
return self.without_holes().area
|
||||
|
|
@ -699,11 +556,6 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
if type(self.geom_adaptor()) == Geom_RectangularTrimmedSurface:
|
||||
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:
|
||||
return Axis(
|
||||
self.geom_adaptor().Cylinder().Axis() # type:ignore[attr-defined]
|
||||
|
|
@ -753,7 +605,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
ValueError: If the face or its underlying representation is empty.
|
||||
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")
|
||||
|
||||
if not self.is_planar_face:
|
||||
|
|
@ -819,13 +671,15 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
).sort_by(Axis(cog, cross_dir))
|
||||
|
||||
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):
|
||||
intersection = flipped_face.intersect(bottom_face)
|
||||
if intersection is None:
|
||||
if intersection is None or isinstance(intersection, list):
|
||||
intersect_area = -1.0
|
||||
break
|
||||
else:
|
||||
intersect_area = sum(f.area for f in intersection.faces())
|
||||
assert isinstance(intersection, Face)
|
||||
intersect_area += intersection.area
|
||||
|
||||
if intersect_area == -1.0:
|
||||
continue
|
||||
|
|
@ -983,17 +837,6 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
else:
|
||||
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
|
||||
def volume(self) -> float:
|
||||
"""volume - the volume of this Face, which is always zero"""
|
||||
|
|
@ -1028,7 +871,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
Returns:
|
||||
Face: extruded shape
|
||||
"""
|
||||
if not obj:
|
||||
if obj.wrapped is None:
|
||||
raise ValueError("Can't extrude empty object")
|
||||
return Face(TopoDS.Face_s(_extrude_topods_shape(obj.wrapped, direction)))
|
||||
|
||||
|
|
@ -1138,7 +981,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
)
|
||||
return single_point_curve
|
||||
|
||||
if not shape:
|
||||
if shape.wrapped is None:
|
||||
raise ValueError("input Edge cannot be empty")
|
||||
|
||||
adaptor = BRepAdaptor_Curve(shape.wrapped)
|
||||
|
|
@ -1172,12 +1015,6 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
plane: Plane = Plane.XY,
|
||||
) -> Face:
|
||||
"""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()
|
||||
return cls(pln_shape)
|
||||
|
||||
|
|
@ -1267,7 +1104,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
raise ValueError("exterior must be a Wire or list of Edges")
|
||||
|
||||
for edge in outside_edges:
|
||||
if not edge:
|
||||
if edge.wrapped is None:
|
||||
raise ValueError("exterior contains empty edges")
|
||||
surface.Add(edge.wrapped, GeomAbs_C0)
|
||||
|
||||
|
|
@ -1298,7 +1135,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
if interior_wires:
|
||||
makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped)
|
||||
for wire in interior_wires:
|
||||
if not wire:
|
||||
if wire.wrapped is None:
|
||||
raise ValueError("interior_wires contain an empty wire")
|
||||
makeface_object.Add(wire.wrapped)
|
||||
try:
|
||||
|
|
@ -1480,7 +1317,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
|
||||
try:
|
||||
patch.Build()
|
||||
result = cls(TopoDS.Face_s(patch.Shape()))
|
||||
result = cls(patch.Shape())
|
||||
except (
|
||||
Standard_Failure,
|
||||
StdFail_NotDone,
|
||||
|
|
@ -1492,7 +1329,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
) from err
|
||||
|
||||
result = result.fix()
|
||||
if not result.is_valid or not result:
|
||||
if not result.is_valid or result.wrapped is None:
|
||||
raise RuntimeError("Non planar face is invalid")
|
||||
|
||||
return result
|
||||
|
|
@ -1596,12 +1433,8 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
|
||||
if len(profile.edges()) != 1 or len(path.edges()) != 1:
|
||||
raise ValueError("Use Shell.sweep for multi Edge objects")
|
||||
profile_edge = profile.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])
|
||||
profile = Wire([profile.edge()])
|
||||
path = Wire([path.edge()])
|
||||
builder = BRepOffsetAPI_MakePipeShell(path.wrapped)
|
||||
builder.Add(profile.wrapped, False, False)
|
||||
builder.SetTransitionMode(Shape._transModeDict[transition])
|
||||
|
|
@ -1625,7 +1458,6 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
Returns:
|
||||
Vector: center
|
||||
"""
|
||||
center_point: Vector | gp_Pnt
|
||||
if (center_of == CenterOf.MASS) or (
|
||||
center_of == CenterOf.GEOMETRY and self.is_planar
|
||||
):
|
||||
|
|
@ -1685,10 +1517,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
|
||||
# Index or iterator access to OCP.TopTools.TopTools_ListOfShape is slow on M1 macs
|
||||
# Using First() and Last() to omit
|
||||
edges = (
|
||||
Edge(TopoDS.Edge_s(edge_list.First())),
|
||||
Edge(TopoDS.Edge_s(edge_list.Last())),
|
||||
)
|
||||
edges = (Edge(edge_list.First()), Edge(edge_list.Last()))
|
||||
|
||||
edge1, edge2 = Wire.order_chamfer_edges(reference_edge, edges)
|
||||
|
||||
|
|
@ -2078,7 +1907,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
BRepAlgoAPI_Common(),
|
||||
)
|
||||
for topods_shell in get_top_level_topods_shapes(topods_shape):
|
||||
intersected_shapes.append(Shell(TopoDS.Shell_s(topods_shell)))
|
||||
intersected_shapes.append(Shell(topods_shell))
|
||||
|
||||
intersected_shapes = intersected_shapes.sort_by(Axis(self.center(), direction))
|
||||
projected_shapes: ShapeList[Face | Shell] = ShapeList()
|
||||
|
|
@ -2111,7 +1940,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if self._wrapped is None:
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Cannot approximate an empty shape")
|
||||
|
||||
return self.__class__.cast(BRepAlgo.ConvertFace_s(self.wrapped, tolerance))
|
||||
|
|
@ -2124,7 +1953,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
Returns:
|
||||
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")
|
||||
|
||||
if not (inner_wires := self.inner_wires()):
|
||||
|
|
@ -2135,7 +1964,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
for hole_wire in inner_wires:
|
||||
reshaper.Remove(hole_wire.wrapped)
|
||||
modified_shape = downcast(reshaper.Apply(self.wrapped))
|
||||
holeless.wrapped = TopoDS.Face_s(modified_shape)
|
||||
holeless.wrapped = modified_shape
|
||||
return holeless
|
||||
|
||||
def wire(self) -> Wire:
|
||||
|
|
@ -2498,7 +2327,7 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
return wrapped_wire
|
||||
|
||||
|
||||
class Shell(Mixin2D[TopoDS_Shell]):
|
||||
class Shell(Mixin2D, Shape[TopoDS_Shell]):
|
||||
"""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
|
||||
part of a geometric model, it defines a watertight enclosure, commonly encountered
|
||||
|
|
@ -2530,7 +2359,7 @@ class Shell(Mixin2D[TopoDS_Shell]):
|
|||
obj = obj_list[0]
|
||||
|
||||
if isinstance(obj, Face):
|
||||
if not obj:
|
||||
if obj.wrapped is None:
|
||||
raise ValueError(f"Can't create a Shell from empty Face")
|
||||
builder = BRep_Builder()
|
||||
shell = TopoDS_Shell()
|
||||
|
|
@ -2538,10 +2367,7 @@ class Shell(Mixin2D[TopoDS_Shell]):
|
|||
builder.Add(shell, obj.wrapped)
|
||||
obj = shell
|
||||
elif isinstance(obj, Iterable):
|
||||
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")
|
||||
obj = _sew_topods_faces([f.wrapped for f in obj])
|
||||
|
||||
super().__init__(
|
||||
obj=obj,
|
||||
|
|
@ -2559,7 +2385,6 @@ class Shell(Mixin2D[TopoDS_Shell]):
|
|||
solid_shell = ShapeFix_Solid().SolidFromShell(self.wrapped)
|
||||
properties = GProp_GProps()
|
||||
calc_function = Shape.shape_properties_LUT[shapetype(solid_shell)]
|
||||
assert calc_function is not None
|
||||
calc_function(solid_shell, properties)
|
||||
return properties.Mass()
|
||||
return 0.0
|
||||
|
|
@ -2602,7 +2427,7 @@ class Shell(Mixin2D[TopoDS_Shell]):
|
|||
Returns:
|
||||
Shell: Lofted object
|
||||
"""
|
||||
return cls(TopoDS.Shell_s(_make_loft(objs, False, ruled)))
|
||||
return cls(_make_loft(objs, False, ruled))
|
||||
|
||||
@classmethod
|
||||
def revolve(
|
||||
|
|
@ -2628,7 +2453,7 @@ class Shell(Mixin2D[TopoDS_Shell]):
|
|||
profile.wrapped, axis.wrapped, angle * DEG2RAD, True
|
||||
)
|
||||
|
||||
return cls(TopoDS.Shell_s(revol_builder.Shape()))
|
||||
return cls(revol_builder.Shape())
|
||||
|
||||
@classmethod
|
||||
def sweep(
|
||||
|
|
@ -2656,7 +2481,7 @@ class Shell(Mixin2D[TopoDS_Shell]):
|
|||
builder.Add(profile.wrapped, False, False)
|
||||
builder.SetTransitionMode(Shape._transModeDict[transition])
|
||||
builder.Build()
|
||||
result = Shell(TopoDS.Shell_s(builder.Shape()))
|
||||
result = Shell(builder.Shape())
|
||||
if SkipClean.clean:
|
||||
result = result.clean()
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ Key Features:
|
|||
- `_make_topods_face_from_wires`: Generates planar faces with optional holes.
|
||||
|
||||
- **Boolean Operations**:
|
||||
- `_topods_bool_op`: Generic Boolean operations for TopoDS_Shapes.
|
||||
- `new_edges`: Identifies newly created edges from combined shapes.
|
||||
|
||||
- **Enhanced Math**:
|
||||
|
|
@ -281,6 +282,45 @@ def _make_topods_face_from_wires(
|
|||
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]:
|
||||
"""Compare the OCCT objects of each list and return the differences"""
|
||||
shapes_one = list(shapes_one)
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@ from OCP.TopExp import TopExp_Explorer
|
|||
from OCP.TopoDS import TopoDS, TopoDS_Shape, TopoDS_Vertex, TopoDS_Edge
|
||||
from OCP.gp import gp_Pnt
|
||||
from build123d.geometry import Matrix, Vector, VectorLike, Location, Axis, Plane
|
||||
from build123d.build_enums import Keep
|
||||
from .shape_core import Shape, ShapeList, TrimmingTool, downcast, shapetype
|
||||
|
||||
from .shape_core import Shape, ShapeList, downcast, shapetype
|
||||
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
|
|
@ -161,7 +161,7 @@ class Vertex(Shape[TopoDS_Vertex]):
|
|||
|
||||
shape_type = shapetype(obj)
|
||||
# NB downcast is needed to handle TopoDS_Shape types
|
||||
return constructor_lut[shape_type](TopoDS.Vertex_s(obj))
|
||||
return constructor_lut[shape_type](downcast(obj))
|
||||
|
||||
@classmethod
|
||||
def extrude(cls, obj: Shape, direction: VectorLike) -> Vertex:
|
||||
|
|
@ -312,10 +312,6 @@ class Vertex(Shape[TopoDS_Vertex]):
|
|||
"""The center of a vertex is itself!"""
|
||||
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]:
|
||||
"""Return vertex as three tuple of floats"""
|
||||
warnings.warn(
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ def to_vtk_poly_data(
|
|||
if not HAS_VTK:
|
||||
warnings.warn("VTK not supported", stacklevel=2)
|
||||
|
||||
if not obj:
|
||||
if obj.wrapped is None:
|
||||
raise ValueError("Cannot convert an empty shape")
|
||||
|
||||
vtk_shape = IVtkOCC_Shape(obj.wrapped)
|
||||
|
|
|
|||
|
|
@ -175,15 +175,6 @@ class ObjectTests(unittest.TestCase):
|
|||
self.assertTupleAlmostEquals(s.bounding_box().min, (-0.5, -0.5, -0.5), 3)
|
||||
self.assertTupleAlmostEquals(s.bounding_box().max, (0.5, 0.5, 0.5), 3)
|
||||
|
||||
def test_convex_polyhedron(self):
|
||||
base = Box(30, 20, 20)
|
||||
base += Box(20, 30, 20)
|
||||
base += Box(20, 20, 30)
|
||||
base += Pos(10, 0, 0) * Box(40, 23, 23)
|
||||
part = ConvexPolyhedron(base.vertices())
|
||||
self.assertAlmostEqual(part.volume, 33876.66666666667, 5)
|
||||
self.assertEqual(len(part.faces()), 26)
|
||||
|
||||
def test_hole(self):
|
||||
obj = Box(10, 10, 10)
|
||||
obj -= Hole(3, 10)
|
||||
|
|
|
|||
|
|
@ -98,14 +98,14 @@ class BuildLineTests(unittest.TestCase):
|
|||
powerup @ 0,
|
||||
tangents=(screw % 1, powerup % 0),
|
||||
)
|
||||
self.assertAlmostEqual(roller_coaster.wires()[0].length, 678.9785865257071, 5)
|
||||
self.assertAlmostEqual(roller_coaster.wires()[0].length, 678.983628932414, 5)
|
||||
|
||||
def test_bezier(self):
|
||||
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]
|
||||
with BuildLine() as bz:
|
||||
b1 = Bezier(*pts, weights=wts)
|
||||
self.assertAlmostEqual(bz.wires()[0].length, 225.98661946375782, 5)
|
||||
self.assertAlmostEqual(bz.wires()[0].length, 225.86389406824566, 5)
|
||||
self.assertTrue(isinstance(b1, Edge))
|
||||
|
||||
def test_double_tangent_arc(self):
|
||||
|
|
@ -174,53 +174,6 @@ class BuildLineTests(unittest.TestCase):
|
|||
self.assertLessEqual(bbox.max.Y, 5)
|
||||
self.assertTrue(isinstance(e1, Edge))
|
||||
|
||||
def test_parabolic_center_arc(self):
|
||||
# General conic section equation: (1+K)x^2-2Rx+y^2=0
|
||||
# parabola (K = -1) => -2Rx+y^2=0
|
||||
center = (0, 0)
|
||||
C = 1
|
||||
R = 1 / C
|
||||
focal_length = R / 2
|
||||
with BuildLine() as el:
|
||||
ParabolicCenterArc(center, focal_length, 0, 90, 0, AngularDirection.COUNTER_CLOCKWISE, Mode.ADD)
|
||||
bbox = el.line.bounding_box()
|
||||
self.assertGreaterEqual(bbox.min.X, -10)
|
||||
self.assertGreaterEqual(bbox.min.Y, 0)
|
||||
self.assertLessEqual(bbox.max.X, 10)
|
||||
self.assertLessEqual(bbox.max.Y, 5)
|
||||
|
||||
e1 = ParabolicCenterArc(center, focal_length, 0, 90, 0, AngularDirection.COUNTER_CLOCKWISE, Mode.ADD)
|
||||
bbox = e1.bounding_box()
|
||||
self.assertGreaterEqual(bbox.min.X, -10)
|
||||
self.assertGreaterEqual(bbox.min.Y, 0)
|
||||
self.assertLessEqual(bbox.max.X, 10)
|
||||
self.assertLessEqual(bbox.max.Y, 5)
|
||||
self.assertTrue(isinstance(e1, Edge))
|
||||
|
||||
def test_hyperbolic_center_arc(self):
|
||||
# General conic section equation: (1+K)x^2-2Rx+y^2=0
|
||||
# hyperbola (K < -1)
|
||||
center = (0, 0)
|
||||
C = 1
|
||||
R = 1 / C
|
||||
K = -2 # => -(x^2)-2Rx+y^2=0
|
||||
a, b = R / (-K - 1), R / sqrt(-K - 1)
|
||||
with BuildLine() as el:
|
||||
HyperbolicCenterArc(center, b, a, 0, 90, 0, AngularDirection.COUNTER_CLOCKWISE, Mode.ADD)
|
||||
bbox = el.line.bounding_box()
|
||||
self.assertGreaterEqual(bbox.min.X, -10)
|
||||
self.assertGreaterEqual(bbox.min.Y, 0)
|
||||
self.assertLessEqual(bbox.max.X, 10)
|
||||
self.assertLessEqual(bbox.max.Y, 5)
|
||||
|
||||
e1 = HyperbolicCenterArc(center, b, a, 0, 90, 0, AngularDirection.COUNTER_CLOCKWISE, Mode.ADD)
|
||||
bbox = e1.bounding_box()
|
||||
self.assertGreaterEqual(bbox.min.X, -10)
|
||||
self.assertGreaterEqual(bbox.min.Y, 0)
|
||||
self.assertLessEqual(bbox.max.X, 10)
|
||||
self.assertLessEqual(bbox.max.Y, 5)
|
||||
self.assertTrue(isinstance(e1, Edge))
|
||||
|
||||
def test_filletpolyline(self):
|
||||
with BuildLine(Plane.YZ):
|
||||
p = FilletPolyline(
|
||||
|
|
@ -230,61 +183,6 @@ class BuildLineTests(unittest.TestCase):
|
|||
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 2)
|
||||
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):
|
||||
p = FilletPolyline(
|
||||
(0, 0, 0), (0, 0, 10), (10, 2, 10), (10, 0, 0), radius=2, close=True
|
||||
|
|
@ -299,33 +197,6 @@ class BuildLineTests(unittest.TestCase):
|
|||
with self.assertRaises(ValueError):
|
||||
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):
|
||||
with BuildLine():
|
||||
l1 = Line((0, 0), (10, 0))
|
||||
|
|
@ -934,9 +805,9 @@ class BuildLineTests(unittest.TestCase):
|
|||
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
|
||||
|
||||
# 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((case[0] - 1 * (r1 + r2)) / 2)
|
||||
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((case[0] - 1 * (r1 + r2)) / 2)
|
||||
|
||||
# Greater than min
|
||||
l1 = ArcArcTangentArc(start_arc, end_arc, min_r + 0.01, keep=case[1])
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ class TestCounterSinkHole(unittest.TestCase):
|
|||
|
||||
|
||||
class TestCylinder(unittest.TestCase):
|
||||
def test_simple_cylinder(self):
|
||||
def test_simple_torus(self):
|
||||
with BuildPart() as test:
|
||||
Cylinder(2, 10)
|
||||
self.assertAlmostEqual(test.part.volume, pi * 2**2 * 10, 5)
|
||||
|
|
@ -330,60 +330,6 @@ class TestExtrude(unittest.TestCase):
|
|||
extrude(until=Until.NEXT)
|
||||
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):
|
||||
with BuildPart(Plane.XZ) as box:
|
||||
with BuildSketch(Plane.XZ, mode=Mode.PRIVATE) as square:
|
||||
|
|
@ -693,18 +639,5 @@ class TestWedge(unittest.TestCase):
|
|||
Wedge(1, 1, 0, 0, 0, 2, 5)
|
||||
|
||||
|
||||
class TestConvexPolyhedron(unittest.TestCase):
|
||||
def test_convex_polyhedron(self):
|
||||
with BuildPart() as test:
|
||||
Box(30, 20, 20)
|
||||
Box(20, 30, 20)
|
||||
Box(20, 20, 30)
|
||||
with Locations((10, 0, 0)):
|
||||
Box(40, 23, 23)
|
||||
ConvexPolyhedron(test.vertices())
|
||||
self.assertAlmostEqual(test.part.volume, 33876.66666666667, 5)
|
||||
self.assertEqual(len(test.faces()), 26)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -70,18 +70,18 @@ class TestAssembly(unittest.TestCase):
|
|||
def test_show_topology_compound(self):
|
||||
assembly = TestAssembly.create_test_assembly()
|
||||
expected = [
|
||||
"assembly Compound at 0x7fced0fd1b50, Location((0, 0, 0), (0, 0, 0))",
|
||||
"├── box Solid at 0x7fced102d3a0, Location((0, 0, 0), (45, 45, 0))",
|
||||
"└── sphere Solid at 0x7fced0fd1f10, Location((1, 2, 3), (0, 0, 0))",
|
||||
"assembly Compound at 0x7fced0fd1b50, Location(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00))",
|
||||
"├── box Solid at 0x7fced102d3a0, Location(p=(0.00, 0.00, 0.00), o=(45.00, 45.00, -0.00))",
|
||||
"└── sphere Solid at 0x7fced0fd1f10, Location(p=(1.00, 2.00, 3.00), o=(-0.00, 0.00, -0.00))",
|
||||
]
|
||||
self.assertTopoEqual(assembly.show_topology("Solid"), expected)
|
||||
|
||||
def test_show_topology_shape_location(self):
|
||||
assembly = TestAssembly.create_test_assembly()
|
||||
expected = [
|
||||
"Solid at 0x7f3754501530, Position(1, 2, 3)",
|
||||
"└── Shell at 0x7f3754501a70, Position(1, 2, 3)",
|
||||
" └── Face at 0x7f3754501030, Position(1, 2, 3)",
|
||||
"Solid at 0x7f3754501530, Position(1.0, 2.0, 3.0)",
|
||||
"└── Shell at 0x7f3754501a70, Position(1.0, 2.0, 3.0)",
|
||||
" └── Face at 0x7f3754501030, Position(1.0, 2.0, 3.0)",
|
||||
]
|
||||
self.assertTopoEqual(
|
||||
assembly.children[1].show_topology("Face", show_center=False), expected
|
||||
|
|
@ -90,9 +90,9 @@ class TestAssembly(unittest.TestCase):
|
|||
def test_show_topology_shape(self):
|
||||
assembly = TestAssembly.create_test_assembly()
|
||||
expected = [
|
||||
"Solid at 0x7f6279043ab0, Center(1, 2, 3)",
|
||||
"└── Shell at 0x7f62790438f0, Center(1, 2, 3)",
|
||||
" └── Face at 0x7f62790439f0, Center(1, 2, 3)",
|
||||
"Solid at 0x7f6279043ab0, Center(1.0, 2.0, 3.0)",
|
||||
"└── Shell at 0x7f62790438f0, Center(1.0, 2.0, 3.0)",
|
||||
" └── Face at 0x7f62790439f0, Center(1.0, 2.0, 3.0)",
|
||||
]
|
||||
self.assertTopoEqual(assembly.children[1].show_topology("Face"), expected)
|
||||
|
||||
|
|
|
|||
|
|
@ -85,17 +85,8 @@ class TestAxis(unittest.TestCase):
|
|||
self.assertAlmostEqual(test_axis.direction, (0, 1, 0), 5)
|
||||
|
||||
def test_axis_repr_and_str(self):
|
||||
self.assertEqual(
|
||||
f"{Axis((1, 2, 3), (4, 5, 6)):.2f}",
|
||||
"((1.00, 2.00, 3.00), (0.46, 0.57, 0.68))",
|
||||
)
|
||||
self.assertEqual(
|
||||
f"{Axis((1, 2, 3), (4, 5, 6)):.2g}", "((1, 2, 3), (0.46, 0.57, 0.68))"
|
||||
)
|
||||
self.assertIn("((1.0, 2.0, 3.0), ", f"{Axis((1, 2, 3), (4, 5, 6)):.2t}")
|
||||
|
||||
self.assertEqual(repr(Axis.X), "Axis((0, 0, 0), (1, 0, 0))")
|
||||
self.assertEqual(str(Axis.Y), "Axis: (position=(0, 0, 0), direction=(0, 1, 0))")
|
||||
self.assertEqual(repr(Axis.X), "((0.0, 0.0, 0.0),(1.0, 0.0, 0.0))")
|
||||
self.assertEqual(str(Axis.Y), "Axis: ((0.0, 0.0, 0.0),(0.0, 1.0, 0.0))")
|
||||
|
||||
def test_axis_copy(self):
|
||||
x_copy = copy.copy(Axis.X)
|
||||
|
|
@ -145,6 +136,10 @@ class TestAxis(unittest.TestCase):
|
|||
self.assertTrue(Axis.X.is_parallel(Axis((1, 1, 1), (1, 0, 0))))
|
||||
self.assertFalse(Axis.X.is_parallel(Axis.Y))
|
||||
|
||||
def test_axis_is_skew(self):
|
||||
self.assertTrue(Axis.X.is_skew(Axis((0, 1, 1), (0, 0, 1))))
|
||||
self.assertFalse(Axis.X.is_skew(Axis.Y))
|
||||
|
||||
def test_axis_is_skew(self):
|
||||
# Skew Axes
|
||||
self.assertTrue(Axis.X.is_skew(Axis((0, 1, 1), (0, 0, 1))))
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ class TestBoundBox(unittest.TestCase):
|
|||
|
||||
# OCC uses some approximations
|
||||
self.assertAlmostEqual(bb1.size.X, 1.0, 1)
|
||||
self.assertAlmostEqual(bb1.measure, 1.0, 5)
|
||||
|
||||
# Test adding to an existing bounding box
|
||||
v0 = Vertex(0, 0, 0)
|
||||
|
|
@ -51,7 +50,6 @@ class TestBoundBox(unittest.TestCase):
|
|||
|
||||
bb3 = bb1.add(bb2)
|
||||
self.assertAlmostEqual(bb3.size, (2, 2, 2), 7)
|
||||
self.assertAlmostEqual(bb3.measure, 8, 5)
|
||||
|
||||
bb3 = bb2.add((3, 3, 3))
|
||||
self.assertAlmostEqual(bb3.size, (3, 3, 3), 7)
|
||||
|
|
@ -63,7 +61,6 @@ class TestBoundBox(unittest.TestCase):
|
|||
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())
|
||||
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
|
||||
self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb1, bb2))
|
||||
self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb2, bb1))
|
||||
|
|
|
|||
|
|
@ -26,297 +26,169 @@ license:
|
|||
|
||||
"""
|
||||
|
||||
import colorsys
|
||||
import copy
|
||||
import math
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from build123d.geometry import Color
|
||||
from OCP.Quantity import Quantity_ColorRGBA
|
||||
|
||||
from build123d.geometry import Color
|
||||
|
||||
class TestColor(unittest.TestCase):
|
||||
# name + alpha overload
|
||||
def test_name1(self):
|
||||
c = Color("blue")
|
||||
np.testing.assert_allclose(tuple(c), (0, 0, 1, 1), 1e-5)
|
||||
|
||||
# Overloads
|
||||
@pytest.mark.parametrize(
|
||||
"color, expected",
|
||||
[
|
||||
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):
|
||||
c = Color("blue", 0.5)
|
||||
np.testing.assert_allclose(tuple(c), (0, 0, 1, 0.5), 1e-5)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"color, expected",
|
||||
[
|
||||
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):
|
||||
c = Color(1.0, 1.0, 0.0, 0.5)
|
||||
self.assertEqual(c.wrapped.GetRGB().Red(), 1.0)
|
||||
self.assertEqual(c.wrapped.GetRGB().Green(), 1.0)
|
||||
self.assertEqual(c.wrapped.GetRGB().Blue(), 0.0)
|
||||
self.assertEqual(c.wrapped.Alpha(), 0.5)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"color, expected",
|
||||
[
|
||||
pytest.param(
|
||||
Color(0x996692), (0x99 / 0xFF, 0x66 / 0xFF, 0x92 / 0xFF, 1), id="color_code"
|
||||
),
|
||||
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):
|
||||
c = Color(red=0.1, green=0.2, blue=0.3, alpha=0.5)
|
||||
np.testing.assert_allclose(tuple(c), (0.1, 0.2, 0.3, 0.5), 1e-5)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"color, expected",
|
||||
[
|
||||
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)
|
||||
np.testing.assert_allclose(
|
||||
tuple(c), (0, 0x66 / 0xFF, 0x92 / 0xFF, 0x80 / 0xFF), 5
|
||||
)
|
||||
|
||||
# ColorLikes
|
||||
@pytest.mark.parametrize(
|
||||
"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)
|
||||
np.testing.assert_allclose(
|
||||
tuple(c), (153 / 255, 102 / 255, 146 / 255, 204 / 255), 5
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"color_like, expected",
|
||||
[
|
||||
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)
|
||||
np.testing.assert_allclose(tuple(c), (0, 0, 1, 1), 1e-5)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"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):
|
||||
c = Color(0.1, 0.2, 0.3, alpha=0.4)
|
||||
c_copy = copy.copy(c)
|
||||
np.testing.assert_allclose(tuple(c_copy), (0.1, 0.2, 0.3, 0.4), 1e-5)
|
||||
|
||||
# Exceptions
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
[
|
||||
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)
|
||||
self.assertEqual(str(c), "Color: (1.0, 0.5, 0.0, 1.0) near 'DARKGOLDENROD1'")
|
||||
self.assertEqual(repr(c), "Color(1.0, 0.5, 0.0, 1.0)")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"color_type",
|
||||
[
|
||||
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
|
||||
def test_color_like(self):
|
||||
red_color_likes = [
|
||||
Quantity_ColorRGBA(1, 0, 0, 1),
|
||||
"red",
|
||||
"red ",
|
||||
("red",),
|
||||
("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)
|
||||
|
||||
# Methods
|
||||
def test_rgba_wrapped():
|
||||
c = Color(1.0, 1.0, 0.0, 0.5)
|
||||
assert c.wrapped.GetRGB().Red() == 1.0
|
||||
assert c.wrapped.GetRGB().Green() == 1.0
|
||||
assert c.wrapped.GetRGB().Blue() == 0.0
|
||||
assert c.wrapped.Alpha() == 0.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 = [
|
||||
Quantity_ColorRGBA(1, 0, 0, 0.6),
|
||||
("red", 0.6),
|
||||
("#ff0000", 0.6),
|
||||
(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)
|
||||
|
||||
def test_copy():
|
||||
c = Color(0.1, 0.2, 0.3, alpha=0.4)
|
||||
c_copy = copy.copy(c)
|
||||
np.testing.assert_allclose(tuple(c_copy), (0.1, 0.2, 0.3, 0.4), rtol=1e-5)
|
||||
# Exceptions
|
||||
def test_bad_color_name(self):
|
||||
with self.assertRaises(ValueError):
|
||||
Color("build123d")
|
||||
|
||||
def test_bad_color_type(self):
|
||||
with self.assertRaises(TypeError):
|
||||
Color(dict({"name": "red", "alpha": 1}))
|
||||
|
||||
def test_str_repr_is():
|
||||
c = Color(1, 0, 0)
|
||||
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):
|
||||
Color(1., "blue")
|
||||
|
||||
def test_str_repr_near():
|
||||
c = Color(1, 0.5, 0)
|
||||
assert str(c) == "Color: (1.0, 0.5, 0.0, 1.0) near 'DARKORANGE1'"
|
||||
assert repr(c) == "Color(1.0, 0.5, 0.0, 1.0)"
|
||||
with self.assertRaises(TypeError):
|
||||
Color(1, "blue")
|
||||
|
||||
|
||||
class TestColorCategoricalSet:
|
||||
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)
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -130,8 +130,8 @@ class TestFace(unittest.TestCase):
|
|||
distance=1, distance2=2, vertices=[vertex], edge=other_edge
|
||||
)
|
||||
|
||||
def test_plane_as_face(self):
|
||||
test_face = Face(Plane.XY)
|
||||
def test_make_rect(self):
|
||||
test_face = Face.make_plane()
|
||||
self.assertAlmostEqual(test_face.normal_at(), (0, 0, 1), 5)
|
||||
|
||||
def test_length_width(self):
|
||||
|
|
|
|||
|
|
@ -152,7 +152,6 @@ def test_shape_0d(obj, target, expected):
|
|||
run_test(obj, target, expected)
|
||||
|
||||
|
||||
# 1d Shapes
|
||||
ed1 = Line((0, 0), (5, 0)).edge()
|
||||
ed2 = Line((0, -1), (5, 1)).edge()
|
||||
ed3 = Line((0, 0, 5), (5, 0, 5)).edge()
|
||||
|
|
@ -221,195 +220,6 @@ def test_shape_1d(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
|
||||
c1 = CenterArc((0, 0), 10, 0, 360).edge()
|
||||
c2 = CenterArc((19, 0), 10, 0, 360).edge()
|
||||
|
|
@ -430,7 +240,7 @@ freecad_matrix = [
|
|||
Case(vert, e1, [Vertex], "vertical, ellipse, tangent", None),
|
||||
Case(horz, e1, [Vertex], "horizontal, ellipse, tangent", None),
|
||||
|
||||
Case(c1, c2, [Vertex, Vertex], "circle, skew, intersect", None),
|
||||
Case(c1, c2, [Vertex, Vertex], "circle, skew, intersect", "Should return 2 Vertices"),
|
||||
Case(c1, horz, [Vertex], "circle, horiz, tangent", None),
|
||||
Case(c2, horz, [Vertex], "circle, horiz, tangent", None),
|
||||
Case(c1, vert, [Vertex], "circle, vert, tangent", None),
|
||||
|
|
@ -453,11 +263,11 @@ w1 = Wire.make_circle(0.5)
|
|||
f1 = Face(Wire.make_circle(0.5))
|
||||
|
||||
issues_matrix = [
|
||||
Case(t, t, [Face, Face], "issue #1015", None),
|
||||
Case(l, s, [Edge], "issue #945", None),
|
||||
Case(a, b, [Edge], "issue #918", None),
|
||||
Case(e1, w1, [Vertex, Vertex], "issue #697", None),
|
||||
Case(e1, f1, [Edge], "issue #697", None),
|
||||
Case(t, t, [Face, Face], "issue #1015", "Returns Compound"),
|
||||
Case(l, s, [Edge], "issue #945", "Edge.intersect only takes 1D"),
|
||||
Case(a, b, [Edge], "issue #918", "Returns empty Compound"),
|
||||
Case(e1, w1, [Vertex, Vertex], "issue #697"),
|
||||
Case(e1, f1, [Edge], "issue #697", "Edge.intersect only takes 1D"),
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("obj, target, expected", make_params(issues_matrix))
|
||||
|
|
@ -469,9 +279,6 @@ def test_issues(obj, target, expected):
|
|||
exception_matrix = [
|
||||
Case(vt1, 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
|
||||
|
|
|
|||
|
|
@ -228,23 +228,16 @@ class TestLocation(unittest.TestCase):
|
|||
|
||||
def test_location_repr_and_str(self):
|
||||
self.assertEqual(
|
||||
f"{Location((1, 2, 3), (4, 5, 6)):.2f}",
|
||||
"((1.00, 2.00, 3.00), (4.00, 5.00, 6.00))",
|
||||
repr(Location()), "(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00))"
|
||||
)
|
||||
self.assertEqual(
|
||||
f"{Location((1, 2, 3), (4, 5, 6)):.2g}", "((1, 2, 3), (4, 5, 6))"
|
||||
)
|
||||
self.assertIn("((1.0, 2.0, 3.0), ", f"{Location((1, 2, 3), (4, 5, 6)):.2t}")
|
||||
|
||||
self.assertEqual(repr(Location()), "Location((0, 0, 0), (0, 0, 0))")
|
||||
self.assertEqual(
|
||||
str(Location()),
|
||||
"Location: (position=(0, 0, 0), orientation=(0, 0, 0))",
|
||||
"Location: (position=(0.00, 0.00, 0.00), orientation=(-0.00, 0.00, -0.00))",
|
||||
)
|
||||
loc = Location((1, 2, 3), (33, 45, 67))
|
||||
self.assertEqual(
|
||||
str(loc),
|
||||
"Location: (position=(1, 2, 3), orientation=(33, 45, 67))",
|
||||
"Location: (position=(1.00, 2.00, 3.00), orientation=(33.00, 45.00, 67.00))",
|
||||
)
|
||||
|
||||
def test_location_inverted(self):
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ license:
|
|||
|
||||
import math
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from build123d.build_enums import (
|
||||
CenterOf,
|
||||
|
|
@ -107,73 +106,13 @@ class TestMixin1D(unittest.TestCase):
|
|||
5,
|
||||
)
|
||||
|
||||
def test_positions_with_distances(self):
|
||||
def test_positions(self):
|
||||
e = Edge.make_line((0, 0, 0), (1, 1, 1))
|
||||
distances = [i / 4 for i in range(3)]
|
||||
pts = e.positions(distances)
|
||||
for i, position in enumerate(pts):
|
||||
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):
|
||||
self.assertAlmostEqual(
|
||||
Edge.make_circle(1, start_angle=0, end_angle=90).tangent_at(1.0),
|
||||
|
|
|
|||