mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
Merge branch 'dev' into ocp781
This commit is contained in:
commit
b665cb5889
10 changed files with 386 additions and 264 deletions
11
mypy.ini
11
mypy.ini
|
|
@ -8,7 +8,7 @@ ignore_missing_imports = True
|
|||
[mypy-build123d.topology.jupyter_tools.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-IPython.lib.pretty.*]
|
||||
[mypy-IPython.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-numpy.*]
|
||||
|
|
@ -28,3 +28,12 @@ ignore_missing_imports = True
|
|||
|
||||
[mypy-vtkmodules.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ezdxf.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-setuptools_scm.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-py_lib3mf.*]
|
||||
ignore_missing_imports = True
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ class Builder(ABC):
|
|||
self.builder_parent = None
|
||||
self.lasts: dict = {Vertex: [], Edge: [], Face: [], Solid: []}
|
||||
self.workplanes_context = None
|
||||
self.exit_workplanes = None
|
||||
self.exit_workplanes: list[Plane] = []
|
||||
self.obj_before: Shape | None = None
|
||||
self.to_combine: list[Shape] = []
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ license:
|
|||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from math import copysign, floor, gcd, log2, pi
|
||||
from typing import ClassVar, Optional, Union
|
||||
from typing import cast, ClassVar, TypeAlias
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ class Arrow(BaseSketchObject):
|
|||
|
||||
Args:
|
||||
arrow_size (float): arrow head tip to tail length
|
||||
shaft_path (Union[Edge, Wire]): line describing the shaft shape
|
||||
shaft_path (Edge | Wire): line describing the shaft shape
|
||||
shaft_width (float): line width of shaft
|
||||
head_at_start (bool, optional): Defaults to True.
|
||||
head_type (HeadType, optional): arrow head shape. Defaults to HeadType.CURVED.
|
||||
|
|
@ -141,17 +141,15 @@ class Arrow(BaseSketchObject):
|
|||
shaft_pen = shaft_path.perpendicular_line(shaft_width, 0)
|
||||
shaft = sweep(shaft_pen, shaft_path, mode=Mode.PRIVATE)
|
||||
|
||||
arrow = arrow_head.fuse(shaft).clean()
|
||||
arrow = cast(Compound, arrow_head.fuse(shaft)).clean()
|
||||
|
||||
super().__init__(arrow, rotation=0, align=None, mode=mode)
|
||||
|
||||
|
||||
PathDescriptor = Union[
|
||||
Wire,
|
||||
Edge,
|
||||
list[Union[Vector, Vertex, tuple[float, float, float]]],
|
||||
]
|
||||
PointLike = Union[Vector, Vertex, tuple[float, float, float]]
|
||||
PointLike: TypeAlias = Vector | Vertex | tuple[float, float, float]
|
||||
"""General type for points in 3D space"""
|
||||
PathDescriptor: TypeAlias = Wire | Edge | list[PointLike]
|
||||
"""General type for a path in 3D space"""
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -223,7 +221,7 @@ class Draft:
|
|||
def _number_with_units(
|
||||
self,
|
||||
number: float,
|
||||
tolerance: float | tuple[float, float] = None,
|
||||
tolerance: float | tuple[float, float] | None = None,
|
||||
display_units: bool | None = None,
|
||||
) -> str:
|
||||
"""Convert a raw number to a unit of measurement string based on the class settings"""
|
||||
|
|
@ -295,7 +293,7 @@ class Draft:
|
|||
|
||||
def _label_to_str(
|
||||
self,
|
||||
label: str,
|
||||
label: str | None,
|
||||
line_wire: Wire,
|
||||
label_angle: bool,
|
||||
tolerance: float | tuple[float, float] | None,
|
||||
|
|
@ -351,7 +349,7 @@ class DimensionLine(BaseSketchObject):
|
|||
argument is desired not an actual measurement. Defaults to None.
|
||||
arrows (tuple[bool, bool], optional): a pair of boolean values controlling the placement
|
||||
of the start and end arrows. Defaults to (True, True).
|
||||
tolerance (Union[float, tuple[float, float]], optional): an optional tolerance
|
||||
tolerance (float | tuple[float, float], optional): an optional tolerance
|
||||
value to add to the extracted length value. If a single tolerance value is provided
|
||||
it is shown as ± the provided value while a pair of values are shown as
|
||||
separate + and - values. Defaults to None.
|
||||
|
|
@ -368,14 +366,14 @@ class DimensionLine(BaseSketchObject):
|
|||
def __init__(
|
||||
self,
|
||||
path: PathDescriptor,
|
||||
draft: Draft = None,
|
||||
sketch: Sketch = None,
|
||||
label: str = None,
|
||||
draft: Draft,
|
||||
sketch: Sketch | None = None,
|
||||
label: str | None = None,
|
||||
arrows: tuple[bool, bool] = (True, True),
|
||||
tolerance: float | tuple[float, float] = None,
|
||||
tolerance: float | tuple[float, float] | None = None,
|
||||
label_angle: bool = False,
|
||||
mode: Mode = Mode.ADD,
|
||||
) -> Sketch:
|
||||
):
|
||||
# pylint: disable=too-many-locals
|
||||
|
||||
context = BuildSketch._get_context(self)
|
||||
|
|
@ -452,22 +450,35 @@ class DimensionLine(BaseSketchObject):
|
|||
flip_label = path_obj.tangent_at(u_value).get_angle(Vector(1, 0, 0)) >= 180
|
||||
loc = Draft._sketch_location(path_obj, u_value, flip_label)
|
||||
placed_label = label_shape.located(loc)
|
||||
self_intersection = Sketch.intersect(d_line, placed_label).area
|
||||
self_intersection = cast(
|
||||
Sketch | None, Sketch.intersect(d_line, placed_label)
|
||||
)
|
||||
if self_intersection is None:
|
||||
self_intersection_area = 0.0
|
||||
else:
|
||||
self_intersection_area = self_intersection.area
|
||||
d_line += placed_label
|
||||
bbox_size = d_line.bounding_box().size
|
||||
|
||||
# Minimize size while avoiding intersections
|
||||
common_area = (
|
||||
0.0 if sketch is None else Sketch.intersect(d_line, sketch).area
|
||||
)
|
||||
common_area += self_intersection
|
||||
if sketch is None:
|
||||
common_area = 0.0
|
||||
else:
|
||||
line_intersection = cast(
|
||||
Sketch | None, Sketch.intersect(d_line, sketch)
|
||||
)
|
||||
if line_intersection is None:
|
||||
common_area = 0.0
|
||||
else:
|
||||
common_area = line_intersection.area
|
||||
common_area += self_intersection_area
|
||||
score = (d_line.area - 10 * common_area) / bbox_size.X
|
||||
d_lines[d_line] = score
|
||||
|
||||
# Sort by score to find the best option
|
||||
d_lines = sorted(d_lines.items(), key=lambda x: x[1])
|
||||
sorted_d_lines = sorted(d_lines.items(), key=lambda x: x[1])
|
||||
|
||||
super().__init__(obj=d_lines[-1][0], rotation=0, align=None, mode=mode)
|
||||
super().__init__(obj=sorted_d_lines[-1][0], rotation=0, align=None, mode=mode)
|
||||
|
||||
|
||||
class ExtensionLine(BaseSketchObject):
|
||||
|
|
@ -489,7 +500,7 @@ class ExtensionLine(BaseSketchObject):
|
|||
is desired not an actual measurement. Defaults to None.
|
||||
arrows (tuple[bool, bool], optional): a pair of boolean values controlling the placement
|
||||
of the start and end arrows. Defaults to (True, True).
|
||||
tolerance (Union[float, tuple[float, float]], optional): an optional tolerance
|
||||
tolerance (float | tuple[float, float], optional): an optional tolerance
|
||||
value to add to the extracted length value. If a single tolerance value is provided
|
||||
it is shown as ± the provided value while a pair of values are shown as
|
||||
separate + and - values. Defaults to None.
|
||||
|
|
@ -507,12 +518,12 @@ class ExtensionLine(BaseSketchObject):
|
|||
border: PathDescriptor,
|
||||
offset: float,
|
||||
draft: Draft,
|
||||
sketch: Sketch = None,
|
||||
label: str = None,
|
||||
sketch: Sketch | None = None,
|
||||
label: str | None = None,
|
||||
arrows: tuple[bool, bool] = (True, True),
|
||||
tolerance: float | tuple[float, float] = None,
|
||||
tolerance: float | tuple[float, float] | None = None,
|
||||
label_angle: bool = False,
|
||||
project_line: VectorLike = None,
|
||||
project_line: VectorLike | None = None,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
# pylint: disable=too-many-locals
|
||||
|
|
@ -531,7 +542,7 @@ class ExtensionLine(BaseSketchObject):
|
|||
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[copysign(1, offset)], closed=False
|
||||
distance=offset, side=side_lut[int(copysign(1, offset))], closed=False
|
||||
)
|
||||
dimension_label_str = (
|
||||
label
|
||||
|
|
@ -629,7 +640,7 @@ class TechnicalDrawing(BaseSketchObject):
|
|||
title: str = "Title",
|
||||
sub_title: str = "Sub Title",
|
||||
drawing_number: str = "B3D-1",
|
||||
sheet_number: int = None,
|
||||
sheet_number: int | None = None,
|
||||
drawing_scale: float = 1.0,
|
||||
nominal_text_size: float = 10.0,
|
||||
line_width: float = 0.5,
|
||||
|
|
@ -691,12 +702,12 @@ class TechnicalDrawing(BaseSketchObject):
|
|||
4: 3 / 12,
|
||||
5: 5 / 12,
|
||||
}
|
||||
for i, label in enumerate(["F", "E", "D", "C", "B", "A"]):
|
||||
for i, grid_label in enumerate(["F", "E", "D", "C", "B", "A"]):
|
||||
for y_index in [-0.5, 0.5]:
|
||||
grid_labels += Pos(
|
||||
x_centers[i] * frame_width,
|
||||
y_index * (frame_height + 1.5 * nominal_text_size),
|
||||
) * Sketch(Compound.make_text(label, nominal_text_size).wrapped)
|
||||
) * Sketch(Compound.make_text(grid_label, nominal_text_size).wrapped)
|
||||
|
||||
# Text Box Frame
|
||||
bf_pnt1 = frame_wire.edges().sort_by(Axis.Y)[0] @ 0.5
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ import math
|
|||
import xml.etree.ElementTree as ET
|
||||
from copy import copy
|
||||
from enum import Enum, auto
|
||||
from os import PathLike, fsdecode, fspath
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from os import PathLike, fsdecode
|
||||
from typing import Any, TypeAlias
|
||||
from warnings import warn
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
|
|
@ -45,16 +45,15 @@ import svgpathtools as PT
|
|||
from ezdxf import zoom
|
||||
from ezdxf.colors import RGB, aci2rgb
|
||||
from ezdxf.math import Vec2
|
||||
from OCP.BRepLib import BRepLib # type: ignore
|
||||
from OCP.BRepTools import BRepTools_WireExplorer # type: ignore
|
||||
from OCP.Geom import Geom_BezierCurve # type: ignore
|
||||
from OCP.GeomConvert import GeomConvert # type: ignore
|
||||
from OCP.GeomConvert import GeomConvert_BSplineCurveToBezierCurve # type: ignore
|
||||
from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Vec, gp_XYZ # type: ignore
|
||||
from OCP.HLRAlgo import HLRAlgo_Projector # type: ignore
|
||||
from OCP.HLRBRep import HLRBRep_Algo, HLRBRep_HLRToShape # type: ignore
|
||||
from OCP.TopAbs import TopAbs_Orientation, TopAbs_ShapeEnum # type: ignore
|
||||
from OCP.TopExp import TopExp_Explorer # type: ignore
|
||||
from OCP.BRepLib import BRepLib
|
||||
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
|
||||
from OCP.HLRAlgo import HLRAlgo_Projector
|
||||
from OCP.HLRBRep import HLRBRep_Algo, HLRBRep_HLRToShape
|
||||
from OCP.TopAbs import TopAbs_Orientation, TopAbs_ShapeEnum
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopoDS import TopoDS
|
||||
from typing_extensions import Self
|
||||
|
||||
|
|
@ -69,7 +68,8 @@ from build123d.topology import (
|
|||
)
|
||||
from build123d.build_common import UNITS_PER_METER
|
||||
|
||||
PathSegment = Union[PT.Line, PT.Arc, PT.QuadraticBezier, PT.CubicBezier]
|
||||
PathSegment: TypeAlias = PT.Line | PT.Arc | PT.QuadraticBezier | PT.CubicBezier
|
||||
"""A type alias for the various path segment types in the svgpathtools library."""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -82,7 +82,7 @@ class Drawing:
|
|||
self,
|
||||
shape: Shape,
|
||||
*,
|
||||
look_at: VectorLike = None,
|
||||
look_at: VectorLike | None = None,
|
||||
look_from: VectorLike = (1, -1, 1),
|
||||
look_up: VectorLike = (0, 0, 1),
|
||||
with_hidden: bool = True,
|
||||
|
|
@ -562,7 +562,7 @@ class ExportDXF(Export2D):
|
|||
"""
|
||||
# ezdxf :doc:`line type <ezdxf-stable:concepts/linetypes>`.
|
||||
|
||||
kwargs = {}
|
||||
kwargs: dict[str, Any] = {}
|
||||
|
||||
if line_type is not None:
|
||||
linetype = self._linetype(line_type)
|
||||
|
|
@ -587,7 +587,7 @@ class ExportDXF(Export2D):
|
|||
# The linetype is not in the doc yet.
|
||||
# Add it from our available definitions.
|
||||
if linetype in Export2D.LINETYPE_DEFS:
|
||||
desc, pattern = Export2D.LINETYPE_DEFS.get(linetype)
|
||||
desc, pattern = Export2D.LINETYPE_DEFS.get(linetype) # type: ignore[misc]
|
||||
self._document.linetypes.add(
|
||||
name=linetype,
|
||||
pattern=[self._linetype_scale * v for v in pattern],
|
||||
|
|
@ -605,7 +605,7 @@ class ExportDXF(Export2D):
|
|||
Adds a shape to the specified layer.
|
||||
|
||||
Args:
|
||||
shape (Union[Shape, Iterable[Shape]]): The shape or collection of shapes to be
|
||||
shape (Shape | Iterable[Shape]): The shape or collection of shapes to be
|
||||
added. It can be a single Shape object or an iterable of Shape objects.
|
||||
layer (str, optional): The name of the layer where the shape will be
|
||||
added. If not specified, the default layer will be used. Defaults to "".
|
||||
|
|
@ -641,8 +641,8 @@ class ExportDXF(Export2D):
|
|||
Writes the DXF data to the specified file name.
|
||||
|
||||
Args:
|
||||
file_name (Union[PathLike, str, bytes]): The file name (including path) where the DXF data will
|
||||
be written.
|
||||
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
|
||||
# extents of its entities.
|
||||
|
|
@ -757,6 +757,8 @@ class ExportDXF(Export2D):
|
|||
)
|
||||
|
||||
# need to apply the transform on the geometry level
|
||||
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)
|
||||
|
||||
|
|
@ -828,17 +830,17 @@ class ExportSVG(Export2D):
|
|||
should fit the strokes of the shapes. Defaults to True.
|
||||
precision (int, optional): The number of decimal places used for rounding
|
||||
coordinates in the SVG. Defaults to 6.
|
||||
fill_color (Union[ColorIndex, RGB, None], optional): The default fill color
|
||||
fill_color (ColorIndex | RGB | None, optional): The default fill color
|
||||
for shapes. It can be specified as a ColorIndex, an RGB tuple, or None.
|
||||
Defaults to None.
|
||||
line_color (Union[ColorIndex, RGB, None], optional): The default line color for
|
||||
line_color (ColorIndex | RGB | None, optional): The default line color for
|
||||
shapes. It can be specified as a ColorIndex or an RGB tuple, or None.
|
||||
Defaults to Export2D.DEFAULT_COLOR_INDEX.
|
||||
line_weight (float, optional): The default line weight (stroke width) for
|
||||
shapes, in millimeters. Defaults to Export2D.DEFAULT_LINE_WEIGHT.
|
||||
line_type (LineType, optional): The default line type for shapes. It should be
|
||||
a LineType enum. Defaults to Export2D.DEFAULT_LINE_TYPE.
|
||||
dot_length (Union[DotLength, float], optional): The width of rendered dots in a
|
||||
dot_length (DotLength | float, optional): The width of rendered dots in a
|
||||
Can be either a DotLength enum or a float value in tenths of an inch.
|
||||
Defaults to DotLength.INKSCAPE_COMPAT.
|
||||
|
||||
|
|
@ -878,21 +880,28 @@ class ExportSVG(Export2D):
|
|||
line_type: LineType,
|
||||
):
|
||||
def convert_color(
|
||||
c: ColorIndex | RGB | Color | None,
|
||||
input_color: ColorIndex | RGB | Color | None,
|
||||
) -> Color | None:
|
||||
if isinstance(c, ColorIndex):
|
||||
if isinstance(input_color, ColorIndex):
|
||||
# The easydxf color indices BLACK and WHITE have the same
|
||||
# value (7), and are both mapped to (255,255,255) by the
|
||||
# aci2rgb() function. We prefer (0,0,0).
|
||||
if c == ColorIndex.BLACK:
|
||||
c = RGB(0, 0, 0)
|
||||
if input_color == ColorIndex.BLACK:
|
||||
rgb_color = RGB(0, 0, 0)
|
||||
else:
|
||||
c = aci2rgb(c.value)
|
||||
elif isinstance(c, tuple):
|
||||
c = RGB(*c)
|
||||
if isinstance(c, RGB):
|
||||
c = Color(*c.to_floats(), 1)
|
||||
return c
|
||||
rgb_color = aci2rgb(input_color.value)
|
||||
elif isinstance(input_color, tuple):
|
||||
rgb_color = RGB(*input_color)
|
||||
else:
|
||||
rgb_color = input_color # If not ColorIndex or tuple, it's already RGB or None
|
||||
|
||||
if isinstance(rgb_color, RGB):
|
||||
red, green, blue = rgb_color.to_floats()
|
||||
final_color = Color(red, green, blue, 1.0)
|
||||
else:
|
||||
final_color = rgb_color # If not RGB, it's None or already a Color
|
||||
|
||||
return final_color
|
||||
|
||||
self.name = name
|
||||
self.fill_color = convert_color(fill_color)
|
||||
|
|
@ -929,7 +938,7 @@ class ExportSVG(Export2D):
|
|||
self.dot_length = dot_length
|
||||
self._non_planar_point_count = 0
|
||||
self._layers: dict[str, ExportSVG._Layer] = {}
|
||||
self._bounds: BoundBox = None
|
||||
self._bounds: BoundBox | None = None
|
||||
|
||||
# Add the default layer.
|
||||
self.add_layer(
|
||||
|
|
@ -957,10 +966,10 @@ class ExportSVG(Export2D):
|
|||
|
||||
Args:
|
||||
name (str): The name of the layer. Must be unique among all layers.
|
||||
fill_color (Union[ColorIndex, RGB, Color, None], optional): The fill color for shapes
|
||||
fill_color (ColorIndex | RGB | Color | None, optional): The fill color for shapes
|
||||
on this layer. It can be specified as a ColorIndex, an RGB tuple,
|
||||
a Color, or None. Defaults to None.
|
||||
line_color (Union[ColorIndex, RGB, Color, None], optional): The line color for shapes on
|
||||
line_color (ColorIndex | RGB | Color | None, optional): The line color for shapes on
|
||||
this layer. It can be specified as a ColorIndex or an RGB tuple,
|
||||
a Color, or None. Defaults to Export2D.DEFAULT_COLOR_INDEX.
|
||||
line_weight (float, optional): The line weight (stroke width) for shapes on
|
||||
|
|
@ -1002,7 +1011,7 @@ class ExportSVG(Export2D):
|
|||
Adds a shape or a collection of shapes to the specified layer.
|
||||
|
||||
Args:
|
||||
shape (Union[Shape, Iterable[Shape]]): The shape or collection of shapes to be
|
||||
shape (Shape | Iterable[Shape]): The shape or collection of shapes to be
|
||||
added. It can be a single Shape object or an iterable of Shape objects.
|
||||
layer (str, optional): The name of the layer where the shape(s) will be added.
|
||||
Defaults to "".
|
||||
|
|
@ -1014,12 +1023,12 @@ class ExportSVG(Export2D):
|
|||
"""
|
||||
if layer not in self._layers:
|
||||
raise ValueError(f"Undefined layer: {layer}.")
|
||||
layer = self._layers[layer]
|
||||
_layer = self._layers[layer]
|
||||
if isinstance(shape, Shape):
|
||||
self._add_single_shape(shape, layer, reverse_wires)
|
||||
self._add_single_shape(shape, _layer, reverse_wires)
|
||||
else:
|
||||
for s in shape:
|
||||
self._add_single_shape(s, layer, reverse_wires)
|
||||
self._add_single_shape(s, _layer, reverse_wires)
|
||||
|
||||
def _add_single_shape(self, shape: Shape, layer: _Layer, reverse_wires: bool):
|
||||
# pylint: disable=too-many-locals
|
||||
|
|
@ -1188,6 +1197,12 @@ class ExportSVG(Export2D):
|
|||
|
||||
def _circle_segments(self, edge: Edge, reverse: bool) -> list[PathSegment]:
|
||||
# pylint: disable=too-many-locals
|
||||
if edge.length < 1e-6:
|
||||
warn(
|
||||
"Skipping arc that is too small to export safely (length < 1e-6).",
|
||||
stacklevel=7,
|
||||
)
|
||||
return []
|
||||
curve = edge.geom_adaptor()
|
||||
circle = curve.Circle()
|
||||
radius = circle.Radius()
|
||||
|
|
@ -1234,6 +1249,12 @@ class ExportSVG(Export2D):
|
|||
|
||||
def _ellipse_segments(self, edge: Edge, reverse: bool) -> list[PathSegment]:
|
||||
# pylint: disable=too-many-locals
|
||||
if edge.length < 1e-6:
|
||||
warn(
|
||||
"Skipping ellipse that is too small to export safely (length < 1e-6).",
|
||||
stacklevel=7,
|
||||
)
|
||||
return []
|
||||
curve = edge.geom_adaptor()
|
||||
ellipse = curve.Ellipse()
|
||||
minor_radius = ellipse.MinorRadius()
|
||||
|
|
@ -1283,6 +1304,8 @@ class ExportSVG(Export2D):
|
|||
u2 = adaptor.LastParameter()
|
||||
|
||||
# Apply the shape location to the geometry.
|
||||
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)
|
||||
# describe_bspline(spline)
|
||||
|
|
@ -1347,6 +1370,8 @@ class ExportSVG(Export2D):
|
|||
}
|
||||
|
||||
def _edge_segments(self, edge: Edge, reverse: bool) -> list[PathSegment]:
|
||||
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
|
||||
segments = self._SEGMENT_LOOKUP.get(geom_type, ExportSVG._other_segments)
|
||||
|
|
@ -1391,10 +1416,12 @@ class ExportSVG(Export2D):
|
|||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def _group_for_layer(self, layer: _Layer, attribs: dict = None) -> ET.Element:
|
||||
def _color_attribs(c: Color) -> tuple[str, str]:
|
||||
if c:
|
||||
(r, g, b, a) = tuple(c)
|
||||
def _group_for_layer(
|
||||
self, layer: _Layer, attribs: dict | None = None
|
||||
) -> ET.Element:
|
||||
def _color_attribs(color: Color | None) -> tuple[str, str | None]:
|
||||
if color is not None:
|
||||
(r, g, b, a) = tuple(color)
|
||||
(r, g, b, a) = (int(r * 255), int(g * 255), int(b * 255), round(a, 3))
|
||||
rgb = f"rgb({r},{g},{b})"
|
||||
opacity = f"{a}" if a < 1 else None
|
||||
|
|
@ -1403,9 +1430,9 @@ class ExportSVG(Export2D):
|
|||
|
||||
if attribs is None:
|
||||
attribs = {}
|
||||
(fill, fill_opacity) = _color_attribs(layer.fill_color)
|
||||
fill, fill_opacity = _color_attribs(layer.fill_color)
|
||||
attribs["fill"] = fill
|
||||
if fill_opacity:
|
||||
if fill_opacity is not None:
|
||||
attribs["fill-opacity"] = fill_opacity
|
||||
(stroke, stroke_opacity) = _color_attribs(layer.line_color)
|
||||
attribs["stroke"] = stroke
|
||||
|
|
@ -1435,10 +1462,12 @@ class ExportSVG(Export2D):
|
|||
Writes the SVG data to the specified file path.
|
||||
|
||||
Args:
|
||||
path (Union[PathLike, str, bytes]): 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
|
||||
if bb is None:
|
||||
raise ValueError("No shapes to export.")
|
||||
doc_margin = self.margin
|
||||
if self.fit_to_stroke:
|
||||
max_line_weight = max(l.line_weight for l in self._layers.values())
|
||||
|
|
@ -1479,4 +1508,5 @@ class ExportSVG(Export2D):
|
|||
|
||||
xml = ET.ElementTree(svg)
|
||||
ET.indent(xml, " ")
|
||||
xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=False)
|
||||
# xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=False)
|
||||
xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=None)
|
||||
|
|
|
|||
|
|
@ -30,12 +30,11 @@ from __future__ import annotations
|
|||
|
||||
from math import radians, tan
|
||||
|
||||
from typing import Union
|
||||
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
|
||||
from build123d.topology import Compound, Part, Solid, tuplify
|
||||
from build123d.geometry import Location, Plane, Rotation, RotationLike
|
||||
from build123d.topology import Compound, Part, ShapeList, Solid, tuplify
|
||||
|
||||
|
||||
class BasePartObject(Part):
|
||||
|
|
@ -46,7 +45,7 @@ class BasePartObject(Part):
|
|||
Args:
|
||||
solid (Solid): object to create
|
||||
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0).
|
||||
align (Union[Align, tuple[Align, Align, Align]], optional): align min, center,
|
||||
align (Align | tuple[Align, Align, Align] | None, optional): align min, center,
|
||||
or max of object. Defaults to None.
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
|
|
@ -57,7 +56,7 @@ class BasePartObject(Part):
|
|||
self,
|
||||
part: Part | Solid,
|
||||
rotation: RotationLike = (0, 0, 0),
|
||||
align: Align | tuple[Align, Align, Align] = None,
|
||||
align: Align | tuple[Align, Align, Align] | None = None,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
if align is not None:
|
||||
|
|
@ -66,7 +65,7 @@ class BasePartObject(Part):
|
|||
offset = bbox.to_align_offset(align)
|
||||
part.move(Location(offset))
|
||||
|
||||
context: BuildPart = BuildPart._get_context(self, log=False)
|
||||
context: BuildPart | None = BuildPart._get_context(self, log=False)
|
||||
rotate = Rotation(*rotation) if isinstance(rotation, tuple) else rotation
|
||||
self.rotation = rotate
|
||||
if context is None:
|
||||
|
|
@ -111,7 +110,7 @@ class Box(BasePartObject):
|
|||
width (float): box size
|
||||
height (float): box size
|
||||
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0).
|
||||
align (Union[Align, tuple[Align, Align, Align]], optional): align min, center,
|
||||
align (Align | tuple[Align, Align, Align] | None, optional): align min, center,
|
||||
or max of object. Defaults to (Align.CENTER, Align.CENTER, Align.CENTER).
|
||||
mode (Mode, optional): combine mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
|
|
@ -131,7 +130,7 @@ class Box(BasePartObject):
|
|||
),
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context: BuildPart = BuildPart._get_context(self)
|
||||
context: BuildPart | None = BuildPart._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.length = length
|
||||
|
|
@ -156,7 +155,7 @@ class Cone(BasePartObject):
|
|||
height (float): cone size
|
||||
arc_size (float, optional): angular size of cone. Defaults to 360.
|
||||
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0).
|
||||
align (Union[Align, tuple[Align, Align, Align]], optional): align min, center,
|
||||
align (Align | tuple[Align, Align, Align] | None, optional): align min, center,
|
||||
or max of object. Defaults to (Align.CENTER, Align.CENTER, Align.CENTER).
|
||||
mode (Mode, optional): combine mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
|
|
@ -177,7 +176,7 @@ class Cone(BasePartObject):
|
|||
),
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context: BuildPart = BuildPart._get_context(self)
|
||||
context: BuildPart | None = BuildPart._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.bottom_radius = bottom_radius
|
||||
|
|
@ -218,10 +217,10 @@ class CounterBoreHole(BasePartObject):
|
|||
radius: float,
|
||||
counter_bore_radius: float,
|
||||
counter_bore_depth: float,
|
||||
depth: float = None,
|
||||
depth: float | None = None,
|
||||
mode: Mode = Mode.SUBTRACT,
|
||||
):
|
||||
context: BuildPart = BuildPart._get_context(self)
|
||||
context: BuildPart | None = BuildPart._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.radius = radius
|
||||
|
|
@ -235,7 +234,7 @@ class CounterBoreHole(BasePartObject):
|
|||
raise ValueError("No depth provided")
|
||||
self.mode = mode
|
||||
|
||||
solid = Solid.make_cylinder(
|
||||
fused = Solid.make_cylinder(
|
||||
radius, self.hole_depth, Plane(origin=(0, 0, 0), z_dir=(0, 0, -1))
|
||||
).fuse(
|
||||
Solid.make_cylinder(
|
||||
|
|
@ -244,6 +243,10 @@ class CounterBoreHole(BasePartObject):
|
|||
Plane((0, 0, -counter_bore_depth)),
|
||||
)
|
||||
)
|
||||
if isinstance(fused, ShapeList):
|
||||
solid = Part(fused)
|
||||
else:
|
||||
solid = fused
|
||||
super().__init__(part=solid, rotation=(0, 0, 0), mode=mode)
|
||||
|
||||
|
||||
|
|
@ -266,11 +269,11 @@ class CounterSinkHole(BasePartObject):
|
|||
self,
|
||||
radius: float,
|
||||
counter_sink_radius: float,
|
||||
depth: float = None,
|
||||
depth: float | None = None,
|
||||
counter_sink_angle: float = 82, # Common tip angle
|
||||
mode: Mode = Mode.SUBTRACT,
|
||||
):
|
||||
context: BuildPart = BuildPart._get_context(self)
|
||||
context: BuildPart | None = BuildPart._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.radius = radius
|
||||
|
|
@ -285,7 +288,7 @@ class CounterSinkHole(BasePartObject):
|
|||
self.mode = mode
|
||||
cone_height = counter_sink_radius / tan(radians(counter_sink_angle / 2.0))
|
||||
|
||||
solid = Solid.make_cylinder(
|
||||
fused = Solid.make_cylinder(
|
||||
radius, self.hole_depth, Plane(origin=(0, 0, 0), z_dir=(0, 0, -1))
|
||||
).fuse(
|
||||
Solid.make_cone(
|
||||
|
|
@ -296,6 +299,11 @@ class CounterSinkHole(BasePartObject):
|
|||
),
|
||||
Solid.make_cylinder(counter_sink_radius, self.hole_depth),
|
||||
)
|
||||
if isinstance(fused, ShapeList):
|
||||
solid = Part(fused)
|
||||
else:
|
||||
solid = fused
|
||||
|
||||
super().__init__(part=solid, rotation=(0, 0, 0), mode=mode)
|
||||
|
||||
|
||||
|
|
@ -309,7 +317,7 @@ class Cylinder(BasePartObject):
|
|||
height (float): cylinder size
|
||||
arc_size (float, optional): angular size of cone. Defaults to 360.
|
||||
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0).
|
||||
align (Union[Align, tuple[Align, Align, Align]], optional): align min, center,
|
||||
align (Align | tuple[Align, Align, Align] | None, optional): align min, center,
|
||||
or max of object. Defaults to (Align.CENTER, Align.CENTER, Align.CENTER).
|
||||
mode (Mode, optional): combine mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
|
|
@ -329,7 +337,7 @@ class Cylinder(BasePartObject):
|
|||
),
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context: BuildPart = BuildPart._get_context(self)
|
||||
context: BuildPart | None = BuildPart._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.radius = radius
|
||||
|
|
@ -363,10 +371,10 @@ class Hole(BasePartObject):
|
|||
def __init__(
|
||||
self,
|
||||
radius: float,
|
||||
depth: float = None,
|
||||
depth: float | None = None,
|
||||
mode: Mode = Mode.SUBTRACT,
|
||||
):
|
||||
context: BuildPart = BuildPart._get_context(self)
|
||||
context: BuildPart | None = BuildPart._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.radius = radius
|
||||
|
|
@ -405,7 +413,7 @@ class Sphere(BasePartObject):
|
|||
arc_size2 (float, optional): angular size of sphere. Defaults to 90.
|
||||
arc_size3 (float, optional): angular size of sphere. Defaults to 360.
|
||||
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0).
|
||||
align (Union[Align, tuple[Align, Align, Align]], optional): align min, center,
|
||||
align (Align | tuple[Align, Align, Align] | None, optional): align min, center,
|
||||
or max of object. Defaults to (Align.CENTER, Align.CENTER, Align.CENTER).
|
||||
mode (Mode, optional): combine mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
|
|
@ -426,7 +434,7 @@ class Sphere(BasePartObject):
|
|||
),
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context: BuildPart = BuildPart._get_context(self)
|
||||
context: BuildPart | None = BuildPart._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.radius = radius
|
||||
|
|
@ -458,7 +466,7 @@ class Torus(BasePartObject):
|
|||
major_arc_size (float, optional): angular size of torus. Defaults to 0.
|
||||
minor_arc_size (float, optional): angular size or torus. Defaults to 360.
|
||||
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0).
|
||||
align (Union[Align, tuple[Align, Align, Align]], optional): align min, center,
|
||||
align (Align | tuple[Align, Align, Align] | None, optional): align min, center,
|
||||
or max of object. Defaults to (Align.CENTER, Align.CENTER, Align.CENTER).
|
||||
mode (Mode, optional): combine mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
|
|
@ -480,7 +488,7 @@ class Torus(BasePartObject):
|
|||
),
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context: BuildPart = BuildPart._get_context(self)
|
||||
context: BuildPart | None = BuildPart._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.major_radius = major_radius
|
||||
|
|
@ -516,7 +524,7 @@ class Wedge(BasePartObject):
|
|||
xmax (float): maximum X location
|
||||
zmax (float): maximum Z location
|
||||
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0).
|
||||
align (Union[Align, tuple[Align, Align, Align]], optional): align min, center,
|
||||
align (Align | tuple[Align, Align, Align] | None, optional): align min, center,
|
||||
or max of object. Defaults to (Align.CENTER, Align.CENTER, Align.CENTER).
|
||||
mode (Mode, optional): combine mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
|
|
@ -540,7 +548,7 @@ class Wedge(BasePartObject):
|
|||
),
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context: BuildPart = BuildPart._get_context(self)
|
||||
context: BuildPart | None = BuildPart._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
if any([value <= 0 for value in [xsize, ysize, zsize]]):
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ from __future__ import annotations
|
|||
import trianglesolver
|
||||
|
||||
from math import cos, degrees, pi, radians, sin, tan
|
||||
from typing import Union
|
||||
from typing import cast
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ class BaseSketchObject(Sketch):
|
|||
align = tuplify(align, 2)
|
||||
obj.move(Location(obj.bounding_box().to_align_offset(align)))
|
||||
|
||||
context: BuildSketch = BuildSketch._get_context(self, log=False)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self, log=False)
|
||||
if context is None:
|
||||
new_faces = obj.moved(Rotation(0, 0, rotation)).faces()
|
||||
|
||||
|
|
@ -95,11 +95,11 @@ class BaseSketchObject(Sketch):
|
|||
|
||||
obj = obj.moved(Rotation(0, 0, rotation))
|
||||
|
||||
new_faces = [
|
||||
new_faces = ShapeList(
|
||||
face.moved(location)
|
||||
for face in obj.faces()
|
||||
for location in LocationList._get_context().local_locations
|
||||
]
|
||||
)
|
||||
if isinstance(context, BuildSketch):
|
||||
context._add_to_context(*new_faces, mode=mode)
|
||||
|
||||
|
|
@ -126,7 +126,7 @@ class Circle(BaseSketchObject):
|
|||
align: Align | tuple[Align, Align] | None = (Align.CENTER, Align.CENTER),
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.radius = radius
|
||||
|
|
@ -160,7 +160,7 @@ class Ellipse(BaseSketchObject):
|
|||
align: Align | tuple[Align, Align] | None = (Align.CENTER, Align.CENTER),
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.x_radius = x_radius
|
||||
|
|
@ -199,7 +199,7 @@ class Polygon(BaseSketchObject):
|
|||
align: Align | tuple[Align, Align] | None = (Align.CENTER, Align.CENTER),
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
flattened_pts = flatten_sequence(*pts)
|
||||
|
|
@ -235,7 +235,7 @@ class Rectangle(BaseSketchObject):
|
|||
align: Align | tuple[Align, Align] | None = (Align.CENTER, Align.CENTER),
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.width = width
|
||||
|
|
@ -272,7 +272,7 @@ class RectangleRounded(BaseSketchObject):
|
|||
align: Align | tuple[Align, Align] | None = (Align.CENTER, Align.CENTER),
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
if width <= 2 * radius or height <= 2 * radius:
|
||||
|
|
@ -317,7 +317,7 @@ class RegularPolygon(BaseSketchObject):
|
|||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
# pylint: disable=too-many-locals
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
if side_count < 3:
|
||||
|
|
@ -381,7 +381,7 @@ class SlotArc(BaseSketchObject):
|
|||
rotation: float = 0,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.arc = arc
|
||||
|
|
@ -417,7 +417,7 @@ class SlotCenterPoint(BaseSketchObject):
|
|||
rotation: float = 0,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
center_v = Vector(center)
|
||||
|
|
@ -472,7 +472,7 @@ class SlotCenterToCenter(BaseSketchObject):
|
|||
f"Requires center_separation > 0. Got: {center_separation=}"
|
||||
)
|
||||
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.center_separation = center_separation
|
||||
|
|
@ -518,14 +518,14 @@ class SlotOverall(BaseSketchObject):
|
|||
f"Slot requires that width > height. Got: {width=}, {height=}"
|
||||
)
|
||||
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.width = width
|
||||
self.slot_height = height
|
||||
|
||||
if width != height:
|
||||
face: Face | None = Face(
|
||||
face = Face(
|
||||
Wire(
|
||||
[
|
||||
Edge.make_line(Vector(-width / 2 + height / 2, 0, 0), Vector()),
|
||||
|
|
@ -534,7 +534,7 @@ class SlotOverall(BaseSketchObject):
|
|||
).offset_2d(height / 2)
|
||||
)
|
||||
else:
|
||||
face = Circle(width / 2, mode=mode).face()
|
||||
face = cast(Face, Circle(width / 2, mode=mode).face())
|
||||
super().__init__(face, rotation, align, mode)
|
||||
|
||||
|
||||
|
|
@ -574,7 +574,7 @@ class Text(BaseSketchObject):
|
|||
rotation: float = 0.0,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
self.txt = txt
|
||||
|
|
@ -633,7 +633,7 @@ class Trapezoid(BaseSketchObject):
|
|||
align: Align | tuple[Align, Align] | None = (Align.CENTER, Align.CENTER),
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
right_side_angle = left_side_angle if not right_side_angle else right_side_angle
|
||||
|
|
@ -720,7 +720,7 @@ class Triangle(BaseSketchObject):
|
|||
rotation: float = 0,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context = BuildSketch._get_context(self)
|
||||
context: BuildSketch | None = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
if [v is None for v in [a, b, c]].count(True) == 3 or [
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ license:
|
|||
import copy
|
||||
import logging
|
||||
from math import radians, tan
|
||||
from typing import Union
|
||||
from typing import cast, TypeAlias
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
|
@ -78,13 +78,13 @@ from build123d.topology import (
|
|||
logging.getLogger("build123d").addHandler(logging.NullHandler())
|
||||
logger = logging.getLogger("build123d")
|
||||
|
||||
#:TypeVar("AddType"): Type of objects which can be added to a builder
|
||||
AddType = Union[Edge, Wire, Face, Solid, Compound, Builder]
|
||||
AddType: TypeAlias = Edge | Wire | Face | Solid | Compound | Builder
|
||||
"""Type of objects which can be added to a builder"""
|
||||
|
||||
|
||||
def add(
|
||||
objects: AddType | Iterable[AddType],
|
||||
rotation: float | RotationLike = None,
|
||||
rotation: float | RotationLike | None = None,
|
||||
clean: bool = True,
|
||||
mode: Mode = Mode.ADD,
|
||||
) -> Compound:
|
||||
|
|
@ -101,22 +101,28 @@ def add(
|
|||
Edges and Wires are added to line.
|
||||
|
||||
Args:
|
||||
objects (Union[Edge, Wire, Face, Solid, Compound] or Iterable of): objects to add
|
||||
rotation (Union[float, RotationLike], optional): rotation angle for sketch,
|
||||
objects (Edge | Wire | Face | Solid | Compound or Iterable of): objects to add
|
||||
rotation (float | RotationLike, optional): rotation angle for sketch,
|
||||
rotation about each axis for part. Defaults to None.
|
||||
clean (bool, optional): Remove extraneous internal structure. Defaults to True.
|
||||
mode (Mode, optional): combine mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
context: Builder = Builder._get_context(None)
|
||||
context: Builder | None = Builder._get_context(None)
|
||||
if context is None:
|
||||
raise RuntimeError("Add must have an active builder context")
|
||||
|
||||
object_iter = objects if isinstance(objects, Iterable) else [objects]
|
||||
if isinstance(objects, Iterable) and not isinstance(objects, Compound):
|
||||
object_list = list(objects)
|
||||
else:
|
||||
object_list = [objects]
|
||||
object_iter = [
|
||||
obj.unwrap(fully=False) if isinstance(obj, Compound) else obj
|
||||
for obj in object_iter
|
||||
(
|
||||
obj.unwrap(fully=False)
|
||||
if isinstance(obj, Compound)
|
||||
else obj._obj if isinstance(obj, Builder) else obj
|
||||
)
|
||||
for obj in object_list
|
||||
]
|
||||
object_iter = [obj._obj if isinstance(obj, Builder) else obj for obj in object_iter]
|
||||
|
||||
validate_inputs(context, "add", object_iter)
|
||||
|
||||
|
|
@ -201,7 +207,7 @@ def add(
|
|||
|
||||
|
||||
def bounding_box(
|
||||
objects: Shape | Iterable[Shape] = None,
|
||||
objects: Shape | Iterable[Shape] | None = None,
|
||||
mode: Mode = Mode.PRIVATE,
|
||||
) -> Sketch | Part:
|
||||
"""Generic Operation: Add Bounding Box
|
||||
|
|
@ -214,7 +220,7 @@ def bounding_box(
|
|||
objects (Shape or Iterable of): objects to create bbox for
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
context: Builder = Builder._get_context("bounding_box")
|
||||
context: Builder | None = Builder._get_context("bounding_box")
|
||||
|
||||
if objects is None:
|
||||
if context is None or context is not None and context._obj is None:
|
||||
|
|
@ -261,16 +267,16 @@ def bounding_box(
|
|||
return Part(Compound(new_objects).wrapped)
|
||||
|
||||
|
||||
#:TypeVar("ChamferFilletType"): Type of objects which can be chamfered or filleted
|
||||
ChamferFilletType = Union[Edge, Vertex]
|
||||
ChamferFilletType: TypeAlias = Edge | Vertex
|
||||
"""Type of objects which can be chamfered or filleted"""
|
||||
|
||||
|
||||
def chamfer(
|
||||
objects: ChamferFilletType | Iterable[ChamferFilletType],
|
||||
length: float,
|
||||
length2: float = None,
|
||||
angle: float = None,
|
||||
reference: Edge | Face = None,
|
||||
length2: float | None = None,
|
||||
angle: float | None = None,
|
||||
reference: Edge | Face | None = None,
|
||||
) -> Sketch | Part:
|
||||
"""Generic Operation: chamfer
|
||||
|
||||
|
|
@ -279,11 +285,11 @@ def chamfer(
|
|||
Chamfer the given sequence of edges or vertices.
|
||||
|
||||
Args:
|
||||
objects (Union[Edge,Vertex] or Iterable of): edges or vertices to chamfer
|
||||
objects (Edge | Vertex or Iterable of): edges or vertices to chamfer
|
||||
length (float): chamfer size
|
||||
length2 (float, optional): asymmetric chamfer size. Defaults to None.
|
||||
angle (float, optional): chamfer angle in degrees. Defaults to None.
|
||||
reference (Union[Edge,Face]): identifies the side where length is measured. Edge(s) must
|
||||
reference (Edge | Face): identifies the side where length is measured. Edge(s) must
|
||||
be part of the face. Vertex/Vertices must be part of edge
|
||||
|
||||
Raises:
|
||||
|
|
@ -293,7 +299,7 @@ def chamfer(
|
|||
ValueError: Only one of length2 or angle should be provided
|
||||
ValueError: reference can only be used in conjunction with length2 or angle
|
||||
"""
|
||||
context: Builder = Builder._get_context("chamfer")
|
||||
context: Builder | None = Builder._get_context("chamfer")
|
||||
if length2 and angle:
|
||||
raise ValueError("Only one of length2 or angle should be provided")
|
||||
|
||||
|
|
@ -367,24 +373,28 @@ def chamfer(
|
|||
raise ValueError("1D fillet operation takes only Vertices")
|
||||
# Remove any end vertices as these can't be filleted
|
||||
if not target.is_closed:
|
||||
object_list = filter(
|
||||
lambda v: not (
|
||||
isclose_b(
|
||||
(Vector(*v.to_tuple()) - target.position_at(0)).length,
|
||||
0.0,
|
||||
)
|
||||
or isclose_b(
|
||||
(Vector(*v.to_tuple()) - target.position_at(1)).length,
|
||||
0.0,
|
||||
)
|
||||
),
|
||||
object_list,
|
||||
object_list = ShapeList(
|
||||
filter(
|
||||
lambda v: not (
|
||||
isclose_b(
|
||||
(Vector(*v.to_tuple()) - target.position_at(0)).length,
|
||||
0.0,
|
||||
)
|
||||
or isclose_b(
|
||||
(Vector(*v.to_tuple()) - target.position_at(1)).length,
|
||||
0.0,
|
||||
)
|
||||
),
|
||||
object_list,
|
||||
)
|
||||
)
|
||||
new_wire = target.chamfer_2d(length, length2, object_list, reference)
|
||||
if context is not None:
|
||||
context._add_to_context(new_wire, mode=Mode.REPLACE)
|
||||
return new_wire
|
||||
|
||||
raise ValueError("Invalid object dimension")
|
||||
|
||||
|
||||
def fillet(
|
||||
objects: ChamferFilletType | Iterable[ChamferFilletType],
|
||||
|
|
@ -398,7 +408,7 @@ def fillet(
|
|||
either end of an open line will be automatically skipped.
|
||||
|
||||
Args:
|
||||
objects (Union[Edge,Vertex] or Iterable of): edges or vertices to fillet
|
||||
objects (Edge | Vertex or Iterable of): edges or vertices to fillet
|
||||
radius (float): fillet size - must be less than 1/2 local width
|
||||
|
||||
Raises:
|
||||
|
|
@ -407,7 +417,7 @@ def fillet(
|
|||
ValueError: objects must be Vertices
|
||||
ValueError: nothing to fillet
|
||||
"""
|
||||
context: Builder = Builder._get_context("fillet")
|
||||
context: Builder | None = Builder._get_context("fillet")
|
||||
if (objects is None and context is None) or (
|
||||
objects is None and context is not None and context._obj is None
|
||||
):
|
||||
|
|
@ -466,31 +476,35 @@ def fillet(
|
|||
raise ValueError("1D fillet operation takes only Vertices")
|
||||
# Remove any end vertices as these can't be filleted
|
||||
if not target.is_closed:
|
||||
object_list = filter(
|
||||
lambda v: not (
|
||||
isclose_b(
|
||||
(Vector(*v.to_tuple()) - target.position_at(0)).length,
|
||||
0.0,
|
||||
)
|
||||
or isclose_b(
|
||||
(Vector(*v.to_tuple()) - target.position_at(1)).length,
|
||||
0.0,
|
||||
)
|
||||
),
|
||||
object_list,
|
||||
object_list = ShapeList(
|
||||
filter(
|
||||
lambda v: not (
|
||||
isclose_b(
|
||||
(Vector(*v.to_tuple()) - target.position_at(0)).length,
|
||||
0.0,
|
||||
)
|
||||
or isclose_b(
|
||||
(Vector(*v.to_tuple()) - target.position_at(1)).length,
|
||||
0.0,
|
||||
)
|
||||
),
|
||||
object_list,
|
||||
)
|
||||
)
|
||||
new_wire = target.fillet_2d(radius, object_list)
|
||||
if context is not None:
|
||||
context._add_to_context(new_wire, mode=Mode.REPLACE)
|
||||
return new_wire
|
||||
|
||||
raise ValueError("Invalid object dimension")
|
||||
|
||||
#:TypeVar("MirrorType"): Type of objects which can be mirrored
|
||||
MirrorType = Union[Edge, Wire, Face, Compound, Curve, Sketch, Part]
|
||||
|
||||
MirrorType: TypeAlias = Edge | Wire | Face | Compound | Curve | Sketch | Part
|
||||
"""Type of objects which can be mirrored"""
|
||||
|
||||
|
||||
def mirror(
|
||||
objects: MirrorType | Iterable[MirrorType] = None,
|
||||
objects: MirrorType | Iterable[MirrorType] | None = None,
|
||||
about: Plane = Plane.XZ,
|
||||
mode: Mode = Mode.ADD,
|
||||
) -> Curve | Sketch | Part | Compound:
|
||||
|
|
@ -501,15 +515,18 @@ def mirror(
|
|||
Mirror a sequence of objects over the given plane.
|
||||
|
||||
Args:
|
||||
objects (Union[Edge, Face,Compound] or Iterable of): objects to mirror
|
||||
objects (Edge | Face | Compound or Iterable of): objects to mirror
|
||||
about (Plane, optional): reference plane. Defaults to "XZ".
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
|
||||
Raises:
|
||||
ValueError: missing objects
|
||||
"""
|
||||
context: Builder = Builder._get_context("mirror")
|
||||
object_list = objects if isinstance(objects, Iterable) else [objects]
|
||||
context: Builder | None = Builder._get_context("mirror")
|
||||
if isinstance(objects, Iterable) and not isinstance(objects, Compound):
|
||||
object_list = list(objects)
|
||||
else:
|
||||
object_list = [objects]
|
||||
|
||||
if objects is None:
|
||||
if context is None or context is not None and context._obj is None:
|
||||
|
|
@ -535,18 +552,18 @@ def mirror(
|
|||
return mirrored_compound
|
||||
|
||||
|
||||
#:TypeVar("OffsetType"): Type of objects which can be offset
|
||||
OffsetType = Union[Edge, Face, Solid, Compound]
|
||||
OffsetType: TypeAlias = Edge | Face | Solid | Compound
|
||||
"""Type of objects which can be offset"""
|
||||
|
||||
|
||||
def offset(
|
||||
objects: OffsetType | Iterable[OffsetType] = None,
|
||||
objects: OffsetType | Iterable[OffsetType] | None = None,
|
||||
amount: float = 0,
|
||||
openings: Face | list[Face] = None,
|
||||
openings: Face | list[Face] | None = None,
|
||||
kind: Kind = Kind.ARC,
|
||||
side: Side = Side.BOTH,
|
||||
closed: bool = True,
|
||||
min_edge_length: float = None,
|
||||
min_edge_length: float | None = None,
|
||||
mode: Mode = Mode.REPLACE,
|
||||
) -> Curve | Sketch | Part | Compound:
|
||||
"""Generic Operation: offset
|
||||
|
|
@ -559,7 +576,7 @@ def offset(
|
|||
a hollow box with no lid.
|
||||
|
||||
Args:
|
||||
objects (Union[Edge, Face, Solid, Compound] or Iterable of): objects to offset
|
||||
objects (Edge | Face | Solid | Compound or Iterable of): objects to offset
|
||||
amount (float): positive values external, negative internal
|
||||
openings (list[Face], optional), sequence of faces to open in part.
|
||||
Defaults to None.
|
||||
|
|
@ -575,7 +592,7 @@ def offset(
|
|||
ValueError: missing objects
|
||||
ValueError: Invalid object type
|
||||
"""
|
||||
context: Builder = Builder._get_context("offset")
|
||||
context: Builder | None = Builder._get_context("offset")
|
||||
|
||||
if objects is None:
|
||||
if context is None or context is not None and context._obj is None:
|
||||
|
|
@ -624,15 +641,19 @@ def offset(
|
|||
pass
|
||||
# inner wires may go beyond the outer wire so subtract faces
|
||||
new_face = Face(outer_wire)
|
||||
if inner_wires:
|
||||
inner_faces = [Face(w) for w in inner_wires]
|
||||
new_face = new_face.cut(*inner_faces)
|
||||
if isinstance(new_face, Compound):
|
||||
new_face = new_face.unwrap(fully=True)
|
||||
|
||||
if (new_face.normal_at() - face.normal_at()).length > 0.001:
|
||||
new_face = -new_face
|
||||
new_faces.append(new_face)
|
||||
if inner_wires:
|
||||
inner_faces = [Face(w) for w in inner_wires]
|
||||
subtraction = new_face.cut(*inner_faces)
|
||||
if isinstance(subtraction, Compound):
|
||||
new_faces.append(new_face.unwrap(fully=True))
|
||||
elif isinstance(subtraction, ShapeList):
|
||||
new_faces.extend(subtraction)
|
||||
else:
|
||||
new_faces.append(subtraction)
|
||||
else:
|
||||
new_faces.append(new_face)
|
||||
if edges:
|
||||
if len(edges) == 1 and edges[0].geom_type == GeomType.LINE:
|
||||
new_wires = [
|
||||
|
|
@ -679,14 +700,14 @@ def offset(
|
|||
return offset_compound
|
||||
|
||||
|
||||
#:TypeVar("ProjectType"): Type of objects which can be projected
|
||||
ProjectType = Union[Edge, Face, Wire, Vector, Vertex]
|
||||
ProjectType: TypeAlias = Edge | Face | Wire | Vector | Vertex
|
||||
"""Type of objects which can be projected"""
|
||||
|
||||
|
||||
def project(
|
||||
objects: ProjectType | Iterable[ProjectType] = None,
|
||||
workplane: Plane = None,
|
||||
target: Solid | Compound | Part = None,
|
||||
objects: ProjectType | Iterable[ProjectType] | None = None,
|
||||
workplane: Plane | None = None,
|
||||
target: Solid | Compound | Part | None = None,
|
||||
mode: Mode = Mode.ADD,
|
||||
) -> Curve | Sketch | Compound | ShapeList[Vector]:
|
||||
"""Generic Operation: project
|
||||
|
|
@ -704,7 +725,7 @@ def project(
|
|||
BuildSketch and Edge/Wires into BuildLine.
|
||||
|
||||
Args:
|
||||
objects (Union[Edge, Face, Wire, VectorLike, Vertex] or Iterable of):
|
||||
objects (Edge | Face | Wire | VectorLike | Vertex or Iterable of):
|
||||
objects or points to project
|
||||
workplane (Plane, optional): screen workplane
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
|
|
@ -716,7 +737,7 @@ def project(
|
|||
ValueError: Edges, wires and points can only be projected in PRIVATE mode
|
||||
RuntimeError: BuildPart doesn't have a project operation
|
||||
"""
|
||||
context: Builder = Builder._get_context("project")
|
||||
context: Builder | None = Builder._get_context("project")
|
||||
|
||||
if isinstance(objects, GroupBy):
|
||||
raise ValueError("project doesn't accept group_by, did you miss [n]?")
|
||||
|
|
@ -742,8 +763,8 @@ def project(
|
|||
]
|
||||
object_size = Compound(children=shape_list).bounding_box(optimal=False).diagonal
|
||||
|
||||
point_list = [o for o in object_list if isinstance(o, (Vector, Vertex))]
|
||||
point_list = [Vector(pnt) for pnt in point_list]
|
||||
vct_vrt_list = [o for o in object_list if isinstance(o, (Vector, Vertex))]
|
||||
point_list = [Vector(pnt) for pnt in vct_vrt_list]
|
||||
face_list = [o for o in object_list if isinstance(o, Face)]
|
||||
line_list = [o for o in object_list if isinstance(o, (Edge, Wire))]
|
||||
|
||||
|
|
@ -764,6 +785,7 @@ def project(
|
|||
raise ValueError(
|
||||
"Edges, wires and points can only be projected in PRIVATE mode"
|
||||
)
|
||||
working_plane = cast(Plane, workplane)
|
||||
|
||||
# BuildLine and BuildSketch are from target to workplane while BuildPart is
|
||||
# from workplane to target so the projection direction needs to be flipped
|
||||
|
|
@ -775,7 +797,7 @@ def project(
|
|||
target = context._obj
|
||||
projection_flip = -1
|
||||
else:
|
||||
target = Face.make_rect(3 * object_size, 3 * object_size, plane=workplane)
|
||||
target = Face.make_rect(3 * object_size, 3 * object_size, plane=working_plane)
|
||||
|
||||
validate_inputs(context, "project")
|
||||
|
||||
|
|
@ -783,37 +805,39 @@ def project(
|
|||
obj: Shape
|
||||
for obj in face_list + line_list:
|
||||
obj_to_screen = (target.center() - obj.center()).normalized()
|
||||
if workplane.from_local_coords(obj_to_screen).Z < 0:
|
||||
projection_direction = -workplane.z_dir * projection_flip
|
||||
if working_plane.from_local_coords(obj_to_screen).Z < 0:
|
||||
projection_direction = -working_plane.z_dir * projection_flip
|
||||
else:
|
||||
projection_direction = workplane.z_dir * projection_flip
|
||||
projection_direction = working_plane.z_dir * projection_flip
|
||||
projection = obj.project_to_shape(target, projection_direction)
|
||||
if projection:
|
||||
if isinstance(context, BuildSketch):
|
||||
projected_shapes.extend(
|
||||
[workplane.to_local_coords(p) for p in projection]
|
||||
[working_plane.to_local_coords(p) for p in projection]
|
||||
)
|
||||
elif isinstance(context, BuildLine):
|
||||
projected_shapes.extend(projection)
|
||||
else: # BuildPart
|
||||
projected_shapes.append(projection[0])
|
||||
|
||||
projected_points = []
|
||||
projected_points: ShapeList[Vector] = ShapeList()
|
||||
for pnt in point_list:
|
||||
pnt_to_target = (workplane.origin - pnt).normalized()
|
||||
if workplane.from_local_coords(pnt_to_target).Z < 0:
|
||||
projection_axis = -Axis(pnt, workplane.z_dir * projection_flip)
|
||||
pnt_to_target = (working_plane.origin - pnt).normalized()
|
||||
if working_plane.from_local_coords(pnt_to_target).Z < 0:
|
||||
projection_axis = -Axis(pnt, working_plane.z_dir * projection_flip)
|
||||
else:
|
||||
projection_axis = Axis(pnt, workplane.z_dir * projection_flip)
|
||||
projection = workplane.to_local_coords(workplane.intersect(projection_axis))
|
||||
if projection is not None:
|
||||
projected_points.append(projection)
|
||||
projection_axis = Axis(pnt, working_plane.z_dir * projection_flip)
|
||||
intersection = working_plane.intersect(projection_axis)
|
||||
if isinstance(intersection, Axis):
|
||||
raise RuntimeError("working_plane and projection_axis are parallel")
|
||||
if intersection is not None:
|
||||
projected_points.append(working_plane.to_local_coords(intersection))
|
||||
|
||||
if context is not None:
|
||||
context._add_to_context(*projected_shapes, mode=mode)
|
||||
|
||||
if projected_points:
|
||||
result = ShapeList(projected_points)
|
||||
result = projected_points
|
||||
else:
|
||||
result = Compound(projected_shapes)
|
||||
if all([obj._dim == 2 for obj in object_list]):
|
||||
|
|
@ -825,7 +849,7 @@ def project(
|
|||
|
||||
|
||||
def scale(
|
||||
objects: Shape | Iterable[Shape] = None,
|
||||
objects: Shape | Iterable[Shape] | None = None,
|
||||
by: float | tuple[float, float, float] = 1,
|
||||
mode: Mode = Mode.REPLACE,
|
||||
) -> Curve | Sketch | Part | Compound:
|
||||
|
|
@ -838,14 +862,14 @@ def scale(
|
|||
line, circle, etc.
|
||||
|
||||
Args:
|
||||
objects (Union[Edge, Face, Compound, Solid] or Iterable of): objects to scale
|
||||
by (Union[float, tuple[float, float, float]]): scale factor
|
||||
objects (Edge | Face | Compound | Solid or Iterable of): objects to scale
|
||||
by (float | tuple[float, float, float]): scale factor
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.REPLACE.
|
||||
|
||||
Raises:
|
||||
ValueError: missing objects
|
||||
"""
|
||||
context: Builder = Builder._get_context("scale")
|
||||
context: Builder | None = Builder._get_context("scale")
|
||||
|
||||
if objects is None:
|
||||
if context is None or context is not None and context._obj is None:
|
||||
|
|
@ -863,12 +887,12 @@ def scale(
|
|||
and len(by) == 3
|
||||
and all(isinstance(s, (int, float)) for s in by)
|
||||
):
|
||||
factor = Vector(by)
|
||||
by_vector = Vector(by)
|
||||
scale_matrix = Matrix(
|
||||
[
|
||||
[factor.X, 0.0, 0.0, 0.0],
|
||||
[0.0, factor.Y, 0.0, 0.0],
|
||||
[0.0, 0.0, factor.Z, 0.0],
|
||||
[by_vector.X, 0.0, 0.0, 0.0],
|
||||
[0.0, by_vector.Y, 0.0, 0.0],
|
||||
[0.0, 0.0, by_vector.Z, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
]
|
||||
)
|
||||
|
|
@ -877,9 +901,12 @@ def scale(
|
|||
|
||||
new_objects = []
|
||||
for obj in object_list:
|
||||
if obj is None:
|
||||
continue
|
||||
current_location = obj.location
|
||||
assert current_location is not None
|
||||
obj_at_origin = obj.located(Location(Vector()))
|
||||
if isinstance(factor, float):
|
||||
if isinstance(by, (int, float)):
|
||||
new_object = obj_at_origin.scale(factor).locate(current_location)
|
||||
else:
|
||||
new_object = obj_at_origin.transform_geometry(scale_matrix).locate(
|
||||
|
|
@ -900,12 +927,12 @@ def scale(
|
|||
return scale_compound.unwrap(fully=False)
|
||||
|
||||
|
||||
#:TypeVar("SplitType"): Type of objects which can be offset
|
||||
SplitType = Union[Edge, Wire, Face, Solid]
|
||||
SplitType: TypeAlias = Edge | Wire | Face | Solid
|
||||
"""Type of objects which can be split"""
|
||||
|
||||
|
||||
def split(
|
||||
objects: SplitType | Iterable[SplitType] = None,
|
||||
objects: SplitType | Iterable[SplitType] | None = None,
|
||||
bisect_by: Plane | Face | Shell = Plane.XZ,
|
||||
keep: Keep = Keep.TOP,
|
||||
mode: Mode = Mode.REPLACE,
|
||||
|
|
@ -917,8 +944,8 @@ def split(
|
|||
Bisect object with plane and keep either top, bottom or both.
|
||||
|
||||
Args:
|
||||
objects (Union[Edge, Wire, Face, Solid] or Iterable of), objects to split
|
||||
bisect_by (Union[Plane, Face], optional): plane to segment part.
|
||||
objects (Edge | Wire | Face | Solid or Iterable of), objects to split
|
||||
bisect_by (Plane | Face, optional): plane to segment part.
|
||||
Defaults to Plane.XZ.
|
||||
keep (Keep, optional): selector for which segment to keep. Defaults to Keep.TOP.
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.REPLACE.
|
||||
|
|
@ -926,7 +953,7 @@ def split(
|
|||
Raises:
|
||||
ValueError: missing objects
|
||||
"""
|
||||
context: Builder = Builder._get_context("split")
|
||||
context: Builder | None = Builder._get_context("split")
|
||||
|
||||
if objects is None:
|
||||
if context is None or context is not None and context._obj is None:
|
||||
|
|
@ -937,7 +964,7 @@ def split(
|
|||
|
||||
validate_inputs(context, "split", object_list)
|
||||
|
||||
new_objects = []
|
||||
new_objects: list[SplitType] = []
|
||||
for obj in object_list:
|
||||
bottom = None
|
||||
if keep == Keep.BOTH:
|
||||
|
|
@ -963,18 +990,18 @@ def split(
|
|||
return split_compound
|
||||
|
||||
|
||||
#:TypeVar("SweepType"): Type of objects which can be swept
|
||||
SweepType = Union[Compound, Edge, Wire, Face, Solid]
|
||||
SweepType: TypeAlias = Compound | Edge | Wire | Face | Solid
|
||||
"""Type of objects which can be swept"""
|
||||
|
||||
|
||||
def sweep(
|
||||
sections: SweepType | Iterable[SweepType] = None,
|
||||
path: Curve | Edge | Wire | Iterable[Edge] = None,
|
||||
sections: SweepType | Iterable[SweepType] | None = None,
|
||||
path: Curve | Edge | Wire | Iterable[Edge] | None = None,
|
||||
multisection: bool = False,
|
||||
is_frenet: bool = False,
|
||||
transition: Transition = Transition.TRANSFORMED,
|
||||
normal: VectorLike = None,
|
||||
binormal: Edge | Wire = None,
|
||||
normal: VectorLike | None = None,
|
||||
binormal: Edge | Wire | None = None,
|
||||
clean: bool = True,
|
||||
mode: Mode = Mode.ADD,
|
||||
) -> Part | Sketch:
|
||||
|
|
@ -983,19 +1010,19 @@ def sweep(
|
|||
Sweep pending 1D or 2D objects along path.
|
||||
|
||||
Args:
|
||||
sections (Union[Compound, Edge, Wire, Face, Solid]): cross sections to sweep into object
|
||||
path (Union[Curve, Edge, Wire], optional): path to follow.
|
||||
sections (Compound | Edge | Wire | Face | Solid): cross sections to sweep into object
|
||||
path (Curve | Edge | Wire, optional): path to follow.
|
||||
Defaults to context pending_edges.
|
||||
multisection (bool, optional): sweep multiple on path. Defaults to False.
|
||||
is_frenet (bool, optional): use frenet algorithm. Defaults to False.
|
||||
transition (Transition, optional): discontinuity handling option.
|
||||
Defaults to Transition.TRANSFORMED.
|
||||
normal (VectorLike, optional): fixed normal. Defaults to None.
|
||||
binormal (Union[Edge, Wire], optional): guide rotation along path. Defaults to None.
|
||||
binormal (Edge | Wire, optional): guide rotation along path. Defaults to None.
|
||||
clean (bool, optional): Remove extraneous internal structure. Defaults to True.
|
||||
mode (Mode, optional): combination. Defaults to Mode.ADD.
|
||||
"""
|
||||
context: Builder = Builder._get_context("sweep")
|
||||
context: Builder | None = Builder._get_context("sweep")
|
||||
|
||||
section_list = (
|
||||
[*sections] if isinstance(sections, (list, tuple, filter)) else [sections]
|
||||
|
|
@ -1005,7 +1032,11 @@ def sweep(
|
|||
validate_inputs(context, "sweep", section_list)
|
||||
|
||||
if path is None:
|
||||
if context is None or context is not None and not context.pending_edges:
|
||||
if (
|
||||
context is None
|
||||
or not isinstance(context, (BuildPart, BuildSketch))
|
||||
or not context.pending_edges
|
||||
):
|
||||
raise ValueError("path must be provided")
|
||||
path_wire = Wire(context.pending_edges)
|
||||
context.pending_edges = []
|
||||
|
|
@ -1030,8 +1061,8 @@ def sweep(
|
|||
else:
|
||||
raise ValueError("No sections provided")
|
||||
|
||||
edge_list = []
|
||||
face_list = []
|
||||
edge_list: list[Edge] = []
|
||||
face_list: list[Face] = []
|
||||
for sec in section_list:
|
||||
if isinstance(sec, (Curve, Wire, Edge)):
|
||||
edge_list.extend(sec.edges())
|
||||
|
|
@ -1040,6 +1071,7 @@ def sweep(
|
|||
|
||||
# sweep to create solids
|
||||
new_solids = []
|
||||
binormal_mode: Wire | Vector | None
|
||||
if face_list:
|
||||
if binormal is None and normal is not None:
|
||||
binormal_mode = Vector(normal)
|
||||
|
|
@ -1066,7 +1098,7 @@ def sweep(
|
|||
]
|
||||
|
||||
# sweep to create faces
|
||||
new_faces = []
|
||||
new_faces: list[Face] = []
|
||||
if edge_list:
|
||||
for sec in section_list:
|
||||
swept = Shell.sweep(sec, path_wire, transition)
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ license:
|
|||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Union
|
||||
|
||||
from collections.abc import Iterable
|
||||
from scipy.spatial import Voronoi
|
||||
from build123d.build_enums import Mode, SortBy
|
||||
from build123d.topology import (
|
||||
Compound,
|
||||
|
|
@ -46,7 +46,6 @@ from build123d.topology import (
|
|||
from build123d.geometry import Vector, TOLERANCE
|
||||
from build123d.build_common import flatten_sequence, validate_inputs
|
||||
from build123d.build_sketch import BuildSketch
|
||||
from scipy.spatial import Voronoi
|
||||
|
||||
|
||||
def full_round(
|
||||
|
|
@ -77,7 +76,7 @@ def full_round(
|
|||
geometric center of the arc, and the third the radius of the arc
|
||||
|
||||
"""
|
||||
context: BuildSketch = BuildSketch._get_context("full_round")
|
||||
context: BuildSketch | None = BuildSketch._get_context("full_round")
|
||||
|
||||
if not isinstance(edge, Edge):
|
||||
raise ValueError("A single Edge must be provided")
|
||||
|
|
@ -108,7 +107,11 @@ def full_round(
|
|||
# Refine the largest empty circle center estimate by averaging the best
|
||||
# three candidates. The minimum distance between the edges and this
|
||||
# center is the circle radius.
|
||||
best_three = [(float("inf"), None), (float("inf"), None), (float("inf"), None)]
|
||||
best_three: list[tuple[float, int]] = [
|
||||
(float("inf"), int()),
|
||||
(float("inf"), int()),
|
||||
(float("inf"), int()),
|
||||
]
|
||||
|
||||
for i, v in enumerate(voronoi_vertices):
|
||||
distances = [edge_group[i].distance_to(v) for i in range(3)]
|
||||
|
|
@ -125,7 +128,9 @@ def full_round(
|
|||
|
||||
# Extract the indices of the best three and average them
|
||||
best_indices = [x[1] for x in best_three]
|
||||
voronoi_circle_center = sum(voronoi_vertices[i] for i in best_indices) / 3
|
||||
voronoi_circle_center: Vector = (
|
||||
sum((voronoi_vertices[i] for i in best_indices), Vector(0, 0, 0)) / 3.0
|
||||
)
|
||||
|
||||
# Determine where the connected edges intersect with the largest empty circle
|
||||
connected_edges_end_points = [
|
||||
|
|
@ -142,7 +147,7 @@ def full_round(
|
|||
for i, e in enumerate(connected_edges)
|
||||
]
|
||||
for param in connected_edges_end_params:
|
||||
if not (0.0 < param < 1.0):
|
||||
if not 0.0 < param < 1.0:
|
||||
raise ValueError("Invalid geometry to create the end arc")
|
||||
|
||||
common_vertex_points = [
|
||||
|
|
@ -177,7 +182,14 @@ def full_round(
|
|||
)
|
||||
|
||||
# Recover other edges
|
||||
other_edges = edge.topo_parent.edges() - topo_explore_connected_edges(edge) - [edge]
|
||||
if edge.topo_parent is None:
|
||||
other_edges: ShapeList[Edge] = ShapeList()
|
||||
else:
|
||||
other_edges = (
|
||||
edge.topo_parent.edges()
|
||||
- topo_explore_connected_edges(edge)
|
||||
- ShapeList([edge])
|
||||
)
|
||||
|
||||
# Rebuild the face
|
||||
# Note that the longest wire must be the perimeter and others holes
|
||||
|
|
@ -195,7 +207,7 @@ def full_round(
|
|||
|
||||
|
||||
def make_face(
|
||||
edges: Edge | Iterable[Edge] = None, mode: Mode = Mode.ADD
|
||||
edges: Edge | Iterable[Edge] | None = None, mode: Mode = Mode.ADD
|
||||
) -> Sketch:
|
||||
"""Sketch Operation: make_face
|
||||
|
||||
|
|
@ -206,7 +218,7 @@ def make_face(
|
|||
sketch pending edges.
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
context: BuildSketch = BuildSketch._get_context("make_face")
|
||||
context: BuildSketch | None = BuildSketch._get_context("make_face")
|
||||
|
||||
if edges is not None:
|
||||
outer_edges = flatten_sequence(edges)
|
||||
|
|
@ -230,7 +242,7 @@ def make_face(
|
|||
|
||||
|
||||
def make_hull(
|
||||
edges: Edge | Iterable[Edge] = None, mode: Mode = Mode.ADD
|
||||
edges: Edge | Iterable[Edge] | None = None, mode: Mode = Mode.ADD
|
||||
) -> Sketch:
|
||||
"""Sketch Operation: make_hull
|
||||
|
||||
|
|
@ -241,7 +253,7 @@ def make_hull(
|
|||
sketch pending edges.
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
context: BuildSketch = BuildSketch._get_context("make_hull")
|
||||
context: BuildSketch | None = BuildSketch._get_context("make_hull")
|
||||
|
||||
if edges is not None:
|
||||
hull_edges = flatten_sequence(edges)
|
||||
|
|
@ -268,7 +280,7 @@ def make_hull(
|
|||
|
||||
|
||||
def trace(
|
||||
lines: Curve | Edge | Wire | Iterable[Curve | Edge | Wire] = None,
|
||||
lines: Curve | Edge | Wire | Iterable[Curve | Edge | Wire] | None = None,
|
||||
line_width: float = 1,
|
||||
mode: Mode = Mode.ADD,
|
||||
) -> Sketch:
|
||||
|
|
@ -277,7 +289,7 @@ def trace(
|
|||
Convert edges, wires or pending edges into faces by sweeping a perpendicular line along them.
|
||||
|
||||
Args:
|
||||
lines (Union[Curve, Edge, Wire, Iterable[Union[Curve, Edge, Wire]]], optional): lines to
|
||||
lines (Curve | Edge | Wire | Iterable[Curve | Edge | Wire]], optional): lines to
|
||||
trace. Defaults to sketch pending edges.
|
||||
line_width (float, optional): Defaults to 1.
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
|
|
@ -288,7 +300,7 @@ def trace(
|
|||
Returns:
|
||||
Sketch: Traced lines
|
||||
"""
|
||||
context: BuildSketch = BuildSketch._get_context("trace")
|
||||
context: BuildSketch | None = BuildSketch._get_context("trace")
|
||||
|
||||
if lines is not None:
|
||||
trace_lines = flatten_sequence(lines)
|
||||
|
|
@ -298,7 +310,7 @@ def trace(
|
|||
else:
|
||||
raise ValueError("No objects to trace")
|
||||
|
||||
new_faces = []
|
||||
new_faces: list[Face] = []
|
||||
for edge in trace_edges:
|
||||
trace_pen = edge.perpendicular_line(line_width, 0)
|
||||
new_faces.extend(Face.sweep(trace_pen, edge).faces())
|
||||
|
|
@ -306,6 +318,7 @@ def trace(
|
|||
context._add_to_context(*new_faces, mode=mode)
|
||||
context.pending_edges = ShapeList()
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
combined_faces = Face.fuse(*new_faces) if len(new_faces) > 1 else new_faces[0]
|
||||
result = (
|
||||
Sketch(combined_faces)
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
|||
font: str = "Arial",
|
||||
font_path: str | None = None,
|
||||
font_style: FontStyle = FontStyle.REGULAR,
|
||||
align: Align | tuple[Align, Align] = (Align.CENTER, Align.CENTER),
|
||||
align: Align | tuple[Align, Align] | None = (Align.CENTER, Align.CENTER),
|
||||
position_on_path: float = 0.0,
|
||||
text_path: Edge | Wire | None = None,
|
||||
) -> Compound:
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from build123d import (
|
|||
add,
|
||||
mirror,
|
||||
section,
|
||||
ThreePointArc,
|
||||
)
|
||||
from build123d.exporters import ExportSVG, ExportDXF, Drawing, LineType
|
||||
|
||||
|
|
@ -173,6 +174,24 @@ class ExportersTestCase(unittest.TestCase):
|
|||
svg.add_shape(sketch)
|
||||
svg.write("test-colors.svg")
|
||||
|
||||
def test_svg_small_arc(self):
|
||||
pnts = ((0, 0), (0, 0.000001), (0.000001, 0))
|
||||
small_arc = ThreePointArc(pnts).scale(0.01)
|
||||
with self.assertWarns(UserWarning):
|
||||
svg_exporter = ExportSVG()
|
||||
segments = svg_exporter._circle_segments(small_arc.edges()[0], False)
|
||||
self.assertEqual(len(segments), 0, "Small arc should produce no segments")
|
||||
|
||||
def test_svg_small_ellipse(self):
|
||||
pnts = ((0, 0), (0, 0.000001), (0.000002, 0))
|
||||
small_ellipse = ThreePointArc(pnts).scale(0.01)
|
||||
with self.assertWarns(UserWarning):
|
||||
svg_exporter = ExportSVG()
|
||||
segments = svg_exporter._ellipse_segments(small_ellipse.edges()[0], False)
|
||||
self.assertEqual(
|
||||
len(segments), 0, "Small ellipse should produce no segments"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"format", (Path, fsencode, fsdecode), ids=["path", "bytes", "str"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue