Skip to content

Commit f355b09

Browse files
Merge pull request #1147 from Geode-solutions/angle_management
feat(Angle): manage degree to radian conversion
2 parents e69831f + 6ea09e0 commit f355b09

File tree

12 files changed

+789
-1
lines changed

12 files changed

+789
-1
lines changed

bindings/python/src/geometry/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ add_geode_python_binding(
2222
NAME "py_geometry"
2323
SOURCES
2424
"geometry.cpp"
25+
"angle.cpp"
2526
"barycentric_coordinates.cpp"
2627
"basic_objects.cpp"
2728
"bounding_box.cpp"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) 2019 - 2025 Geode-solutions
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*
22+
*/
23+
24+
#include "../common.hpp"
25+
26+
#include <geode/geometry/angle.hpp>
27+
28+
namespace geode
29+
{
30+
void define_angle( pybind11::module& module )
31+
{
32+
pybind11::class_< Angle >( module, "Angle" )
33+
.def( "create_from_radians", &Angle::create_from_radians )
34+
.def( "create_from_degrees", &Angle::create_from_degrees )
35+
.def( "radians", &Angle::radians )
36+
.def( "degrees", &Angle::degrees )
37+
.def( "sin", &Angle::sin )
38+
.def( "cos", &Angle::cos )
39+
.def( "tan", &Angle::tan )
40+
.def( "inexact_equal", &Angle::inexact_equal )
41+
.def( pybind11::self == pybind11::self )
42+
.def( pybind11::self < pybind11::self )
43+
.def( pybind11::self > pybind11::self )
44+
.def( pybind11::self + pybind11::self )
45+
.def( pybind11::self - pybind11::self )
46+
.def( pybind11::self * double() )
47+
.def( pybind11::self / double() )
48+
.def( "normalized_between_0_and_2pi",
49+
&Angle::normalized_between_0_and_2pi )
50+
.def( "normalized_between_minuspi_and_pi",
51+
&Angle::normalized_between_minuspi_and_pi )
52+
.def( "normalized_between_0_and_pi",
53+
&Angle::normalized_between_0_and_pi );
54+
}
55+
} // namespace geode

bindings/python/src/geometry/geometry.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
namespace geode
3030
{
31+
void define_angle( pybind11::module& );
3132
void define_barycentric( pybind11::module& );
3233
void define_basic_objects( pybind11::module& );
3334
void define_bounding_box( pybind11::module& );
@@ -55,6 +56,7 @@ PYBIND11_MODULE( opengeode_py_geometry, module )
5556
pybind11::class_< geode::OpenGeodeGeometryLibrary >(
5657
module, "OpenGeodeGeometryLibrary" )
5758
.def( "initialize", &geode::OpenGeodeGeometryLibrary::initialize );
59+
geode::define_angle( module );
5860
geode::define_barycentric( module );
5961
geode::define_basic_objects( module );
6062
geode::define_bounding_box( module );

bindings/python/tests/geometry/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919
# SOFTWARE.
2020

21+
add_geode_python_test(
22+
SOURCE "test-py-angle.py"
23+
DEPENDENCIES
24+
${PROJECT_NAME}::py_geometry
25+
)
2126
add_geode_python_test(
2227
SOURCE "test-py-barycentric-coordinates.py"
2328
DEPENDENCIES
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) 2019 - 2025 Geode-solutions
3+
#
4+
# Permission is hereby granted, free of charge, to any person obtaining a copy
5+
# of this software and associated documentation files (the "Software"), to deal
6+
# in the Software without restriction, including without limitation the rights
7+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
# copies of the Software, and to permit persons to whom the Software is
9+
# furnished to do so, subject to the following conditions:
10+
#
11+
# The above copyright notice and this permission notice shall be included in
12+
# all copies or substantial portions of the Software.
13+
#
14+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
# SOFTWARE.
21+
22+
import os
23+
import sys
24+
import math
25+
import platform
26+
27+
if sys.version_info >= (3, 8, 0) and platform.system() == "Windows":
28+
for path in [x.strip() for x in os.environ["PATH"].split(";") if x]:
29+
os.add_dll_directory(path)
30+
31+
import opengeode_py_geometry as geom
32+
33+
EPSILON = 1e-3
34+
35+
36+
def test_factory_methods():
37+
degrees_radians = [
38+
(-720.0, -4.0 * math.pi),
39+
(-360.0, -2.0 * math.pi),
40+
(-270.0, -3.0 * math.pi / 2.0),
41+
(-180.0, -math.pi),
42+
(-135.0, -3.0 * math.pi / 4.0),
43+
(-90.0, -math.pi / 2.0),
44+
(-45.0, -math.pi / 4.0),
45+
(0.0, 0.0),
46+
(22.5, math.pi / 8.0),
47+
(45.0, math.pi / 4.0),
48+
(90.0, math.pi / 2.0),
49+
(135.0, 3.0 * math.pi / 4.0),
50+
(180.0, math.pi),
51+
(270.0, 3.0 * math.pi / 2.0),
52+
(360.0, 2.0 * math.pi),
53+
(540.0, 3.0 * math.pi),
54+
(720.0, 4.0 * math.pi),
55+
(1080.0, 6.0 * math.pi),
56+
]
57+
for degrees, radians in degrees_radians:
58+
angle_deg = geom.Angle.create_from_degrees(degrees)
59+
if not abs(angle_deg.degrees() - degrees) < EPSILON:
60+
raise ValueError(
61+
f"[Test] Wrong Angle from degree value {angle_deg.degrees()} should be {degrees}!"
62+
)
63+
if not abs(angle_deg.radians() - radians) < EPSILON:
64+
raise ValueError(
65+
f"[Test] Wrong Angle from degree value {angle_deg.radians()} should be {radians}!"
66+
)
67+
68+
angle_rad = geom.Angle.create_from_radians(radians)
69+
if not abs(angle_rad.degrees() - degrees) < EPSILON:
70+
raise ValueError(
71+
f"[Test] Wrong Angle from radian value {angle_rad.degrees()} should be {degrees}!"
72+
)
73+
if not abs(angle_rad.radians() - radians) < EPSILON:
74+
raise ValueError(
75+
f"[Test] Wrong Angle from radian value {angle_rad.radians()} should be {radians}!"
76+
)
77+
78+
79+
def test_comparison_operators():
80+
angle60 = geom.Angle.create_from_degrees(60.00)
81+
angle_almost60 = geom.Angle.create_from_degrees(60.05)
82+
angle_not60 = geom.Angle.create_from_degrees(60.07)
83+
angle30 = geom.Angle.create_from_degrees(30.00)
84+
85+
if not angle60 == angle60:
86+
raise ValueError(
87+
f"[Test] Wrong == comparison: angles {angle60} and {angle60} should be equal!"
88+
)
89+
if angle60 == angle_not60:
90+
raise ValueError(
91+
f"[Test] Wrong == comparison: angles {angle60} and {angle_not60} should not be equal!"
92+
)
93+
if angle60 == angle_almost60:
94+
raise ValueError(
95+
f"[Test] Wrong == comparison: angles {angle60} and {angle_almost60} should not be equal!"
96+
)
97+
98+
if not angle60.inexact_equal(angle60):
99+
raise ValueError(
100+
f"[Test] Wrong inexact_equal: angles {angle60} and {angle60} should be equal!"
101+
)
102+
if angle60.inexact_equal(angle_not60):
103+
raise ValueError(
104+
f"[Test] Wrong inexact_equal: angles {angle60} and {angle_not60} should not be equal!"
105+
)
106+
if not angle60.inexact_equal(angle_almost60):
107+
raise ValueError(
108+
f"[Test] Wrong inexact_equal: angles {angle60} and {angle_almost60} should be equal!"
109+
)
110+
111+
if not angle_almost60 > angle60:
112+
raise ValueError(
113+
f"[Test] Wrong comparison: {angle_almost60} > {angle60} should be true!"
114+
)
115+
if not angle_almost60 < angle_not60:
116+
raise ValueError(
117+
f"[Test] Wrong comparison: {angle_almost60} < {angle_not60} should be true!"
118+
)
119+
120+
121+
def test_arithmetic():
122+
angle30 = geom.Angle.create_from_degrees(30)
123+
angle60 = geom.Angle.create_from_degrees(60)
124+
125+
sum_angle = angle30 + angle60
126+
if not sum_angle.inexact_equal(geom.Angle.create_from_degrees(90)):
127+
raise ValueError("[Test] Wrong sum of angles!")
128+
129+
diff_angle = angle60 - angle30
130+
if not diff_angle.inexact_equal(angle30):
131+
raise ValueError("[Test] Wrong difference of angles!")
132+
133+
scaled_angle = angle30 * 2
134+
if not scaled_angle.inexact_equal(angle60):
135+
raise ValueError("[Test] Wrong scaled angle!")
136+
137+
divided_angle = angle60 / 2
138+
if not divided_angle.inexact_equal(angle30):
139+
raise ValueError("[Test] Wrong divided angle!")
140+
141+
142+
def test_normalization():
143+
raw_angles = [
144+
geom.Angle.create_from_radians(rad)
145+
for rad in [
146+
-4.0 * math.pi,
147+
-2.0 * math.pi,
148+
-3.0 * math.pi / 2.0,
149+
-math.pi,
150+
-3.0 * math.pi / 4.0,
151+
-math.pi / 2.0,
152+
-math.pi / 4.0,
153+
0.0,
154+
math.pi / 8.0,
155+
math.pi / 4.0,
156+
math.pi / 2.0,
157+
3.0 * math.pi / 4.0,
158+
math.pi,
159+
3.0 * math.pi / 2.0,
160+
2.0 * math.pi,
161+
3.0 * math.pi,
162+
4.0 * math.pi,
163+
6.0 * math.pi,
164+
]
165+
]
166+
167+
expected_0_2pi = [
168+
geom.Angle.create_from_radians(rad)
169+
for rad in [
170+
0.0,
171+
0.0,
172+
math.pi / 2.0,
173+
math.pi,
174+
5.0 * math.pi / 4.0,
175+
3.0 * math.pi / 2.0,
176+
7.0 * math.pi / 4.0,
177+
0.0,
178+
math.pi / 8.0,
179+
math.pi / 4.0,
180+
math.pi / 2.0,
181+
3.0 * math.pi / 4.0,
182+
math.pi,
183+
3.0 * math.pi / 2.0,
184+
0.0,
185+
math.pi,
186+
0.0,
187+
0.0,
188+
]
189+
]
190+
191+
expected_minuspi_pi = [
192+
geom.Angle.create_from_radians(rad)
193+
for rad in [
194+
0.0,
195+
0.0,
196+
math.pi / 2.0,
197+
math.pi,
198+
-3.0 * math.pi / 4.0,
199+
-math.pi / 2.0,
200+
-math.pi / 4.0,
201+
0.0,
202+
math.pi / 8.0,
203+
math.pi / 4.0,
204+
math.pi / 2.0,
205+
3.0 * math.pi / 4.0,
206+
math.pi,
207+
-math.pi / 2.0,
208+
0.0,
209+
math.pi,
210+
0.0,
211+
0.0,
212+
]
213+
]
214+
215+
expected_0_pi = [
216+
geom.Angle.create_from_radians(rad)
217+
for rad in [
218+
0.0,
219+
0.0,
220+
math.pi / 2.0,
221+
0.0,
222+
math.pi / 4.0,
223+
math.pi / 2.0,
224+
3.0 * math.pi / 4.0,
225+
0.0,
226+
math.pi / 8.0,
227+
math.pi / 4.0,
228+
math.pi / 2.0,
229+
3.0 * math.pi / 4.0,
230+
0.0,
231+
math.pi / 2.0,
232+
0.0,
233+
0.0,
234+
0.0,
235+
0.0,
236+
]
237+
]
238+
239+
for i, angle in enumerate(raw_angles):
240+
if not angle.normalized_between_0_and_2pi().inexact_equal(expected_0_2pi[i]):
241+
raise ValueError(
242+
f"[Test] Wrong normalization 0–2pi at index {i}! - should be {angle.normalized_between_0_and_2pi().degrees()} and get {expected_0_2pi[i].degrees()} instead."
243+
)
244+
if not angle.normalized_between_minuspi_and_pi().inexact_equal(
245+
expected_minuspi_pi[i]
246+
):
247+
raise ValueError(
248+
f"[Test] Wrong normalization -pi–pi at index {i}!- should be {angle.normalized_between_minuspi_and_pi().degrees()} and get {expected_minuspi_pi[i].degrees()} instead."
249+
)
250+
if not angle.normalized_between_0_and_pi().inexact_equal(expected_0_pi[i]):
251+
raise ValueError(
252+
f"[Test] Wrong normalization 0–pi at index {i}!- should be {angle.normalized_between_0_and_pi().degrees()} and get {expected_0_pi[i].degrees()} instead."
253+
)
254+
255+
256+
if __name__ == "__main__":
257+
test_factory_methods()
258+
test_comparison_operators()
259+
test_arithmetic()
260+
test_normalization()

cmake/CppTargets.cmake

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function(add_geode_library)
3434
cmake_parse_arguments(GEODE_LIB
3535
"STATIC"
3636
"NAME;FOLDER"
37-
"PUBLIC_HEADERS;ADVANCED_HEADERS;INTERNAL_HEADERS;SOURCES;PUBLIC_DEPENDENCIES;PRIVATE_DEPENDENCIES"
37+
"PUBLIC_HEADERS;ADVANCED_HEADERS;INTERNAL_HEADERS;SOURCES;SKIP_UNITY;PUBLIC_DEPENDENCIES;PRIVATE_DEPENDENCIES"
3838
${ARGN}
3939
)
4040
foreach(file ${GEODE_LIB_SOURCES})
@@ -57,6 +57,13 @@ function(add_geode_library)
5757
"${PROJECT_SOURCE_DIR}/include/${GEODE_LIB_FOLDER}/${file}"
5858
)
5959
endforeach()
60+
foreach(file ${GEODE_LIB_SKIP_UNITY})
61+
set_property(
62+
SOURCE "${PROJECT_SOURCE_DIR}/src/${GEODE_LIB_FOLDER}/${file}"
63+
PROPERTY
64+
SKIP_UNITY_BUILD_INCLUSION ON
65+
)
66+
endforeach()
6067
set(PROJECT_LIB_NAME ${PROJECT_NAME}::${GEODE_LIB_NAME})
6168
set(VERSION_RC_FILE_IN ${PROJECT_SOURCE_DIR}/cmake/version.rc.in)
6269
if(EXISTS ${VERSION_RC_FILE_IN})

include/geode/basic/types.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
namespace geode
3535
{
3636
static constexpr double GLOBAL_EPSILON{ 1E-6 };
37+
// 0.001 radians --> 0.057 degrees
3738
static constexpr double GLOBAL_ANGULAR_EPSILON{ 1E-3 };
3839

3940
using index_t = unsigned int;

0 commit comments

Comments
 (0)