mirror of
https://github.com/gumyr/build123d.git
synced 2026-05-10 22:23:10 -07:00
Merge branch 'gumyr:dev' into fix_issue_1296
This commit is contained in:
commit
f083704d41
54 changed files with 93642 additions and 140 deletions
13
docs/assets/example_bspline.svg
Normal file
13
docs/assets/example_bspline.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<svg width="102.089996mm" height="42.089998mm" viewBox="-0.05225 -2.05225 5.1045 2.1045" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="scale(1,-1)" stroke-linecap="round">
|
||||
<g fill="none" stroke="rgb(0,0,0)" stroke-width="0.004500000180000001">
|
||||
<path d="M 0.0,0.0 C 1.0,2.0 2.0,2.0 2.75,1.5 C 3.5,1.0 4.0,-0.0 5.0,1.0" />
|
||||
<circle cx="0.0" cy="0.0" r="0.05" />
|
||||
<circle cx="1.0" cy="2.0" r="0.05" />
|
||||
<circle cx="3.0" cy="2.0" r="0.05" />
|
||||
<circle cx="4.0" cy="0.0" r="0.05" />
|
||||
<circle cx="5.0" cy="1.0" r="0.05" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 632 B |
|
|
@ -20,6 +20,7 @@ Cheat Sheet
|
|||
| :class:`~objects_curve.ArcArcTangentLine`
|
||||
| :class:`~objects_curve.Bezier`
|
||||
| :class:`~objects_curve.BlendCurve`
|
||||
| :class:`~objects_curve.BSpline`
|
||||
| :class:`~objects_curve.CenterArc`
|
||||
| :class:`~objects_curve.ConstrainedArcs`
|
||||
| :class:`~objects_curve.ConstrainedLines`
|
||||
|
|
|
|||
|
|
@ -257,8 +257,11 @@ For example:
|
|||
|
||||
2D Importers
|
||||
============
|
||||
.. py:module:: importers
|
||||
|
||||
.. py:module:: import_dxf
|
||||
.. autofunction:: import_dxf
|
||||
|
||||
.. py:module:: importers
|
||||
.. autofunction:: import_svg
|
||||
.. autofunction:: import_svg_as_buildline_code
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,13 @@ The following objects all can be used in BuildLine contexts. Note that
|
|||
+++
|
||||
Curve blending curvature of two curves
|
||||
|
||||
.. grid-item-card:: :class:`~objects_curve.BSpline`
|
||||
|
||||
.. image:: assets/example_bspline.svg
|
||||
|
||||
+++
|
||||
B-spline from control points and knot data
|
||||
|
||||
.. grid-item-card:: :class:`~objects_curve.CenterArc`
|
||||
|
||||
.. image:: assets/center_arc_example.svg
|
||||
|
|
@ -275,6 +282,7 @@ Reference
|
|||
.. autoclass:: Airfoil
|
||||
.. autoclass:: Bezier
|
||||
.. autoclass:: BlendCurve
|
||||
.. autoclass:: BSpline
|
||||
.. autoclass:: CenterArc
|
||||
.. autoclass:: ConstrainedArcs
|
||||
.. autoclass:: ConstrainedLines
|
||||
|
|
|
|||
17
docs/objects_1d_bspline.py
Normal file
17
docs/objects_1d_bspline.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from build123d import *
|
||||
|
||||
# from ocp_vscode import show_all
|
||||
|
||||
dot = Circle(0.05)
|
||||
|
||||
control_points = [(0, 0), (1, 2), (3, 2), (4, 0), (5, 1)]
|
||||
knots = [0.0, 0.0, 0.0, 0.0, 1.0, 2.0, 2.0, 2.0, 2.0]
|
||||
spline = BSpline(control_points, knots, degree=3)
|
||||
|
||||
s = 100 / max(*spline.bounding_box().size)
|
||||
svg = ExportSVG(scale=s)
|
||||
svg.add_shape(spline)
|
||||
for p in control_points:
|
||||
svg.add_shape(Pos(*p) * dot.scale(1))
|
||||
svg.write("assets/example_bspline.svg")
|
||||
# show_all()
|
||||
|
|
@ -8,6 +8,7 @@ from build123d.build_sketch import *
|
|||
from build123d.exporters import *
|
||||
from build123d.geometry import *
|
||||
from build123d.importers import *
|
||||
from build123d.import_dxf import import_dxf
|
||||
from build123d.joints import *
|
||||
from build123d.mesher import *
|
||||
from build123d.objects_curve import *
|
||||
|
|
@ -86,6 +87,7 @@ __all__ = [
|
|||
"Airfoil",
|
||||
"Bezier",
|
||||
"BlendCurve",
|
||||
"BSpline",
|
||||
"CenterArc",
|
||||
"ConstrainedArcs",
|
||||
"ConstrainedLines",
|
||||
|
|
@ -188,6 +190,7 @@ __all__ = [
|
|||
# Importer functions
|
||||
"detect_primitives",
|
||||
"import_brep",
|
||||
"import_dxf",
|
||||
"import_step",
|
||||
"import_stl",
|
||||
"import_svg",
|
||||
|
|
|
|||
|
|
@ -569,10 +569,7 @@ class Builder(ABC, Generic[ShapeT]):
|
|||
all_vertices = self.vertices(select)
|
||||
vertex_count = len(all_vertices)
|
||||
if vertex_count != 1:
|
||||
warnings.warn(
|
||||
f"Found {vertex_count} vertices, returning first",
|
||||
stacklevel=2,
|
||||
)
|
||||
raise ValueError(f"Expected exactly one vertex, found {vertex_count}")
|
||||
return all_vertices[0]
|
||||
|
||||
def edges(self, select: Select = Select.ALL) -> ShapeList[Edge]:
|
||||
|
|
@ -612,10 +609,7 @@ class Builder(ABC, Generic[ShapeT]):
|
|||
all_edges = self.edges(select)
|
||||
edge_count = len(all_edges)
|
||||
if edge_count != 1:
|
||||
warnings.warn(
|
||||
f"Found {edge_count} edges, returning first",
|
||||
stacklevel=2,
|
||||
)
|
||||
raise ValueError(f"Expected exactly one edge, found {edge_count}")
|
||||
return all_edges[0]
|
||||
|
||||
def wires(self, select: Select = Select.ALL) -> ShapeList[Wire]:
|
||||
|
|
@ -655,10 +649,7 @@ class Builder(ABC, Generic[ShapeT]):
|
|||
all_wires = self.wires(select)
|
||||
wire_count = len(all_wires)
|
||||
if wire_count != 1:
|
||||
warnings.warn(
|
||||
f"Found {wire_count} wires, returning first",
|
||||
stacklevel=2,
|
||||
)
|
||||
raise ValueError(f"Expected exactly one wire, found {wire_count}")
|
||||
return all_wires[0]
|
||||
|
||||
def faces(self, select: Select = Select.ALL) -> ShapeList[Face]:
|
||||
|
|
@ -698,10 +689,7 @@ class Builder(ABC, Generic[ShapeT]):
|
|||
all_faces = self.faces(select)
|
||||
face_count = len(all_faces)
|
||||
if face_count != 1:
|
||||
warnings.warn(
|
||||
f"Found {face_count} faces, returning first",
|
||||
stacklevel=2,
|
||||
)
|
||||
raise ValueError(f"Expected exactly one face, found {face_count}")
|
||||
return all_faces[0]
|
||||
|
||||
def solids(self, select: Select = Select.ALL) -> ShapeList[Solid]:
|
||||
|
|
@ -741,10 +729,7 @@ class Builder(ABC, Generic[ShapeT]):
|
|||
all_solids = self.solids(select)
|
||||
solid_count = len(all_solids)
|
||||
if solid_count != 1:
|
||||
warnings.warn(
|
||||
f"Found {solid_count} solids, returning first",
|
||||
stacklevel=2,
|
||||
)
|
||||
raise ValueError(f"Expected exactly one solid, found {solid_count}")
|
||||
return all_solids[0]
|
||||
|
||||
def _shapes(
|
||||
|
|
|
|||
492
src/build123d/import_dxf.py
Normal file
492
src/build123d/import_dxf.py
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
"""
|
||||
build123d import dxf
|
||||
|
||||
name: import_dxf.py
|
||||
by: Gumyr
|
||||
date: November 10th, 2024
|
||||
|
||||
desc:
|
||||
This python module imports a DXF file as build123d objects.
|
||||
|
||||
license:
|
||||
|
||||
Copyright 2024 Gumyr
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
"""
|
||||
|
||||
import math
|
||||
import warnings
|
||||
from io import BytesIO, StringIO, TextIOBase
|
||||
from os import PathLike
|
||||
from typing import BinaryIO, Callable, TextIO, cast
|
||||
|
||||
import ezdxf
|
||||
from ezdxf.entities import DXFGraphic
|
||||
from ezdxf.entities.boundary_paths import (
|
||||
ArcEdge,
|
||||
EdgePath,
|
||||
EllipseEdge,
|
||||
LineEdge,
|
||||
PolylinePath,
|
||||
SplineEdge,
|
||||
)
|
||||
|
||||
from build123d.build_enums import TextAlign
|
||||
from build123d.geometry import TOLERANCE, Axis, Pos, Vector, VectorLike
|
||||
from build123d.objects_curve import (
|
||||
BSpline,
|
||||
CenterArc,
|
||||
EllipticalCenterArc,
|
||||
Line,
|
||||
SagittaArc,
|
||||
Spline,
|
||||
)
|
||||
from build123d.objects_sketch import Circle, Polygon, Text
|
||||
from build123d.operations_generic import scale
|
||||
from build123d.topology import Edge, Shape, ShapeList, Vertex, Wire
|
||||
|
||||
# Unfortunately exdxf is not fully typed
|
||||
# mypy: disable-error-code="attr-defined"
|
||||
|
||||
|
||||
def process_arc(entity: DXFGraphic) -> CenterArc:
|
||||
"""Convert ARC"""
|
||||
start, _, end = entity.angles(3)
|
||||
arc_size = (end - start + 360.0) % 360.0
|
||||
return CenterArc(entity.dxf.center, entity.dxf.radius, start, arc_size)
|
||||
|
||||
|
||||
def process_circle(entity: DXFGraphic) -> Circle:
|
||||
"""Convert CIRCLE"""
|
||||
return Circle(entity.dxf.radius).edge().moved(Pos(*entity.dxf.center))
|
||||
|
||||
|
||||
def process_ellipse(entity: DXFGraphic) -> EllipticalCenterArc:
|
||||
"""Convert ELLIPSE"""
|
||||
center = entity.dxf.center
|
||||
major_axis = entity.dxf.major_axis
|
||||
x_radius = (major_axis[0] ** 2 + major_axis[1] ** 2) ** 0.5
|
||||
y_radius = x_radius * entity.dxf.ratio
|
||||
rotation = math.degrees(math.atan2(major_axis[1], major_axis[0]))
|
||||
start_angle = math.degrees(entity.dxf.start_param)
|
||||
arc_size = math.degrees(entity.dxf.end_param - entity.dxf.start_param)
|
||||
arc_size = (arc_size + 360.0) % 360.0
|
||||
|
||||
return EllipticalCenterArc(
|
||||
center=center,
|
||||
x_radius=x_radius,
|
||||
y_radius=y_radius,
|
||||
start_angle=start_angle,
|
||||
arc_size=arc_size,
|
||||
rotation=rotation,
|
||||
)
|
||||
|
||||
|
||||
def process_insert(entity, doc):
|
||||
"""Process INSERT by referencing block definition and applying transformations."""
|
||||
block_name = entity.dxf.name
|
||||
insert_point = Vector(entity.dxf.insert)
|
||||
scale_factors = (
|
||||
entity.dxf.xscale,
|
||||
entity.dxf.yscale,
|
||||
entity.dxf.zscale if entity.dxf.zscale != 0 else 1.0,
|
||||
)
|
||||
rotation_angle = entity.dxf.rotation
|
||||
column_count = entity.dxf.column_count
|
||||
row_count = entity.dxf.row_count
|
||||
column_spacing = entity.dxf.column_spacing
|
||||
row_spacing = entity.dxf.row_spacing
|
||||
|
||||
# Retrieve the block definition
|
||||
block = doc.blocks.get(block_name)
|
||||
block_base_point = Vector(block.block.dxf.base_point)
|
||||
transformed_entities = []
|
||||
|
||||
# Process each entity in the block definition
|
||||
for block_entity in block:
|
||||
for entity_object in _process_entity(block_entity, doc):
|
||||
for row_index in range(row_count):
|
||||
for column_index in range(column_count):
|
||||
array_offset = Vector(
|
||||
column_index * column_spacing,
|
||||
row_index * row_spacing,
|
||||
0,
|
||||
)
|
||||
array_offset = Vector(
|
||||
array_offset.X * scale_factors[0],
|
||||
array_offset.Y * scale_factors[1],
|
||||
array_offset.Z * scale_factors[2],
|
||||
).rotate(Axis.Z, rotation_angle)
|
||||
# INSERT places the block definition base point at insert_point.
|
||||
# Normalize block geometry to that local origin before scaling/rotation.
|
||||
transformed_entity = entity_object.translate(-block_base_point)
|
||||
transformed_entity = scale(transformed_entity, scale_factors)
|
||||
transformed_entity = transformed_entity.rotate(
|
||||
Axis.Z, rotation_angle
|
||||
)
|
||||
transformed_entity = transformed_entity.translate(
|
||||
insert_point + array_offset
|
||||
)
|
||||
transformed_entities.append(transformed_entity)
|
||||
|
||||
return ShapeList(transformed_entities)
|
||||
|
||||
|
||||
def process_line(entity: DXFGraphic) -> Line | None:
|
||||
"""Convert LINE"""
|
||||
start, end = Vector(*entity.dxf.start), Vector(*entity.dxf.end)
|
||||
if (start - end).length < TOLERANCE:
|
||||
warnings.warn("Skipping degenerate LINE", stacklevel=3)
|
||||
return None
|
||||
return Line(start, end)
|
||||
|
||||
|
||||
def process_lwpolyline(entity: DXFGraphic) -> Edge | Wire | None:
|
||||
"""Convert LWPOLYLINE"""
|
||||
elevation = entity.dxf.elevation
|
||||
# elevation could be a vector or just a single value
|
||||
try:
|
||||
z_value = elevation.z
|
||||
except AttributeError:
|
||||
z_value = elevation
|
||||
|
||||
points = entity.get_points("xyb")
|
||||
if len(points) < 2:
|
||||
warnings.warn("Skipping degenerate LWPOLYLINE", stacklevel=3)
|
||||
return None
|
||||
return _convert_bulge_polyline(points, entity.closed, z_value, "LWPOLYLINE")
|
||||
|
||||
|
||||
def process_point(entity: DXFGraphic) -> Vertex:
|
||||
"""Convert POINT"""
|
||||
point = entity.dxf.location
|
||||
return Vertex(point[0], point[1], point[2])
|
||||
|
||||
|
||||
def process_polyline(entity: DXFGraphic) -> Edge | Wire | None:
|
||||
"""Convert 2D POLYLINE - a collection of LINE and ARC segments."""
|
||||
if entity.get_mode() != "AcDb2dPolyline":
|
||||
raise ValueError(f"Unsupported POLYLINE mode: {entity.get_mode()}")
|
||||
|
||||
vertices = list(entity.vertices)
|
||||
if len(vertices) < 2:
|
||||
warnings.warn("Skipping degenerate POLYLINE", stacklevel=3)
|
||||
return None
|
||||
# Note: the bulge data is not a z value - processed by _convert_bulge_polyline
|
||||
points = [
|
||||
(
|
||||
cast(float, vertex.dxf.location.x),
|
||||
cast(float, vertex.dxf.location.y),
|
||||
cast(float, vertex.dxf.get("bulge", 0)),
|
||||
)
|
||||
for vertex in vertices
|
||||
]
|
||||
z_value = vertices[0].dxf.location.z
|
||||
return _convert_bulge_polyline(points, entity.is_closed, z_value, "POLYLINE")
|
||||
|
||||
|
||||
def _convert_bulge_polyline(
|
||||
points: list[tuple[float, float, float]], closed: bool, z_value: float, label: str
|
||||
) -> Edge | Wire | None:
|
||||
"""Convert a 2D polyline described by vertices with optional bulge values."""
|
||||
edges = []
|
||||
segment_count = len(points) if closed else len(points) - 1
|
||||
|
||||
for i in range(segment_count):
|
||||
start_data = points[i]
|
||||
end_data = points[(i + 1) % len(points)]
|
||||
start_point = (start_data[0], start_data[1], z_value)
|
||||
end_point = (end_data[0], end_data[1], z_value)
|
||||
bulge = start_data[2] if len(start_data) > 2 else 0
|
||||
|
||||
if math.dist(start_point, end_point) < TOLERANCE:
|
||||
continue
|
||||
|
||||
if abs(bulge) < TOLERANCE:
|
||||
edge = Line(start_point, end_point)
|
||||
else:
|
||||
sagitta = bulge * math.dist(start_point, end_point) / 2
|
||||
edge = SagittaArc(start_point, end_point, sagitta)
|
||||
edges.append(edge)
|
||||
|
||||
if not edges:
|
||||
warnings.warn(f"Skipping degenerate {label}", stacklevel=3)
|
||||
return None
|
||||
if len(edges) == 1:
|
||||
return edges[0]
|
||||
return Wire(edges=edges)
|
||||
|
||||
|
||||
def _convert_hatch_edge(edge, z_value: float) -> Edge:
|
||||
"""Convert a hatch edge-path edge into build123d geometry."""
|
||||
if isinstance(edge, LineEdge):
|
||||
return Line(
|
||||
(edge.start.x, edge.start.y, z_value), (edge.end.x, edge.end.y, z_value)
|
||||
)
|
||||
if isinstance(edge, ArcEdge):
|
||||
arc_size = edge.end_angle - edge.start_angle
|
||||
if not edge.ccw:
|
||||
arc_size = -arc_size
|
||||
return CenterArc(
|
||||
(edge.center.x, edge.center.y, z_value),
|
||||
edge.radius,
|
||||
start_angle=edge.start_angle,
|
||||
arc_size=arc_size,
|
||||
)
|
||||
if isinstance(edge, EllipseEdge):
|
||||
major_axis = Vector(edge.major_axis.x, edge.major_axis.y, 0)
|
||||
x_radius = major_axis.length
|
||||
rotation = math.degrees(math.atan2(major_axis.Y, major_axis.X))
|
||||
arc_size = edge.end_angle - edge.start_angle
|
||||
if not edge.ccw:
|
||||
arc_size = -arc_size
|
||||
return EllipticalCenterArc(
|
||||
center=(edge.center.x, edge.center.y, z_value),
|
||||
x_radius=x_radius,
|
||||
y_radius=x_radius * edge.ratio,
|
||||
start_angle=edge.start_angle,
|
||||
arc_size=arc_size,
|
||||
rotation=rotation,
|
||||
)
|
||||
if isinstance(edge, SplineEdge):
|
||||
return BSpline(
|
||||
control_points=[(p[0], p[1], z_value) for p in edge.control_points],
|
||||
knots=edge.knot_values,
|
||||
degree=edge.degree,
|
||||
weights=edge.weights if edge.weights else None,
|
||||
periodic=bool(edge.periodic),
|
||||
)
|
||||
raise ValueError(f"Unsupported HATCH edge type: {type(edge).__name__}")
|
||||
|
||||
|
||||
def process_hatch(entity: DXFGraphic) -> ShapeList[Edge | Wire]:
|
||||
"""Convert HATCH by importing only its perimeter boundary paths."""
|
||||
elevation = entity.dxf.elevation
|
||||
try:
|
||||
z_value = elevation.z
|
||||
except AttributeError:
|
||||
z_value = elevation
|
||||
|
||||
boundaries: ShapeList[Edge | Wire] = ShapeList()
|
||||
for path in entity.paths.rendering_paths(entity.dxf.hatch_style):
|
||||
if isinstance(path, PolylinePath):
|
||||
boundary = _convert_bulge_polyline(
|
||||
path.vertices, path.is_closed, z_value, "HATCH"
|
||||
)
|
||||
elif isinstance(path, EdgePath):
|
||||
edges = [_convert_hatch_edge(edge, z_value) for edge in path.edges]
|
||||
if not edges:
|
||||
continue
|
||||
boundary = edges[0] if len(edges) == 1 else Wire(edges=edges)
|
||||
else:
|
||||
warnings.warn(
|
||||
f"Unsupported HATCH boundary path: {type(path).__name__}", stacklevel=3
|
||||
)
|
||||
continue
|
||||
|
||||
if boundary is not None:
|
||||
boundaries.append(boundary)
|
||||
|
||||
return boundaries
|
||||
|
||||
|
||||
def process_solid_trace_3dface(entity: DXFGraphic):
|
||||
"""Convert filled objects - i.e. Faces"""
|
||||
# Gather vertices as a list of (x, y, z) tuples
|
||||
vertices = []
|
||||
for i in range(4):
|
||||
# Some entities like SOLID or TRACE may define only 3 vertices, repeating the last one
|
||||
# if the fourth vertex is not defined.
|
||||
try:
|
||||
vertex = entity.dxf.get(f"v{i}")
|
||||
vertices.append((vertex.x, vertex.y, vertex.z))
|
||||
except AttributeError:
|
||||
break
|
||||
|
||||
# Create the Polygon object
|
||||
polygon_obj = Polygon(*vertices)
|
||||
return polygon_obj
|
||||
|
||||
|
||||
def process_spline(entity: DXFGraphic) -> Edge:
|
||||
"""Convert SPLINE"""
|
||||
control_points = list(entity.control_points)
|
||||
fit_points = list(entity.fit_points)
|
||||
knots = list(entity.knots)
|
||||
weights = list(entity.weights)
|
||||
degree = entity.dxf.degree
|
||||
periodic = bool(entity.dxf.flags & 2)
|
||||
|
||||
if control_points and knots:
|
||||
return BSpline(
|
||||
control_points=control_points,
|
||||
knots=knots,
|
||||
degree=degree,
|
||||
weights=weights if weights else None,
|
||||
periodic=periodic,
|
||||
)
|
||||
|
||||
start_tangent = entity.dxf.get("start_tangent")
|
||||
end_tangent = entity.dxf.get("end_tangent")
|
||||
if fit_points:
|
||||
tangents: tuple[VectorLike, ...] = ()
|
||||
if start_tangent is not None and end_tangent is not None:
|
||||
tangents = (start_tangent, end_tangent)
|
||||
return Spline(*fit_points, tangents=tangents)
|
||||
|
||||
raise ValueError("Unsupported SPLINE entity: missing control points and knots")
|
||||
|
||||
|
||||
def process_text(entity: DXFGraphic) -> Text:
|
||||
"""Convert TEXT."""
|
||||
v_alignment = {
|
||||
0: TextAlign.BOTTOM, # baseline approximation
|
||||
1: TextAlign.BOTTOM,
|
||||
2: TextAlign.CENTER,
|
||||
3: TextAlign.TOP,
|
||||
}
|
||||
h_alignment = {
|
||||
0: TextAlign.LEFT,
|
||||
1: TextAlign.CENTER,
|
||||
2: TextAlign.RIGHT,
|
||||
3: TextAlign.LEFT, # aligned
|
||||
4: TextAlign.CENTER, # middle
|
||||
5: TextAlign.LEFT, # fit
|
||||
}
|
||||
|
||||
position = entity.dxf.insert
|
||||
if (entity.dxf.halign != 0 or entity.dxf.valign != 0) and entity.dxf.hasattr(
|
||||
"align_point"
|
||||
):
|
||||
position = entity.dxf.align_point
|
||||
|
||||
return Text(
|
||||
entity.dxf.text,
|
||||
font_size=entity.dxf.height,
|
||||
rotation=entity.dxf.get("rotation", 0),
|
||||
text_align=(
|
||||
h_alignment.get(entity.dxf.halign, TextAlign.LEFT),
|
||||
v_alignment.get(entity.dxf.valign, TextAlign.BOTTOM),
|
||||
),
|
||||
).moved(Pos(*position))
|
||||
|
||||
|
||||
def process_mtext(entity: DXFGraphic) -> Text:
|
||||
"""Convert MTEXT."""
|
||||
attachment_align = {
|
||||
1: (TextAlign.LEFT, TextAlign.TOPFIRSTLINE),
|
||||
2: (TextAlign.CENTER, TextAlign.TOPFIRSTLINE),
|
||||
3: (TextAlign.RIGHT, TextAlign.TOPFIRSTLINE),
|
||||
4: (TextAlign.LEFT, TextAlign.CENTER),
|
||||
5: (TextAlign.CENTER, TextAlign.CENTER),
|
||||
6: (TextAlign.RIGHT, TextAlign.CENTER),
|
||||
7: (TextAlign.LEFT, TextAlign.BOTTOM),
|
||||
8: (TextAlign.CENTER, TextAlign.BOTTOM),
|
||||
9: (TextAlign.RIGHT, TextAlign.BOTTOM),
|
||||
}
|
||||
|
||||
if hasattr(entity, "plain_text"):
|
||||
content = entity.plain_text()
|
||||
else:
|
||||
content = entity.text
|
||||
|
||||
return Text(
|
||||
content,
|
||||
font_size=entity.dxf.char_height,
|
||||
rotation=entity.dxf.get("rotation", 0),
|
||||
text_align=attachment_align.get(
|
||||
entity.dxf.attachment_point,
|
||||
(TextAlign.LEFT, TextAlign.TOPFIRSTLINE),
|
||||
),
|
||||
).moved(Pos(*entity.dxf.insert))
|
||||
|
||||
|
||||
# Dispatch dictionary mapping entity types to processing functions
|
||||
entity_dispatch: dict[str, Callable] = {
|
||||
"3DFACE": process_solid_trace_3dface,
|
||||
"ARC": process_arc,
|
||||
"CIRCLE": process_circle,
|
||||
"ELLIPSE": process_ellipse,
|
||||
"HATCH": process_hatch,
|
||||
"INSERT": process_insert,
|
||||
"LINE": process_line,
|
||||
"LWPOLYLINE": process_lwpolyline,
|
||||
"MTEXT": process_mtext,
|
||||
"POINT": process_point,
|
||||
"POLYLINE": process_polyline,
|
||||
"SOLID": process_solid_trace_3dface,
|
||||
"SPLINE": process_spline,
|
||||
"TEXT": process_text,
|
||||
"TRACE": process_solid_trace_3dface,
|
||||
}
|
||||
|
||||
|
||||
def _flatten_import_result(new_object) -> list[Shape]:
|
||||
"""Normalize handler results into a flat list of shapes."""
|
||||
if new_object is None:
|
||||
return []
|
||||
if isinstance(new_object, ShapeList):
|
||||
return [obj for obj in new_object if obj is not None]
|
||||
if isinstance(new_object, list):
|
||||
return [obj for obj in new_object if obj is not None]
|
||||
return [new_object]
|
||||
|
||||
|
||||
def _process_entity(entity, doc) -> list[Shape]:
|
||||
"""Convert a single DXF entity into zero or more build123d shapes."""
|
||||
dxftype = entity.dxftype()
|
||||
if dxftype not in entity_dispatch:
|
||||
warnings.warn(f"Unable to convert {dxftype}", stacklevel=3)
|
||||
return []
|
||||
|
||||
if dxftype == "INSERT":
|
||||
new_object = entity_dispatch[dxftype](entity, doc)
|
||||
else:
|
||||
new_object = entity_dispatch[dxftype](entity)
|
||||
return _flatten_import_result(new_object)
|
||||
|
||||
|
||||
def import_dxf(dxf_file: str | PathLike | TextIO | BinaryIO) -> ShapeList:
|
||||
"""Import shapes from a DXF file
|
||||
|
||||
Args:
|
||||
dxf_file (str | PathLike | TextIO | BinaryIO): dxf file path or readable stream
|
||||
|
||||
Raises:
|
||||
DXFStructureError: file not found
|
||||
|
||||
Returns:
|
||||
ShapeList: build123d objects
|
||||
"""
|
||||
try:
|
||||
if isinstance(dxf_file, (str, PathLike)):
|
||||
doc = ezdxf.readfile(dxf_file)
|
||||
elif isinstance(dxf_file, TextIOBase):
|
||||
doc = ezdxf.read(dxf_file)
|
||||
elif isinstance(dxf_file, BytesIO) or hasattr(dxf_file, "read"):
|
||||
data = dxf_file.read()
|
||||
text = data.decode("latin1") if isinstance(data, bytes) else data
|
||||
doc = ezdxf.read(StringIO(text))
|
||||
else:
|
||||
raise TypeError(f"Unsupported DXF input type: {type(dxf_file).__name__}")
|
||||
except ezdxf.DXFStructureError as exc:
|
||||
raise ValueError(f"Failed to read {dxf_file}") from exc
|
||||
build123d_objects = []
|
||||
|
||||
# Iterate over all entities in the model space
|
||||
for entity in doc.modelspace():
|
||||
build123d_objects.extend(_process_entity(entity, doc))
|
||||
|
||||
return ShapeList(build123d_objects)
|
||||
|
|
@ -440,6 +440,67 @@ class BlendCurve(BaseEdgeObject):
|
|||
super().__init__(joining_curve, mode=mode)
|
||||
|
||||
|
||||
class BSpline(BaseEdgeObject):
|
||||
"""Line Object: BSpline
|
||||
|
||||
An exact B-spline edge defined directly from control points and knot data.
|
||||
|
||||
BSpline creates an exact B-spline from control points, a knot sequence, and
|
||||
optional weights. Control points define the control polygon that pulls the curve,
|
||||
but the curve does not generally pass through them. Knots define the parameter-space
|
||||
structure of the spline: they determine where polynomial spans begin and
|
||||
end and how smoothly those spans join. Repeated knot values indicate knot multiplicity.
|
||||
For a spline of degree p, a knot with multiplicity m has continuity
|
||||
C^(p-m) at that location, so increasing multiplicity reduces smoothness. Repeating the
|
||||
first and last knots degree + 1 times creates a clamped spline that
|
||||
starts and ends at the first and last control points. Optional weights create a
|
||||
rational B-spline, allowing some control points to pull more strongly than
|
||||
others and enabling exact representation of conic sections.`
|
||||
|
||||
Unlike :class:`~build123d.objects_curve.Spline`, which creates an interpolated curve
|
||||
through a set of points using ``GeomAPI_Interpolate``, ``BSpline`` preserves
|
||||
the supplied spline definition by building the underlying OCCT
|
||||
``Geom_BSplineCurve`` from its poles, knot vector, optional weights,
|
||||
degree, and periodic flag.
|
||||
|
||||
Args:
|
||||
control_points (Iterable[VectorLike]): Control points (poles) defining the
|
||||
spline shape. These are not generally points on the curve.
|
||||
knots (Iterable[float]): Knot sequence for the spline. Repeated knot
|
||||
values are allowed and are converted internally into unique knot
|
||||
values plus multiplicities as required by OCCT.
|
||||
degree (int): Polynomial degree of the spline.
|
||||
weights (Iterable[float] | None, optional): Optional per-control-point
|
||||
weights for rational B-splines. If omitted, the spline is
|
||||
non-rational.
|
||||
periodic (bool, optional): Whether to create a periodic spline. Defaults
|
||||
to ``False``.
|
||||
mode (Mode, optional): Builder combination mode. Defaults to ``Mode.ADD``.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
control_points: Iterable[VectorLike],
|
||||
knots: Iterable[float],
|
||||
degree: int,
|
||||
weights: Iterable[float] | None = None,
|
||||
periodic: bool = False,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context: BuildLine | None = BuildLine._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
spline = Edge.make_bspline(
|
||||
WorkplaneList.localize(*control_points),
|
||||
knots,
|
||||
degree,
|
||||
weights=weights,
|
||||
periodic=periodic,
|
||||
)
|
||||
super().__init__(spline, mode=mode)
|
||||
|
||||
|
||||
class CenterArc(BaseEdgeObject):
|
||||
"""Line Object: Center Arc
|
||||
|
||||
|
|
@ -1639,6 +1700,11 @@ class FilletPolyline(BaseLineObject):
|
|||
validate_inputs(context, self)
|
||||
points = flatten_sequence(*pts)
|
||||
|
||||
# Handle user closed polylines
|
||||
if (Vector(points[0]) - Vector(points[-1])).length < TOLERANCE:
|
||||
close = True
|
||||
points.pop(-1)
|
||||
|
||||
if len(points) < 2:
|
||||
raise ValueError("FilletPolyline requires two or more pts")
|
||||
|
||||
|
|
@ -1659,6 +1725,7 @@ class FilletPolyline(BaseLineObject):
|
|||
raise ValueError(f"radius {r} must be non-negative")
|
||||
|
||||
lines_pts = WorkplaneList.localize(*points)
|
||||
|
||||
# Create the polyline
|
||||
|
||||
new_edges = [
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ def deserialize_shape(buffer: bytes) -> TopoDS_Shape:
|
|||
return downcast(shape)
|
||||
|
||||
|
||||
def serialize_location(location: TopLoc_Location) -> bytes | None:
|
||||
def serialize_location(location: TopLoc_Location) -> bytearray | None:
|
||||
"""
|
||||
Serialize a OCP location, this method can be used to provide
|
||||
a custom serialization algo for pickle
|
||||
|
|
|
|||
|
|
@ -108,12 +108,12 @@ from .shape_core import (
|
|||
downcast,
|
||||
shapetype,
|
||||
topods_dim,
|
||||
_make_topods_compound_from_shapes,
|
||||
)
|
||||
from .three_d import Mixin3D, Solid
|
||||
from .two_d import Face, Shell
|
||||
from .utils import (
|
||||
_extrude_topods_shape,
|
||||
_make_topods_compound_from_shapes,
|
||||
tuplify,
|
||||
unwrapped_shapetype,
|
||||
)
|
||||
|
|
@ -593,16 +593,13 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
middle = self.bounding_box().center()
|
||||
return middle
|
||||
|
||||
def compound(self) -> Compound | None:
|
||||
def compound(self) -> Compound:
|
||||
"""Return the Compound"""
|
||||
shape_list = self.compounds()
|
||||
entity_count = len(shape_list)
|
||||
if entity_count > 1:
|
||||
warnings.warn(
|
||||
f"Found {entity_count} compounds, returning first",
|
||||
stacklevel=2,
|
||||
)
|
||||
return shape_list[0] if shape_list else None
|
||||
if entity_count != 1:
|
||||
raise ValueError(f"Expected exactly one compound, found {entity_count}")
|
||||
return shape_list[0]
|
||||
|
||||
def compounds(self) -> ShapeList[Compound]:
|
||||
"""compounds - all the compounds in this Shape"""
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ from OCP.Standard import (
|
|||
)
|
||||
from OCP.TColgp import TColgp_Array1OfPnt, TColgp_Array1OfVec, TColgp_HArray1OfPnt
|
||||
from OCP.TColStd import (
|
||||
TColStd_Array1OfInteger,
|
||||
TColStd_Array1OfReal,
|
||||
TColStd_HArray1OfBoolean,
|
||||
TColStd_HArray1OfReal,
|
||||
|
|
@ -2471,6 +2472,85 @@ class Edge(Mixin1D[TopoDS_Edge]):
|
|||
|
||||
return cls(BRepBuilderAPI_MakeEdge(spline_geom).Edge())
|
||||
|
||||
@classmethod
|
||||
def make_bspline(
|
||||
cls,
|
||||
control_points: Iterable[VectorLike],
|
||||
knots: Iterable[float],
|
||||
degree: int,
|
||||
weights: Iterable[float] | None = None,
|
||||
periodic: bool = False,
|
||||
) -> Edge:
|
||||
"""Create an exact B-spline edge from control points and knot data.
|
||||
|
||||
Args:
|
||||
control_points (Iterable[VectorLike]): Control points (poles) defining
|
||||
the spline shape.
|
||||
knots (Iterable[float]): Knot sequence for the spline. Repeated knot
|
||||
values are converted to unique knot values plus multiplicities.
|
||||
degree (int): Polynomial degree of the spline.
|
||||
weights (Iterable[float] | None, optional): Optional per-control-point
|
||||
weights for rational B-splines. Defaults to ``None``.
|
||||
periodic (bool, optional): Whether to create a periodic spline.
|
||||
Defaults to ``False``.
|
||||
|
||||
Raises:
|
||||
ValueError: B-spline requires at least one knot.
|
||||
|
||||
Returns:
|
||||
Edge: the B-spline edge
|
||||
"""
|
||||
|
||||
knot_list = list(knots)
|
||||
if not knot_list:
|
||||
raise ValueError("B-spline requires at least one knot")
|
||||
|
||||
point_vectors = [Vector(point) for point in control_points]
|
||||
unique_knots = [knot_list[0]]
|
||||
multiplicities = [1]
|
||||
for knot in knot_list[1:]:
|
||||
if abs(knot - unique_knots[-1]) <= TOLERANCE:
|
||||
multiplicities[-1] += 1
|
||||
else:
|
||||
unique_knots.append(knot)
|
||||
multiplicities.append(1)
|
||||
|
||||
poles_array = TColgp_Array1OfPnt(1, len(point_vectors))
|
||||
for index, point in enumerate(point_vectors, start=1):
|
||||
poles_array.SetValue(index, point.to_pnt())
|
||||
|
||||
knots_array = TColStd_Array1OfReal(1, len(unique_knots))
|
||||
for index, knot in enumerate(unique_knots, start=1):
|
||||
knots_array.SetValue(index, float(knot))
|
||||
|
||||
multiplicities_array = TColStd_Array1OfInteger(1, len(multiplicities))
|
||||
for index, multiplicity in enumerate(multiplicities, start=1):
|
||||
multiplicities_array.SetValue(index, multiplicity)
|
||||
|
||||
weights_list = list(weights) if weights is not None else []
|
||||
if weights_list:
|
||||
weights_array = TColStd_Array1OfReal(1, len(weights_list))
|
||||
for index, weight in enumerate(weights_list, start=1):
|
||||
weights_array.SetValue(index, float(weight))
|
||||
spline_geom = Geom_BSplineCurve(
|
||||
poles_array,
|
||||
weights_array,
|
||||
knots_array,
|
||||
multiplicities_array,
|
||||
degree,
|
||||
periodic,
|
||||
)
|
||||
else:
|
||||
spline_geom = Geom_BSplineCurve(
|
||||
poles_array,
|
||||
knots_array,
|
||||
multiplicities_array,
|
||||
degree,
|
||||
periodic,
|
||||
)
|
||||
|
||||
return cls(BRepBuilderAPI_MakeEdge(spline_geom).Edge())
|
||||
|
||||
@classmethod
|
||||
def make_spline_approx(
|
||||
cls,
|
||||
|
|
@ -3234,32 +3314,36 @@ class Edge(Mixin1D[TopoDS_Edge]):
|
|||
Edge: trimmed edge
|
||||
"""
|
||||
|
||||
# Get the normalized edge parameters
|
||||
start_u = Mixin1D._to_param(self, start, "start")
|
||||
end_u = Mixin1D._to_param(self, end, "end")
|
||||
|
||||
start_u, end_u = sorted([start_u, end_u])
|
||||
start_point = self.position_at(start_u)
|
||||
|
||||
# if start_u >= end_u:
|
||||
# raise ValueError(f"start ({start_u}) must be less than end ({end_u})")
|
||||
# Convert the normalized edge parameters into curve parameters
|
||||
comp_curve_start, parm_start, _ = self._occt_param_at(start_u)
|
||||
_comp_curve_end, parm_end, _ = self._occt_param_at(end_u)
|
||||
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't trim empty edge")
|
||||
|
||||
self_copy = copy.deepcopy(self)
|
||||
assert self_copy.wrapped is not None
|
||||
|
||||
new_curve = BRep_Tool.Curve_s(
|
||||
self_copy.wrapped, self.param_at(0), self.param_at(1)
|
||||
# Rebuild the edge
|
||||
trimmed_edge = Edge(
|
||||
BRepBuilderAPI_MakeEdge(
|
||||
BRep_Tool.Curve_s(
|
||||
self.wrapped,
|
||||
comp_curve_start.FirstParameter(),
|
||||
comp_curve_start.LastParameter(),
|
||||
),
|
||||
*sorted([parm_start, parm_end]),
|
||||
).Edge()
|
||||
)
|
||||
parm_start = self.param_at(start_u)
|
||||
parm_end = self.param_at(end_u)
|
||||
trimmed_curve = Geom_TrimmedCurve(
|
||||
new_curve,
|
||||
parm_start,
|
||||
parm_end,
|
||||
|
||||
# Reverse it if necessary
|
||||
same_start = (trimmed_edge.position_at(0) - start_point).length < TOLERANCE
|
||||
same_direction = (
|
||||
self.tangent_at(start_u).dot(trimmed_edge.tangent_at(0)) > 1 - TOLERANCE
|
||||
)
|
||||
new_edge = BRepBuilderAPI_MakeEdge(trimmed_curve).Edge()
|
||||
return Edge(new_edge)
|
||||
if same_start and same_direction:
|
||||
return trimmed_edge
|
||||
return trimmed_edge.reversed(reconstruct=True)
|
||||
|
||||
def trim_to_length(self, start: float | VectorLike, length: float) -> Edge:
|
||||
"""trim_to_length
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ from OCP.TopExp import TopExp, TopExp_Explorer
|
|||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.TopoDS import (
|
||||
TopoDS,
|
||||
TopoDS_Builder,
|
||||
TopoDS_Compound,
|
||||
TopoDS_Edge,
|
||||
TopoDS_Face,
|
||||
|
|
@ -818,6 +819,48 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
calc_function(obj.wrapped, properties)
|
||||
return properties.Mass()
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_shape_list(
|
||||
shape: Shape, entity_type: Literal["Vertex"]
|
||||
) -> ShapeList[Vertex]: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_shape_list(
|
||||
shape: Shape, entity_type: Literal["Edge"]
|
||||
) -> ShapeList[Edge]: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_shape_list(
|
||||
shape: Shape, entity_type: Literal["Wire"]
|
||||
) -> ShapeList[Wire]: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_shape_list(
|
||||
shape: Shape, entity_type: Literal["Face"]
|
||||
) -> ShapeList[Face]: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_shape_list(
|
||||
shape: Shape, entity_type: Literal["Shell"]
|
||||
) -> ShapeList[Shell]: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_shape_list(
|
||||
shape: Shape, entity_type: Literal["Solid"]
|
||||
) -> ShapeList[Solid]: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_shape_list(
|
||||
shape: Shape, entity_type: Literal["Compound"]
|
||||
) -> ShapeList[Compound]: ...
|
||||
|
||||
@staticmethod
|
||||
def get_shape_list(
|
||||
shape: Shape,
|
||||
|
|
@ -835,25 +878,55 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
item.topo_parent = shape if shape.topo_parent is None else shape.topo_parent
|
||||
return shape_list
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_single_shape(shape: Shape, entity_type: Literal["Vertex"]) -> Vertex: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_single_shape(shape: Shape, entity_type: Literal["Edge"]) -> Edge: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_single_shape(shape: Shape, entity_type: Literal["Wire"]) -> Wire: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_single_shape(shape: Shape, entity_type: Literal["Face"]) -> Face: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_single_shape(shape: Shape, entity_type: Literal["Shell"]) -> Shell: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_single_shape(shape: Shape, entity_type: Literal["Solid"]) -> Solid: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def get_single_shape(
|
||||
shape: Shape, entity_type: Literal["Compound"]
|
||||
) -> Compound: ...
|
||||
|
||||
@staticmethod
|
||||
def get_single_shape(
|
||||
shape: Shape,
|
||||
entity_type: Literal[
|
||||
"Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "Compound"
|
||||
],
|
||||
) -> Shape | None:
|
||||
"""Helper to extract a single entity of a specific type from a shape,
|
||||
with a warning if count != 1."""
|
||||
) -> Shape:
|
||||
"""Return the single entity of the requested type.
|
||||
|
||||
Raises:
|
||||
ValueError: if the number of matching entities is not exactly one.
|
||||
"""
|
||||
shape_list = Shape.get_shape_list(shape, entity_type)
|
||||
entity_count = len(shape_list)
|
||||
if entity_count == 0:
|
||||
return None
|
||||
if entity_count > 1:
|
||||
warnings.warn(
|
||||
f"Found {entity_count} {entity_type.lower()}s, returning first",
|
||||
stacklevel=3,
|
||||
if entity_count != 1:
|
||||
raise ValueError(
|
||||
f"Expected exactly one {entity_type.lower()}, found {entity_count}"
|
||||
)
|
||||
return shape_list[0] if shape_list else None
|
||||
return shape_list[0]
|
||||
|
||||
# ---- Instance Methods ----
|
||||
|
||||
|
|
@ -1104,9 +1177,9 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
"""Points on two shapes where the distance between them is minimal"""
|
||||
return self.distance_to_with_closest_points(other)[1:3]
|
||||
|
||||
def compound(self) -> Compound | None:
|
||||
def compound(self) -> Compound:
|
||||
"""Return the Compound"""
|
||||
return None
|
||||
return Shape.get_single_shape(self, "Compound")
|
||||
|
||||
def compounds(self) -> ShapeList[Compound]:
|
||||
"""compounds - all the compounds in this Shape"""
|
||||
|
|
@ -1223,7 +1296,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
yield dist_calc.Value()
|
||||
|
||||
def edge(self) -> Edge | None:
|
||||
def edge(self) -> Edge:
|
||||
"""Return the Edge"""
|
||||
return Shape.get_single_shape(self, "Edge")
|
||||
|
||||
|
|
@ -1240,7 +1313,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
return []
|
||||
return _topods_entities(self.wrapped, topo_type)
|
||||
|
||||
def face(self) -> Face | None:
|
||||
def face(self) -> Face:
|
||||
"""Return the Face"""
|
||||
return Shape.get_single_shape(self, "Face")
|
||||
|
||||
|
|
@ -1788,7 +1861,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
return self._apply_transform(transformation)
|
||||
|
||||
def shell(self) -> Shell | None:
|
||||
def shell(self) -> Shell:
|
||||
"""Return the Shell"""
|
||||
return Shape.get_single_shape(self, "Shell")
|
||||
|
||||
|
|
@ -1846,7 +1919,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
result = Shape._show_tree(tree[0], show_center)
|
||||
return result
|
||||
|
||||
def solid(self) -> Solid | None:
|
||||
def solid(self) -> Solid:
|
||||
"""Return the Solid"""
|
||||
return Shape.get_single_shape(self, "Solid")
|
||||
|
||||
|
|
@ -2307,7 +2380,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
self_translated = self.moved(Location(TopLoc_Location(transformation)))
|
||||
return self_translated
|
||||
|
||||
def wire(self) -> Wire | None:
|
||||
def wire(self) -> Wire:
|
||||
"""Return the Wire"""
|
||||
return Shape.get_single_shape(self, "Wire")
|
||||
|
||||
|
|
@ -2370,21 +2443,45 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
arg = TopTools_ListOfShape()
|
||||
for obj in args:
|
||||
if obj.wrapped is not None:
|
||||
arg.Append(obj.wrapped)
|
||||
if obj._wrapped is not None:
|
||||
arg.Append(obj._wrapped)
|
||||
|
||||
tool = TopTools_ListOfShape()
|
||||
for obj in tools:
|
||||
if obj.wrapped is not None:
|
||||
tool.Append(obj.wrapped)
|
||||
if obj._wrapped is not None:
|
||||
tool.Append(obj._wrapped)
|
||||
|
||||
operation.SetArguments(arg)
|
||||
operation.SetTools(tool)
|
||||
# Handle operations with "zero" shapes
|
||||
topo_result = None
|
||||
if isinstance(operation, BRepAlgoAPI_Cut):
|
||||
if tool.IsEmpty():
|
||||
if arg.Extent() == 1:
|
||||
topo_result = arg.First()
|
||||
else:
|
||||
topo_result = _make_topods_compound_from_shapes(arg)
|
||||
elif isinstance(operation, BRepAlgoAPI_Fuse):
|
||||
if tool.IsEmpty():
|
||||
if arg.Extent() == 1:
|
||||
topo_result = arg.First()
|
||||
else:
|
||||
topo_result = _make_topods_compound_from_shapes(arg)
|
||||
elif arg.IsEmpty():
|
||||
if tool.Extent() == 1:
|
||||
topo_result = tool.First()
|
||||
else:
|
||||
topo_result = _make_topods_compound_from_shapes(tool)
|
||||
elif isinstance(operation, BRepAlgoAPI_Common):
|
||||
if tool.IsEmpty() or arg.IsEmpty():
|
||||
return self.__class__()
|
||||
|
||||
operation.SetRunParallel(True)
|
||||
operation.Build()
|
||||
if topo_result is None:
|
||||
operation.SetArguments(arg)
|
||||
operation.SetTools(tool)
|
||||
|
||||
topo_result = downcast(operation.Shape())
|
||||
operation.SetRunParallel(True)
|
||||
operation.Build()
|
||||
|
||||
topo_result = downcast(operation.Shape())
|
||||
|
||||
# Clean
|
||||
if SkipClean.clean:
|
||||
|
|
@ -2501,7 +2598,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
return shape_to_html(self)._repr_html_()
|
||||
return repr(self)
|
||||
|
||||
def vertex(self) -> Vertex | None:
|
||||
def vertex(self) -> Vertex:
|
||||
"""Return the Vertex"""
|
||||
return Shape.get_single_shape(self, "Vertex")
|
||||
|
||||
|
|
@ -2749,9 +2846,7 @@ class ShapeList(list[T]):
|
|||
compounds = self.compounds()
|
||||
compound_count = len(compounds)
|
||||
if compound_count != 1:
|
||||
warnings.warn(
|
||||
f"Found {compound_count} compounds, returning first", stacklevel=2
|
||||
)
|
||||
raise ValueError(f"Expected exactly one compound, found {compound_count}")
|
||||
return compounds[0]
|
||||
|
||||
def compounds(self) -> ShapeList[Compound]:
|
||||
|
|
@ -2763,7 +2858,7 @@ class ShapeList(list[T]):
|
|||
edges = self.edges()
|
||||
edge_count = len(edges)
|
||||
if edge_count != 1:
|
||||
warnings.warn(f"Found {edge_count} edges, returning first", stacklevel=2)
|
||||
raise ValueError(f"Expected exactly one edge, found {edge_count}")
|
||||
return edges[0]
|
||||
|
||||
def edges(self) -> ShapeList[Edge]:
|
||||
|
|
@ -2775,8 +2870,7 @@ class ShapeList(list[T]):
|
|||
faces = self.faces()
|
||||
face_count = len(faces)
|
||||
if face_count != 1:
|
||||
msg = f"Found {face_count} faces, returning first"
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
raise ValueError(f"Expected exactly one face, found {face_count}")
|
||||
return faces[0]
|
||||
|
||||
def faces(self) -> ShapeList[Face]:
|
||||
|
|
@ -3073,7 +3167,7 @@ class ShapeList(list[T]):
|
|||
shells = self.shells()
|
||||
shell_count = len(shells)
|
||||
if shell_count != 1:
|
||||
warnings.warn(f"Found {shell_count} shells, returning first", stacklevel=2)
|
||||
raise ValueError(f"Expected exactly one shell, found {shell_count}")
|
||||
return shells[0]
|
||||
|
||||
def shells(self) -> ShapeList[Shell]:
|
||||
|
|
@ -3085,7 +3179,7 @@ class ShapeList(list[T]):
|
|||
solids = self.solids()
|
||||
solid_count = len(solids)
|
||||
if solid_count != 1:
|
||||
warnings.warn(f"Found {solid_count} solids, returning first", stacklevel=2)
|
||||
raise ValueError(f"Expected exactly one solid, found {solid_count}")
|
||||
return solids[0]
|
||||
|
||||
def solids(self) -> ShapeList[Solid]:
|
||||
|
|
@ -3217,9 +3311,7 @@ class ShapeList(list[T]):
|
|||
vertices = self.vertices()
|
||||
vertex_count = len(vertices)
|
||||
if vertex_count != 1:
|
||||
warnings.warn(
|
||||
f"Found {vertex_count} vertices, returning first", stacklevel=2
|
||||
)
|
||||
raise ValueError(f"Expected exactly one vertex, found {vertex_count}")
|
||||
return vertices[0]
|
||||
|
||||
def vertices(self) -> ShapeList[Vertex]:
|
||||
|
|
@ -3231,7 +3323,7 @@ class ShapeList(list[T]):
|
|||
wires = self.wires()
|
||||
wire_count = len(wires)
|
||||
if wire_count != 1:
|
||||
warnings.warn(f"Found {wire_count} wires, returning first", stacklevel=2)
|
||||
raise ValueError(f"Expected exactly one wire, found {wire_count}")
|
||||
return wires[0]
|
||||
|
||||
def wires(self) -> ShapeList[Wire]:
|
||||
|
|
@ -3516,3 +3608,27 @@ def unwrap_topods_compound(
|
|||
|
||||
# If there are no elements or more than one element, return TopoDS_Compound
|
||||
return compound
|
||||
|
||||
|
||||
def _make_topods_compound_from_shapes(
|
||||
occt_shapes: Iterable[TopoDS_Shape | None],
|
||||
) -> TopoDS_Compound:
|
||||
"""Create an OCCT TopoDS_Compound
|
||||
|
||||
Create an OCCT TopoDS_Compound object from an iterable of TopoDS_Shape objects
|
||||
|
||||
Args:
|
||||
occt_shapes (Iterable[TopoDS_Shape]): OCCT shapes
|
||||
|
||||
Returns:
|
||||
TopoDS_Compound: OCCT compound
|
||||
"""
|
||||
comp = TopoDS_Compound()
|
||||
comp_builder = TopoDS_Builder()
|
||||
comp_builder.MakeCompound(comp)
|
||||
|
||||
for shape in occt_shapes:
|
||||
if shape is not None:
|
||||
comp_builder.Add(comp, shape)
|
||||
|
||||
return comp
|
||||
|
|
|
|||
|
|
@ -125,12 +125,12 @@ from .shape_core import (
|
|||
get_top_level_topods_shapes,
|
||||
shapetype,
|
||||
unwrap_topods_compound,
|
||||
_make_topods_compound_from_shapes,
|
||||
)
|
||||
from .two_d import Face, Mixin2D, Shell, sort_wires_by_build_order
|
||||
from .utils import (
|
||||
_extrude_topods_shape,
|
||||
_make_loft,
|
||||
_make_topods_compound_from_shapes,
|
||||
find_max_dimension,
|
||||
)
|
||||
from .zero_d import Vertex
|
||||
|
|
|
|||
|
|
@ -84,7 +84,14 @@ from OCP.TopoDS import (
|
|||
)
|
||||
from build123d.geometry import TOLERANCE, BoundBox, Vector, VectorLike
|
||||
|
||||
from .shape_core import Shape, ShapeList, downcast, shapetype, unwrap_topods_compound
|
||||
from .shape_core import (
|
||||
Shape,
|
||||
ShapeList,
|
||||
downcast,
|
||||
shapetype,
|
||||
unwrap_topods_compound,
|
||||
_make_topods_compound_from_shapes,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
|
|
@ -197,30 +204,6 @@ def _make_loft(
|
|||
return loft_builder.Shape()
|
||||
|
||||
|
||||
def _make_topods_compound_from_shapes(
|
||||
occt_shapes: Iterable[TopoDS_Shape | None],
|
||||
) -> TopoDS_Compound:
|
||||
"""Create an OCCT TopoDS_Compound
|
||||
|
||||
Create an OCCT TopoDS_Compound object from an iterable of TopoDS_Shape objects
|
||||
|
||||
Args:
|
||||
occt_shapes (Iterable[TopoDS_Shape]): OCCT shapes
|
||||
|
||||
Returns:
|
||||
TopoDS_Compound: OCCT compound
|
||||
"""
|
||||
comp = TopoDS_Compound()
|
||||
comp_builder = TopoDS_Builder()
|
||||
comp_builder.MakeCompound(comp)
|
||||
|
||||
for shape in occt_shapes:
|
||||
if shape is not None:
|
||||
comp_builder.Add(comp, shape)
|
||||
|
||||
return comp
|
||||
|
||||
|
||||
def _make_topods_face_from_wires(
|
||||
outer_wire: TopoDS_Wire, inner_wires: Iterable[TopoDS_Wire] | None = None
|
||||
) -> TopoDS_Face:
|
||||
|
|
|
|||
2916
tests/dxf/accumulatortest.dxf
Normal file
2916
tests/dxf/accumulatortest.dxf
Normal file
File diff suppressed because it is too large
Load diff
2934
tests/dxf/blocks1.dxf
Normal file
2934
tests/dxf/blocks1.dxf
Normal file
File diff suppressed because it is too large
Load diff
3576
tests/dxf/blocks2.dxf
Normal file
3576
tests/dxf/blocks2.dxf
Normal file
File diff suppressed because it is too large
Load diff
4184
tests/dxf/bridge.dxf
Normal file
4184
tests/dxf/bridge.dxf
Normal file
File diff suppressed because it is too large
Load diff
2830
tests/dxf/circlesellipsesarcs.dxf
Normal file
2830
tests/dxf/circlesellipsesarcs.dxf
Normal file
File diff suppressed because it is too large
Load diff
3952
tests/dxf/closedlwpolylinebug.dxf
Normal file
3952
tests/dxf/closedlwpolylinebug.dxf
Normal file
File diff suppressed because it is too large
Load diff
414
tests/dxf/cube.dxf
Normal file
414
tests/dxf/cube.dxf
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
0
|
||||
SECTION
|
||||
2
|
||||
HEADER
|
||||
999
|
||||
cube.dxf created by IVREAD.
|
||||
999
|
||||
Original data in cube.iv
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
SECTION
|
||||
2
|
||||
TABLES
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
SECTION
|
||||
2
|
||||
BLOCKS
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
SECTION
|
||||
2
|
||||
ENTITIES
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-0.5000
|
||||
20
|
||||
-0.5000
|
||||
30
|
||||
1.000
|
||||
11
|
||||
0.5000
|
||||
21
|
||||
-0.5000
|
||||
31
|
||||
1.000
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.5000
|
||||
20
|
||||
-0.5000
|
||||
30
|
||||
1.000
|
||||
11
|
||||
0.5000
|
||||
21
|
||||
0.5000
|
||||
31
|
||||
1.000
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.5000
|
||||
20
|
||||
0.5000
|
||||
30
|
||||
1.000
|
||||
11
|
||||
-0.5000
|
||||
21
|
||||
0.5000
|
||||
31
|
||||
1.000
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-0.5000
|
||||
20
|
||||
0.5000
|
||||
30
|
||||
1.000
|
||||
11
|
||||
-0.5000
|
||||
21
|
||||
-0.5000
|
||||
31
|
||||
1.000
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.5000
|
||||
20
|
||||
-0.5000
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
-0.5000
|
||||
21
|
||||
-0.5000
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-0.5000
|
||||
20
|
||||
-0.5000
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
-0.5000
|
||||
21
|
||||
0.5000
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-0.5000
|
||||
20
|
||||
0.5000
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
0.5000
|
||||
21
|
||||
0.5000
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.5000
|
||||
20
|
||||
0.5000
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
0.5000
|
||||
21
|
||||
-0.5000
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-0.5000
|
||||
20
|
||||
-0.5000
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
-0.5000
|
||||
21
|
||||
-0.5000
|
||||
31
|
||||
1.000
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-0.5000
|
||||
20
|
||||
-0.5000
|
||||
30
|
||||
1.000
|
||||
11
|
||||
-0.5000
|
||||
21
|
||||
0.5000
|
||||
31
|
||||
1.000
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-0.5000
|
||||
20
|
||||
0.5000
|
||||
30
|
||||
1.000
|
||||
11
|
||||
-0.5000
|
||||
21
|
||||
0.5000
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-0.5000
|
||||
20
|
||||
0.5000
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
-0.5000
|
||||
21
|
||||
-0.5000
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.5000
|
||||
20
|
||||
-0.5000
|
||||
30
|
||||
1.000
|
||||
11
|
||||
0.5000
|
||||
21
|
||||
-0.5000
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.5000
|
||||
20
|
||||
-0.5000
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
0.5000
|
||||
21
|
||||
0.5000
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.5000
|
||||
20
|
||||
0.5000
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
0.5000
|
||||
21
|
||||
0.5000
|
||||
31
|
||||
1.000
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.5000
|
||||
20
|
||||
0.5000
|
||||
30
|
||||
1.000
|
||||
11
|
||||
0.5000
|
||||
21
|
||||
-0.5000
|
||||
31
|
||||
1.000
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-0.5000
|
||||
20
|
||||
-0.5000
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
0.5000
|
||||
21
|
||||
-0.5000
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.5000
|
||||
20
|
||||
-0.5000
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
0.5000
|
||||
21
|
||||
-0.5000
|
||||
31
|
||||
1.000
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.5000
|
||||
20
|
||||
-0.5000
|
||||
30
|
||||
1.000
|
||||
11
|
||||
-0.5000
|
||||
21
|
||||
-0.5000
|
||||
31
|
||||
1.000
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-0.5000
|
||||
20
|
||||
-0.5000
|
||||
30
|
||||
1.000
|
||||
11
|
||||
-0.5000
|
||||
21
|
||||
-0.5000
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-0.5000
|
||||
20
|
||||
0.5000
|
||||
30
|
||||
1.000
|
||||
11
|
||||
0.5000
|
||||
21
|
||||
0.5000
|
||||
31
|
||||
1.000
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.5000
|
||||
20
|
||||
0.5000
|
||||
30
|
||||
1.000
|
||||
11
|
||||
0.5000
|
||||
21
|
||||
0.5000
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.5000
|
||||
20
|
||||
0.5000
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
-0.5000
|
||||
21
|
||||
0.5000
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-0.5000
|
||||
20
|
||||
0.5000
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
-0.5000
|
||||
21
|
||||
0.5000
|
||||
31
|
||||
1.000
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
EOF
|
||||
200
tests/dxf/diamond.dxf
Normal file
200
tests/dxf/diamond.dxf
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
0
|
||||
SECTION
|
||||
2
|
||||
ENTITIES
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
45.00
|
||||
20
|
||||
45.00
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
45.00
|
||||
21
|
||||
-45.00
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
45.00
|
||||
20
|
||||
-45.00
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
0.0000E+00
|
||||
21
|
||||
0.0000E+00
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.0000E+00
|
||||
20
|
||||
0.0000E+00
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
-45.00
|
||||
21
|
||||
45.00
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-45.00
|
||||
20
|
||||
45.00
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
45.00
|
||||
21
|
||||
45.00
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
45.00
|
||||
20
|
||||
45.00
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
0.0000E+00
|
||||
21
|
||||
0.0000E+00
|
||||
31
|
||||
-78.00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.0000E+00
|
||||
20
|
||||
0.0000E+00
|
||||
30
|
||||
-78.00
|
||||
11
|
||||
0.0000E+00
|
||||
21
|
||||
0.0000E+00
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.0000E+00
|
||||
20
|
||||
0.0000E+00
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
-45.00
|
||||
21
|
||||
-45.00
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-45.00
|
||||
20
|
||||
-45.00
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
45.00
|
||||
21
|
||||
45.00
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
45.00
|
||||
20
|
||||
-45.00
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
-45.00
|
||||
21
|
||||
-45.00
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-45.00
|
||||
20
|
||||
-45.00
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
-45.00
|
||||
21
|
||||
45.00
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
-45.00
|
||||
20
|
||||
45.00
|
||||
30
|
||||
0.0000E+00
|
||||
11
|
||||
0.0000E+00
|
||||
21
|
||||
0.0000E+00
|
||||
31
|
||||
-78.00
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.0000E+00
|
||||
20
|
||||
0.0000E+00
|
||||
30
|
||||
-78.00
|
||||
11
|
||||
45.00
|
||||
21
|
||||
-45.00
|
||||
31
|
||||
0.0000E+00
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
EOF
|
||||
2986
tests/dxf/ellipticalarcs.dxf
Normal file
2986
tests/dxf/ellipticalarcs.dxf
Normal file
File diff suppressed because it is too large
Load diff
2816
tests/dxf/ellipticalarcs2.dxf
Normal file
2816
tests/dxf/ellipticalarcs2.dxf
Normal file
File diff suppressed because it is too large
Load diff
3044
tests/dxf/empty.dxf
Normal file
3044
tests/dxf/empty.dxf
Normal file
File diff suppressed because it is too large
Load diff
2870
tests/dxf/hatches.dxf
Normal file
2870
tests/dxf/hatches.dxf
Normal file
File diff suppressed because it is too large
Load diff
2936
tests/dxf/layers.dxf
Normal file
2936
tests/dxf/layers.dxf
Normal file
File diff suppressed because it is too large
Load diff
2946
tests/dxf/lines.dxf
Normal file
2946
tests/dxf/lines.dxf
Normal file
File diff suppressed because it is too large
Load diff
2772
tests/dxf/lwpolylines.dxf
Normal file
2772
tests/dxf/lwpolylines.dxf
Normal file
File diff suppressed because it is too large
Load diff
3144
tests/dxf/output.dxf
Normal file
3144
tests/dxf/output.dxf
Normal file
File diff suppressed because it is too large
Load diff
2720
tests/dxf/points.dxf
Normal file
2720
tests/dxf/points.dxf
Normal file
File diff suppressed because it is too large
Load diff
1282
tests/dxf/polylines.dxf
Normal file
1282
tests/dxf/polylines.dxf
Normal file
File diff suppressed because it is too large
Load diff
1092
tests/dxf/rectangle.dxf
Normal file
1092
tests/dxf/rectangle.dxf
Normal file
File diff suppressed because it is too large
Load diff
4210
tests/dxf/shaft_simple.dxf
Normal file
4210
tests/dxf/shaft_simple.dxf
Normal file
File diff suppressed because it is too large
Load diff
2758
tests/dxf/splineA.dxf
Normal file
2758
tests/dxf/splineA.dxf
Normal file
File diff suppressed because it is too large
Load diff
2868
tests/dxf/splines.dxf
Normal file
2868
tests/dxf/splines.dxf
Normal file
File diff suppressed because it is too large
Load diff
1106
tests/dxf/squareandcircle.dxf
Normal file
1106
tests/dxf/squareandcircle.dxf
Normal file
File diff suppressed because it is too large
Load diff
3502
tests/dxf/test-angled-section.dxf
Normal file
3502
tests/dxf/test-angled-section.dxf
Normal file
File diff suppressed because it is too large
Load diff
3628
tests/dxf/test-circle-rotation.dxf
Normal file
3628
tests/dxf/test-circle-rotation.dxf
Normal file
File diff suppressed because it is too large
Load diff
3274
tests/dxf/test-conic-section.dxf
Normal file
3274
tests/dxf/test-conic-section.dxf
Normal file
File diff suppressed because it is too large
Load diff
4132
tests/dxf/test-drawing.dxf
Normal file
4132
tests/dxf/test-drawing.dxf
Normal file
File diff suppressed because it is too large
Load diff
6662
tests/dxf/test-ellipse-rotation.dxf
Normal file
6662
tests/dxf/test-ellipse-rotation.dxf
Normal file
File diff suppressed because it is too large
Load diff
3296
tests/dxf/test-sketch.dxf
Normal file
3296
tests/dxf/test-sketch.dxf
Normal file
File diff suppressed because it is too large
Load diff
4082
tests/dxf/test_export.dxf
Normal file
4082
tests/dxf/test_export.dxf
Normal file
File diff suppressed because it is too large
Load diff
2832
tests/dxf/texts.dxf
Normal file
2832
tests/dxf/texts.dxf
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -127,7 +127,7 @@ class TestBuilder(unittest.TestCase):
|
|||
|
||||
with BuildLine() as l:
|
||||
CenterArc((0, 0), 1, 0, 90)
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one vertex"):
|
||||
l.vertex()
|
||||
|
||||
def test_edge(self):
|
||||
|
|
@ -138,7 +138,7 @@ class TestBuilder(unittest.TestCase):
|
|||
|
||||
with BuildSketch() as s:
|
||||
Rectangle(1, 1)
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one edge"):
|
||||
s.edge()
|
||||
|
||||
def test_wire(self):
|
||||
|
|
@ -149,7 +149,7 @@ class TestBuilder(unittest.TestCase):
|
|||
|
||||
with BuildPart() as p:
|
||||
Box(1, 1, 1)
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one wire"):
|
||||
p.wire()
|
||||
|
||||
def test_face(self):
|
||||
|
|
@ -160,7 +160,7 @@ class TestBuilder(unittest.TestCase):
|
|||
|
||||
with BuildPart() as p:
|
||||
Box(1, 1, 1)
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one face"):
|
||||
p.face()
|
||||
|
||||
def test_solid(self):
|
||||
|
|
@ -171,7 +171,7 @@ class TestBuilder(unittest.TestCase):
|
|||
with BuildSketch():
|
||||
Text("Two", 10)
|
||||
extrude(amount=5)
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one solid"):
|
||||
p.solid()
|
||||
|
||||
def test_workplanes_as_list(self):
|
||||
|
|
|
|||
|
|
@ -99,6 +99,22 @@ class BuildLineTests(unittest.TestCase):
|
|||
self.assertAlmostEqual(bz.wires()[0].length, 225.98661946375782, 5)
|
||||
self.assertTrue(isinstance(b1, Edge))
|
||||
|
||||
def test_bspline(self):
|
||||
control_points = [(0, 0), (1, 1), (2, 0)]
|
||||
knots = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0]
|
||||
|
||||
with BuildLine() as bl:
|
||||
spline = BSpline(control_points, knots, degree=2)
|
||||
|
||||
self.assertTrue(isinstance(spline, Edge))
|
||||
self.assertEqual(spline.geom_type, GeomType.BSPLINE)
|
||||
self.assertEqual(len(bl.edges()), 1)
|
||||
self.assertAlmostEqual(bl.edge().start_point(), (0, 0, 0), 5)
|
||||
self.assertAlmostEqual(bl.edge().end_point(), (2, 0, 0), 5)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
BSpline(control_points, knots=[], degree=2)
|
||||
|
||||
def test_double_tangent_arc(self):
|
||||
l1 = Line((10, 0), (30, 20))
|
||||
l2 = DoubleTangentArc((0, 5), (1, 0), l1)
|
||||
|
|
@ -435,6 +451,15 @@ class BuildLineTests(unittest.TestCase):
|
|||
)
|
||||
assert len(p.edges()) > 0
|
||||
|
||||
# test FilletPolyline with a user closed shape
|
||||
l1 = FilletPolyline(
|
||||
(0, 0), (2, 6), (0, 5), (-2, 6), (0, 0), radius=(0, 0.1, 0, 0)
|
||||
)
|
||||
assert all(
|
||||
sum(v in e.vertices() for e in l1.edges()) == 2 for v in l1.vertices()
|
||||
)
|
||||
assert len(l1.edges().filter_by(GeomType.CIRCLE)) == 1
|
||||
|
||||
def test_intersecting_line(self):
|
||||
with BuildLine():
|
||||
l1 = Line((0, 0), (10, 0))
|
||||
|
|
|
|||
|
|
@ -113,6 +113,22 @@ class TestEdge(unittest.TestCase):
|
|||
)
|
||||
self.assertAlmostEqual(spline.end_point(), (3, 0, 0), 5)
|
||||
|
||||
def test_make_bspline(self):
|
||||
control_points = [(0, 0), (1, 1), (2, 0)]
|
||||
knots = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0]
|
||||
|
||||
spline = Edge.make_bspline(control_points, knots, degree=2)
|
||||
weighted_spline = Edge.make_bspline(
|
||||
control_points, knots, degree=2, weights=[1.0, 2.0, 1.0]
|
||||
)
|
||||
|
||||
for edge in [spline, weighted_spline]:
|
||||
self.assertEqual(edge.geom_type, GeomType.BSPLINE)
|
||||
self.assertAlmostEqual(edge.start_point(), (0, 0, 0), 5)
|
||||
self.assertAlmostEqual(edge.end_point(), (2, 0, 0), 5)
|
||||
|
||||
self.assertGreater((weighted_spline @ 0.5).Y, (spline @ 0.5).Y)
|
||||
|
||||
def test_distribute_locations(self):
|
||||
line = Edge.make_line((0, 0, 0), (10, 0, 0))
|
||||
locs = line.distribute_locations(3)
|
||||
|
|
|
|||
|
|
@ -27,12 +27,16 @@ license:
|
|||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from anytree import PreOrderIter
|
||||
|
||||
from build123d.exporters3d import export_brep, export_step
|
||||
from build123d.importers import import_brep, import_step, import_stl
|
||||
from build123d.geometry import Location
|
||||
from build123d.mesher import Mesher
|
||||
from build123d.topology import Solid
|
||||
from build123d.topology import Compound, Shape, Solid
|
||||
|
||||
|
||||
class TestImportExport(unittest.TestCase):
|
||||
|
|
@ -62,6 +66,54 @@ class TestImportExport(unittest.TestCase):
|
|||
stl_box = import_stl("test.stl")
|
||||
self.assertAlmostEqual(stl_box.position, (0, 0, 0), 5)
|
||||
|
||||
def test_step_round_trip_preserves_assembly_child_locations(self):
|
||||
def labeled_node(root: Shape, label: str) -> Shape:
|
||||
return next(iter(PreOrderIter(root, filter_=lambda n: n.label == label)))
|
||||
|
||||
box_a = Solid.make_box(1, 1, 1)
|
||||
box_a.label = "box_a"
|
||||
box_a.move(Location((10, 0, 0)))
|
||||
|
||||
box_b = Solid.make_box(1, 1, 1)
|
||||
box_b.label = "box_b"
|
||||
box_b.move(Location((0, 15, 0), (0, 0, 45)))
|
||||
|
||||
cylinder = Solid.make_cylinder(0.5, 2)
|
||||
cylinder.label = "cylinder"
|
||||
cylinder.move(Location((0, 0, 5), (90, 0, 0)))
|
||||
|
||||
sub_assembly = Compound(label="sub_assembly", children=[box_a, cylinder])
|
||||
sub_assembly.move(Location((0, 0, 30), (0, 90, 0)))
|
||||
|
||||
root_assembly = Compound(label="root_assembly", children=[sub_assembly, box_b])
|
||||
root_assembly.move(Location((3, 6, 9), (0, 0, 90)))
|
||||
|
||||
expected_locations = {
|
||||
label: labeled_node(root_assembly, label).global_location
|
||||
for label in ("box_a", "box_b", "cylinder")
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".step", delete=False) as tmp_step:
|
||||
step_path = tmp_step.name
|
||||
try:
|
||||
export_step(root_assembly, step_path)
|
||||
reloaded = import_step(step_path)
|
||||
|
||||
for label, expected in expected_locations.items():
|
||||
reloaded_node = labeled_node(reloaded, label)
|
||||
self.assertAlmostEqual(
|
||||
reloaded_node.global_location.position,
|
||||
expected.position,
|
||||
places=5,
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
reloaded_node.global_location.orientation,
|
||||
expected.orientation,
|
||||
places=5,
|
||||
)
|
||||
finally:
|
||||
os.remove(step_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ from random import uniform
|
|||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
import numpy as np
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse
|
||||
from anytree import PreOrderIter
|
||||
from build123d.build_enums import CenterOf, GeomType, Keep
|
||||
from build123d.geometry import (
|
||||
|
|
@ -59,6 +60,7 @@ from build123d.topology import (
|
|||
Vertex,
|
||||
Wire,
|
||||
)
|
||||
from build123d.topology.composite import Part
|
||||
from build123d.joints import RigidJoint
|
||||
|
||||
|
||||
|
|
@ -118,6 +120,44 @@ class TestShape(unittest.TestCase):
|
|||
self.assertTrue(fuzzy.is_valid)
|
||||
self.assertAlmostEqual(fuzzy.volume, 2, 5)
|
||||
|
||||
def test_boolean_zero_cut(self):
|
||||
box = Solid.make_box(1, 2, 3)
|
||||
zero = Solid()
|
||||
|
||||
result = box.cut(zero)
|
||||
self.assertTrue(isinstance(result, Solid))
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertAlmostEqual(result.volume, box.volume, 5)
|
||||
|
||||
result = box - zero
|
||||
self.assertTrue(isinstance(result, Solid))
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertAlmostEqual(result.volume, box.volume, 5)
|
||||
|
||||
def test_boolean_zero_fuse(self):
|
||||
box = Solid.make_box(1, 2, 3)
|
||||
zero = Solid()
|
||||
|
||||
result = box.fuse(zero)
|
||||
self.assertTrue(isinstance(result, Solid))
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertAlmostEqual(result.volume, box.volume, 5)
|
||||
|
||||
result = zero.fuse(box)
|
||||
self.assertTrue(isinstance(result, Solid))
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertAlmostEqual(result.volume, box.volume, 5)
|
||||
|
||||
result = box + zero
|
||||
self.assertTrue(isinstance(result, Solid))
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertAlmostEqual(result.volume, box.volume, 5)
|
||||
|
||||
result = zero + box
|
||||
self.assertTrue(isinstance(result, Solid))
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertAlmostEqual(result.volume, box.volume, 5)
|
||||
|
||||
def test_faces_intersected_by_axis(self):
|
||||
box = Solid.make_box(1, 1, 1, Plane((0, 0, 1)))
|
||||
intersected_faces = box.faces_intersected_by_axis(Axis.Z)
|
||||
|
|
@ -339,6 +379,46 @@ class TestShape(unittest.TestCase):
|
|||
self.assertAlmostEqual(Vector(intersections[0]), (0.5, 0.5, 0), 5)
|
||||
self.assertAlmostEqual(Vector(intersections[1]), (0.5, 0.5, 1), 5)
|
||||
|
||||
def test_boolean_zero_intersection(self):
|
||||
box = Solid.make_box(1, 2, 3)
|
||||
zero = Solid()
|
||||
|
||||
self.assertIsNone(box.intersect(zero))
|
||||
self.assertIsNone(zero.intersect(box))
|
||||
|
||||
def test_boolean_zero_cut_multi_args(self):
|
||||
box1 = Solid.make_box(1, 1, 1)
|
||||
box2 = Solid.make_box(1, 1, 1, Plane((2, 0, 0)))
|
||||
zero = Solid()
|
||||
|
||||
result = box1._bool_op((box1, box2), (zero,), BRepAlgoAPI_Cut())
|
||||
self.assertTrue(isinstance(result, Part))
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertEqual(len(result.solids()), 2)
|
||||
self.assertAlmostEqual(result.volume, box1.volume + box2.volume, 5)
|
||||
|
||||
def test_boolean_zero_fuse_multi_args(self):
|
||||
box1 = Solid.make_box(1, 1, 1)
|
||||
box2 = Solid.make_box(1, 1, 1, Plane((2, 0, 0)))
|
||||
zero = Solid()
|
||||
|
||||
result = box1._bool_op((box1, box2), (zero,), BRepAlgoAPI_Fuse())
|
||||
self.assertTrue(isinstance(result, Part))
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertEqual(len(result.solids()), 2)
|
||||
self.assertAlmostEqual(result.volume, box1.volume + box2.volume, 5)
|
||||
|
||||
def test_boolean_zero_fuse_multi_tools(self):
|
||||
box1 = Solid.make_box(1, 1, 1)
|
||||
box2 = Solid.make_box(1, 1, 1, Plane((2, 0, 0)))
|
||||
zero = Solid()
|
||||
|
||||
result = box1._bool_op((zero,), (box1, box2), BRepAlgoAPI_Fuse())
|
||||
self.assertTrue(isinstance(result, Part))
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertEqual(len(result.solids()), 2)
|
||||
self.assertAlmostEqual(result.volume, box1.volume + box2.volume, 5)
|
||||
|
||||
def test_clean_error(self):
|
||||
"""Note that this test is here to alert build123d to changes in bad OCCT clean behavior
|
||||
with spheres or hemispheres. The extra edge in a sphere seems to be the cause of this.
|
||||
|
|
@ -395,44 +475,44 @@ class TestShape(unittest.TestCase):
|
|||
def test_vertex(self):
|
||||
v = Edge.make_circle(1).vertex()
|
||||
self.assertTrue(isinstance(v, Vertex))
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one vertex"):
|
||||
Wire.make_rect(1, 1).vertex()
|
||||
|
||||
def test_edge(self):
|
||||
e = Edge.make_circle(1).edge()
|
||||
self.assertTrue(isinstance(e, Edge))
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one edge"):
|
||||
Wire.make_rect(1, 1).edge()
|
||||
|
||||
def test_wire(self):
|
||||
w = Wire.make_circle(1).wire()
|
||||
self.assertTrue(isinstance(w, Wire))
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one wire"):
|
||||
Solid.make_box(1, 1, 1).wire()
|
||||
|
||||
def test_compound(self):
|
||||
c = Compound.make_text("hello", 10)
|
||||
self.assertTrue(isinstance(c, Compound))
|
||||
c2 = Compound.make_text("world", 10)
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one compound"):
|
||||
Compound(children=[c, c2]).compound()
|
||||
|
||||
def test_face(self):
|
||||
f = Face.make_rect(1, 1)
|
||||
self.assertTrue(isinstance(f, Face))
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one face"):
|
||||
Solid.make_box(1, 1, 1).face()
|
||||
|
||||
def test_shell(self):
|
||||
s = Solid.make_sphere(1).shell()
|
||||
self.assertTrue(isinstance(s, Shell))
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one shell"):
|
||||
extrude(Compound.make_text("two", 10), amount=5).shell()
|
||||
|
||||
def test_solid(self):
|
||||
s = Solid.make_sphere(1).solid()
|
||||
self.assertTrue(isinstance(s, Solid))
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one solid"):
|
||||
Compound(Solid.make_sphere(1).split(Plane.XY, keep=Keep.BOTH)).solid()
|
||||
|
||||
def test_manifold(self):
|
||||
|
|
@ -625,12 +705,18 @@ class TestShape(unittest.TestCase):
|
|||
self.assertEqual(Vertex(1, 1, 1).shells(), ShapeList())
|
||||
self.assertEqual(Vertex(1, 1, 1).solids(), ShapeList())
|
||||
self.assertEqual(Vertex(1, 1, 1).compounds(), ShapeList())
|
||||
self.assertIsNone(Vertex(1, 1, 1).edge())
|
||||
self.assertIsNone(Vertex(1, 1, 1).wire())
|
||||
self.assertIsNone(Vertex(1, 1, 1).face())
|
||||
self.assertIsNone(Vertex(1, 1, 1).shell())
|
||||
self.assertIsNone(Vertex(1, 1, 1).solid())
|
||||
self.assertIsNone(Vertex(1, 1, 1).compound())
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one edge"):
|
||||
Vertex(1, 1, 1).edge()
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one wire"):
|
||||
Vertex(1, 1, 1).wire()
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one face"):
|
||||
Vertex(1, 1, 1).face()
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one shell"):
|
||||
Vertex(1, 1, 1).shell()
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one solid"):
|
||||
Vertex(1, 1, 1).solid()
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one compound"):
|
||||
Vertex(1, 1, 1).compound()
|
||||
|
||||
def test_rotate(self):
|
||||
line = Edge.make_line((0, 0), (1, 0))
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ class TestShapeList(unittest.TestCase):
|
|||
sl = ShapeList([Edge.make_circle(1)])
|
||||
self.assertAlmostEqual(tuple(sl.vertex()), (1, 0, 0), 5)
|
||||
sl = ShapeList([Face.make_rect(1, 1), Face.make_rect(1, 1, Plane((4, 4)))])
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one vertex"):
|
||||
sl.vertex()
|
||||
self.assertEqual(len(Edge().vertices()), 0)
|
||||
|
||||
|
|
@ -336,7 +336,7 @@ class TestShapeList(unittest.TestCase):
|
|||
sl = ShapeList([Edge.make_circle(1)])
|
||||
self.assertAlmostEqual(sl.edge().length, 2 * 1 * math.pi, 5)
|
||||
sl = ShapeList([Face.make_rect(1, 1), Face.make_rect(1, 1, Plane((4, 4)))])
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one edge"):
|
||||
sl.edge()
|
||||
|
||||
def test_wires(self):
|
||||
|
|
@ -348,7 +348,7 @@ class TestShapeList(unittest.TestCase):
|
|||
sl = ShapeList([Wire.make_circle(1)])
|
||||
self.assertAlmostEqual(sl.wire().length, 2 * 1 * math.pi, 5)
|
||||
sl = ShapeList([Face.make_rect(1, 1), Face.make_rect(1, 1, Plane((4, 4)))])
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one wire"):
|
||||
sl.wire()
|
||||
|
||||
def test_faces(self):
|
||||
|
|
@ -362,7 +362,7 @@ class TestShapeList(unittest.TestCase):
|
|||
)
|
||||
self.assertAlmostEqual(sl.face().area, 2 * 1, 5)
|
||||
sl = ShapeList([Solid.make_box(1, 1, 1), Solid.make_cylinder(1, 1)])
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one face"):
|
||||
sl.face()
|
||||
|
||||
def test_shells(self):
|
||||
|
|
@ -374,7 +374,7 @@ class TestShapeList(unittest.TestCase):
|
|||
sl = ShapeList([Vertex(1, 1, 1), Solid.make_box(1, 1, 1)])
|
||||
self.assertAlmostEqual(sl.shell().area, 6 * 1 * 1, 5)
|
||||
sl = ShapeList([Solid.make_box(1, 1, 1), Solid.make_cylinder(1, 1)])
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one shell"):
|
||||
sl.shell()
|
||||
|
||||
def test_solids(self):
|
||||
|
|
@ -384,7 +384,7 @@ class TestShapeList(unittest.TestCase):
|
|||
|
||||
def test_solid(self):
|
||||
sl = ShapeList([Solid.make_box(1, 1, 1), Solid.make_cylinder(1, 1)])
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one solid"):
|
||||
sl.solid()
|
||||
sl = ShapeList([Solid.make_box(1, 2, 3), Vertex(1, 1, 1)])
|
||||
self.assertAlmostEqual(sl.solid().volume, 1 * 2 * 3, 5)
|
||||
|
|
@ -396,7 +396,7 @@ class TestShapeList(unittest.TestCase):
|
|||
|
||||
def test_compound(self):
|
||||
sl = ShapeList([Box(1, 1, 1), Cylinder(1, 1)])
|
||||
with self.assertWarns(UserWarning):
|
||||
with self.assertRaisesRegex(ValueError, "Expected exactly one compound"):
|
||||
sl.compound()
|
||||
sl = ShapeList([Box(1, 2, 3), Vertex(1, 1, 1)])
|
||||
self.assertAlmostEqual(sl.compound().volume, 1 * 2 * 3, 5)
|
||||
|
|
|
|||
|
|
@ -221,6 +221,22 @@ class TestWire(unittest.TestCase):
|
|||
t8 = c.trim(0.4, 0.9)
|
||||
self.assertAlmostEqual(c.length * 0.5, t8.length, 5)
|
||||
|
||||
reversed_arc = Edge.make_circle(
|
||||
3, Plane((5, 3, 0)), start_angle=-90, end_angle=0
|
||||
).reversed()
|
||||
explicit_reversed_wire = Wire(
|
||||
[
|
||||
Edge.make_line((0, 0), (5, 0)),
|
||||
reversed_arc,
|
||||
Edge.make_line((8, 3), (12, 3)),
|
||||
]
|
||||
)
|
||||
trimmed_reversed_wire = explicit_reversed_wire.trim(0.2, 0.8)
|
||||
self.assertEqual(len(trimmed_reversed_wire.edges()), 3)
|
||||
self.assertAlmostEqual(trimmed_reversed_wire.length, 8.227433388230814, 5)
|
||||
self.assertAlmostEqual(trimmed_reversed_wire @ 0, (2.7424777960769386, 0, 0), 5)
|
||||
self.assertAlmostEqual(trimmed_reversed_wire @ 1, (9.257522203923063, 3, 0), 5)
|
||||
|
||||
def test_param_at_point(self):
|
||||
e = Edge.make_three_point_arc((0, -20), (5, 0), (0, 20))
|
||||
# Three edges are created 0->0.5->0.75->1.0
|
||||
|
|
|
|||
574
tests/test_import_dxf.py
Normal file
574
tests/test_import_dxf.py
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
"""
|
||||
Tests for the DXF importer
|
||||
|
||||
name: test_import_dxf.py
|
||||
by: Gumyr
|
||||
date: May 7 2026
|
||||
|
||||
desc:
|
||||
This python module tests the dxf importer.
|
||||
|
||||
license:
|
||||
|
||||
Copyright 2026 Gumyr
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from io import BytesIO, StringIO
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from build123d import import_dxf
|
||||
from build123d.build_enums import TextAlign
|
||||
from build123d.objects_curve import (
|
||||
BSpline,
|
||||
CenterArc,
|
||||
EllipticalCenterArc,
|
||||
Line,
|
||||
SagittaArc,
|
||||
)
|
||||
from build123d.objects_sketch import Polygon, Text
|
||||
from build123d.topology import Edge, Vertex, Wire
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def tests_dir() -> Path:
|
||||
"""Reference the main tests directory."""
|
||||
return Path(__file__).resolve().parent
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def dxf_dir(tests_dir: Path) -> Path:
|
||||
"""Reference DXF fixtures stored under the tests directory."""
|
||||
return tests_dir / "dxf"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def import_dxf_module():
|
||||
"""Load the importer module so helper functions can be tested directly."""
|
||||
return importlib.import_module("build123d.import_dxf")
|
||||
|
||||
|
||||
# Input contract
|
||||
|
||||
|
||||
def test_import_empty(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "empty.dxf"))
|
||||
assert len(result) == 0
|
||||
|
||||
|
||||
def test_import_empty_from_text_stream(dxf_dir: Path):
|
||||
dxf_text = (dxf_dir / "empty.dxf").read_text(encoding="latin1")
|
||||
result = import_dxf(StringIO(dxf_text))
|
||||
assert len(result) == 0
|
||||
|
||||
|
||||
def test_import_empty_from_binary_stream(dxf_dir: Path):
|
||||
dxf_bytes = (dxf_dir / "empty.dxf").read_bytes()
|
||||
result = import_dxf(BytesIO(dxf_bytes))
|
||||
assert len(result) == 0
|
||||
|
||||
|
||||
def test_import_unsupported_input_type():
|
||||
with pytest.raises(TypeError, match="Unsupported DXF input type"):
|
||||
import_dxf(1.23) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_import_invalid_dxf_raises_value_error():
|
||||
with pytest.raises(ValueError, match="Failed to read"):
|
||||
import_dxf(StringIO("not a dxf"))
|
||||
|
||||
|
||||
# Core entity fixtures
|
||||
|
||||
|
||||
def test_import_lines(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "lines.dxf"))
|
||||
assert len(result) == 11
|
||||
assert all(isinstance(obj, Line) for obj in result)
|
||||
|
||||
|
||||
def test_import_points(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "points.dxf"))
|
||||
assert len(result) == 2
|
||||
assert all(isinstance(obj, Vertex) for obj in result)
|
||||
assert tuple(result[0]) == (10.0, 20.0, 0.0)
|
||||
assert tuple(result[1]) == (30.0, 10.0, 0.0)
|
||||
|
||||
|
||||
def test_import_lwpolylines(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "lwpolylines.dxf"))
|
||||
assert len(result) == 2
|
||||
assert all(isinstance(obj, Wire) for obj in result)
|
||||
|
||||
assert result[0].is_closed
|
||||
assert len(result[0].edges()) == 4
|
||||
|
||||
assert not result[1].is_closed
|
||||
assert len(result[1].edges()) == 6
|
||||
|
||||
|
||||
def test_import_circles_ellipses_arcs(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "circlesellipsesarcs.dxf"))
|
||||
assert len(result) == 5
|
||||
assert sum(isinstance(obj, EllipticalCenterArc) for obj in result) == 2
|
||||
assert sum(isinstance(obj, CenterArc) for obj in result) == 2
|
||||
assert sum(type(obj) is Edge for obj in result) == 1
|
||||
|
||||
|
||||
def test_import_single_spline(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "splineA.dxf"))
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], BSpline)
|
||||
|
||||
|
||||
def test_import_multiple_splines(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "splines.dxf"))
|
||||
assert len(result) == 2
|
||||
assert all(isinstance(obj, BSpline) for obj in result)
|
||||
|
||||
|
||||
def test_import_polylines(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "polylines.dxf"))
|
||||
assert len(result) == 2
|
||||
assert all(isinstance(obj, Wire) for obj in result)
|
||||
assert all(obj.is_closed for obj in result)
|
||||
assert [len(obj.edges()) for obj in result] == [8, 8]
|
||||
|
||||
|
||||
def test_import_rectangle_polyline(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "rectangle.dxf"))
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], Wire)
|
||||
assert result[0].is_closed
|
||||
assert len(result[0].edges()) == 4
|
||||
|
||||
|
||||
def test_import_square_and_circle(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "squareandcircle.dxf"))
|
||||
assert len(result) == 2
|
||||
assert sum(type(obj) is Edge for obj in result) == 1
|
||||
assert sum(isinstance(obj, Wire) for obj in result) == 1
|
||||
|
||||
|
||||
def test_import_hatch_perimeter(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "hatches.dxf"))
|
||||
assert len(result.wires()) == 1
|
||||
|
||||
|
||||
def test_import_mtext(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "texts.dxf"))
|
||||
assert len(result) == 2
|
||||
assert all(isinstance(obj, Text) for obj in result)
|
||||
assert result[0].text_align == (TextAlign.LEFT, TextAlign.TOPFIRSTLINE)
|
||||
assert result[1].text_align == (TextAlign.LEFT, TextAlign.BOTTOM)
|
||||
|
||||
|
||||
# Regression fixtures
|
||||
|
||||
|
||||
def test_import_blocks(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "blocks1.dxf"))
|
||||
assert len(result) == 10
|
||||
assert all(hasattr(obj, "bounding_box") for obj in result)
|
||||
|
||||
|
||||
def test_import_closed_lwpolyline_block(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "closedlwpolylinebug.dxf"))
|
||||
assert len(result) == 1
|
||||
bbox = result[0].bounding_box()
|
||||
assert round(bbox.min.X, 5) == 30.0
|
||||
assert round(bbox.min.Y, 5) == 40.0
|
||||
assert round(bbox.max.X, 5) == 50.0
|
||||
assert round(bbox.max.Y, 5) == 70.0
|
||||
|
||||
|
||||
def test_import_text_and_block_mtext(dxf_dir: Path):
|
||||
result = import_dxf(str(dxf_dir / "blocks2.dxf"))
|
||||
assert len(result) == 14
|
||||
assert sum(isinstance(obj, Text) for obj in result) == 1
|
||||
assert sum(type(obj).__name__ in {"Text", "Sketch"} for obj in result) == 3
|
||||
|
||||
|
||||
# Unit branch coverage
|
||||
|
||||
|
||||
def test_process_line_degenerate(import_dxf_module):
|
||||
entity = SimpleNamespace(dxf=SimpleNamespace(start=(0, 0, 0), end=(0, 0, 0)))
|
||||
with pytest.warns(UserWarning, match="Skipping degenerate LINE"):
|
||||
assert import_dxf_module.process_line(entity) is None
|
||||
|
||||
|
||||
def test_process_lwpolyline_degenerate(import_dxf_module):
|
||||
entity = SimpleNamespace(
|
||||
dxf=SimpleNamespace(elevation=0.0),
|
||||
get_points=lambda _: [(0.0, 0.0, 0.0)],
|
||||
)
|
||||
with pytest.warns(UserWarning, match="Skipping degenerate LWPOLYLINE"):
|
||||
assert import_dxf_module.process_lwpolyline(entity) is None
|
||||
|
||||
|
||||
def test_process_polyline_bad_mode(import_dxf_module):
|
||||
entity = SimpleNamespace(get_mode=lambda: "AcDb3dPolyline")
|
||||
with pytest.raises(ValueError, match="Unsupported POLYLINE mode"):
|
||||
import_dxf_module.process_polyline(entity)
|
||||
|
||||
|
||||
def test_process_polyline_degenerate(import_dxf_module):
|
||||
vertex = SimpleNamespace(
|
||||
dxf=SimpleNamespace(location=SimpleNamespace(x=0, y=0, z=0))
|
||||
)
|
||||
entity = SimpleNamespace(
|
||||
get_mode=lambda: "AcDb2dPolyline",
|
||||
vertices=[vertex],
|
||||
)
|
||||
with pytest.warns(UserWarning, match="Skipping degenerate POLYLINE"):
|
||||
assert import_dxf_module.process_polyline(entity) is None
|
||||
|
||||
|
||||
def test_convert_bulge_polyline_single_edge(import_dxf_module):
|
||||
edge = import_dxf_module._convert_bulge_polyline(
|
||||
[(0.0, 0.0, 0.0), (1.0, 0.0, 0.0)], False, 0.0, "TEST"
|
||||
)
|
||||
assert isinstance(edge, Line)
|
||||
|
||||
|
||||
def test_convert_bulge_polyline_sagitta_arc(import_dxf_module):
|
||||
edge = import_dxf_module._convert_bulge_polyline(
|
||||
[(0.0, 0.0, 1.0), (2.0, 0.0, 0.0)], False, 0.0, "TEST"
|
||||
)
|
||||
assert isinstance(edge, SagittaArc)
|
||||
|
||||
|
||||
def test_convert_bulge_polyline_degenerate(import_dxf_module):
|
||||
with pytest.warns(UserWarning, match="Skipping degenerate TEST"):
|
||||
result = import_dxf_module._convert_bulge_polyline(
|
||||
[(0.0, 0.0, 0.0), (0.0, 0.0, 0.0)], False, 0.0, "TEST"
|
||||
)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_process_spline_fit_points_fallback(import_dxf_module):
|
||||
entity = SimpleNamespace(
|
||||
control_points=[],
|
||||
fit_points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)],
|
||||
knots=[],
|
||||
weights=[],
|
||||
dxf=SimpleNamespace(
|
||||
degree=3,
|
||||
flags=0,
|
||||
get=lambda key: (1, 0, 0) if key == "start_tangent" else (-1, 0, 0),
|
||||
),
|
||||
)
|
||||
result = import_dxf_module.process_spline(entity)
|
||||
assert result.geom_type.name == "BSPLINE"
|
||||
assert tuple(result.start_point()) == (0.0, 0.0, 0.0)
|
||||
assert tuple(result.end_point()) == (2.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def test_process_spline_invalid(import_dxf_module):
|
||||
entity = SimpleNamespace(
|
||||
control_points=[],
|
||||
fit_points=[],
|
||||
knots=[],
|
||||
weights=[],
|
||||
dxf=SimpleNamespace(degree=3, flags=0, get=lambda _key: None),
|
||||
)
|
||||
with pytest.raises(ValueError, match="Unsupported SPLINE entity"):
|
||||
import_dxf_module.process_spline(entity)
|
||||
|
||||
|
||||
def test_process_text_uses_align_point(import_dxf_module):
|
||||
entity = SimpleNamespace(
|
||||
dxf=SimpleNamespace(
|
||||
text="Aligned",
|
||||
height=2.0,
|
||||
halign=1,
|
||||
valign=3,
|
||||
insert=(0.0, 0.0, 0.0),
|
||||
align_point=(5.0, 6.0, 0.0),
|
||||
get=lambda key, default=0: 0 if key == "rotation" else default,
|
||||
hasattr=lambda key: key == "align_point",
|
||||
)
|
||||
)
|
||||
result = import_dxf_module.process_text(entity)
|
||||
assert isinstance(result, Text)
|
||||
assert tuple(result.location.position) == (5.0, 6.0, 0.0)
|
||||
assert result.text_align == (TextAlign.CENTER, TextAlign.TOP)
|
||||
|
||||
|
||||
def test_process_hatch_unsupported_path_warning(import_dxf_module):
|
||||
unsupported_path = object()
|
||||
entity = SimpleNamespace(
|
||||
dxf=SimpleNamespace(elevation=0.0, hatch_style=0),
|
||||
paths=SimpleNamespace(rendering_paths=lambda _style: [unsupported_path]),
|
||||
)
|
||||
with pytest.warns(UserWarning, match="Unsupported HATCH boundary path"):
|
||||
result = import_dxf_module.process_hatch(entity)
|
||||
assert len(result) == 0
|
||||
|
||||
|
||||
def test_process_hatch_edgepath_single_line(import_dxf_module, monkeypatch):
|
||||
class FakeLineEdge:
|
||||
pass
|
||||
|
||||
class FakeEdgePath:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(import_dxf_module, "LineEdge", FakeLineEdge)
|
||||
monkeypatch.setattr(import_dxf_module, "EdgePath", FakeEdgePath)
|
||||
|
||||
line_edge = FakeLineEdge()
|
||||
line_edge.start = SimpleNamespace(x=0.0, y=0.0)
|
||||
line_edge.end = SimpleNamespace(x=1.0, y=0.0)
|
||||
edge_path = FakeEdgePath()
|
||||
edge_path.edges = [line_edge]
|
||||
entity = SimpleNamespace(
|
||||
dxf=SimpleNamespace(elevation=0.0, hatch_style=0),
|
||||
paths=SimpleNamespace(rendering_paths=lambda _style: [edge_path]),
|
||||
)
|
||||
result = import_dxf_module.process_hatch(entity)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], Line)
|
||||
|
||||
|
||||
def test_process_hatch_polyline_path(import_dxf_module, monkeypatch):
|
||||
class FakePolylinePath:
|
||||
def __init__(self):
|
||||
self.vertices = [(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 1.0, 0.0)]
|
||||
self.is_closed = False
|
||||
|
||||
monkeypatch.setattr(import_dxf_module, "PolylinePath", FakePolylinePath)
|
||||
|
||||
entity = SimpleNamespace(
|
||||
dxf=SimpleNamespace(elevation=0.0, hatch_style=0),
|
||||
paths=SimpleNamespace(rendering_paths=lambda _style: [FakePolylinePath()]),
|
||||
)
|
||||
result = import_dxf_module.process_hatch(entity)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], Wire)
|
||||
|
||||
|
||||
def test_process_hatch_edgepath_empty(import_dxf_module, monkeypatch):
|
||||
class FakeEdgePath:
|
||||
def __init__(self):
|
||||
self.edges = []
|
||||
|
||||
monkeypatch.setattr(import_dxf_module, "EdgePath", FakeEdgePath)
|
||||
|
||||
entity = SimpleNamespace(
|
||||
dxf=SimpleNamespace(elevation=0.0, hatch_style=0),
|
||||
paths=SimpleNamespace(rendering_paths=lambda _style: [FakeEdgePath()]),
|
||||
)
|
||||
result = import_dxf_module.process_hatch(entity)
|
||||
assert len(result) == 0
|
||||
|
||||
|
||||
def test_convert_hatch_edge_arc(import_dxf_module, monkeypatch):
|
||||
class FakeArcEdge:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(import_dxf_module, "ArcEdge", FakeArcEdge)
|
||||
|
||||
edge = FakeArcEdge()
|
||||
edge.center = SimpleNamespace(x=1.0, y=2.0)
|
||||
edge.radius = 3.0
|
||||
edge.start_angle = 10.0
|
||||
edge.end_angle = 70.0
|
||||
edge.ccw = True
|
||||
|
||||
result = import_dxf_module._convert_hatch_edge(edge, 0.0)
|
||||
assert isinstance(result, CenterArc)
|
||||
assert tuple(result.arc_center) == (1.0, 2.0, 0.0)
|
||||
|
||||
|
||||
def test_convert_hatch_edge_arc_clockwise(import_dxf_module, monkeypatch):
|
||||
class FakeArcEdge:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(import_dxf_module, "ArcEdge", FakeArcEdge)
|
||||
|
||||
edge = FakeArcEdge()
|
||||
edge.center = SimpleNamespace(x=1.0, y=2.0)
|
||||
edge.radius = 3.0
|
||||
edge.start_angle = 10.0
|
||||
edge.end_angle = 70.0
|
||||
edge.ccw = False
|
||||
|
||||
result = import_dxf_module._convert_hatch_edge(edge, 0.0)
|
||||
assert isinstance(result, CenterArc)
|
||||
assert tuple(result.arc_center) == (1.0, 2.0, 0.0)
|
||||
assert result.length > 0
|
||||
|
||||
|
||||
def test_convert_hatch_edge_ellipse(import_dxf_module, monkeypatch):
|
||||
class FakeEllipseEdge:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(import_dxf_module, "EllipseEdge", FakeEllipseEdge)
|
||||
|
||||
edge = FakeEllipseEdge()
|
||||
edge.center = SimpleNamespace(x=1.0, y=2.0)
|
||||
edge.major_axis = SimpleNamespace(x=4.0, y=0.0)
|
||||
edge.ratio = 0.5
|
||||
edge.start_angle = 0.0
|
||||
edge.end_angle = 90.0
|
||||
edge.ccw = True
|
||||
|
||||
result = import_dxf_module._convert_hatch_edge(edge, 0.0)
|
||||
assert isinstance(result, EllipticalCenterArc)
|
||||
assert tuple(result.arc_center) == (1.0, 2.0, 0.0)
|
||||
|
||||
|
||||
def test_convert_hatch_edge_ellipse_clockwise(import_dxf_module, monkeypatch):
|
||||
class FakeEllipseEdge:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(import_dxf_module, "EllipseEdge", FakeEllipseEdge)
|
||||
|
||||
edge = FakeEllipseEdge()
|
||||
edge.center = SimpleNamespace(x=1.0, y=2.0)
|
||||
edge.major_axis = SimpleNamespace(x=4.0, y=0.0)
|
||||
edge.ratio = 0.5
|
||||
edge.start_angle = 0.0
|
||||
edge.end_angle = 90.0
|
||||
edge.ccw = False
|
||||
|
||||
result = import_dxf_module._convert_hatch_edge(edge, 0.0)
|
||||
assert isinstance(result, EllipticalCenterArc)
|
||||
assert tuple(result.arc_center) == (1.0, 2.0, 0.0)
|
||||
assert result.length > 0
|
||||
|
||||
|
||||
def test_convert_hatch_edge_spline(import_dxf_module, monkeypatch):
|
||||
class FakeSplineEdge:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(import_dxf_module, "SplineEdge", FakeSplineEdge)
|
||||
|
||||
edge = FakeSplineEdge()
|
||||
edge.control_points = [(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)]
|
||||
edge.knot_values = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0]
|
||||
edge.degree = 2
|
||||
edge.weights = []
|
||||
edge.periodic = 0
|
||||
|
||||
result = import_dxf_module._convert_hatch_edge(edge, 0.0)
|
||||
assert isinstance(result, BSpline)
|
||||
|
||||
|
||||
def test_convert_hatch_edge_unsupported(import_dxf_module):
|
||||
with pytest.raises(ValueError, match="Unsupported HATCH edge type"):
|
||||
import_dxf_module._convert_hatch_edge(object(), 0.0)
|
||||
|
||||
|
||||
def test_process_solid_trace_3dface(import_dxf_module):
|
||||
vertices = {
|
||||
"v0": SimpleNamespace(x=0.0, y=0.0, z=0.0),
|
||||
"v1": SimpleNamespace(x=1.0, y=0.0, z=0.0),
|
||||
"v2": SimpleNamespace(x=1.0, y=1.0, z=0.0),
|
||||
"v3": SimpleNamespace(x=0.0, y=1.0, z=0.0),
|
||||
}
|
||||
entity = SimpleNamespace(dxf=SimpleNamespace(get=lambda key: vertices[key]))
|
||||
result = import_dxf_module.process_solid_trace_3dface(entity)
|
||||
assert isinstance(result, Polygon)
|
||||
assert len(result.vertices()) == 4
|
||||
|
||||
|
||||
def test_process_solid_trace_3dface_three_vertices(import_dxf_module):
|
||||
vertices = {
|
||||
"v0": SimpleNamespace(x=0.0, y=0.0, z=0.0),
|
||||
"v1": SimpleNamespace(x=1.0, y=0.0, z=0.0),
|
||||
"v2": SimpleNamespace(x=0.0, y=1.0, z=0.0),
|
||||
}
|
||||
|
||||
def get_vertex(key):
|
||||
if key not in vertices:
|
||||
raise AttributeError
|
||||
return vertices[key]
|
||||
|
||||
entity = SimpleNamespace(dxf=SimpleNamespace(get=get_vertex))
|
||||
result = import_dxf_module.process_solid_trace_3dface(entity)
|
||||
assert isinstance(result, Polygon)
|
||||
assert len(result.vertices()) == 3
|
||||
|
||||
|
||||
def test_process_mtext_text_fallback(import_dxf_module):
|
||||
entity = SimpleNamespace(
|
||||
text="Fallback",
|
||||
dxf=SimpleNamespace(
|
||||
char_height=2.0,
|
||||
attachment_point=7,
|
||||
insert=(1.0, 2.0, 0.0),
|
||||
get=lambda key, default=0: 0 if key == "rotation" else default,
|
||||
),
|
||||
)
|
||||
result = import_dxf_module.process_mtext(entity)
|
||||
assert isinstance(result, Text)
|
||||
assert result.txt == "Fallback"
|
||||
|
||||
|
||||
def test_flatten_import_result(import_dxf_module):
|
||||
assert import_dxf_module._flatten_import_result(None) == []
|
||||
assert len(import_dxf_module._flatten_import_result([None, Vertex(0, 0, 0)])) == 1
|
||||
assert (
|
||||
len(
|
||||
import_dxf_module._flatten_import_result(
|
||||
import_dxf_module.ShapeList([Vertex(0, 0, 0)])
|
||||
)
|
||||
)
|
||||
== 1
|
||||
)
|
||||
|
||||
|
||||
def test_process_entity_unsupported_warning(import_dxf_module):
|
||||
entity = SimpleNamespace(dxftype=lambda: "NOPE")
|
||||
with pytest.warns(UserWarning, match="Unable to convert NOPE"):
|
||||
result = import_dxf_module._process_entity(entity, doc=None)
|
||||
assert result == []
|
||||
|
||||
|
||||
# Integration fixtures
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filename",
|
||||
[
|
||||
"dxf/test-conic-section.dxf",
|
||||
"dxf/test-circle-rotation.dxf",
|
||||
"dxf/test-sketch.dxf",
|
||||
"dxf/test-drawing.dxf",
|
||||
"dxf/test-angled-section.dxf",
|
||||
"dxf/test-ellipse-rotation.dxf",
|
||||
"dxf/diamond.dxf",
|
||||
"dxf/test_export.dxf",
|
||||
"dxf/accumulatortest.dxf",
|
||||
"dxf/shaft_simple.dxf",
|
||||
"dxf/layers.dxf",
|
||||
"dxf/bridge.dxf",
|
||||
"dxf/output.dxf",
|
||||
"dxf/ellipticalarcs.dxf",
|
||||
"dxf/ellipticalarcs2.dxf",
|
||||
"dxf/cube.dxf",
|
||||
],
|
||||
)
|
||||
def test_import_integration_fixtures(filename: str, tests_dir: Path):
|
||||
result = import_dxf(str(tests_dir / filename))
|
||||
assert len(result) > 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue