Merge pull request #1178 from slobberingant/dev
Some checks failed
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (macos-15-intel, 3.10) (push) Has been cancelled
tests / tests (macos-15-intel, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled

Added project_line feature to ExtensionLine.
This commit is contained in:
Roger Maitland 2025-12-30 18:59:05 -05:00 committed by GitHub
commit 4783f5e9df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 923 additions and 762 deletions

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Before After
Before After

View file

@ -162,9 +162,13 @@ vis, _ = project_to_2d(
)
visible_lines.extend(vis)
side_bbox = Curve(vis).bounding_box()
perimeter = Pos(*side_bbox.center()) * Rectangle(side_bbox.size.X, side_bbox.size.Y)
shaft_top_corner = vis.edges().sort_by(Axis.Y)[-1].vertices().sort_by(Axis.X)[-1]
body_bottom_corner = (side_bbox.max.X, side_bbox.min.Y)
d4 = ExtensionLine(
border=perimeter.edges().sort_by(Axis.X)[-1], offset=1 * CM, draft=drafting_options
border=(shaft_top_corner, body_bottom_corner),
offset=-(side_bbox.max.X - shaft_top_corner.X) - 1 * CM, # offset to outside view.
measurement_direction=(0, 1, 0),
draft=drafting_options,
)
l3 = Text("Side Elevation", 6)
l3.position = vis.group_by(Axis.Y)[0].sort_by(Edge.length)[-1].center() + (0, -5 * MM)

View file

@ -504,7 +504,8 @@ class ExtensionLine(BaseSketchObject):
label_angle (bool, optional): a flag indicating that instead of an extracted length
value, the size of the circular arc extracted from the path should be displayed
in degrees. Defaults to False.
project_line (Vector, optional): Vector line which to project dimension against.
measurement_direction (VectorLike, optional): Vector line which to project the dimension
against. Offset start point is the position of the start of border.
Defaults to None.
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
@ -520,7 +521,7 @@ class ExtensionLine(BaseSketchObject):
arrows: tuple[bool, bool] = (True, True),
tolerance: float | tuple[float, float] | None = None,
label_angle: bool = False,
project_line: VectorLike | None = None,
measurement_direction: VectorLike | None = None,
mode: Mode = Mode.ADD,
):
# pylint: disable=too-many-locals
@ -528,24 +529,41 @@ class ExtensionLine(BaseSketchObject):
context = BuildSketch._get_context(self)
if sketch is None and not (context is None or context.sketch is None):
sketch = context.sketch
if project_line is not None:
raise NotImplementedError("project_line is currently unsupported")
if offset == 0:
raise ValueError("A dimension line should be used if offset is 0")
# Create a wire modelling the path of the dimension lines from a variety of input types
object_to_measure = Draft._process_path(border)
if object_to_measure.position_at(0) == object_to_measure.position_at(1):
raise ValueError("Start and end points of border must be different.")
if measurement_direction is not None:
if isinstance(measurement_direction, Iterable):
measurement_direction = Vector(measurement_direction)
measure_object_span = object_to_measure.position_at(
1
) - object_to_measure.position_at(0)
extent_along_wire = measure_object_span.project_to_line(
measurement_direction
)
object_to_dimension = Edge.make_line(
object_to_measure.position_at(0),
object_to_measure.position_at(0) + extent_along_wire,
)
else:
object_to_dimension = object_to_measure
side_lut = {1: Side.RIGHT, -1: Side.LEFT}
if offset == 0:
raise ValueError("A dimension line should be used if offset is 0")
dimension_path = object_to_measure.offset_2d(
dimension_path = object_to_dimension.offset_2d(
distance=offset, side=side_lut[int(copysign(1, offset))], closed=False
)
dimension_label_str = (
label
if label is not None
else draft._label_to_str(label, object_to_measure, label_angle, tolerance)
else draft._label_to_str(label, object_to_dimension, label_angle, tolerance)
)
extension_lines = [
Edge.make_line(
object_to_measure.position_at(e), dimension_path.position_at(e)

View file

@ -37,7 +37,9 @@ from build123d import (
Axis,
BuildLine,
BuildSketch,
CenterOf,
Color,
Compound,
Edge,
Face,
FontStyle,
@ -50,6 +52,7 @@ from build123d import (
Rectangle,
Sketch,
Unit,
Vector,
add,
make_face,
offset,
@ -292,14 +295,150 @@ class ExtensionLineTestCase(unittest.TestCase):
self.assertAlmostEqual(bbox.size.X, 30 + metric.line_width, 5)
self.assertAlmostEqual(bbox.size.Y, 10, 5)
def test_not_implemented(self):
shape, outer, inner = create_test_sketch()
with self.assertRaises(NotImplementedError):
def test_vectorlike_in_extension_function(self):
diagonal_line = Edge.make_line((100, 100), (200, 200))
ext = ExtensionLine(
border=diagonal_line,
offset=-10,
draft=metric,
measurement_direction=(0, 1, 0),
)
self.assertIsNotNone(ext)
def test_vertical_projection_with_dim_outside_shape(self):
diagonal_line = Edge.make_line((100, 100), (200, 200))
ext = ExtensionLine(
border=diagonal_line,
offset=-10,
draft=metric,
measurement_direction=Vector(0, 1, 0),
)
self.assertIsNotNone(ext)
self.assertGreater(
Compound(children=[diagonal_line, ext]).bounding_box().size.X,
diagonal_line.bounding_box().size.X,
) # dimension should be outside shape.
self.assertEqual(
diagonal_line.bounding_box().size.Y + 0.25, # plus line_width
ext.bounding_box().size.Y,
)
self.assertEqual(
diagonal_line.center(CenterOf.BOUNDING_BOX).Y,
ext.center(CenterOf.BOUNDING_BOX).Y,
)
self.assertEqual(ext.dimension, 100)
def test_vertical_projection_with_dim_inside_shape(self):
diagonal_line = Edge.make_line((100, 100), (200, 200))
ext = ExtensionLine(
border=diagonal_line,
offset=10,
draft=metric,
measurement_direction=Vector(0, 1, 0),
)
self.assertIsNotNone(ext)
self.assertEqual(
Compound(children=[diagonal_line, ext]).bounding_box().size.Y,
diagonal_line.bounding_box().size.Y + 0.25,
) # plus line_width
self.assertEqual(
diagonal_line.center(CenterOf.BOUNDING_BOX).Y,
ext.center(CenterOf.BOUNDING_BOX).Y,
)
self.assertEqual(ext.dimension, 100)
def test_vertical_projection_with_dim_otherside(self):
diagonal_line = Edge.make_line((100, 100), (200, 200))
x_size = diagonal_line.bounding_box().size.X
ext = ExtensionLine(
border=diagonal_line,
offset=x_size + 10,
draft=metric,
measurement_direction=Vector(0, 1, 0),
)
self.assertIsNotNone(ext)
self.assertGreater(
Compound(children=[diagonal_line, ext]).bounding_box().size.Y,
diagonal_line.bounding_box().size.Y,
) # plus line_width
self.assertEqual(
diagonal_line.center(CenterOf.BOUNDING_BOX).Y,
ext.center(CenterOf.BOUNDING_BOX).Y,
)
self.assertEqual(ext.dimension, 100)
def test_vertical_projection_with_vertical_line(self):
diagonal_line = Edge.make_line((100, 100), (100, 200))
ext = ExtensionLine(
border=diagonal_line,
offset=10,
draft=metric,
measurement_direction=Vector(0, 1, 0),
)
self.assertIsNotNone(ext)
self.assertEqual(
diagonal_line.center(CenterOf.BOUNDING_BOX).Y,
ext.center(CenterOf.BOUNDING_BOX).Y,
)
self.assertEqual(ext.dimension, 100)
def test_horizontal_projection_with_dim_outside_shape(self):
diagonal_line = Edge.make_line((100, 100), (200, 200))
ext = ExtensionLine(
border=diagonal_line,
offset=10,
draft=metric,
measurement_direction=Vector(1, 0, 0),
)
self.assertIsNotNone(ext)
self.assertGreater(
Compound(children=[diagonal_line, ext]).bounding_box().size.Y,
diagonal_line.bounding_box().size.Y,
) # dimension should be outside shape.
self.assertEqual(
diagonal_line.bounding_box().size.X + 0.25, # plus line_width
ext.bounding_box().size.X,
)
self.assertEqual(
diagonal_line.center(CenterOf.BOUNDING_BOX).X,
ext.center(CenterOf.BOUNDING_BOX).X,
)
self.assertEqual(ext.dimension, 100)
def test_angled_projection(self):
diagonal_line = Edge.make_line((100, 100), (200, 200))
ext = ExtensionLine(
border=diagonal_line,
offset=10,
draft=metric,
measurement_direction=Vector(1, 1, 0),
)
self.assertIsNotNone(ext)
self.assertAlmostEqual(ext.dimension, 141.421, places=2)
def test_half_circle(self):
half_circle = Edge.make_circle(50, start_angle=0, end_angle=180)
ext = ExtensionLine(
border=half_circle,
offset=-10,
draft=metric,
measurement_direction=Vector(1, 0, 0),
)
self.assertIsNotNone(ext)
self.assertEqual(ext.dimension, 100)
self.assertGreater(
Compound(children=[half_circle, ext]).bounding_box().size.Y,
half_circle.bounding_box().size.Y,
) # dimension should be outside shape.
def test_full_circle(self):
half_circle = Edge.make_circle(50)
with pytest.raises(ValueError):
ExtensionLine(
outer.edges().sort_by(Axis.Y)[0],
border=half_circle,
offset=10,
project_line=(1, 0, 0),
draft=metric,
measurement_direction=Vector(0, 1, 0),
)