diff --git a/src/fscad/fscad.py b/src/fscad/fscad.py index 6410e8f..416e068 100644 --- a/src/fscad/fscad.py +++ b/src/fscad/fscad.py @@ -15,7 +15,7 @@ __all__ = ['app', 'root', 'ui', 'brep', 'design', 'Translation', 'Place', 'BoundedEntity', 'BRepEntity', 'Body', 'Loop', 'Face', 'Edge', 'Point', 'BoundingBox', 'Component', 'ComponentWithChildren', 'Shape', 'BRepComponent', 'PlanarShape', 'Box', 'Cylinder', 'Sphere', 'Torus', 'Rect', 'Circle', 'Builder2D', 'Polygon', - 'RegularPolygon', 'import_fusion_archive', 'import_dxf', 'Combination', 'Union', 'Difference', + 'RegularPolygon', 'Text', 'import_fusion_archive', 'import_dxf', 'Combination', 'Union', 'Difference', 'Intersection', 'Group', 'Loft', 'Revolve', 'Sweep', 'ExtrudeBase', 'Extrude', 'ExtrudeTo', 'OffsetEdges', 'SplitFace', 'Silhouette', 'Hull', 'RawThreads', 'Threads', 'ConicalThreads', 'Fillet', 'Chamfer', 'Scale', 'Thicken', 'MemoizableDesign', 'setup_document', 'run_design', 'relative_import'] @@ -2326,6 +2326,74 @@ def __init__(self, sides: int, radius: float, is_outer_radius: bool = True, name super().__init__(*points, name=name) +class Text(Shape): + """Defines 2D text geometry suitable for extrusion. + + Args: + text: The text string to create + height: The height of the text in cm + font: The font name to use. If None, uses the default font. + name: The name of the component + """ + def __init__(self, text: str, height: float = 1.0, font: str = None, name: str = None): + root_comp = root() + occ = root_comp.occurrences.addNewComponent(Matrix3D.create()) + sketch = occ.component.sketches.add(occ.component.xYConstructionPlane) + + text_input = sketch.sketchTexts.createInput(text, height, Point3D.create(0, 0, 0)) + if font is not None: + text_input.fontName = font + sketch_text = sketch.sketchTexts.add(text_input) + sketch_text.explode() + + profiles = sketch.profiles + if profiles.count == 0: + occ.deleteMe() + super().__init__(name=name) + return + + thin_depth = 0.01 + extent = adsk.fusion.DistanceExtentDefinition.create( + adsk.core.ValueInput.createByReal(thin_depth)) + for i in range(profiles.count): + profile = profiles.item(i) + extrude_input = occ.component.features.extrudeFeatures.createInput( + profile, adsk.fusion.FeatureOperations.NewBodyFeatureOperation) + extrude_input.setOneSideExtent( + extent, adsk.fusion.ExtentDirections.PositiveExtentDirection, + adsk.core.ValueInput.createByReal(0)) + occ.component.features.extrudeFeatures.add(extrude_input) + + all_bodies = sorted(occ.bRepBodies, key=lambda b: b.volume, reverse=True) + kept_bodies = [] + for body in all_bodies: + centroid = body.physicalProperties.centerOfMass + is_inner = False + for larger in kept_bodies: + bb = larger.boundingBox + if (bb.minPoint.x < centroid.x < bb.maxPoint.x and + bb.minPoint.y < centroid.y < bb.maxPoint.y): + is_inner = True + break + if not is_inner: + kept_bodies.append(body) + + planar_bodies = [] + for body in kept_bodies: + for face in body.faces: + if isinstance(face.geometry, adsk.core.Plane) and face.geometry.normal.isParallelTo(self._pos_z): + planar_bodies.append(brep().copy(face)) + break + + occ.deleteMe() + super().__init__(*planar_bodies, name=name) + + def get_plane(self) -> Optional[adsk.core.Plane]: + if not self._bodies: + return None + return adsk.core.Plane.create(Point3D.create(0, 0, 0), Vector3D.create(0, 0, 1)) + + def import_fusion_archive(filename, name="import"): """Imports the given fusion archive as a new Component diff --git a/tests/TextTest/text.f3d b/tests/TextTest/text.f3d new file mode 100644 index 0000000..ae90d12 Binary files /dev/null and b/tests/TextTest/text.f3d differ diff --git a/tests/TextTest/text_empty.f3d b/tests/TextTest/text_empty.f3d new file mode 100644 index 0000000..9681cc7 Binary files /dev/null and b/tests/TextTest/text_empty.f3d differ diff --git a/tests/TextTest/text_extrude.f3d b/tests/TextTest/text_extrude.f3d new file mode 100644 index 0000000..cca9805 Binary files /dev/null and b/tests/TextTest/text_extrude.f3d differ diff --git a/tests/TextTest/text_font.f3d b/tests/TextTest/text_font.f3d new file mode 100644 index 0000000..76baef0 Binary files /dev/null and b/tests/TextTest/text_font.f3d differ diff --git a/tests/TextTest/text_get_plane.f3d b/tests/TextTest/text_get_plane.f3d new file mode 100644 index 0000000..e1eaf40 Binary files /dev/null and b/tests/TextTest/text_get_plane.f3d differ diff --git a/tests/TextTest/text_multiple_chars.f3d b/tests/TextTest/text_multiple_chars.f3d new file mode 100644 index 0000000..dd38551 Binary files /dev/null and b/tests/TextTest/text_multiple_chars.f3d differ diff --git a/tests/TextTest/text_with_holes.f3d b/tests/TextTest/text_with_holes.f3d new file mode 100644 index 0000000..9e17658 Binary files /dev/null and b/tests/TextTest/text_with_holes.f3d differ diff --git a/tests/edge_test.py b/tests/edge_test.py index 83b15a1..0f4d86d 100644 --- a/tests/edge_test.py +++ b/tests/edge_test.py @@ -68,7 +68,7 @@ def test_component_edges(self): ~box2 == ~box1, ~box2 == ~box1) - self.assertEquals(len(Group([box1, box2]).edges), 24) + self.assertEqual(len(Group([box1, box2]).edges), 24) def test_edge_after_translate(self): box = Box(1, 1, 1) diff --git a/tests/face_test.py b/tests/face_test.py index b461d37..2f7930d 100644 --- a/tests/face_test.py +++ b/tests/face_test.py @@ -129,7 +129,7 @@ def test_component_faces(self): ~box2 == ~box1, ~box2 == ~box1) - self.assertEquals(len(Group([box1, box2]).faces), 12) + self.assertEqual(len(Group([box1, box2]).faces), 12) def test_face_after_translate(self): box = Box(1, 1, 1) diff --git a/tests/memoizable_design_test.py b/tests/memoizable_design_test.py index dd3eb8d..1170ef9 100644 --- a/tests/memoizable_design_test.py +++ b/tests/memoizable_design_test.py @@ -41,7 +41,7 @@ def test_design(self): box2.tx(5) box1.create_occurrence() box2.create_occurrence() - self.assertEquals(times_called, 1) + self.assertEqual(times_called, 1) def test_called_twice_with_name_as_keyword(self): @@ -62,7 +62,7 @@ def test_design(self, name): box2.tx(5) box1.create_occurrence() box2.create_occurrence() - self.assertEquals(times_called, 1) + self.assertEqual(times_called, 1) def test_called_twice_with_name_as_positional(self): @@ -83,7 +83,7 @@ def test_design(self, name): box2.tx(5) box1.create_occurrence() box2.create_occurrence() - self.assertEquals(times_called, 1) + self.assertEqual(times_called, 1) def test_with_arguments(self): @@ -107,7 +107,7 @@ def test_design(self, tx, name): box1.create_occurrence() box2.create_occurrence() box3.create_occurrence() - self.assertEquals(times_called, 2) + self.assertEqual(times_called, 2) def test_different_instances(self): @@ -138,7 +138,7 @@ def test_design(self, tx, name): box2.create_occurrence() box3.create_occurrence() box4.create_occurrence() - self.assertEquals(times_called, 2) + self.assertEqual(times_called, 2) def run(context): diff --git a/tests/misc_test.py b/tests/misc_test.py index 36f7ac6..8c0bc45 100644 --- a/tests/misc_test.py +++ b/tests/misc_test.py @@ -22,6 +22,7 @@ # note: load_tests is required for the "pattern" test filtering functionality in loadTestsFromModule in run() from fscad.test_utils import FscadTestCase, load_tests +import fscad.fscad from fscad.fscad import * import fscad.fscad diff --git a/tests/revolve_test.py b/tests/revolve_test.py index eaad0bd..bc12cfa 100644 --- a/tests/revolve_test.py +++ b/tests/revolve_test.py @@ -30,7 +30,7 @@ def test_full_revolve(self): revolve = Revolve(rect, adsk.core.Line3D.create(Point3D.create(0, 0, 0), Point3D.create(0, 1, 0))) revolve.create_occurrence(True) - self.assertEquals(len(revolve.faces), 4) + self.assertEqual(len(revolve.faces), 4) def test_infinite_line(self): rect = Rect(1, 1) @@ -41,7 +41,7 @@ def test_infinite_line(self): adsk.core.Line3D.create(Point3D.create(0, 0, 0), Point3D.create(0, 1, 0)).asInfiniteLine()) revolve.create_occurrence(True) - self.assertEquals(len(revolve.faces), 4) + self.assertEqual(len(revolve.faces), 4) def test_partial_revolve(self): rect = Rect(1, 1) @@ -50,7 +50,7 @@ def test_partial_revolve(self): revolve = Revolve(rect, adsk.core.Line3D.create(Point3D.create(0, 0, 0), Point3D.create(0, 1, 0)), 180) revolve.create_occurrence(True) - self.assertEquals(len(revolve.faces), 6) + self.assertEqual(len(revolve.faces), 6) def test_partial_revolve_negative(self): rect = Rect(1, 1) @@ -59,7 +59,7 @@ def test_partial_revolve_negative(self): revolve = Revolve(rect, adsk.core.Line3D.create(Point3D.create(0, 0, 0), Point3D.create(0, 1, 0)), -180) revolve.create_occurrence(True) - self.assertEquals(len(revolve.faces), 6) + self.assertEqual(len(revolve.faces), 6) def test_revolve_off_axis(self): rect = Rect(1, 1) @@ -68,7 +68,7 @@ def test_revolve_off_axis(self): revolve = Revolve(rect, adsk.core.Line3D.create(Point3D.create(0, 0, 0), Point3D.create(0, 1, 1))) revolve.create_occurrence(True) - self.assertEquals(len(revolve.faces), 4) + self.assertEqual(len(revolve.faces), 4) def test_revolve_around_edge(self): rect = Rect(1, 1) @@ -77,7 +77,7 @@ def test_revolve_around_edge(self): revolve.create_occurrence(True) - self.assertEquals(len(revolve.faces), 3) + self.assertEqual(len(revolve.faces), 3) def test_revolve_face(self): box = Box(1, 1, 1) @@ -123,7 +123,7 @@ def test_revolve_with_edge_axis(self): revolve = Revolve(box.top, upper_right_edge, 180) revolve.create_occurrence(True) - self.assertEquals(revolve.size().asArray(), (box.size().x * 2, box.size().y, box.size().z*2)) + self.assertEqual(revolve.size().asArray(), (box.size().x * 2, box.size().y, box.size().z*2)) def run(context): diff --git a/tests/silhouette_test.py b/tests/silhouette_test.py index 09092d3..568bb14 100644 --- a/tests/silhouette_test.py +++ b/tests/silhouette_test.py @@ -30,7 +30,7 @@ def test_orthogonal_face_silhouette(self): Vector3D.create(0, 0, 1))) silhouette.create_occurrence(True) - self.assertEquals(silhouette.size().asArray(), rect.size().asArray()) + self.assertEqual(silhouette.size().asArray(), rect.size().asArray()) def test_non_orthogonal_face_silhouette(self): rect = Rect(1, 1) @@ -40,7 +40,7 @@ def test_non_orthogonal_face_silhouette(self): Vector3D.create(0, 0, 1))) silhouette.create_occurrence(True) - self.assertEquals(silhouette.size().asArray(), (rect.size().x, rect.size().y, 0)) + self.assertEqual(silhouette.size().asArray(), (rect.size().x, rect.size().y, 0)) def test_parallel_face_silhouette(self): rect = Rect(1, 1) @@ -50,7 +50,7 @@ def test_parallel_face_silhouette(self): Vector3D.create(0, 0, 1))) silhouette.create_occurrence(True) - self.assertEquals(silhouette.size().asArray(), (0, 0, 0)) + self.assertEqual(silhouette.size().asArray(), (0, 0, 0)) def test_body_silhouette(self): box = Box(1, 1, 1) @@ -60,7 +60,7 @@ def test_body_silhouette(self): Vector3D.create(0, 0, 1))) silhouette.create_occurrence(True) - self.assertEquals(silhouette.size().asArray(), (box.size().x, box.size().y, 0)) + self.assertEqual(silhouette.size().asArray(), (box.size().x, box.size().y, 0)) def test_component_silhouette(self): rect = Rect(1, 1) @@ -70,7 +70,7 @@ def test_component_silhouette(self): Vector3D.create(0, 0, 1))) silhouette.create_occurrence(True) - self.assertEquals(silhouette.size().asArray(), (rect.size().x, rect.size().y, 0)) + self.assertEqual(silhouette.size().asArray(), (rect.size().x, rect.size().y, 0)) def test_multiple_disjoint_faces_silhouette(self): rect1 = Rect(1, 1) @@ -88,7 +88,7 @@ def test_multiple_disjoint_faces_silhouette(self): self.assertTrue(abs(silhouette.size().x - assembly.size().x) < app().pointTolerance) self.assertTrue(abs(silhouette.size().y - assembly.size().y) < app().pointTolerance) - self.assertEquals(silhouette.size().z, 0) + self.assertEqual(silhouette.size().z, 0) def test_multiple_overlapping_faces_silhouette(self): rect1 = Rect(1, 1) @@ -106,7 +106,7 @@ def test_multiple_overlapping_faces_silhouette(self): self.assertTrue(abs(silhouette.size().x - assembly.size().x) < app().pointTolerance) self.assertTrue(abs(silhouette.size().y - assembly.size().y) < app().pointTolerance) - self.assertEquals(silhouette.size().z, 0) + self.assertEqual(silhouette.size().z, 0) def test_cylinder_silhouette(self): cyl = Cylinder(1, 1) @@ -115,7 +115,7 @@ def test_cylinder_silhouette(self): Vector3D.create(0, 0, 1))) silhouette.create_occurrence(True) - self.assertEquals(silhouette.size().asArray(), (cyl.size().x, cyl.size().y, 0)) + self.assertEqual(silhouette.size().asArray(), (cyl.size().x, cyl.size().y, 0)) def test_single_edge(self): circle = Circle(1) @@ -125,7 +125,7 @@ def test_single_edge(self): Vector3D.create(0, 0, 1))) silhouette.create_occurrence(True) - self.assertEquals(silhouette.size().asArray(), circle.size().asArray()) + self.assertEqual(silhouette.size().asArray(), circle.size().asArray()) def test_multiple_edges(self): rect = Rect(1, 1) @@ -149,8 +149,8 @@ def test_multiple_edges(self): silhouette.create_occurrence(True) - self.assertEquals(silhouette.size().asArray(), rect.size().asArray()) - self.assertEquals(len(silhouette.edges), 4) + self.assertEqual(silhouette.size().asArray(), rect.size().asArray()) + self.assertEqual(len(silhouette.edges), 4) def test_named_edges(self): box = Box(1, 1, 1) @@ -173,8 +173,8 @@ def test_named_edges(self): ~edge_finder == ~silhouette) found_edges = silhouette.find_edges(edge_finder) named_edges = silhouette.named_edges("front") - self.assertEquals(len(found_edges), 1) - self.assertEquals(found_edges, named_edges) + self.assertEqual(len(found_edges), 1) + self.assertEqual(found_edges, named_edges) edge_finder.place( ~edge_finder == ~silhouette, @@ -182,8 +182,8 @@ def test_named_edges(self): ~edge_finder == ~silhouette) found_edges = silhouette.find_edges(edge_finder) named_edges = silhouette.named_edges("back") - self.assertEquals(len(found_edges), 1) - self.assertEquals(found_edges, named_edges) + self.assertEqual(len(found_edges), 1) + self.assertEqual(found_edges, named_edges) edge_finder.place( +edge_finder == +silhouette, @@ -191,8 +191,8 @@ def test_named_edges(self): ~edge_finder == ~silhouette) found_edges = silhouette.find_edges(edge_finder) named_edges = silhouette.named_edges("right") - self.assertEquals(len(found_edges), 1) - self.assertEquals(found_edges, named_edges) + self.assertEqual(len(found_edges), 1) + self.assertEqual(found_edges, named_edges) edge_finder.place( -edge_finder == -silhouette, @@ -200,8 +200,8 @@ def test_named_edges(self): ~edge_finder == ~silhouette) found_edges = silhouette.find_edges(edge_finder) named_edges = silhouette.named_edges("left") - self.assertEquals(len(found_edges), 1) - self.assertEquals(found_edges, named_edges) + self.assertEqual(len(found_edges), 1) + self.assertEqual(found_edges, named_edges) diff --git a/tests/tests.py b/tests/tests.py index 11e5308..05f7283 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -49,6 +49,7 @@ "thicken_test", "thread_test", "transform_test", + "text_test", "union_test", ] diff --git a/tests/text_test.py b/tests/text_test.py new file mode 100644 index 0000000..504fe49 --- /dev/null +++ b/tests/text_test.py @@ -0,0 +1,83 @@ +# Copyright 2019 Google LLC +# +# 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 +# +# https://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 unittest + +from adsk.core import Vector3D + +# note: load_tests is required for the "pattern" test filtering functionality in loadTestsFromModule in run() +from fscad.test_utils import FscadTestCase, load_tests +from fscad.fscad import * + + +class TextTest(FscadTestCase): + def test_text(self): + text = Text("A", 1.0) + + self.assertGreater(len(text.bodies), 0) + self.assertGreater(text.size().x, 0) + self.assertGreater(text.size().y, 0) + + text.create_occurrence(True) + + def test_text_with_holes(self): + text = Text("8", 1.0) + + self.assertEqual(len(text.bodies), 1) + + text.create_occurrence(True) + + def test_text_multiple_chars(self): + text = Text("AB", 1.0) + + self.assertGreater(len(text.bodies), 1) + + text.create_occurrence(True) + + def test_text_get_plane(self): + text = Text("A", 1.0) + + plane = text.get_plane() + self.assertIsNotNone(plane) + self.assertTrue(plane.normal.isParallelTo(Vector3D.create(0, 0, 1))) + + text.create_occurrence(True) + + def test_text_extrude(self): + text = Text("A", 1.0) + extruded = Extrude(text, 1.0) + + self.assertGreater(extruded.size().z, 0) + + extruded.create_occurrence(True) + + def test_text_font(self): + text = Text("A", 1.0, font="Arial") + + self.assertGreater(len(text.bodies), 0) + + text.create_occurrence(True) + + def test_text_empty(self): + text = Text(" ", 1.0) + + self.assertEqual(len(text.bodies), 0) + + +def run(context): + import sys + test_suite = unittest.defaultTestLoader.loadTestsFromModule( + sys.modules[__name__], + ) + unittest.TextTestRunner(failfast=True).run(test_suite)