build123d/tests/test_drafting.py

455 lines
15 KiB
Python

"""
drafting unittests
name: test_drafting.py
by: Gumyr
date: September 17th, 2023
desc:
This python module contains the unittests for the drafting functionality.
license:
Copyright 2023 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 unittest
from datetime import date
import pytest
from build123d import (
IN,
Axis,
BuildLine,
BuildSketch,
CenterOf,
Color,
Compound,
Edge,
Face,
FontStyle,
GeomType,
HeadType,
Mode,
NumberDisplay,
Polyline,
RadiusArc,
Rectangle,
Sketch,
Unit,
Vector,
add,
make_face,
offset,
)
from build123d.drafting import (
ArrowHead,
DimensionLine,
Draft,
ExtensionLine,
TechnicalDrawing,
)
metric = Draft(
font_size=3.0,
font="Arial",
font_style=FontStyle.REGULAR,
head_type=HeadType.CURVED,
arrow_length=3.0,
line_width=0.25,
unit=Unit.MM,
number_display=NumberDisplay.DECIMAL,
display_units=True,
decimal_precision=2,
fractional_precision=64,
extension_gap=2.0,
)
imperial = Draft(
font_size=5.0,
font="Arial",
font_style=FontStyle.REGULAR,
head_type=HeadType.CURVED,
arrow_length=3.0,
line_width=0.25,
unit=Unit.IN,
number_display=NumberDisplay.FRACTION,
display_units=True,
decimal_precision=2,
fractional_precision=64,
extension_gap=2.0,
)
def create_test_sketch() -> tuple[Sketch, Sketch, Sketch]:
with BuildSketch() as sketchy:
with BuildLine():
l1 = Polyline((10, 20), (-20, 20), (-20, -20), (10, -20))
RadiusArc(l1 @ 0, l1 @ 1, 25)
make_face()
outside = sketchy.sketch
inside = offset(amount=-0.5, mode=Mode.SUBTRACT)
return (sketchy.sketch, outside, inside)
class TestClassInstantiation(unittest.TestCase):
"""Test Draft class instantiation"""
def test_draft_instantiation(self):
"""Parameter parsing"""
with self.assertRaises(ValueError):
Draft(fractional_precision=37)
class TestDraftFunctionality(unittest.TestCase):
"""Test core drafting functionality"""
def test_number_with_units(self):
metric_drawing = Draft(decimal_precision=2)
self.assertEqual(metric_drawing._number_with_units(3.141), "3.14mm")
self.assertEqual(metric_drawing._number_with_units(3.149), "3.15mm")
self.assertEqual(metric_drawing._number_with_units(0), "0.00mm")
self.assertEqual(
metric_drawing._number_with_units(3.14, tolerance=0.01), "3.14 ±0.01mm"
)
self.assertEqual(
metric_drawing._number_with_units(3.14, tolerance=(0.01, 0)),
"3.14 +0.01 -0.00mm",
)
whole_number_drawing = Draft(decimal_precision=-1)
self.assertEqual(whole_number_drawing._number_with_units(314.1), "310mm")
imperial_drawing = Draft(unit=Unit.IN)
self.assertEqual(imperial_drawing._number_with_units((5 / 8) * IN), '0.62"')
imperial_fractional_drawing = Draft(
unit=Unit.IN, number_display=NumberDisplay.FRACTION, fractional_precision=64
)
self.assertEqual(
imperial_fractional_drawing._number_with_units((5 / 8) * IN), '5/8"'
)
self.assertEqual(
imperial_fractional_drawing._number_with_units(math.pi * IN), '3 9/64"'
)
imperial_fractional_drawing.fractional_precision = 16
self.assertEqual(
imperial_fractional_drawing._number_with_units(math.pi * IN), '3 1/8"'
)
def test_label_to_str(self):
metric_drawing = Draft(decimal_precision=0)
line = Edge.make_line((0, 0, 0), (100, 0, 0))
with self.assertRaises(ValueError):
metric_drawing._label_to_str(
label=None,
line_wire=line,
label_angle=True,
tolerance=0,
)
arc1 = Edge.make_circle(100, start_angle=0, end_angle=30)
angle_str = metric_drawing._label_to_str(
label=None,
line_wire=arc1,
label_angle=True,
tolerance=0,
)
self.assertEqual(angle_str, "30°")
class ArrowHeadTests(unittest.TestCase):
def test_arrowhead_types(self):
arrow = ArrowHead(10, HeadType.CURVED)
bbox = arrow.bounding_box()
self.assertEqual(len(arrow.edges().filter_by(GeomType.CIRCLE)), 2)
self.assertAlmostEqual(bbox.size.X, 10, 5)
arrow = ArrowHead(10, HeadType.FILLETED)
bbox = arrow.bounding_box()
self.assertEqual(len(arrow.edges().filter_by(GeomType.CIRCLE)), 5)
self.assertLess(bbox.size.X, 10)
arrow = ArrowHead(10, HeadType.STRAIGHT)
self.assertEqual(len(arrow.edges().filter_by(GeomType.CIRCLE)), 0)
bbox = arrow.bounding_box()
self.assertAlmostEqual(bbox.size.X, 10, 5)
class DimensionLineTestCase(unittest.TestCase):
def test_two_points(self):
d_line = DimensionLine([(0, 0, 0), (100, 0, 0)], draft=metric)
bbox = d_line.bounding_box()
self.assertAlmostEqual(bbox.max.X, 100, 5)
self.assertAlmostEqual(d_line.dimension, 100, 5)
self.assertEqual(len(d_line.faces()), 10)
def test_three_points(self):
with self.assertRaises(ValueError):
DimensionLine([(0, 0, 0), (50, 0, 0), (50, 50, 0)], draft=metric)
def test_edge(self):
d_line = DimensionLine(Edge.make_line((0, 0), (100, 0)), draft=metric)
bbox = d_line.bounding_box()
self.assertAlmostEqual(bbox.max.X, 100, 5)
self.assertEqual(len(d_line.faces()), 10)
def test_vertices(self):
d_line = DimensionLine(
Edge.make_line((0, 0), (100, 0)).vertices(), draft=metric
)
bbox = d_line.bounding_box()
self.assertAlmostEqual(bbox.max.X, 100, 5)
self.assertEqual(len(d_line.faces()), 10)
def test_label(self):
d_line = DimensionLine([(0, 0, 0), (100, 0, 0)], label="Test", draft=metric)
bbox = d_line.bounding_box()
self.assertAlmostEqual(bbox.max.X, 100, 5)
self.assertEqual(len(d_line.faces()), 6)
def test_face(self):
with self.assertRaises(ValueError):
DimensionLine(Face.make_rect(100, 100), draft=metric)
def test_builder_mode(self):
with BuildSketch() as s1:
Rectangle(100, 100)
hole = offset(amount=-5, mode=Mode.SUBTRACT)
d_line = DimensionLine(
[
hole.vertices().group_by(Axis.Y)[-1].sort_by(Axis.X)[-1],
hole.vertices().group_by(Axis.Y)[0].sort_by(Axis.X)[0],
],
draft=metric,
)
area = sum(f.area for f in hole.intersect(d_line).faces())
self.assertGreater(area, 0)
def test_outside_arrows(self):
d_line = DimensionLine([(0, 0, 0), (15, 0, 0)], draft=metric)
bbox = d_line.bounding_box()
self.assertAlmostEqual(bbox.size.X, 15 + 4 * metric.arrow_length, 5)
self.assertAlmostEqual(d_line.dimension, 15, 5)
self.assertEqual(len(d_line.faces()), 9)
def test_outside_label(self):
d_line = DimensionLine([(0, 0, 0), (5, 0, 0)], draft=metric)
bbox = d_line.bounding_box()
self.assertGreater(bbox.size.X, 5 + 4 * metric.arrow_length)
self.assertAlmostEqual(d_line.dimension, 5, 5)
self.assertEqual(len(d_line.faces()), 8)
def test_single_outside_label(self):
d_line = DimensionLine(
[(0, 0, 0), (5, 0, 0)], draft=metric, arrows=(False, True)
)
bbox = d_line.bounding_box()
self.assertAlmostEqual(bbox.min.X, 5, 5)
self.assertAlmostEqual(d_line.dimension, 5, 5)
self.assertEqual(len(d_line.faces()), 7)
def test_no_arrows(self):
with self.assertRaises(ValueError):
DimensionLine([(0, 0, 0), (5, 0, 0)], draft=metric, arrows=(False, False))
def test_vertical(self):
d_line = DimensionLine([(0, 0), (0, 100)], Draft())
bbox = d_line.bounding_box()
self.assertAlmostEqual(bbox.size.Y, 100, 5) # numbers within
class ExtensionLineTestCase(unittest.TestCase):
def test_min_x(self):
shape, outer, inner = create_test_sketch()
e_line = ExtensionLine(
outer.edges().sort_by(Axis.X)[0], offset=10, draft=metric
)
bbox = e_line.bounding_box()
self.assertAlmostEqual(bbox.size.Y, 40 + metric.line_width, 5)
self.assertAlmostEqual(bbox.size.X, 10, 5)
with self.assertRaises(ValueError):
ExtensionLine(outer.edges().sort_by(Axis.X)[0], offset=0, draft=metric)
def test_builder_mode(self):
shape, outer, inner = create_test_sketch()
with BuildSketch() as test:
add(shape)
e_line = ExtensionLine(
outer.edges().sort_by(Axis.Y)[0], offset=10, draft=metric
)
bbox = e_line.bounding_box()
self.assertAlmostEqual(bbox.size.X, 30 + metric.line_width, 5)
self.assertAlmostEqual(bbox.size.Y, 10, 5)
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(
border=half_circle,
offset=10,
draft=metric,
measurement_direction=Vector(0, 1, 0),
)
@pytest.mark.parametrize("design_date", [date(2023, 9, 17), None])
def test_basic_drawing(design_date):
drawing = TechnicalDrawing(design_date=design_date, sheet_number=1)
bbox = drawing.bounding_box()
assert bbox.size.X > 280
assert bbox.size.Y > 195
assert len(drawing.faces()) > 110
if __name__ == "__main__":
unittest.main()