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