From 50cbb3854abf72554602e92a492eb7944c773794 Mon Sep 17 00:00:00 2001 From: Anthony Sokolowski Date: Thu, 25 Dec 2025 16:55:10 +1100 Subject: [PATCH 1/2] Added project_line feature to ExtensionLine. --- docs/assets/stepper_drawing.svg | 1494 +++++++++++++++---------------- docs/technical_drawing.py | 8 +- src/build123d/drafting.py | 29 +- tests/test_drafting.py | 149 ++- 4 files changed, 919 insertions(+), 761 deletions(-) diff --git a/docs/assets/stepper_drawing.svg b/docs/assets/stepper_drawing.svg index 6504639..b994fbd 100644 --- a/docs/assets/stepper_drawing.svg +++ b/docs/assets/stepper_drawing.svg @@ -1,308 +1,308 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -319,453 +319,453 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/technical_drawing.py b/docs/technical_drawing.py index 55a6efc..e1a60da 100644 --- a/docs/technical_drawing.py +++ b/docs/technical_drawing.py @@ -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. + project_line=(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) diff --git a/src/build123d/drafting.py b/src/build123d/drafting.py index 415d33e..121bd8d 100644 --- a/src/build123d/drafting.py +++ b/src/build123d/drafting.py @@ -504,7 +504,7 @@ class ExtensionLine(BaseSketchObject): label_angle (bool, optional): a flag indicating that instead of an extracted length value, the size of the circular arc extracted from the path should be displayed in degrees. Defaults to False. - project_line (Vector, optional): Vector line which to project dimension against. + project_line (Vector, optional): Vector line which to project 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. @@ -528,24 +528,39 @@ 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 project_line is not None: + if isinstance(project_line, Iterable): + project_line = Vector(project_line) + measure_object_span = object_to_measure.position_at( + 1 + ) - object_to_measure.position_at(0) + extent_along_wire = measure_object_span.project_to_line(project_line) + 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) diff --git a/tests/test_drafting.py b/tests/test_drafting.py index 2f1b301..cec8075 100644 --- a/tests/test_drafting.py +++ b/tests/test_drafting.py @@ -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, + project_line=(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, + project_line=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, + project_line=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, + project_line=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, + project_line=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, + project_line=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, + project_line=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, + project_line=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, + project_line=Vector(0, 1, 0), ) From 4dca3b823f56f9b4a552e586e34dec80d20a6528 Mon Sep 17 00:00:00 2001 From: Anthony Sokolowski Date: Wed, 31 Dec 2025 08:01:55 +1100 Subject: [PATCH 2/2] project_line variable name changed to measurement_direction. Docstring type for this variable changed from Vector to VectorLike. --- docs/assets/stepper_drawing.svg | 12 ++++++------ docs/technical_drawing.py | 2 +- src/build123d/drafting.py | 15 +++++++++------ tests/test_drafting.py | 18 +++++++++--------- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/docs/assets/stepper_drawing.svg b/docs/assets/stepper_drawing.svg index b994fbd..8b87d79 100644 --- a/docs/assets/stepper_drawing.svg +++ b/docs/assets/stepper_drawing.svg @@ -350,8 +350,8 @@ - + @@ -383,12 +383,12 @@ - + - + @@ -436,8 +436,8 @@ - + @@ -475,8 +475,8 @@ - + @@ -490,8 +490,8 @@ - + diff --git a/docs/technical_drawing.py b/docs/technical_drawing.py index e1a60da..0c45587 100644 --- a/docs/technical_drawing.py +++ b/docs/technical_drawing.py @@ -167,7 +167,7 @@ body_bottom_corner = (side_bbox.max.X, side_bbox.min.Y) d4 = ExtensionLine( border=(shaft_top_corner, body_bottom_corner), offset=-(side_bbox.max.X - shaft_top_corner.X) - 1 * CM, # offset to outside view. - project_line=(0, 1, 0), + measurement_direction=(0, 1, 0), draft=drafting_options, ) l3 = Text("Side Elevation", 6) diff --git a/src/build123d/drafting.py b/src/build123d/drafting.py index 121bd8d..782c0d8 100644 --- a/src/build123d/drafting.py +++ b/src/build123d/drafting.py @@ -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. Offset start point is the position of the start of border. + 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 @@ -536,13 +537,15 @@ class ExtensionLine(BaseSketchObject): 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 project_line is not None: - if isinstance(project_line, Iterable): - project_line = Vector(project_line) + 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(project_line) + 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, diff --git a/tests/test_drafting.py b/tests/test_drafting.py index cec8075..6d4dae5 100644 --- a/tests/test_drafting.py +++ b/tests/test_drafting.py @@ -301,7 +301,7 @@ class ExtensionLineTestCase(unittest.TestCase): border=diagonal_line, offset=-10, draft=metric, - project_line=(0, 1, 0), + measurement_direction=(0, 1, 0), ) self.assertIsNotNone(ext) @@ -311,7 +311,7 @@ class ExtensionLineTestCase(unittest.TestCase): border=diagonal_line, offset=-10, draft=metric, - project_line=Vector(0, 1, 0), + measurement_direction=Vector(0, 1, 0), ) self.assertIsNotNone(ext) self.assertGreater( @@ -334,7 +334,7 @@ class ExtensionLineTestCase(unittest.TestCase): border=diagonal_line, offset=10, draft=metric, - project_line=Vector(0, 1, 0), + measurement_direction=Vector(0, 1, 0), ) self.assertIsNotNone(ext) self.assertEqual( @@ -354,7 +354,7 @@ class ExtensionLineTestCase(unittest.TestCase): border=diagonal_line, offset=x_size + 10, draft=metric, - project_line=Vector(0, 1, 0), + measurement_direction=Vector(0, 1, 0), ) self.assertIsNotNone(ext) self.assertGreater( @@ -373,7 +373,7 @@ class ExtensionLineTestCase(unittest.TestCase): border=diagonal_line, offset=10, draft=metric, - project_line=Vector(0, 1, 0), + measurement_direction=Vector(0, 1, 0), ) self.assertIsNotNone(ext) self.assertEqual( @@ -388,7 +388,7 @@ class ExtensionLineTestCase(unittest.TestCase): border=diagonal_line, offset=10, draft=metric, - project_line=Vector(1, 0, 0), + measurement_direction=Vector(1, 0, 0), ) self.assertIsNotNone(ext) self.assertGreater( @@ -411,7 +411,7 @@ class ExtensionLineTestCase(unittest.TestCase): border=diagonal_line, offset=10, draft=metric, - project_line=Vector(1, 1, 0), + measurement_direction=Vector(1, 1, 0), ) self.assertIsNotNone(ext) self.assertAlmostEqual(ext.dimension, 141.421, places=2) @@ -422,7 +422,7 @@ class ExtensionLineTestCase(unittest.TestCase): border=half_circle, offset=-10, draft=metric, - project_line=Vector(1, 0, 0), + measurement_direction=Vector(1, 0, 0), ) self.assertIsNotNone(ext) self.assertEqual(ext.dimension, 100) @@ -438,7 +438,7 @@ class ExtensionLineTestCase(unittest.TestCase): border=half_circle, offset=10, draft=metric, - project_line=Vector(0, 1, 0), + measurement_direction=Vector(0, 1, 0), )