Compare commits
No commits in common. "9a170f93d301faede7967f4a4a11517bf0fdd81b" and "f8e50ca04862e756d72aee365730a1f1c7f1d4d6" have entirely different histories.
9a170f93d3
...
f8e50ca048
|
|
@ -134,7 +134,7 @@ ipython_config.py
|
|||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
poetry.lock
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
[submodule "cgal"]
|
||||
path = cgal
|
||||
url = https://gitee.com/atari/cgal.git
|
||||
branch = 5.6.x-branch
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
# toydesigner-cgal
|
||||
|
||||
python bindings for some geometry algorithms in `cgal` (for [toydesigner](https://gitea.winkinshly.site/songsenand/ToyDesigner) only)
|
||||
|
||||
>`cgal`中部分几何算法的python绑定(仅用于[toydesigner](https://gitea.winkinshly.site/songsenand/ToyDesigner))
|
||||
python bindings for some geometry algorithms in 'cgal' (for 'toydesigner' only)
|
||||
`cgal`中部分几何算法的python绑定(仅用于`toydesigner`)
|
||||
9
build.py
9
build.py
|
|
@ -1,9 +0,0 @@
|
|||
import warnings
|
||||
import subprocess
|
||||
|
||||
res = subprocess.run(["python3", "setup.py", "build_ext", "--inplace"])
|
||||
if res.returncode != 0:
|
||||
warnings.warn(res.stderr.decode())
|
||||
warnings.warn("Error building toydesigner-cgal")
|
||||
elif res.returncode == 0:
|
||||
print("Successfully built toydesigner-cgal")
|
||||
1
cgal
1
cgal
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 11fc1943fbc75135d3ec5776cebab1f4aaa43fb6
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
[tool.poetry]
|
||||
name = "toydesigner-cgal"
|
||||
version = "0.0.1"
|
||||
description = "python bindings for some geometry algorithms in `cgal` (for [toydesigner](https://gitea.winkinshly.site/songsenand/ToyDesigner) only)"
|
||||
authors = ["songsenand <songsenand@163.com>"]
|
||||
license = "GPL"
|
||||
readme = "README.md"
|
||||
packages = [{include = "tdcgal"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
pybind11 = "^2.11.1"
|
||||
loguru = "^0.7.2"
|
||||
sympy = "^1.12"
|
||||
|
||||
[[tool.poetry.source]]
|
||||
name = "ali"
|
||||
url = "https://mirrors.aliyun.com/pypi/simple/"
|
||||
priority = "primary"
|
||||
|
||||
|
||||
[[tool.poetry.source]]
|
||||
name = "PyPI"
|
||||
priority = "primary"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.0.0"
|
||||
|
||||
[build-system]
|
||||
requires = [
|
||||
"poetry-core>=1.1.0",
|
||||
"setuptools>=65.4.1",
|
||||
"pybind11>=2.11.1"
|
||||
]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.build]
|
||||
script = "build.py"
|
||||
generate-setup-file = false
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/operators.h>
|
||||
|
||||
#include <CGAL/Simple_cartesian.h>
|
||||
|
||||
# include "cgal.hpp"
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
typedef CGAL::Simple_cartesian<double> Kernel;
|
||||
|
||||
PYBIND11_MODULE(cgal_bindings, m) {
|
||||
py::module_ plane = m.def_submodule("plane");
|
||||
init_plane(plane);
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#include <pybind11/pybind11.h>
|
||||
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
void init_plane(py::module_ &);
|
||||
|
|
@ -1,315 +0,0 @@
|
|||
from math import isclose
|
||||
|
||||
from sympy import symbols, solve
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..cgal_bindings.plane import Point_2, Segment_2, Line_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()
|
||||
|
||||
|
||||
# 直线类
|
||||
class Line(Shape2D, Line_2):
|
||||
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
|
||||
|
||||
"""
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
#include <pybind11/operators.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
#include <CGAL/Simple_cartesian.h>
|
||||
|
||||
#include "../cgal.hpp"
|
||||
|
||||
typedef CGAL::Simple_cartesian<double> 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_<Point_2>(m, "Point_2")
|
||||
.def(py::init<double, double>())
|
||||
.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 "<Point_2 x=" + std::to_string(a.x()) +
|
||||
" y=" + std::to_string(a.y()) + ">";
|
||||
});
|
||||
py::class_<Segment_2>(m, "Segment_2")
|
||||
.def(py::init<Point_2, Point_2>())
|
||||
.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_<Line_2>(m, "Line_2")
|
||||
.def(py::init<Point_2, Point_2>())
|
||||
.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);
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
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},
|
||||
)
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
from math import isclose
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from tdcgal.plane.objects import *
|
||||
|
||||
class TestPoint2D:
|
||||
|
||||
def setup_method(self):
|
||||
self.point1 = Point2D(1.0, 2.0)
|
||||
self.point2 = Point2D(3.0, 4.0)
|
||||
|
||||
def test_distance(self):
|
||||
assert isclose(self.point1.distance_to_point(self.point2), 8 ** 0.5)
|
||||
|
||||
def test_x(self):
|
||||
assert self.point1.x() == 1
|
||||
assert self.point2.x() == 3
|
||||
|
||||
def test_y(self):
|
||||
assert self.point1.y() == 2
|
||||
assert self.point2.y() == 4
|
||||
"""
|
||||
|
||||
class TestLine:
|
||||
def setup_method(self):
|
||||
self.line1 = Line((1, 3), (1, 4))
|
||||
self.line2 = Line((1, 1), (2, 2))
|
||||
self.line3 = Line((37, 5), (7, 29))
|
||||
|
||||
def test_slope(self):
|
||||
assert self.line1.slope == float("inf")
|
||||
assert self.line2.slope == 1
|
||||
|
||||
def test_y_intercept(self):
|
||||
assert self.line1.y_intercept == float("inf")
|
||||
assert self.line2.y_intercept == 0
|
||||
assert self.line3.y_intercept == 34.6
|
||||
|
||||
def test_equation(self):
|
||||
assert self.line1.expression == "1.0*x - 1.0 = 0"
|
||||
assert self.line2.expression == "1.0*x - 1.0*y = 0"
|
||||
assert self.line3.expression == "24.0*x + 30.0*y - 1038.0 = 0"
|
||||
|
||||
def test_contains(self):
|
||||
assert (1, 5) not in self.line1
|
||||
assert (1, 3.2) in self.line1
|
||||
assert (1.45, 1.45) in self.line2
|
||||
assert (3, 3) not in self.line2
|
||||
assert (37, 5) in self.line3
|
||||
assert (7, 29) in self.line3
|
||||
assert (12, 25) in self.line3
|
||||
assert (12, 12) not in self.line3
|
||||
assert (3, 32.2) not in self.line3
|
||||
assert self.line3.contains((6, 29.8), allow_on_extended=True)
|
||||
|
||||
def test_length(self):
|
||||
assert self.line1.length == 1
|
||||
assert self.line2.length == 2 ** 0.5
|
||||
assert isclose(self.line3.length, 38.41874542459709)
|
||||
|
||||
def test_distance(self):
|
||||
assert self.line1.point_distance((1, 5)) == 1
|
||||
assert self.line2.point_distance((1.5, 1.5)) == 0
|
||||
assert isclose(self.line3.point_distance((12, 24)),0.780868809443030)
|
||||
|
||||
"""
|
||||
Loading…
Reference in New Issue