diff --git a/tdcgal/__init__.py b/tdcgal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tdcgal/cgal.cpp b/tdcgal/cgal.cpp new file mode 100644 index 0000000..7b58041 --- /dev/null +++ b/tdcgal/cgal.cpp @@ -0,0 +1,15 @@ +#include +#include + +#include + +# include "cgal.hpp" + +namespace py = pybind11; + +typedef CGAL::Simple_cartesian Kernel; + +PYBIND11_MODULE(cgal_bindings, m) { + py::module_ plane = m.def_submodule("plane"); + init_plane(plane); +} \ No newline at end of file diff --git a/tdcgal/cgal.hpp b/tdcgal/cgal.hpp new file mode 100644 index 0000000..695f75b --- /dev/null +++ b/tdcgal/cgal.hpp @@ -0,0 +1,6 @@ +#include + + +namespace py = pybind11; + +void init_plane(py::module_ &); diff --git a/tdcgal/plane/__init__.py b/tdcgal/plane/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tdcgal/plane/objects.py b/tdcgal/plane/objects.py new file mode 100644 index 0000000..6083f9f --- /dev/null +++ b/tdcgal/plane/objects.py @@ -0,0 +1,317 @@ +from math import isclose + +from sympy import symbols, solve + +from loguru import logger + +from ..cgal_bindings.plane import Point_2, Segment_2, squared_distance + + +def point_maker(pre_point): + if isinstance(pre_point, Point2D): + return pre_point + elif isinstance(pre_point, tuple): + return Point2D(pre_point[0], pre_point[1]) + else: + print(type(pre_point)) + raise ValueError("Invalid input for Point2D") + + +class Shape2D(object): + def __init__(self, **kwargs): + self.color = kwargs.get("color", "white") + + +class Point2D(Shape2D, Point_2): + def __init__(self, x: float, y: float, **kwargs): + Point_2.__init__(self, x, y) + Shape2D.__init__(self, **kwargs) + + # 计算点到另一点的距离 + def distance_to_point(self, point): + point = point_maker(point) + return (squared_distance(self, point)) ** 0.5 + + # 重载==运算符,判断两个Point2D实例是否为同一点 + def __eq__(self, other): + self.point == point_maker(other) + + def __repr__(self): + return f"Point2D(x: {self.x}, y: {self.y})" + + +class Segment2D(Shape2D, Segment_2): + def __init__(self, point1, point2, **kwargs): + point1 = point_maker(point1) + point2 = point_maker(point2) + super(Shape2D, self).__init__(**kwargs) + super(Segment_2, self).__init__(point1, point2) + + # 计算线段的长度 + def length(self): + return self.squared_length() ** 0.5 + + # 重载==运算符,判断两个Segment2D实例是否为同一条线段 + def __eq__(self, other) -> bool: + return self == other + + # 重载`in`运算符,判断点是否在线段上 + def __contains__(self, point) -> bool: + return self.has_on(point) + + # 重载`repr`方法,返回Segment2D的字符串表示 + def __repr__(self) -> str: + return f"Segment2D({self.point1}, {self.point2})" + + # 线段的起点`source` + @property + def source(self): + return self._source() + + # 线段的终点`target` + @property + def target(self): + return self._target() + + # 与线段共线的直线 + def supporting_line(self) -> Line: + return self.supporting_line() + +"""class Line(Shape2D): + def __init__(self, point1, point2, **kwargs): + super(Line, self).__init__(**kwargs) + self.point1 = point_maker(point1) + self.point2 = point_maker(point2) + if self.point1 == self.point2: + raise ValueError("Invalid input for Line: point1 == point2") + self._a = self.point2.y - self.point1.y + self._b = self.point1.x - self.point2.x + self._c = self.point2.x * self.point1.y - self.point1.x * self.point2.y + + if isclose(self.point1.x, self.point2.x): + self._slope = float("inf") + self._y_intercept = float("inf") + else: + self._slope = -self._a / self._b + self._y_intercept = -self._c / self._b + self._x, self._y = symbols("x y") + self._expression = self._a * self._x + self._b * self._y + self._c + self._length = ( + (self.point1.x - self.point2.x) ** 2 + (self.point1.y - self.point2.y) ** 2 + ) ** 0.5 + + def contains(self, point, allow_on_extended=False): + """"""判断点`point`是否在线段上 + Args: + point (Point2D or tuple): 需要判断的点 + allow_on_extended (bool, optional): 是否允许点在线段的延长线上. Defaults to False. + + Returns: + _type_: boolean + """""" + point = point_maker(point) + + f = self.function + if allow_on_extended: + if isclose(self._b, 0.0): + return isclose(point.x, self.point1.x) + return isclose(f(point.x), point.y) + + if isclose(self._b, 0.0): + return isclose(point.x, self.point1.x) and ( + point.y >= min(self.point1.y, self.point2.y) + and point.y <= max(self.point1.y, self.point2.y) + ) + return isclose(f(point.x), point.y) and ( + ( + point.x >= min(self.point1.x, self.point2.x) + and point.x <= max(self.point1.x, self.point2.x) + ) + ) + + # 通过关键字`in`判断点是否在线段上 + def __contains__(self, point): + return self.contains(point) + + # 计算点`point`到线段的垂足 + def foot_point(self, point): + point = point_maker(point) + if self.contains(point, allow_on_extended=True): + return point + else: + denominator = self._a**2 + self._b**2 + x = ( + (self._b**2) * point.x + - self._a * self._b * point.y + - self._a * self._c + ) / denominator + y = ( + (self._a**2) * point.y + - self._a * self._b * point.x + - self._b * self._c + ) / denominator + return Point2D(x, y) + + # 计算点`point`到线段的距离 + def point_distance(self, point, is_to_line=False): + point = point_maker(point) + if self.contains(point): + return 0 + else: + dis_to_line = ( + abs(self._expression.evalf(subs={self._x: point.x, self._y: point.y})) + / (self._a**2 + self._b**2) ** 0.5 + ) + if is_to_line: + return dis_to_line + if self.foot_point(point) not in self: + return min( + self.point1.distance_to_point(point), + self.point2.distance_to_point(point), + ) + return dis_to_line + + # 该线段所在直线的纵截距 + @property + def y_intercept(self): + return self._y_intercept + + # 该线段的斜率 + @property + def slope(self): + return self._slope + + # 该线段所在直线的函数方程 + @property + def function(self): + def f(x): + expr = self._expression.subs(self._x, x) + res = solve(expr, self._y) + if len(res) == 1: + return res[0].evalf() + else: + return None + return f + + # 该线段的函数式 + @property + def expression(self): + return f"{str(self._expression)} = 0" + + @property + # 该线段的长度 + def length(self): + return self._length + + """""" + # 该线段与另一线段的交点 + def intersection(self, other): + if isinstance(other, Line): + if self.slope() == other.slope(): + return None + else: + x = ( + self.slope() * other.point1.x + - self.point1.y + + other.slope() * self.point1.x + - other.point1.y + ) / (self.slope() - other.slope()) + y = self.slope() * (x - self.point1.x) + self.point1.y + return Point2D(x, y) + else: + raise ValueError("Invalid input for Line") + """""" + + +class Rectangle(Shape2D): + def __init__(self, point1, point2, **kwargs): + super(Rectangle, self).__init__(**kwargs) + p1 = point_maker(point1) + p2 = point_maker(point2) + self.point1 = point_maker(p1) + self.point2 = point_maker((point1.x, point2.y)) + self.point3 = point_maker(point2) + self.point4 = point_maker((point2.x, point1.y)) + + # 该矩形的四条边 + def sides(self): + return [ + Line(self.point1, self.point2), + Line(self.point2, self.point3), + Line(self.point3, self.point4), + Line(self.point4, self.point1), + ] + + # 该矩形的边长 + def side_lengths(self): + return [line.length() for line in self.sides()] + + # 该矩形是否包含点`point` + def contains(self, point): + point = point_maker(point) + lines = self.sides() + return isclose( + sum([line.point_distance(point) for line in lines]), + sum(self.side_lengths()), + ) + + # 通过关键字`in`判断点是否在矩形内 + def __contains__(self, point): + return self.contains(point) + + # 该矩形的面积 + def area(self): + return self.side_lengths()[0] * self.side_lengths()[1] + + # 该矩形的周长 + def perimeter(self): + return sum(self.side_lengths()) + + # 该矩形的中心点 + @property + def center(self): + return Point2D( + (self.point1.x + self.point3.x) / 2, (self.point1.y + self.point2.y) / 2 + ) + + # 该矩形的四个顶点 + @property + def vertices(self): + return [self.point1, self.point2, self.point3, self.point4] + + # 该矩形的外接圆 + @property + def circumcircle(self): + pass + + +class Polygon(Shape2D): + def __init__(self, points, **kwargs): + super(Polygon, self).__init__(**kwargs) + self._points = [point_maker(point) for point in points] + self._lines = [Line(self.points[i], self.points[(i + 1) % len(self.points)]) for i in range(len(self.points))] + self._sides = [line.length() for line in self.lines] + self._perimeter = sum(self.sides) + self._center = Point2D(sum([point.x for point in self.points]) / len(self.points), sum([point.y for point in self.points]) / len(self.points)) + + @property + def points(self): + return self._points + + @property + def lines(self): + return self._lines + + @property + def sides(self): + return self._sides + + @property + def perimeter(self): + return self._perimeter + + @property + def center(self): + return self._center + + """ \ No newline at end of file diff --git a/tdcgal/plane/plane.cpp b/tdcgal/plane/plane.cpp new file mode 100644 index 0000000..88c80ba --- /dev/null +++ b/tdcgal/plane/plane.cpp @@ -0,0 +1,54 @@ +#include +#include + +#include + +#include "../cgal.hpp" + +typedef CGAL::Simple_cartesian Kernel; +typedef Kernel::Point_2 Point_2; +typedef Kernel::Segment_2 Segment_2; +typedef Kernel::Line_2 Line_2; + +namespace py = pybind11; + +double squared_distance(const Point_2 &a, const Point_2 &b) { + return CGAL::squared_distance(a, b); +} + +void init_plane(py::module_ &m) { + py::class_(m, "Point_2") + .def(py::init()) + .def("x", &Point_2::x) + .def("y", &Point_2::y) + .def("hx", &Point_2::hx) + .def("hy", &Point_2::hy) + .def("hw", &Point_2::hw) + .def(py::self == py::self) + .def("__repr__", [](const Point_2 &a) { + return ""; + }); + py::class_(m, "Segment_2") + .def(py::init()) + .def("_source", &Segment_2::source) + .def("_target", &Segment_2::target) + .def("min", &Segment_2::min, "返回线段的端点中较小的那个") + .def("max", &Segment_2::max, "返回线段的端点中较大的那个") + .def("squared_length", &Segment_2::squared_length, "返回线段的平方长度") + .def("opposite", &Segment_2::opposite, "返回反向的线段") + .def("has_on", &Segment_2::collinear_has_on, "判断点是否在线段上") + .def("supporting_line", &Segment_2::supporting_line, "返回与线段共线的直线") + .def("is_degenerate", &Segment_2::is_degenerate, "判断线段端点是否重合") + .def("is_horizontal", &Segment_2::is_horizontal, "判断线段是否水平") + .def("is_vertical", &Segment_2::is_vertical, "判断线段是否垂直") + .def(py::self == py::self) + .def(py::self != py::self); + py::class_(m, "Line_2") + .def(py::init()) + .def("a", &Line_2::a) + .def("b", &Line_2::b) + .def("c", &Line_2::c) + .def(py::self == py::self); + m.def("squared_distance", &squared_distance); +}; diff --git a/tdcgal/setup.py b/tdcgal/setup.py new file mode 100644 index 0000000..9935030 --- /dev/null +++ b/tdcgal/setup.py @@ -0,0 +1,28 @@ +from pathlib import Path +from pybind11.setup_helpers import Pybind11Extension, ParallelCompile, build_ext +from setuptools import setup + + +__version__ = "0.0.1" + +ext_modules = [ + Pybind11Extension( + "cgal_bindings", + sources=sorted([str(i.absolute()) for i in (Path(".").rglob("*.cpp"))]), + define_macros=[("VERSION_INFO", __version__)], + include_dirs=["../cgal/"], + ), +] + +ParallelCompile("NPY_NUM_BUILD_JOBS").install() + +setup( + name="cgal_bindings", + version=__version__, + author="songsenand", + author_email="songsenand@163.com", + url="https://gitea.winkinshly.site/songsenand/toydesigner-cgal", + description="python bindings for some geometry algorithms in `cgal` (for [toydesigner](https://gitea.winkinshly.site/songsenand/ToyDesigner) only)", + ext_modules=ext_modules, + cmdclass={"build_ext": build_ext}, +) \ No newline at end of file