From: DUC ANH HOANG Date: Mon, 13 Mar 2023 12:55:06 +0000 (+0100) Subject: SAM Integrating X-Git-Url: http://git.salome-platform.org/gitweb/?a=commitdiff_plain;h=e483f439811b97e7442ac817cbe203b20552cd6d;p=modules%2Fshaper.git SAM Integrating --- diff --git a/CMakeLists.txt b/CMakeLists.txt index ec218bf98..14072287a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -214,6 +214,7 @@ ADD_SUBDIRECTORY (src/SketchAPI) ADD_SUBDIRECTORY (src/GDMLAPI) ADD_SUBDIRECTORY (src/ConnectorAPI) ADD_SUBDIRECTORY (src/FiltersAPI) +ADD_SUBDIRECTORY (src/SAM) # Tests ADD_SUBDIRECTORY (test.API/SHAPER) diff --git a/src/CTestTestfileInstall.cmake b/src/CTestTestfileInstall.cmake index 591d5ca18..a8f845936 100644 --- a/src/CTestTestfileInstall.cmake +++ b/src/CTestTestfileInstall.cmake @@ -47,4 +47,5 @@ SUBDIRS(ConnectorAPI SAMConverter Locale test_API + SAM ) diff --git a/src/SAM/CMakeLists.txt b/src/SAM/CMakeLists.txt new file mode 100644 index 000000000..e1076094e --- /dev/null +++ b/src/SAM/CMakeLists.txt @@ -0,0 +1,31 @@ +# Copyright (C) 2014-2022 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +INCLUDE(Common) +INCLUDE(UnitTest) + +SET(SOURCE_DIR + sam +) + +ADD_CUSTOM_TARGET(SAM SOURCES ${SOURCE_DIR}) + +INSTALL(DIRECTORY ${SOURCE_DIR} DESTINATION ${SHAPER_INSTALL_PYTHON_FILES}) + +ADD_SUBDIRECTORY(Test) diff --git a/src/SAM/README.md b/src/SAM/README.md new file mode 100755 index 000000000..cdc29c604 --- /dev/null +++ b/src/SAM/README.md @@ -0,0 +1,122 @@ +# SAM: Sketch dAta Model + +## Primitive Parameters + +We describe all the parameters for primitives corresponding to the code in the files contained in the folder `/sam/catalog_primitive/`. Each primitive is a python class inheriting from the abstract class `Primitive` defined in `/sam/primitive.py`. In particular all primitives have a Boolean parameter `status_construction` indicating if a primitive is to be physically realized or simply serve as a reference for other primitives. + +- **point** (dof: 2): + - x (float): x coordinate + - y (float): y coordinate + +- **line** (dof: 4): + - pnt1 (point): starting point + - pnt2 (point): ending point + +- **circle** (dof: 3): + - center (point): center + - radius (float): radius + +- **arc** (dof: 5): + - center (point): center + - radius (float): radius + - angle_start (float): starting angle relative to the absolute horizontal axis + - angle_end (float): ending angle relative to the absolute horizontal axis + - radian (bool): if true then angles are in radians otherwise in degrees + +Note that a point can have a field `parent` refering to the primitive it is used for in the case of a line, circle or arc. + +## Constraint Parameters + +Each constraint is a python class inheriting from the abstract class `Constraint` defined in `/sam/constraint.py`. Primitives involved by the constraint are stored as python references in the `references` attribute. The `references` attribute is a python list and accepts any type and any number of primitives. If the constraint involves only one primitive, there is only one element in the list. Constraints are divided into two groups: [geometric constraints](sam/catalog_constraint/geometric_constraint.py) and [dimension constraints](sam/catalog_constraint/dimension_constraint.py). Dimension constraints accept parameter values while geometric constraints do not. + +#### Dimension constraints + +- **angle**: + - angle (float) + +- **distance**: + - distance_min (float) + +- **length**: + - length (float) + +- **horizontalLength**: + - length (float) + +- **verticalLength**: + - length (float) + +- **radius**: + - radius (float) + +#### Geometric constraints + +- **coincident** +- **equal** +- **horizontal** +- **midpoint** +- **vertical** +- **tangent** +- **parallel** +- **perpendicular** + +## Installation (pip) + +Clone the repository, ensure your pip version is at least 22.0 and run + +```sh + cd sam + pip install . +``` + +## Installation (dev) + + +We use conda as an environment manager and poetry as dependency manager. + +1. Generate a conda env + +First, create and activate a basic conda env from the [env_prep.yml](./env/env.yml) file. + +Run +``` + conda env create -f ./env/env.yml +``` + +then + +``` + conda activate basic_env +``` + + + +2. Install poetry and package dependencies + +To install package dependencies with poetry, + +``` + poetry install +``` + +To update package dependencies, +``` + poetry update +``` + + +## Testing + +For running all the tests: + +``` + poetry run pytest +``` + +For running a specific test: +``` + poetry run pytest path/my_test +``` + + +See test coverage : [TO COMPLETE] \ No newline at end of file diff --git a/src/SAM/Test/CMakeLists.txt b/src/SAM/Test/CMakeLists.txt new file mode 100644 index 000000000..6e6cad710 --- /dev/null +++ b/src/SAM/Test/CMakeLists.txt @@ -0,0 +1,49 @@ +# Copyright (C) 2014-2022 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +#find_package (Python3 REQUIRED COMPONENTS Interpreter Development Pytest) + +SET(TEST_INSTALL_DIRECTORY "${SALOME_SHAPER_INSTALL_TESTS}/SAM") + +INSTALL(FILES CTestTestfileInstall.cmake + DESTINATION ${TEST_INSTALL_DIRECTORY} + RENAME CTestTestfile.cmake) + +SET(TEST_DIR + unit + unit/primitives + unit/constraints +) + +FOREACH(tdir ${TEST_DIR}) + + INCLUDE(${tdir}/tests.set) + SET(TEST_INSTALL_DIRECTORY "${SALOME_SHAPER_INSTALL_TESTS}/SAM/${tdir}") + + INSTALL(FILES ${tdir}/CTestTestfileInstall.cmake + DESTINATION ${TEST_INSTALL_DIRECTORY} + RENAME CTestTestfile.cmake) + + FOREACH(tfile ${TEST_NAMES}) + INSTALL(FILES ${tdir}/${tfile}.py DESTINATION ${TEST_INSTALL_DIRECTORY}) + ENDFOREACH() + + INSTALL(FILES ${tdir}/tests.set DESTINATION ${TEST_INSTALL_DIRECTORY}) + +ENDFOREACH() \ No newline at end of file diff --git a/src/SAM/Test/CTestTestfileInstall.cmake b/src/SAM/Test/CTestTestfileInstall.cmake new file mode 100644 index 000000000..605e2e582 --- /dev/null +++ b/src/SAM/Test/CTestTestfileInstall.cmake @@ -0,0 +1,24 @@ +# Copyright (C) 2016-2022 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +SET(COMPONENT_NAME SHAPER) +SET(PYTHON_TEST_DRIVER "$ENV{KERNEL_ROOT_DIR}/bin/salome/appliskel/python_test_driver.py") +SET(TIMEOUT 300) + +SUBDIRS(unit unit/constraints unit/primitives) diff --git a/src/SAM/Test/__init__.py b/src/SAM/Test/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/src/SAM/Test/conftest.py b/src/SAM/Test/conftest.py new file mode 100755 index 000000000..e69de29bb diff --git a/src/SAM/Test/unit/CTestTestfileInstall.cmake b/src/SAM/Test/unit/CTestTestfileInstall.cmake new file mode 100644 index 000000000..cafe5eec4 --- /dev/null +++ b/src/SAM/Test/unit/CTestTestfileInstall.cmake @@ -0,0 +1,26 @@ +# Copyright (C) 2016-2022 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +INCLUDE(tests.set) + +FOREACH(tfile ${TEST_NAMES}) + SET(TEST_NAME ${COMPONENT_NAME}_SAM_unit_${tfile}) + ADD_TEST(${TEST_NAME} python ${PYTHON_TEST_DRIVER} ${TIMEOUT} ${tfile}.py) + SET_TESTS_PROPERTIES(${TEST_NAME} PROPERTIES LABELS "${COMPONENT_NAME}") +ENDFOREACH() diff --git a/src/SAM/Test/unit/constraints/CTestTestfileInstall.cmake b/src/SAM/Test/unit/constraints/CTestTestfileInstall.cmake new file mode 100644 index 000000000..c0f1afc46 --- /dev/null +++ b/src/SAM/Test/unit/constraints/CTestTestfileInstall.cmake @@ -0,0 +1,26 @@ +# Copyright (C) 2016-2022 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +INCLUDE(tests.set) + +FOREACH(tfile ${TEST_NAMES}) + SET(TEST_NAME ${COMPONENT_NAME}_SAM_unit_constraints_${tfile}) + ADD_TEST(${TEST_NAME} python ${PYTHON_TEST_DRIVER} ${TIMEOUT} ${tfile}.py) + SET_TESTS_PROPERTIES(${TEST_NAME} PROPERTIES LABELS "${COMPONENT_NAME}") +ENDFOREACH() diff --git a/src/SAM/Test/unit/constraints/test_global.py b/src/SAM/Test/unit/constraints/test_global.py new file mode 100755 index 000000000..47e6286d7 --- /dev/null +++ b/src/SAM/Test/unit/constraints/test_global.py @@ -0,0 +1,184 @@ +from sam.catalog_constraint.dimension_constraint import * #Distance, Radius +from sam.catalog_constraint.geometric_constraint import * +from sam.catalog_primitive import Point, Line, Arc, Circle +from sam.catalog_constraint import * +import unittest +import logging + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + +class TestConstraintGlobal(unittest.TestCase): + + #================================================================================== + # Contraintes purement géométriques, non dimensionnelles : + #================================================================================== + def test_horizontal(self): + + # construction + line = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + horizontal = Horizontal(references=[line]) + expected_output = 'HORIZONTAL: ref_1: Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7)' + self.assertEqual(expected_output, str(horizontal)) + logger.debug(f'constraint: {horizontal}') + + def test_vertical(self): + + # construction + line = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + vertical = Vertical(references=[line]) + expected_output = 'VERTICAL: ref_1: Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7)' + self.assertEqual(expected_output, str(vertical)) + logger.debug(f'constraint: {vertical}') + + def test_parallel(self): + + # construction + line_1 = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + line_2 = Line(pnt1= [0.0,0.2], pnt2 = [0.3, 0.7]) + parallel = Parallel(references=[line_1, line_2]) + expected_output = 'PARALLEL: ref_1: Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7), ref_2: Line p1=Point P(0.0, 0.2), p2=Point P(0.3, 0.7)' + self.assertEqual(expected_output, str(parallel)) + logger.debug(f'constraint: {parallel}') + + def test_coincident(self): + + # construction + line_1 = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + line_2 = Line(pnt1= [0.0,0.2], pnt2 = [0.3, 0.7]) + coincident = Coincident(references=[line_1.pnt1, line_2.pnt2]) + expected_output = 'COINCIDENT: ref_1: Point P(0.2, 0.2), Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7)-- COINCIDENT: ref_2: Point P(0.3, 0.7), Line p1=Point P(0.0, 0.2), p2=Point P(0.3, 0.7)' + self.assertEqual(expected_output, str(coincident)) + logger.debug(f'constraint: {coincident}') + + def test_equal(self): + + # same lengths for 2 lines : + line_1 = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + line_2 = Line(pnt1= [0.0,0.2], pnt2 = [0.1, 0.5]) + constraint = Equal(references=[line_1, line_2]) + expected_output = 'EQUAL: ref_1: Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7), ref_2: Line p1=Point P(0.0, 0.2), p2=Point P(0.1, 0.5)' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + # same radius for a circle and an arc of circle :: + circle = Circle(status_construction=False, center=[0., 5.], radius=1) + arc = Arc(status_construction=False, center=[0., 5.], radius=1, angles=[90., 180.]) + constraint = Equal(references=[circle, arc]) + expected_output = 'EQUAL: ref_1: Circle: center=Point P(0.0, 5.0), radius= 1, ref_2: Arc center=Point P(0.0, 5.0), radius= 1, start angle= 90.0, end angle= 180.0' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + def test_midpoint(self): + + # construction + line = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + point = Point(status_construction=False, point=[0., 8.]) + constraint = Midpoint(references=[line, point]) + expected_output = 'MIDPOINT: ref_1: Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7), ref_2: Point P(0.0, 8.0)' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + def test_perpendicular(self): + + # construction + line_1 = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + line_2 = Line(pnt1= [0.0,0.2], pnt2 = [0.3, 0.7]) + constraint = Perpendicular(references=[line_1, line_2]) + expected_output = 'PERPENDICULAR: ref_1: Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7), ref_2: Line p1=Point P(0.0, 0.2), p2=Point P(0.3, 0.7)' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + def test_tangent(self): + + # tangence line-circle : + line = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + circle = Circle(status_construction=False, center=[0., 5.], radius=1) + constraint = Tangent(references=[line, circle]) + expected_output = 'TANGENT: ref_1: Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7), ref_2: Circle: center=Point P(0.0, 5.0), radius= 1' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + # tangence line-arc : + line = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + arc = Arc(status_construction=False, center=[0., 5.], radius=1, angles=[90., 180.]) + constraint = Tangent(references=[line, arc]) + expected_output = 'TANGENT: ref_1: Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7), ref_2: Arc center=Point P(0.0, 5.0), radius= 1, start angle= 90.0, end angle= 180.0' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + #================================================================================== + # Contraintes dimensionnelles : + #================================================================================== + def test_angle(self): + + # angle constraint between 2 lines : + line_1 = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + line_2 = Line(pnt1= [0.0,0.2], pnt2 = [0.3, 0.7]) + constraint = Angle(references=[line_1, line_2], angle = 45.) + expected_output = 'ANGLE: ref_1: Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7), ref_2: Line p1=Point P(0.0, 0.2), p2=Point P(0.3, 0.7), angle = 45.0' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + def test_length(self): + + # construction + line = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + length = Length(references=[line], length=2.) + expected_output = 'LENGTH: ref= Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7), length = 2.0' + self.assertEqual(expected_output, str(length)) + logger.debug(f'constraint: {length}') + + def test_distance(self): + + # distance entre 2 segments (line) : + line_1 = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + line_2 = Line(pnt1= [0.0,0.2], pnt2 = [0.3, 0.7]) + constraint = Distance(references=[line_1, line_2], distance_min=5) + expected_output = 'DISTANCE: ref_1: Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7), ref_2: Line p1=Point P(0.0, 0.2), p2=Point P(0.3, 0.7), distance_min = 5' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + # distance entre un segment (line) et un point : + line = Line(pnt1= [0.1,0.2], pnt2 = [0.3, 0.7]) + point = Point(status_construction=False, point=[0., 8.]) + constraint = Distance(references=[line, point], distance_min=5) + expected_output = 'DISTANCE: ref_1: Line p1=Point P(0.1, 0.2), p2=Point P(0.3, 0.7), ref_2: Point P(0.0, 8.0), distance_min = 5' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + def test_horizontaldistance(self): + + # distance entre 2 segments (line) : + line_1 = Line(pnt1= [0.2,0.2], pnt2 = [0.3, 0.7]) + line_2 = Line(pnt1= [0.0,0.2], pnt2 = [0.3, 0.7]) + constraint = HorizontalDistance(references=[line_1, line_2], distance_min=5) + expected_output = 'HORIZONTAL_DISTANCE: ref_1: Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7), ref_2: Line p1=Point P(0.0, 0.2), p2=Point P(0.3, 0.7), distance_min = 5' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + # distance entre un segment (line) et un point : + line = Line(pnt1= [0.1,0.2], pnt2 = [0.3, 0.7]) + point = Point(status_construction=False, point=[0., 8.]) + constraint = HorizontalDistance(references=[line, point], distance_min=5) + expected_output = 'HORIZONTAL_DISTANCE: ref_1: Line p1=Point P(0.1, 0.2), p2=Point P(0.3, 0.7), ref_2: Point P(0.0, 8.0), distance_min = 5' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + def test_radius(self): + + # test de contrainte rayon sur un cercle : + circle = Circle(status_construction=False, center=[0., 5.], radius=1) + constraint = Radius(references=[circle], radius=2.) + expected_output = 'RADIUS: ref= Circle: center=Point P(0.0, 5.0), radius= 1, radius = 2.0' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + # test de contrainte rayon sur un arc de cercle : + arc = Arc(status_construction=False, center=[0., 5.], radius=1, angles=[90., 180.]) + constraint = Radius(references=[arc], radius=2.) + expected_output = 'RADIUS: ref= Arc center=Point P(0.0, 5.0), radius= 1, start angle= 90.0, end angle= 180.0, radius = 2.0' + self.assertEqual(expected_output, str(constraint)) + logger.debug(f'constraint: {constraint}') + + diff --git a/src/SAM/Test/unit/constraints/tests.set b/src/SAM/Test/unit/constraints/tests.set new file mode 100644 index 000000000..97e48a72d --- /dev/null +++ b/src/SAM/Test/unit/constraints/tests.set @@ -0,0 +1,22 @@ +# Copyright (C) 2021-2022 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +SET(TEST_NAMES + test_global +) diff --git a/src/SAM/Test/unit/primitives/CTestTestfileInstall.cmake b/src/SAM/Test/unit/primitives/CTestTestfileInstall.cmake new file mode 100644 index 000000000..263b32b59 --- /dev/null +++ b/src/SAM/Test/unit/primitives/CTestTestfileInstall.cmake @@ -0,0 +1,26 @@ +# Copyright (C) 2016-2022 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +INCLUDE(tests.set) + +FOREACH(tfile ${TEST_NAMES}) + SET(TEST_NAME ${COMPONENT_NAME}_SAM_unit_primitives_${tfile}) + ADD_TEST(${TEST_NAME} python ${PYTHON_TEST_DRIVER} ${TIMEOUT} ${tfile}.py) + SET_TESTS_PROPERTIES(${TEST_NAME} PROPERTIES LABELS "${COMPONENT_NAME}") +ENDFOREACH() diff --git a/src/SAM/Test/unit/primitives/test_arc.py b/src/SAM/Test/unit/primitives/test_arc.py new file mode 100755 index 000000000..46faaf410 --- /dev/null +++ b/src/SAM/Test/unit/primitives/test_arc.py @@ -0,0 +1,34 @@ +from sam.catalog_primitive.arc import Arc +import unittest +import logging + + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + + +class TestArc(unittest.TestCase): + + def test_construction(self): + + # construction + arc = Arc(status_construction=False, center=[0., 5.], radius=1, angles=[90., 180.]) + self.assertEqual('Arc center=Point P(0.0, 5.0), radius= 1, start angle= 90.0, end angle= 180.0', str(arc)) + logger.debug(f'arc: {arc}') + + def test_update_parms(self): + arc = Arc(status_construction=False, center=[0., 5.], radius=1, angles=[90., 180.]) + self.assertEqual('Arc center=Point P(0.0, 5.0), radius= 1, start angle= 90.0, end angle= 180.0', str(arc)) + arc.update_parms({'center' : [1., 7.], 'radius' : 0.8, 'angle_start': 50.0, 'angle_end': 66.0}) + self.assertEqual('Arc center=Point P(1.0, 7.0), radius= 0.8, start angle= 50.0, end angle= 66.0', str(arc)) + + def test_lineage(self): + + # Test 1: test that the three objects have the same references + arc = Arc(status_construction=False, center=[0., 5.], radius=1, angles=[90., 180.]) + self.assertTrue(arc is arc.center.parent) + + # Test 2: test that when line is modify, parents are modify + arc.update_parms({'center' : [1., 7.], 'radius' : 0.8, 'angle_start': 50.0, 'angle_end': 66.0}) + self.assertEqual('Arc center=Point P(1.0, 7.0), radius= 0.8, start angle= 50.0, end angle= 66.0', str(arc)) + self.assertEqual('Arc center=Point P(1.0, 7.0), radius= 0.8, start angle= 50.0, end angle= 66.0', str(arc.center.parent)) diff --git a/src/SAM/Test/unit/primitives/test_circle.py b/src/SAM/Test/unit/primitives/test_circle.py new file mode 100755 index 000000000..66dd9f3dd --- /dev/null +++ b/src/SAM/Test/unit/primitives/test_circle.py @@ -0,0 +1,34 @@ +from sam.catalog_primitive.circle import Circle +import unittest +import logging + + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + + +class TestCircle(unittest.TestCase): + + def test_construction(self): + + # construction + circle = Circle(status_construction=False, center=[0., 5.], radius=1) + self.assertEqual('Circle: center=Point P(0.0, 5.0), radius= 1', str(circle)) + logger.debug(f'circle: {circle}') + + def test_update_parms(self): + circle = Circle(status_construction=False, center=[0., 5.], radius=1) + self.assertEqual('Circle: center=Point P(0.0, 5.0), radius= 1', str(circle)) + circle.update_parms({'center' : [1., 7.], 'radius' : 0.8}) + self.assertEqual('Circle: center=Point P(1.0, 7.0), radius= 0.8', str(circle)) + + def test_lineage(self): + + # Test 1: test that the three objects have the same references + circle = Circle(status_construction=False, center=[0., 5.], radius=1) + self.assertTrue(circle is circle.center.parent) + + # Test 2: test that when line is modify, parents are modify + circle.update_parms({'center' : [1., 7.], 'radius' : 0.8}) + self.assertEqual('Circle: center=Point P(1.0, 7.0), radius= 0.8', str(circle)) + self.assertEqual('Circle: center=Point P(1.0, 7.0), radius= 0.8', str(circle.center.parent)) diff --git a/src/SAM/Test/unit/primitives/test_line.py b/src/SAM/Test/unit/primitives/test_line.py new file mode 100755 index 000000000..f29769325 --- /dev/null +++ b/src/SAM/Test/unit/primitives/test_line.py @@ -0,0 +1,40 @@ +from sam.catalog_primitive.line import Line +import unittest +import logging + + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + + +class TestPrimitive(unittest.TestCase): + + def test_construction(self): + + # construction + line = Line(status_construction=False, pnt1 = [0.2,0.2], pnt2= [0.3, 0.7]) + self.assertEqual('Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7)', str(line)) + logger.debug(f'line: {line}') + + def test_update_parms(self): + line = Line(status_construction=False, pnt1 = [0.2,0.2], pnt2= [0.3, 0.7]) + self.assertEqual('Line p1=Point P(0.2, 0.2), p2=Point P(0.3, 0.7)', str(line)) + line.update_parms({'pnt1' : [0.3, 0.4], 'pnt2' : [0.1,0.8]}) + self.assertEqual('Line p1=Point P(0.3, 0.4), p2=Point P(0.1, 0.8)', str(line)) + + + def test_lineage(self): + + # Test 1: test that the three objects have the same references + line = Line(status_construction=False, pnt1 = [0.2,0.2], pnt2= [0.3, 0.7]) + self.assertTrue(line is line.pnt1.parent) + self.assertTrue(line is line.pnt2.parent) + self.assertTrue(line.pnt1.parent is line.pnt2.parent) + logger.debug(f'parent: {line.pnt1.parent}') + logger.debug(f'line: {line}') + + # Test 2: test that when line is modify, parents are modify + line.update_parms({'pnt1' : [0.3, 0.4], 'pnt2' : [0.1,0.8]}) + self.assertEqual('Line p1=Point P(0.3, 0.4), p2=Point P(0.1, 0.8)', str(line)) + self.assertEqual('Line p1=Point P(0.3, 0.4), p2=Point P(0.1, 0.8)', str(line.pnt1.parent)) + self.assertEqual('Line p1=Point P(0.3, 0.4), p2=Point P(0.1, 0.8)', str(line.pnt2.parent)) diff --git a/src/SAM/Test/unit/primitives/test_point.py b/src/SAM/Test/unit/primitives/test_point.py new file mode 100755 index 000000000..b6e7d2860 --- /dev/null +++ b/src/SAM/Test/unit/primitives/test_point.py @@ -0,0 +1,23 @@ +from sam.catalog_primitive.point import Point +import unittest +import logging + + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + + +class TestCircle(unittest.TestCase): + + def test_construction(self): + + # construction + point = Point(status_construction=False, point=[0., 1.]) + self.assertEqual('Point P(0.0, 1.0)', str(point)) + logger.debug(f'point: {point}') + + def test_update_parms(self): + point = Point(status_construction=False, point=[0., 1.]) + self.assertEqual('Point P(0.0, 1.0)', str(point)) + point.update_parms({'x' : 0.3, 'y' : 0.4}) + self.assertEqual('Point P(0.3, 0.4)', str(point)) \ No newline at end of file diff --git a/src/SAM/Test/unit/primitives/tests.set b/src/SAM/Test/unit/primitives/tests.set new file mode 100644 index 000000000..e7a9929ea --- /dev/null +++ b/src/SAM/Test/unit/primitives/tests.set @@ -0,0 +1,25 @@ +# Copyright (C) 2021-2022 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +SET(TEST_NAMES + test_arc + test_circle + test_line + test_point +) diff --git a/src/SAM/Test/unit/test_sketch.py b/src/SAM/Test/unit/test_sketch.py new file mode 100755 index 000000000..7f22d523a --- /dev/null +++ b/src/SAM/Test/unit/test_sketch.py @@ -0,0 +1,43 @@ +from sam.catalog_primitive import Circle, Line, Arc +from sam.sketch import Sketch +import unittest +import logging +import matplotlib.pyplot as plt +import pickle + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + + +class TestSketch(unittest.TestCase): + + def test_init(self): + + # construction + sketch = Sketch() + + sketch.add(Circle(center=[10., 5.], radius=1)) + sketch.add(Line(pnt1 = [0.2, 0.2], pnt2 = [0.3, 0.7])) + + for s in sketch.sequence: + logger.info(f'{s}') + + # def test_show(self): + # sketch = Sketch() + + # sketch.add(Circle(center=[0.,5.], radius=1)) + # sketch.add(Arc(center=[0.,5.], radius=1, angles = [90.,180.])) + # sketch.add(Line(pnt1 = [0.2, 0.2], pnt2 = [0.3, 0.7])) + + # fig = sketch.draw() + # plt.show() + + def test_export(self): + sketch = Sketch() + sketch.add(Circle(center=[0., 5.], radius=1)) + + out_path = 'tests/asset/out/sketch.pkl' + sketch.export(out_path=out_path) + + seq = pickle.load(open(out_path, "rb")) + logger.info(f'seq: {seq}') diff --git a/src/SAM/Test/unit/tests.set b/src/SAM/Test/unit/tests.set new file mode 100644 index 000000000..b857a93e7 --- /dev/null +++ b/src/SAM/Test/unit/tests.set @@ -0,0 +1,20 @@ +# Copyright (C) 2021-2022 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +SET(TEST_NAMES test_sketch) diff --git a/src/SAM/env/env.yml b/src/SAM/env/env.yml new file mode 100755 index 000000000..1b1061031 --- /dev/null +++ b/src/SAM/env/env.yml @@ -0,0 +1,12 @@ +name: basic_env + +channels: + - default + - conda-forge + +dependencies: + - python=3.9 + - pip=20.3.3 + - poetry=1.1.12 + - pip: + - requests==2.25.0 \ No newline at end of file diff --git a/src/SAM/pyproject.toml b/src/SAM/pyproject.toml new file mode 100755 index 000000000..17c376bc3 --- /dev/null +++ b/src/SAM/pyproject.toml @@ -0,0 +1,38 @@ +[tool.poetry] +name = "sam" +version = "0.1.0" +description = "" +authors = ["EDF - F.Robin"] + +[tool.poetry.dependencies] +python = "^3.7.3" +pyyaml = "6.0" + +[tool.poetry.dev-dependencies] +pytest = "^6.2.1" +autopep8 = "1.6.0" + + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + + + +[tool.pytest.ini_options] +testpaths = [ + "tests", # You should have a "tests" directory +] +log_cli = true +log_cli_level = "DEBUG" +log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" +log_cli_date_format = "%Y-%m-%d %H:%M:%S" + + +[tool.autopep8] +max_line_length = 120 +ignore = "E501,W6" # or ["E501", "W6"] +in-place = true +recursive = true +aggressive = 3 + diff --git a/src/SAM/sam/catalog_constraint/__init__.py b/src/SAM/sam/catalog_constraint/__init__.py new file mode 100755 index 000000000..23d271871 --- /dev/null +++ b/src/SAM/sam/catalog_constraint/__init__.py @@ -0,0 +1,4 @@ +from .dimension_constraint import Length, Distance, Angle, Radius, HorizontalDistance, VerticalDistance +from .geometric_constraint import Horizontal, Vertical, Parallel, Coincident, Perpendicular, Midpoint, Tangent +from .geometric_constraint import Equal + diff --git a/src/SAM/sam/catalog_constraint/dimension_constraint.py b/src/SAM/sam/catalog_constraint/dimension_constraint.py new file mode 100755 index 000000000..50cc6711d --- /dev/null +++ b/src/SAM/sam/catalog_constraint/dimension_constraint.py @@ -0,0 +1,95 @@ +from typing import Dict, List +from sam.constraint import Constraint, ConstraintType + +class Angle(Constraint): + + def __init__(self, references: List = [], angle: float = None): + super(Angle, self).__init__(elt_type=ConstraintType.ANGLE, references=references) + self.angle = angle + + def __repr__(self): + elt1 = self.references[0] + elt2 = self.references[1] + return f"{self.get_name()}: ref_1: {elt1}, ref_2: {elt2}, angle = {self.angle}" + + def _update_angle(self, angle: float): + self.angle = angle + + def _construct_mapp(self) -> Dict: + """Construct a mapp to update parameters""" + mapp = {'angle' : lambda angle : self._update_angle(angle)} + return mapp + +class Distance(Constraint): + + def __init__(self, references: List = [], distance_min: float = None): + super(Distance, self).__init__(elt_type=ConstraintType.DISTANCE, references=references) + self.distance_min = distance_min + + def __repr__(self): + elt1 = self.references[0] + elt2 = self.references[1] + return f"{self.get_name()}: ref_1: {elt1}, ref_2: {elt2}, distance_min = {self.distance_min}" + + def _update_distance_min(self, distance_min: float): + self.distance_min = distance_min + + def _construct_mapp(self) -> Dict: + """Construct a mapp to update parameters""" + mapp = {'distance_min' : lambda x : self._update_distance_min(x)} + return mapp + +class HorizontalDistance(Distance): + + def __init__(self, references: List = [], distance_min: float = None): + super(Distance, self).__init__(elt_type=ConstraintType.DISTANCE, references=references) + self.type = ConstraintType.HORIZONTAL_DISTANCE + self.distance_min = distance_min + + +class VerticalDistance(Distance): + + def __init__(self, references: List = [], distance_min: float = None): + super(Distance, self).__init__(elt_type=ConstraintType.DISTANCE, references=references) + self.type = ConstraintType.VERTICAL_DISTANCE + self.distance_min = distance_min + + +class Length(Constraint): + + def __init__(self, references: List = [], length: float = None): + super(Length, self).__init__(elt_type=ConstraintType.LENGTH, references=references) + self.length = length + + def __repr__(self): + elt = self.references[0] + return f"{self.get_name()}: ref= {elt}, length = {self.length}" + + def _update_length(self, length: float): + self.length = length + + def _construct_mapp(self) -> Dict: + """Construct a mapp to update parameters""" + mapp = {'length' : lambda x : self._update_length(x)} + return mapp + + + + +class Radius(Constraint): + + def __init__(self, references: List = [], radius: float = None): + super(Radius, self).__init__(elt_type=ConstraintType.RADIUS, references=references) + self.radius = radius + + def __repr__(self): + elt = self.references[0] + return f"{self.get_name()}: ref= {elt}, radius = {self.radius}" + + def _update_radius(self, radius: float): + self.radius = radius + + def _construct_mapp(self) -> Dict: + """Construct a mapp to update parameters""" + mapp = {'radius' : lambda x : self._update_radius(x)} + return mapp \ No newline at end of file diff --git a/src/SAM/sam/catalog_constraint/geometric_constraint.py b/src/SAM/sam/catalog_constraint/geometric_constraint.py new file mode 100755 index 000000000..21d469c1b --- /dev/null +++ b/src/SAM/sam/catalog_constraint/geometric_constraint.py @@ -0,0 +1,62 @@ +from typing import Dict, List +from sam.constraint import Constraint, ConstraintType + +class Coincident(Constraint): + + def __init__(self, references: List = []): + super(Coincident, self).__init__(elt_type=ConstraintType.COINCIDENT, references=references) + + def __repr__(self): + elt1 = self.references[0] + elt2 = self.references[1] + if hasattr(elt1, 'parent'): + rep = f"{self.get_name()}: ref_1: {elt1}, {elt1.parent}" + else : + rep = f"{self.get_name()}: ref_1: {elt1}" + if hasattr(elt2, 'parent'): + rep += f"-- {self.get_name()}: ref_2: {elt2}, {elt2.parent}" + else : + rep += f"-- {self.get_name()}: ref_2: {elt2}" + + return rep + +class Equal(Constraint): + """Equal Constraint (same length for lines or same radius for circles or arcs""" + + def __init__(self, references: List = []): + super(Equal, self).__init__(elt_type=ConstraintType.EQUAL, references=references) + +class Horizontal(Constraint): + """Horizontal constraint""" + + def __init__(self, references: List = []): + super(Horizontal, self).__init__(elt_type=ConstraintType.HORIZONTAL, references=references) + +class Midpoint(Constraint): + + def __init__(self, references: List = []): + super(Midpoint, self).__init__(elt_type=ConstraintType.MIDPOINT, references=references) + +class Vertical(Constraint): + """Vertical constraint""" + + def __init__(self, references: List = []): + super(Vertical, self).__init__(elt_type=ConstraintType.VERTICAL, references=references) + +class Tangent(Constraint): + """Tangent Constraint""" + + def __init__(self, references: List = []): + super(Tangent, self).__init__(elt_type=ConstraintType.TANGENT, references=references) + +class Parallel(Constraint): + """Parallel Constraint""" + + def __init__(self, references: List = []): + super(Parallel, self).__init__(elt_type=ConstraintType.PARALLEL, references=references) + +class Perpendicular(Constraint): + """Perpendicular Constraint""" + + def __init__(self, references: List = []): + super(Perpendicular, self).__init__(elt_type=ConstraintType.PERPENDICULAR, references=references) diff --git a/src/SAM/sam/catalog_primitive/__init__.py b/src/SAM/sam/catalog_primitive/__init__.py new file mode 100755 index 000000000..ab0111017 --- /dev/null +++ b/src/SAM/sam/catalog_primitive/__init__.py @@ -0,0 +1,6 @@ +from .arc import Arc +from .circle import Circle +from .line import Line +from .point import Point +from .projection import Projection +from .intersection import Intersection diff --git a/src/SAM/sam/catalog_primitive/arc.py b/src/SAM/sam/catalog_primitive/arc.py new file mode 100755 index 000000000..58de78981 --- /dev/null +++ b/src/SAM/sam/catalog_primitive/arc.py @@ -0,0 +1,101 @@ +from typing import List, Dict +from sam.primitive import Primitive, PrimitiveType +from .point import Point + +from matplotlib import patches +import numpy as np + +import logging + + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + + +class Arc(Primitive): + """Arc Primitive.""" + + def __init__(self, status_construction: bool = False, center: List = [], radius: float = 0., angles: List = [], radian=False): + super().__init__(elt_type=PrimitiveType.ARC, status_construction=status_construction) + self.center: Point = Point(point = center, status_construction=status_construction) + self.radius: float = radius + self.angle_start: float = angles[0] # in degrees + self.angle_end: float = angles[1] # in degrees + self.radian: bool = radian + + # add lineage + self.center.add_parent(self) + + def __hash__(self): + return hash(self.center) + hash(self.radius) + hash(self.pnt1) + hash(self.pnt2) + + def __eq__(self, other): + if not isinstance(other, Arc): + return False + + return self.center == other.center and self.radius == other.radius and self.pnt1 == other.pnt1 and self.pnt2 == other.pnt2 and self.angle_start == other.angle_start and self.angle_end == other.angle_end + + def __repr__(self): + return f"Arc center={self.center}, radius= {self.radius}, start angle= {self.angle_start}, end angle= {self.angle_end}" + + def add_points_startend(self): + if not self.radian: + angle_start = np.deg2rad(self.angle_start) + angle_end = np.deg2rad(self.angle_end) + else: + angle_start = self.angle_start + angle_end = self.angle_end + self.pnt1 = Point(point = [self.center.x + self.radius*np.cos(angle_start), self.center.y + self.radius*np.sin(angle_start)]) + self.pnt2 = Point(point = [self.center.x + self.radius*np.cos(angle_end), self.center.y + self.radius*np.sin(angle_end)]) + + # add lineage + self.pnt1.add_parent(self) + self.pnt2.add_parent(self) + + def _update_angle_start(self,angle: float): + self.angle_start = angle + + def _update_angle_end(self, angle: float): + self.angle_end = angle + + def _update_radius(self, radius: float): + self.radius = radius + + def _update_center(self, center: List): + self.center.update_parms({'x' : center[0], 'y': center[1]}) + + def _construct_mapp(self) -> None: + """Construct a mapp to update parameters""" + + def _construct_mapp(self) -> None: + """Construct a mapp to update parameters""" + mapp = { + 'center': lambda center: self._update_center(center), + 'radius': lambda radius: self._update_radius(radius), + 'angle_start': lambda angle : self._update_angle_start(angle), + 'angle_end': lambda angle : self._update_angle_end(angle),} + if hasattr(self,'pnt1'): + mapp['pnt1'] = lambda x: self.pnt1.update_parms({'x' : x[0], 'y': x[1]}) + mapp['pnt2'] = lambda x: self.pnt2.update_parms({'x' : x[0], 'y': x[1]}) + return mapp + + def point_belongs_to_primitive(self, point: object) -> bool: + """Check if a point belongs to the line""" + + def plot(self, ax, color='black', linewidth=1): + #angle = math.atan2(arc.yDir, arc.xDir) * 180 / math.pi + #startParam = arc.startParam * 180 / math.pi + #endParam = arc.endParam * 180 / math.pi + theta1 = self.angle_start + theta2 = self.angle_end + if self.radian: + theta1 = np.rad2deg(theta1) + theta2 = np.rad2deg(theta2) + ax.add_patch(patches.Arc(xy=self.center.get_point(), # center of the ellipse + # angle=self.angle_start - self.angle_end, # rotation of the ellipse, counterclockwise, in degrees + theta1=theta1, # starting angle, in degrees + theta2=theta2, # ending angle, in degrees + width=2 * self.radius, # The length of the horizontal axis. + height=2 * self.radius, # The length of the vertical axis. + linewidth=linewidth, + linestyle=self._get_linestyle(), color=color)) diff --git a/src/SAM/sam/catalog_primitive/circle.py b/src/SAM/sam/catalog_primitive/circle.py new file mode 100755 index 000000000..4ddffca82 --- /dev/null +++ b/src/SAM/sam/catalog_primitive/circle.py @@ -0,0 +1,49 @@ +from typing import List, Dict +from sam.primitive import Primitive, PrimitiveType +from .point import Point + +from matplotlib import patches + + +class Circle(Primitive): + """Line Primitive.""" + + def __init__(self, status_construction: bool = False, center: List = [], radius: float = 0.): + super(Circle, self).__init__(elt_type=PrimitiveType.CIRCLE, status_construction=status_construction) + self.center: Point = Point(point = center, status_construction=status_construction) + self.radius: float = radius + + # add lineage + self.center.add_parent(self) + + def __hash__(self): + return hash(self.center) + hash(self.radius) + + def __eq__(self, other): + if not isinstance(other, Circle): + return False + + return self.center == other.center and self.radius == other.radius + + def __repr__(self): + return f"Circle: center={self.center}, radius= {self.radius}" + + def _update_radius(self, radius: float): + self.radius = radius + + def _update_center(self, center: List): + self.center.update_parms({'x' : center[0], 'y': center[1]}) + + def _construct_mapp(self) -> None: + """Construct a mapp to update parameters""" + return {'center' : lambda center: self._update_center(center), + 'radius' : lambda radius: self._update_radius(radius)} + + + def point_belongs_to_primitive(self, point: object) -> bool: + """Check if a point belongs to the line""" + + + def plot(self, ax, color='black', linewidth=1): + patch = patches.Circle(self.center.get_point(), self.radius, fill=False, linestyle=self._get_linestyle(), color=color, linewidth=linewidth) + ax.add_patch(patch) diff --git a/src/SAM/sam/catalog_primitive/intersection.py b/src/SAM/sam/catalog_primitive/intersection.py new file mode 100755 index 000000000..8feecead9 --- /dev/null +++ b/src/SAM/sam/catalog_primitive/intersection.py @@ -0,0 +1,30 @@ +from typing import List, Dict +from sam.primitive import Primitive + +class Intersection(): + """Intersection feature.""" + + def __init__(self, objects: List = []): + self.intersected_objects = objects + + def __repr__(self): + return f"Intersection - {self.intersected_objects[0].__repr__()}" + + def _update_intersected_objects(self,intersected_objects : List): + self.intersected_objects = intersected_objects + + def _construct_mapp(self) -> None: + """Construct a mapp to update parameters""" + #return {'x' : lambda x: self._update_x(x), 'y' : lambda y:self._update_y(y)} + return {} + + def add_parent(self,parent:object) -> None: + self.parent = parent + + def get_objects(self): + return self.intersected_objects + + def get_object(self, index): + if index < 0 or index > len(self.intersected_objects): + return None + return self.intersected_objects[index] diff --git a/src/SAM/sam/catalog_primitive/line.py b/src/SAM/sam/catalog_primitive/line.py new file mode 100755 index 000000000..fe53a0057 --- /dev/null +++ b/src/SAM/sam/catalog_primitive/line.py @@ -0,0 +1,45 @@ +from typing import Dict, List +from sam.primitive import Primitive, PrimitiveType +from .point import Point + +import logging +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + +class Line(Primitive): + """Line Primitive.""" + + def __init__(self, status_construction: bool = False, pnt1: List = [], pnt2: List = []): + super(Line, self).__init__(elt_type=PrimitiveType.LINE, status_construction=status_construction) + self.pnt1: Point=Point(point=pnt1) + self.pnt2: Point=Point(point=pnt2) + + # add lineage + self.pnt1.add_parent(self) + self.pnt2.add_parent(self) + + def __hash__(self): + return hash(self.pnt1) + hash(self.pnt2) + + def __eq__(self, other): + if not isinstance(other, Line): + return False + + return self.pnt1 == other.pnt1 and self.pnt2 == other.pnt2 + + def __repr__(self): + return f"Line p1={self.pnt1}, p2={self.pnt2}" + + def point_belongs_to_primitive(self, point: object) -> bool: + """Check if a point belongs to the line""" + + def _construct_mapp(self) -> None: + """Construct a mapp to update parameters""" + return { 'pnt1' : lambda x: self.pnt1.update_parms({'x' : x[0], 'y': x[1]}), + 'pnt2' : lambda x: self.pnt2.update_parms({'x' : x[0], 'y': x[1]})} + + + def plot(self, ax, color='black', linewidth=1): + ax.scatter(self.pnt1.x, self.pnt1.y, c='red', marker='.') + ax.scatter(self.pnt2.x, self.pnt2.y, c='blue', marker='.') + ax.plot([self.pnt1.x, self.pnt2.x], [self.pnt1.y, self.pnt2.y], color, linestyle=self._get_linestyle(), linewidth=linewidth) diff --git a/src/SAM/sam/catalog_primitive/point.py b/src/SAM/sam/catalog_primitive/point.py new file mode 100755 index 000000000..689eb4d6c --- /dev/null +++ b/src/SAM/sam/catalog_primitive/point.py @@ -0,0 +1,48 @@ +from typing import List, Dict +from sam.primitive import Primitive, PrimitiveType + + +class Point(Primitive): + """Point Primitive.""" + + def __init__(self, status_construction: bool = False, point: List = []): + super(Point, self).__init__(elt_type=PrimitiveType.POINT, status_construction=status_construction) + self.x: float = point[0] + self.y: float = point[1] + + def __hash__(self): + return hash(self.x + self.y) + + def __eq__(self, other): + if not isinstance(other, Point): + return False + + return self.x == other.x and self.y == other.y + + def __repr__(self): + return f"Point P({self.x}, {self.y})" + + def _update_x(self,x : float): + self.x = x + + def _update_y(self,y : float): + self.y = y + + def _construct_mapp(self) -> None: + """Construct a mapp to update parameters""" + return {'x' : lambda x: self._update_x(x), 'y' : lambda y:self._update_y(y)} + + def add_parent(self,parent:object) -> None: + self.parent = parent + + def get_point(self): + return [self.x , self.y ] + + def point_belongs_to_primitive(self, point: List, threshold: float = 0.00001) -> bool: + """Check if a point belongs to the point""" + return np.linalg.norm(np.array([self.x, self.y]), point) < self.threshold + + + + def plot(self, ax, color='black',**kwargs): + ax.scatter(self.x, self.y, color=color, marker='.',**kwargs) diff --git a/src/SAM/sam/catalog_primitive/projection.py b/src/SAM/sam/catalog_primitive/projection.py new file mode 100755 index 000000000..a040e1101 --- /dev/null +++ b/src/SAM/sam/catalog_primitive/projection.py @@ -0,0 +1,25 @@ +from typing import List, Dict +from sam.primitive import Primitive + +class Projection(): + """Projection feature.""" + + def __init__(self, object: Primitive): + self.projected_object = object + + def __repr__(self): + return f"Projection - {self.projected_object.__repr__()}" + + def _update_projected_object(self,projected_object : Primitive): + self.projected_objects = projected_objects + + def _construct_mapp(self) -> None: + """Construct a mapp to update parameters""" + #return {'x' : lambda x: self._update_x(x), 'y' : lambda y:self._update_y(y)} + return {} + + def add_parent(self,parent:object) -> None: + self.parent = parent + + def get_object(self): + return self.projected_object diff --git a/src/SAM/sam/constraint.py b/src/SAM/sam/constraint.py new file mode 100755 index 000000000..0b629a02a --- /dev/null +++ b/src/SAM/sam/constraint.py @@ -0,0 +1,87 @@ +import enum +import abc +from typing import Dict, List + + +class ConstraintType(enum.IntEnum): + """This enumeration represents the type of the constraints.""" + HORIZONTAL = 0 + VERTICAL = 1 + PARALLEL = 2 + LENGTH = 3 + COINCIDENT = 4 + PERPENDICULAR = 5 + DISTANCE = 7 + RADIUS = 8 + TANGENT = 9 + MIDPOINT = 10 + EQUAL = 11 + ANGLE = 12 + HORIZONTAL_DISTANCE = 13 + VERTICAL_DISTANCE = 14 + # PROJECTED = 1 + # MIRROR = 2 + # + + # + + # + # OFFSET = 13 + + # CONCENTRIC = 15 + # FIX = 16 + + # CIRCULAR_PATTERN = 18 + # PIERCE = 19 + # LINEAR_PATTERN = 20 + # CENTERLINE_DIMENSION = 21 + # INTERSECTED = 22 + # SILHOUTTED = 23 + # QUADRANT = 24 + # NORMAL = 25 + # MINOR_DIAM = 26 + # MAJOR_DIAM = 27 + # RHO = 28 + +class Constraint(abc.ABC): + """Abstract class representing a geometry entity. + + This abstract class represents a geometric entity in a sketch. + It is identified by its entity id, a unique string within the sketch. + """ + + def __init__(self, elt_type: str, references: List = []): + self.type: str = elt_type + self.references: str = references + + #def __hash__(self): + # return hash(self.center + self.radius) + hash(self.pnt1) + hash(self.pnt2) + + #def __eq__(self, other): + # if not isinstance(other, Constraint): + # return False + + # return self.center == other.center and self.radius == other.radius and self.pnt1 == other.pnt1 and self.pnt2 == other.pnt2 and self.angle_start == other.angle_start and self.angle_end == other.angle_end + + def __repr__(self): + l_ref = [f'ref_{i+1}: {ref}' for i, ref in enumerate(self.references)] + refs = ', '.join(l_ref) + return f"{self.get_name()}: {refs}" + + def get_name(self) -> int: + return self.type.name + + def get_type(self) -> int: + return self.type + + def _construct_mapp(self) -> object: + """Construct a mapp to update parameters""" + pass + + def update_parms(self, parms: Dict) -> None: + """Update the current parameters""" + mapp = self._construct_mapp() + + for key, elt in parms.items(): + mapp[key](elt) + diff --git a/src/SAM/sam/primitive.py b/src/SAM/sam/primitive.py new file mode 100755 index 000000000..e022149c1 --- /dev/null +++ b/src/SAM/sam/primitive.py @@ -0,0 +1,67 @@ +import enum +import abc +from typing import Dict + +import logging + + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + + +class PrimitiveType(enum.IntEnum): + """This enumeration represents the type of the geometries.""" + LINE = 0 + ARC = 1 + CIRCLE = 2 + POINT = 3 + + +class Primitive(abc.ABC): + """Abstract class representing a geometry entity. + + This abstract class represents a geometric entity in a sketch. + It is identified by its entity id, a unique string within the sketch. + """ + + def __init__(self, elt_type: object, status_construction: bool = False): + self.status_construction: bool = status_construction + self.type: int = elt_type + + def _get_linestyle(self): + return '--' if self.status_construction else '-' + + def is_construction(self) -> bool: + return self.status_construction + + @property + def get_name(self) -> int: + return self.type.name + + @property + def get_type(self) -> int: + return self.type + + + def _construct_mapp(self) -> object: + """Construct a mapp to update parameters""" + pass + + def point_belongs_to_primitive(self, point: object) -> object: + """Construct from a shaper object""" + pass + + def update_parms(self, parms: Dict) -> None: + """Update the current parameters""" + mapp = self._construct_mapp() + + + for key, elt in parms.items(): + mapp[key](elt) + + # @abc.abstractmethod + # def to_dict(self) -> dict: + # """Obtains a serialized representation of this entity as a dictionary. + + # The returned dictionary should be compatible with the json representation from onshape. + # """ diff --git a/src/SAM/sam/sketch.py b/src/SAM/sam/sketch.py new file mode 100755 index 000000000..21ffdcece --- /dev/null +++ b/src/SAM/sam/sketch.py @@ -0,0 +1,60 @@ +from typing import List +import pickle, sys +import matplotlib.pyplot as plt + +from sam.primitive import Primitive +from sam.constraint import Constraint + +import logging +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + +class Sketch: + def __init__(self): + self.sequence: List = [] + + def add(self, elt: object): + self.sequence.append(elt) + + def _prepare_draw(self, ax=None, show_axes: bool = True): + if ax is None: + fig = plt.figure() + ax = fig.add_subplot(111, aspect='equal') + else: + fig = None + + # Eliminate upper and right axes + ax.spines['right'].set_color('none') + ax.spines['top'].set_color('none') + + if not show_axes: + ax.set_yticklabels([]) + ax.set_xticklabels([]) + _ = [line.set_marker('None') for line in ax.get_xticklines()] + _ = [line.set_marker('None') for line in ax.get_yticklines()] + + # Eliminate lower and left axes + ax.spines['left'].set_color('none') + ax.spines['bottom'].set_color('none') + + return fig, ax + + def draw(self, show_axes: bool = True): + + fig, ax = self._prepare_draw() + for s in self.sequence: + if isinstance(s, Primitive): + p_fn = s.plot(ax) + if p_fn is None: + continue + + # Rescale axis limits + ax.grid(True) + ax.relim() + ax.autoscale_view() + + return fig + + def export(self, out_path: str): + logger.info(f'save in {out_path}') + pickle.dump({'seq': self.sequence}, open(out_path, "wb"))