From 14e28827eeb1129a4e0ee85f1d28b4cad3d05367 Mon Sep 17 00:00:00 2001 From: KmolYuan Date: Tue, 28 May 2019 20:00:15 +0800 Subject: [PATCH] Add cython modules. --- cython/README.md | 40 ++ cython/platform/patch.diff | 26 + cython/python_solvespace/__init__.py | 34 ++ cython/python_solvespace/slvs.pxd | 325 +++++++++++ cython/python_solvespace/slvs.pyi | 347 ++++++++++++ cython/python_solvespace/slvs.pyx | 796 +++++++++++++++++++++++++++ cython/requirements.txt | 1 + cython/setup.py | 116 ++++ cython/tests/__init__.py | 1 + cython/tests/test_slvs.py | 248 +++++++++ 10 files changed, 1934 insertions(+) create mode 100644 cython/README.md create mode 100644 cython/platform/patch.diff create mode 100644 cython/python_solvespace/__init__.py create mode 100644 cython/python_solvespace/slvs.pxd create mode 100644 cython/python_solvespace/slvs.pyi create mode 100644 cython/python_solvespace/slvs.pyx create mode 100644 cython/requirements.txt create mode 100644 cython/setup.py create mode 100644 cython/tests/__init__.py create mode 100644 cython/tests/test_slvs.py diff --git a/cython/README.md b/cython/README.md new file mode 100644 index 00000000..08aeba9d --- /dev/null +++ b/cython/README.md @@ -0,0 +1,40 @@ +[![Build status](https://ci.appveyor.com/api/projects/status/b2o8jw7xnfqghqr5?svg=true)](https://ci.appveyor.com/project/KmolYuan/solvespace) +[![Build status](https://travis-ci.org/KmolYuan/solvespace.svg)](https://travis-ci.org/KmolYuan/solvespace) +![OS](https://img.shields.io/badge/OS-Windows%2C%20Mac%20OS%2C%20Ubuntu-blue.svg) +[![GitHub license](https://img.shields.io/badge/license-GPLv3+-blue.svg)](https://raw.githubusercontent.com/KmolYuan/solvespace/master/LICENSE) + +python-solvespace +=== + +Python library from solver of SolveSpace. + +Feature for CDemo and Python interface can see [here](https://github.com/KmolYuan/python-solvespace/blob/master/Cython/DOC.txt). + +Build and Test +=== + +Requirement: + ++ [Cython] + +Build and install the module: + +```bash +python setup.py install +python setup.py install --user # User mode +``` + +Run unit test: + +```bash +python tests/test_slvs.py +``` + +Uninstall the module: + +```bash +pip uninstall python_solvespace +``` + +[GNU Make]: https://sourceforge.net/projects/mingw-w64/files/latest/download?source=files +[Cython]: https://cython.org/ diff --git a/cython/platform/patch.diff b/cython/platform/patch.diff new file mode 100644 index 00000000..6b2726dc --- /dev/null +++ b/cython/platform/patch.diff @@ -0,0 +1,26 @@ +--- cygwinccompiler.py ++++ cygwinccompiler.py +@@ -82,7 +82,25 @@ def get_msvcr(): + elif msc_ver == '1600': + # VS2010 / MSVC 10.0 + return ['msvcr100'] ++ elif msc_ver == '1700': ++ # Visual Studio 2012 / Visual C++ 11.0 ++ return ['msvcr110'] ++ elif msc_ver == '1800': ++ # Visual Studio 2013 / Visual C++ 12.0 ++ return ['msvcr120'] ++ elif msc_ver == '1900': ++ # Visual Studio 2015 / Visual C++ 14.0 ++ # "msvcr140.dll no longer exists" http://blogs.msdn.com/b/vcblog/archive/2014/06/03/visual-studio-14-ctp.aspx ++ return ['vcruntime140'] ++ elif msc_ver == '1910': ++ return ['vcruntime140'] ++ elif msc_ver == '1914': ++ return ['vcruntime140'] ++ elif msc_ver == '1915': ++ return ['vcruntime140'] ++ elif msc_ver == '1916': ++ return ['vcruntime140'] + else: + raise ValueError("Unknown MS Compiler version %s " % msc_ver) diff --git a/cython/python_solvespace/__init__.py b/cython/python_solvespace/__init__.py new file mode 100644 index 00000000..77adc7c3 --- /dev/null +++ b/cython/python_solvespace/__init__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +"""'python_solvespace' module is a wrapper of +Python binding Solvespace solver libraries. +""" + +__author__ = "Yuan Chang" +__copyright__ = "Copyright (C) 2016-2019" +__license__ = "GPLv3+" +__email__ = "pyslvs@gmail.com" + +from .slvs import ( + quaternion_u, + quaternion_v, + quaternion_n, + make_quaternion, + Constraint, + ResultFlag, + Params, + Entity, + SolverSystem, +) + +__all__ = [ + 'quaternion_u', + 'quaternion_v', + 'quaternion_n', + 'make_quaternion', + 'Constraint', + 'ResultFlag', + 'Params', + 'Entity', + 'SolverSystem', +] diff --git a/cython/python_solvespace/slvs.pxd b/cython/python_solvespace/slvs.pxd new file mode 100644 index 00000000..7208fcd8 --- /dev/null +++ b/cython/python_solvespace/slvs.pxd @@ -0,0 +1,325 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 + +"""Wrapper header of Solvespace. + +author: Yuan Chang +copyright: Copyright (C) 2016-2019 +license: GPLv3+ +email: pyslvs@gmail.com +""" + +from libc.stdint cimport uint32_t +from libcpp.vector cimport vector +from libcpp.map cimport map as cmap + +cdef extern from "slvs.h" nogil: + + ctypedef uint32_t Slvs_hParam + ctypedef uint32_t Slvs_hEntity + ctypedef uint32_t Slvs_hConstraint + ctypedef uint32_t Slvs_hGroup + + # Virtual work plane entity + Slvs_hEntity SLVS_FREE_IN_3D + + ctypedef struct Slvs_Param: + Slvs_hParam h + Slvs_hGroup group + double val + + # Entity type + int SLVS_E_POINT_IN_3D + int SLVS_E_POINT_IN_2D + + int SLVS_E_NORMAL_IN_2D + int SLVS_E_NORMAL_IN_3D + + int SLVS_E_DISTANCE + + int SLVS_E_WORKPLANE + int SLVS_E_LINE_SEGMENT + int SLVS_E_CUBIC + int SLVS_E_CIRCLE + int SLVS_E_ARC_OF_CIRCLE + + ctypedef struct Slvs_Entity: + Slvs_hEntity h + Slvs_hGroup group + int type + Slvs_hEntity wrkpl + Slvs_hEntity point[4] + Slvs_hEntity normal + Slvs_hEntity distance + Slvs_hParam param[4] + + ctypedef struct Slvs_Constraint: + Slvs_hConstraint h + Slvs_hGroup group + int type + Slvs_hEntity wrkpl + double valA + Slvs_hEntity ptA + Slvs_hEntity ptB + Slvs_hEntity entityA + Slvs_hEntity entityB + Slvs_hEntity entityC + Slvs_hEntity entityD + int other + int other2 + + ctypedef struct Slvs_System: + Slvs_Param *param + int params + Slvs_Entity *entity + int entities + Slvs_Constraint *constraint + int constraints + Slvs_hParam dragged[4] + int calculateFaileds + Slvs_hConstraint *failed + int faileds + int dof + int result + + void Slvs_Solve(Slvs_System *sys, Slvs_hGroup hg) + void Slvs_QuaternionU( + double qw, double qx, double qy, double qz, + double *x, double *y, double *z + ) + void Slvs_QuaternionV( + double qw, double qx, double qy, double qz, + double *x, double *y, double *z + ) + void Slvs_QuaternionN( + double qw, double qx, double qy, double qz, + double *x, double *y, double *z + ) + void Slvs_MakeQuaternion( + double ux, double uy, double uz, + double vx, double vy, double vz, + double *qw, double *qx, double *qy, double *qz + ) + Slvs_Param Slvs_MakeParam(Slvs_hParam h, Slvs_hGroup group, double val) + Slvs_Entity Slvs_MakePoint2d( + Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hParam u, Slvs_hParam v + ) + Slvs_Entity Slvs_MakePoint3d( + Slvs_hEntity h, Slvs_hGroup group, + Slvs_hParam x, Slvs_hParam y, Slvs_hParam z + ) + Slvs_Entity Slvs_MakeNormal3d( + Slvs_hEntity h, Slvs_hGroup group, + Slvs_hParam qw, Slvs_hParam qx, + Slvs_hParam qy, Slvs_hParam qz + ) + Slvs_Entity Slvs_MakeNormal2d( + Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl + ) + Slvs_Entity Slvs_MakeDistance( + Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, Slvs_hParam d + ) + Slvs_Entity Slvs_MakeLineSegment( + Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hEntity ptA, Slvs_hEntity ptB + ) + Slvs_Entity Slvs_MakeCubic( + Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hEntity pt0, Slvs_hEntity pt1, + Slvs_hEntity pt2, Slvs_hEntity pt3 + ) + Slvs_Entity Slvs_MakeArcOfCircle( + Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hEntity normal, + Slvs_hEntity center, + Slvs_hEntity start, Slvs_hEntity end + ) + Slvs_Entity Slvs_MakeCircle( + Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hEntity center, + Slvs_hEntity normal, Slvs_hEntity radius + ) + Slvs_Entity Slvs_MakeWorkplane( + Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity origin, Slvs_hEntity normal + ) + Slvs_Constraint Slvs_MakeConstraint( + Slvs_hConstraint h, + Slvs_hGroup group, + int type, + Slvs_hEntity wrkpl, + double valA, + Slvs_hEntity ptA, + Slvs_hEntity ptB, + Slvs_hEntity entityA, + Slvs_hEntity entityB + ) + + +cpdef enum Constraint: + # Expose macro of constraint types + POINTS_COINCIDENT = 100000 + PT_PT_DISTANCE + PT_PLANE_DISTANCE + PT_LINE_DISTANCE + PT_FACE_DISTANCE + PT_IN_PLANE + PT_ON_LINE + PT_ON_FACE + EQUAL_LENGTH_LINES + LENGTH_RATIO + EQ_LEN_PT_LINE_D + EQ_PT_LN_DISTANCES + EQUAL_ANGLE + EQUAL_LINE_ARC_LEN + SYMMETRIC + SYMMETRIC_HORIZ + SYMMETRIC_VERT + SYMMETRIC_LINE + AT_MIDPOINT + HORIZONTAL + VERTICAL + DIAMETER + PT_ON_CIRCLE + SAME_ORIENTATION + ANGLE + PARALLEL + PERPENDICULAR + ARC_LINE_TANGENT + CUBIC_LINE_TANGENT + EQUAL_RADIUS + PROJ_PT_DISTANCE + WHERE_DRAGGED + CURVE_CURVE_TANGENT + LENGTH_DIFFERENCE + + +cpdef enum ResultFlag: + # Expose macro of result flags + OKAY + INCONSISTENT + DIDNT_CONVERGE + TOO_MANY_UNKNOWNS + + +cpdef tuple quaternion_u(double qw, double qx, double qy, double qz) +cpdef tuple quaternion_v(double qw, double qx, double qy, double qz) +cpdef tuple quaternion_n(double qw, double qx, double qy, double qz) +cpdef tuple make_quaternion(double ux, double uy, double uz, double vx, double vy, double vz) + + +cdef class Params: + + cdef vector[Slvs_hParam] param_list + + @staticmethod + cdef Params create(Slvs_hParam *p, size_t count) + + +cdef class Entity: + + cdef int t + cdef Slvs_hEntity h, wp + cdef Slvs_hGroup g + cdef readonly Params params + + @staticmethod + cdef Entity create(Slvs_Entity *e, size_t p_size) + + cpdef bint is_3d(self) + cpdef bint is_none(self) + cpdef bint is_point_2d(self) + cpdef bint is_point_3d(self) + cpdef bint is_point(self) + cpdef bint is_normal_2d(self) + cpdef bint is_normal_3d(self) + cpdef bint is_normal(self) + cpdef bint is_distance(self) + cpdef bint is_work_plane(self) + cpdef bint is_line_2d(self) + cpdef bint is_line_3d(self) + cpdef bint is_line(self) + cpdef bint is_cubic(self) + cpdef bint is_circle(self) + cpdef bint is_arc(self) + + +cdef class SolverSystem: + + cdef Slvs_hGroup g + cdef Slvs_System sys + cdef cmap[Slvs_hParam, Slvs_Param] param_list + cdef vector[Slvs_Entity] entity_list + cdef vector[Slvs_Constraint] cons_list + cdef vector[Slvs_hConstraint] failed_list + + cdef void copy_to_sys(self) nogil + cdef void copy_from_sys(self) nogil + cpdef void clear(self) + cdef void failed_collecting(self) nogil + cdef void free(self) + cpdef void set_group(self, size_t g) + cpdef int group(self) + cpdef tuple params(self, Params p) + cpdef int dof(self) + cpdef object constraints(self) + cpdef list faileds(self) + cpdef int solve(self) + cpdef Entity create_2d_base(self) + cdef Slvs_hParam new_param(self, double val) nogil + cdef Slvs_hEntity eh(self) nogil + + cpdef Entity add_point_2d(self, double u, double v, Entity wp) + cpdef Entity add_point_3d(self, double x, double y, double z) + cpdef Entity add_normal_2d(self, Entity wp) + cpdef Entity add_normal_3d(self, double qw, double qx, double qy, double qz) + cpdef Entity add_distance(self, double d, Entity wp) + cpdef Entity add_line_2d(self, Entity p1, Entity p2, Entity wp) + cpdef Entity add_line_3d(self, Entity p1, Entity p2) + cpdef Entity add_cubic(self, Entity p1, Entity p2, Entity p3, Entity p4, Entity wp) + cpdef Entity add_arc(self, Entity nm, Entity ct, Entity start, Entity end, Entity wp) + cpdef Entity add_circle(self, Entity nm, Entity ct, Entity radius, Entity wp) + cpdef Entity add_work_plane(self, Entity origin, Entity nm) + cpdef void add_constraint( + self, + Constraint c_type, + Entity wp, + double v, + Entity p1, + Entity p2, + Entity e1, + Entity e2, + Entity e3 = *, + Entity e4 = *, + int other = *, + int other2 = * + ) + + cpdef void coincident(self, Entity e1, Entity e2, Entity wp = *) + cpdef void distance(self, Entity e1, Entity e2, double value, Entity wp = *) + cpdef void equal(self, Entity e1, Entity e2, Entity wp = *) + cpdef void equal_included_angle(self, Entity e1, Entity e2, Entity e3, Entity e4, Entity wp) + cpdef void equal_point_to_line(self, Entity e1, Entity e2, Entity e3, Entity e4, Entity wp) + cpdef void ratio(self, Entity e1, Entity e2, double value, Entity wp) + cpdef void symmetric(self, Entity e1, Entity e2, Entity e3 = *, Entity wp = *) + cpdef void symmetric_h(self, Entity e1, Entity e2, Entity wp) + cpdef void symmetric_v(self, Entity e1, Entity e2, Entity wp) + cpdef void midpoint(self, Entity e1, Entity e2, Entity wp = *) + cpdef void horizontal(self, Entity e1, Entity wp) + cpdef void vertical(self, Entity e1, Entity wp) + cpdef void diameter(self, Entity e1, double value, Entity wp) + cpdef void same_orientation(self, Entity e1, Entity e2) + cpdef void angle(self, Entity e1, Entity e2, double value, Entity wp, bint inverse = *) + cpdef void perpendicular(self, Entity e1, Entity e2, Entity wp, bint inverse = *) + cpdef void parallel(self, Entity e1, Entity e2, Entity wp = *) + cpdef void tangent(self, Entity e1, Entity e2, Entity wp = *) + cpdef void distance_proj(self, Entity e1, Entity e2, double value) + cpdef void dragged(self, Entity e1, Entity wp = *) diff --git a/cython/python_solvespace/slvs.pyi b/cython/python_solvespace/slvs.pyi new file mode 100644 index 00000000..2a5b80d7 --- /dev/null +++ b/cython/python_solvespace/slvs.pyi @@ -0,0 +1,347 @@ +# -*- coding: utf-8 -*- + +from typing import Tuple, List, Counter +from enum import IntEnum, auto + + +def quaternion_u( + qw: float, + qx: float, + qy: float, + qz: float +) -> Tuple[float, float, float]: + ... + +def quaternion_v( + qw: float, + qx: float, + qy: float, + qz: float +) -> Tuple[float, float, float]: + ... + +def quaternion_n( + qw: float, + qx: float, + qy: float, + qz: float +) -> Tuple[float, float, float]: + ... + +def make_quaternion( + ux: float, + uy: float, + uz: float, + vx: float, + vy: float, + vz: float +) -> Tuple[float, float, float, float]: + ... + + +class Constraint(IntEnum): + # Expose macro of constraint types + POINTS_COINCIDENT = 100000 + PT_PT_DISTANCE = auto() + PT_PLANE_DISTANCE = auto() + PT_LINE_DISTANCE = auto() + PT_FACE_DISTANCE = auto() + PT_IN_PLANE = auto() + PT_ON_LINE = auto() + PT_ON_FACE = auto() + EQUAL_LENGTH_LINES = auto() + LENGTH_RATIO = auto() + EQ_LEN_PT_LINE_D = auto() + EQ_PT_LN_DISTANCES = auto() + EQUAL_ANGLE = auto() + EQUAL_LINE_ARC_LEN = auto() + SYMMETRIC = auto() + SYMMETRIC_HORIZ = auto() + SYMMETRIC_VERT = auto() + SYMMETRIC_LINE = auto() + AT_MIDPOINT = auto() + HORIZONTAL = auto() + VERTICAL = auto() + DIAMETER = auto() + PT_ON_CIRCLE = auto() + SAME_ORIENTATION = auto() + ANGLE = auto() + PARALLEL = auto() + PERPENDICULAR = auto() + ARC_LINE_TANGENT = auto() + CUBIC_LINE_TANGENT = auto() + EQUAL_RADIUS = auto() + PROJ_PT_DISTANCE = auto() + WHERE_DRAGGED = auto() + CURVE_CURVE_TANGENT = auto() + LENGTH_DIFFERENCE = auto() + + +class ResultFlag(IntEnum): + # Expose macro of result flags + OKAY = 0 + INCONSISTENT = auto() + DIDNT_CONVERGE = auto() + TOO_MANY_UNKNOWNS = auto() + + +class Params: + + def __repr__(self) -> str: + ... + + +class Entity: + + FREE_IN_3D: Entity + NONE: Entity + + params: Params + + def is_3d(self) -> bool: + ... + + def is_none(self) -> bool: + ... + + def is_point_2d(self) -> bool: + ... + + def is_point_3d(self) -> bool: + ... + + def is_point(self) -> bool: + ... + + def is_normal_2d(self) -> bool: + ... + + def is_normal_3d(self) -> bool: + ... + + def is_normal(self) -> bool: + ... + + def is_distance(self) -> bool: + ... + + def is_work_plane(self) -> bool: + ... + + def is_line_2d(self) -> bool: + ... + + def is_line_3d(self) -> bool: + ... + + def is_line(self) -> bool: + ... + + def is_cubic(self) -> bool: + ... + + def is_circle(self) -> bool: + ... + + def is_arc(self) -> bool: + ... + + def __repr__(self) -> str: + ... + + +class SolverSystem: + + def __init__(self): + ... + + def clear(self) -> None: + ... + + def set_group(self, g: int) -> None: + ... + + def group(self) -> int: + ... + + def params(self, p: Params) -> Tuple[float, ...]: + ... + + def dof(self) -> int: + ... + + def constraints(self) -> Counter[str]: + ... + + def faileds(self) -> List[int]: + ... + + def solve(self) -> ResultFlag: + ... + + def create_2d_base(self) -> Entity: + ... + + def add_point_2d(self, u: float, v: float, wp: Entity) -> Entity: + ... + + def add_point_3d(self, x: float, y: float, z: float) -> Entity: + ... + + def add_normal_2d(self, wp: Entity) -> Entity: + ... + + def add_normal_3d(self, qw: float, qx: float, qy: float, qz: float) -> Entity: + ... + + def add_distance(self, d: float, wp: Entity) -> Entity: + ... + + def add_line_2d(self, p1: Entity, p2: Entity, wp: Entity) -> Entity: + ... + + def add_line_3d(self, p1: Entity, p2: Entity) -> Entity: + ... + + def add_cubic(self, p1: Entity, p2: Entity, p3: Entity, p4: Entity, wp: Entity) -> Entity: + ... + + def add_arc(self, nm: Entity, ct: Entity, start: Entity, end: Entity, wp: Entity) -> Entity: + ... + + def add_circle(self, nm: Entity, ct: Entity, radius: Entity, wp: Entity) -> Entity: + ... + + def add_work_plane(self, origin: Entity, nm: Entity) -> Entity: + ... + + def add_constraint( + self, + c_type: Constraint, + wp: Entity, + v: float, + p1: Entity, + p2: Entity, + e1: Entity, + e2: Entity, + e3: Entity = Entity.NONE, + e4: Entity = Entity.NONE, + other: int = 0, + other2: int = 0 + ) -> None: + ... + + def coincident(self, e1: Entity, e2: Entity, wp: Entity = Entity.FREE_IN_3D) -> None: + """Coincident two entities.""" + ... + + def distance( + self, + e1: Entity, + e2: Entity, + value: float, + wp: Entity = Entity.FREE_IN_3D + ) -> None: + """Distance constraint between two entities.""" + ... + + def equal(self, e1: Entity, e2: Entity, wp: Entity = Entity.FREE_IN_3D) -> None: + """Equal constraint between two entities.""" + ... + + def equal_included_angle( + self, + e1: Entity, + e2: Entity, + e3: Entity, + e4: Entity, + wp: Entity + ) -> None: + """Constraint that point 1 and line 1, point 2 and line 2 + must have same distance. + """ + ... + + def equal_point_to_line( + self, + e1: Entity, + e2: Entity, + e3: Entity, + e4: Entity, + wp: Entity + ) -> None: + """Constraint that line 1 and line 2, line 3 and line 4 + must have same included angle. + """ + ... + + def ratio(self, e1: Entity, e2: Entity, value: float, wp: Entity) -> None: + """The ratio constraint between two lines.""" + ... + + def symmetric( + self, + e1: Entity, + e2: Entity, + e3: Entity = Entity.NONE, + wp: Entity = Entity.FREE_IN_3D + ) -> None: + """Symmetric constraint between two points.""" + ... + + def symmetric_h(self, e1: Entity, e2: Entity, wp: Entity) -> None: + """Symmetric constraint between two points with horizontal line.""" + ... + + def symmetric_v(self, e1: Entity, e2: Entity, wp: Entity) -> None: + """Symmetric constraint between two points with vertical line.""" + ... + + def midpoint( + self, + e1: Entity, + e2: Entity, + wp: Entity = Entity.FREE_IN_3D + ) -> None: + """Midpoint constraint between a point and a line.""" + ... + + def horizontal(self, e1: Entity, wp: Entity) -> None: + """Horizontal constraint of a 2d point.""" + ... + + def vertical(self, e1: Entity, wp: Entity) -> None: + """Vertical constraint of a 2d point.""" + ... + + def diameter(self, e1: Entity, value: float, wp: Entity) -> None: + """Diameter constraint of a circular entities.""" + ... + + def same_orientation(self, e1: Entity, e2: Entity) -> None: + """Equal orientation constraint between two 3d normals.""" + ... + + def angle(self, e1: Entity, e2: Entity, value: float, wp: Entity, inverse: bool = False) -> None: + """Degrees angle constraint between two 2d lines.""" + ... + + def perpendicular(self, e1: Entity, e2: Entity, wp: Entity, inverse: bool = False) -> None: + """Perpendicular constraint between two 2d lines.""" + ... + + def parallel(self, e1: Entity, e2: Entity, wp: Entity = Entity.FREE_IN_3D) -> None: + """Parallel constraint between two lines.""" + ... + + def tangent(self, e1: Entity, e2: Entity, wp: Entity = Entity.FREE_IN_3D) -> None: + """Parallel constraint between two entities.""" + ... + + def distance_proj(self, e1: Entity, e2: Entity, value: float) -> None: + """Projected distance constraint between two 3d points.""" + ... + + def dragged(self, e1: Entity, wp: Entity = Entity.FREE_IN_3D) -> None: + """Dragged constraint of a point.""" + ... diff --git a/cython/python_solvespace/slvs.pyx b/cython/python_solvespace/slvs.pyx new file mode 100644 index 00000000..586b15cb --- /dev/null +++ b/cython/python_solvespace/slvs.pyx @@ -0,0 +1,796 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3, embedsignature=True, cdivision=True + +"""Wrapper source code of Solvespace. + +author: Yuan Chang +copyright: Copyright (C) 2016-2019 +license: GPLv3+ +email: pyslvs@gmail.com +""" + +cimport cython +from cpython.mem cimport PyMem_Malloc, PyMem_Free +from cpython.object cimport Py_EQ, Py_NE +from libcpp.pair cimport pair +from collections import Counter + + +cpdef tuple quaternion_u(double qw, double qx, double qy, double qz): + cdef double x, y, z + Slvs_QuaternionV(qw, qx, qy, qz, &x, &y, &z) + return x, y, z + + +cpdef tuple quaternion_v(double qw, double qx, double qy, double qz): + cdef double x, y, z + Slvs_QuaternionV(qw, qx, qy, qz, &x, &y, &z) + return x, y, z + + +cpdef tuple quaternion_n(double qw, double qx, double qy, double qz): + cdef double x, y, z + Slvs_QuaternionN(qw, qx, qy, qz, &x, &y, &z) + return x, y, z + + +cpdef tuple make_quaternion(double ux, double uy, double uz, double vx, double vy, double vz): + cdef double qw, qx, qy, qz + Slvs_MakeQuaternion(ux, uy, uz, vx, vy, vz, &qw, &qx, &qy, &qz) + return qw, qx, qy, qz + + +cdef class Params: + + """Python object to handle multiple parameter handles.""" + + @staticmethod + cdef Params create(Slvs_hParam *p, size_t count): + """Constructor.""" + cdef Params params = Params.__new__(Params) + cdef size_t i + for i in range(count): + params.param_list.push_back(p[i]) + return params + + def __repr__(self) -> str: + cdef str m = f"{self.__class__.__name__}([" + cdef size_t i + cdef size_t s = self.param_list.size() + for i in range(s): + m += str(self.param_list[i]) + if i != s - 1: + m += ", " + m += "])" + return m + +# A virtual work plane that present 3D entity or constraint. +cdef Entity _E_FREE_IN_3D = Entity.__new__(Entity) +_E_FREE_IN_3D.t = SLVS_E_WORKPLANE +_E_FREE_IN_3D.h = SLVS_FREE_IN_3D +_E_FREE_IN_3D.g = 0 +_E_FREE_IN_3D.params = Params.create(NULL, 0) + +# A "None" entity used to fill in constraint option. +cdef Entity _E_NONE = Entity.__new__(Entity) +_E_NONE.t = 0 +_E_NONE.h = 0 +_E_NONE.g = 0 +_E_NONE.params = Params.create(NULL, 0) + +# Entity names +cdef dict _NAME_OF_ENTITIES = { + SLVS_E_POINT_IN_3D: "point 3d", + SLVS_E_POINT_IN_2D: "point 2d", + SLVS_E_NORMAL_IN_2D: "normal 2d", + SLVS_E_NORMAL_IN_3D: "normal 3d", + SLVS_E_DISTANCE: "distance", + SLVS_E_WORKPLANE: "work plane", + SLVS_E_LINE_SEGMENT: "line segment", + SLVS_E_CUBIC: "cubic", + SLVS_E_CIRCLE: "circle", + SLVS_E_ARC_OF_CIRCLE: "arc", +} + +# Constraint names +cdef dict _NAME_OF_CONSTRAINTS = { + POINTS_COINCIDENT: "points coincident", + PT_PT_DISTANCE: "point point distance", + PT_PLANE_DISTANCE: "point plane distance", + PT_LINE_DISTANCE: "point line distance", + PT_FACE_DISTANCE: "point face distance", + PT_IN_PLANE: "point in plane", + PT_ON_LINE: "point on line", + PT_ON_FACE: "point on face", + EQUAL_LENGTH_LINES: "equal length lines", + LENGTH_RATIO: "length ratio", + EQ_LEN_PT_LINE_D: "equal length point line distance", + EQ_PT_LN_DISTANCES: "equal point line distance", + EQUAL_ANGLE: "equal angle", + EQUAL_LINE_ARC_LEN: "equal line arc length", + SYMMETRIC: "symmetric", + SYMMETRIC_HORIZ: "symmetric horizontal", + SYMMETRIC_VERT: "symmetric vertical", + SYMMETRIC_LINE: "symmetric line", + AT_MIDPOINT: "at midpoint", + HORIZONTAL: "horizontal", + VERTICAL: "vertical", + DIAMETER: "diameter", + PT_ON_CIRCLE: "point on circle", + SAME_ORIENTATION: "same orientation", + ANGLE: "angle", + PARALLEL: "parallel", + PERPENDICULAR: "perpendicular", + ARC_LINE_TANGENT: "arc line tangent", + CUBIC_LINE_TANGENT: "cubic line tangent", + EQUAL_RADIUS: "equal radius", + PROJ_PT_DISTANCE: "project point distance", + WHERE_DRAGGED: "where dragged", + CURVE_CURVE_TANGENT: "curve curve tangent", + LENGTH_DIFFERENCE: "length difference", +} + + +cdef class Entity: + + """Python object to handle a pointer of 'Slvs_hEntity'.""" + + FREE_IN_3D = _E_FREE_IN_3D + NONE = _E_NONE + + @staticmethod + cdef Entity create(Slvs_Entity *e, size_t p_size): + """Constructor.""" + cdef Entity entity = Entity.__new__(Entity) + with nogil: + entity.t = e.type + entity.h = e.h + entity.wp = e.wrkpl + entity.g = e.group + entity.params = Params.create(e.param, p_size) + return entity + + def __richcmp__(self, other: Entity, op: cython.int) -> bint: + """Compare the entities.""" + if op == Py_EQ: + return ( + self.t == other.t and + self.h == other.h and + self.wp == other.wp and + self.g == other.g and + self.params == other.params + ) + elif op == Py_NE: + return ( + self.t != other.t or + self.h != other.h or + self.wp != other.wp or + self.g != other.g or + self.params != other.params + ) + else: + raise TypeError( + f"'{op}' not support between instances of " + f"{type(self)} and {type(other)}" + ) + + cpdef bint is_3d(self): + return self.wp == SLVS_FREE_IN_3D + + cpdef bint is_none(self): + return self.h == 0 + + cpdef bint is_point_2d(self): + return self.t == SLVS_E_POINT_IN_2D + + cpdef bint is_point_3d(self): + return self.t == SLVS_E_POINT_IN_3D + + cpdef bint is_point(self): + return self.is_point_2d() or self.is_point_3d() + + cpdef bint is_normal_2d(self): + return self.t == SLVS_E_NORMAL_IN_2D + + cpdef bint is_normal_3d(self): + return self.t == SLVS_E_NORMAL_IN_3D + + cpdef bint is_normal(self): + return self.is_normal_2d() or self.is_normal_3d() + + cpdef bint is_distance(self): + return self.t == SLVS_E_DISTANCE + + cpdef bint is_work_plane(self): + return self.t == SLVS_E_WORKPLANE + + cpdef bint is_line_2d(self): + return self.is_line() and not self.is_3d() + + cpdef bint is_line_3d(self): + return self.is_line() and self.is_3d() + + cpdef bint is_line(self): + return self.t == SLVS_E_LINE_SEGMENT + + cpdef bint is_cubic(self): + return self.t == SLVS_E_CUBIC + + cpdef bint is_circle(self): + return self.t == SLVS_E_CIRCLE + + cpdef bint is_arc(self): + return self.t == SLVS_E_ARC_OF_CIRCLE + + def __repr__(self) -> str: + cdef int h = self.h + cdef int g = self.g + cdef str t = _NAME_OF_ENTITIES[self.t] + return ( + f"{self.__class__.__name__}" + f"(handle={h}, group={g}, type=<{t}>, is_3d={self.is_3d()}, params={self.params})" + ) + + +cdef class SolverSystem: + + """Python object of 'Slvs_System'.""" + + def __cinit__(self): + self.g = 0 + self.sys.params = self.sys.entities = self.sys.constraints = 0 + + def __dealloc__(self): + self.free() + + cdef inline void copy_to_sys(self) nogil: + """Copy data from stack into system.""" + cdef int i = 0 + cdef pair[Slvs_hParam, Slvs_Param] param + for param in self.param_list: + self.sys.param[i] = param.second + i += 1 + + i = 0 + cdef Slvs_Entity entity + for entity in self.entity_list: + self.sys.entity[i] = entity + i += 1 + + i = 0 + cdef Slvs_Constraint con + for con in self.cons_list: + self.sys.constraint[i] = con + i += 1 + + cdef inline void copy_from_sys(self) nogil: + """Copy data from system into stack.""" + self.param_list.clear() + self.entity_list.clear() + self.cons_list.clear() + cdef int i + for i in range(self.sys.params): + self.param_list[self.sys.param[i].h] = self.sys.param[i] + for i in range(self.sys.entities): + self.entity_list.push_back(self.sys.entity[i]) + for i in range(self.sys.constraints): + self.cons_list.push_back(self.sys.constraint[i]) + + cpdef void clear(self): + self.g = 0 + self.param_list.clear() + self.entity_list.clear() + self.cons_list.clear() + self.failed_list.clear() + self.free() + + cdef inline void failed_collecting(self) nogil: + """Collecting the failed constraints.""" + cdef int i + for i in range(self.sys.faileds): + self.failed_list.push_back(self.sys.failed[i]) + + cdef inline void free(self): + PyMem_Free(self.sys.param) + PyMem_Free(self.sys.entity) + PyMem_Free(self.sys.constraint) + PyMem_Free(self.sys.failed) + self.sys.param = NULL + self.sys.entity = NULL + self.sys.constraint = NULL + self.sys.failed = NULL + self.sys.params = self.sys.entities = self.sys.constraints = 0 + + cpdef void set_group(self, size_t g): + """Set the current group by integer.""" + self.g = g + + cpdef int group(self): + """Return the current group by integer.""" + return self.g + + cpdef tuple params(self, Params p): + """Get the parameters by Params object.""" + cdef list param_list = [] + cdef Slvs_hParam h + for h in p.param_list: + param_list.append(self.param_list[h].val) + return tuple(param_list) + + cpdef int dof(self): + """Return the DOF of system.""" + return self.sys.dof + + cpdef object constraints(self): + """Return the list of all constraints.""" + cons_list = [] + cdef Slvs_Constraint con + for con in self.cons_list: + cons_list.append(_NAME_OF_CONSTRAINTS[con.type]) + return Counter(cons_list) + + cpdef list faileds(self): + """Return the count of failed constraint.""" + failed_list = [] + cdef Slvs_hConstraint error + for error in self.failed_list: + failed_list.append(error) + return failed_list + + cpdef int solve(self): + """Solve the system.""" + # Parameters + self.sys.param = PyMem_Malloc(self.param_list.size() * sizeof(Slvs_Param)) + # Entities + self.sys.entity = PyMem_Malloc(self.entity_list.size() * sizeof(Slvs_Entity)) + # Constraints + cdef size_t cons_size = self.cons_list.size() + self.sys.constraint = PyMem_Malloc(cons_size * sizeof(Slvs_Constraint)) + self.sys.failed = PyMem_Malloc(cons_size * sizeof(Slvs_hConstraint)) + self.sys.faileds = cons_size + + # Copy to system + self.copy_to_sys() + # Solve + Slvs_Solve(&self.sys, self.g) + # Failed constraints and free memory. + self.copy_from_sys() + self.failed_collecting() + self.free() + return self.sys.result + + cpdef Entity create_2d_base(self): + """Create a basic 2D system and return the work plane.""" + cdef double qw, qx, qy, qz + qw, qx, qy, qz = make_quaternion(1, 0, 0, 0, 1, 0) + cdef Entity nm = self.add_normal_3d(qw, qx, qy, qz) + return self.add_work_plane(self.add_point_3d(0, 0, 0), nm) + + cdef inline Slvs_hParam new_param(self, double val) nogil: + """Add a parameter.""" + self.sys.params += 1 + cdef Slvs_hParam h = self.sys.params + self.param_list[h] = Slvs_MakeParam(h, self.g, val) + return h + + cdef inline Slvs_hEntity eh(self) nogil: + """Return new entity handle.""" + self.sys.entities += 1 + return self.sys.entities + + cpdef Entity add_point_2d(self, double u, double v, Entity wp): + """Add 2D point.""" + if wp is None or not wp.is_work_plane(): + raise TypeError(f"{wp} is not a work plane") + + cdef Slvs_hParam u_p = self.new_param(u) + cdef Slvs_hParam v_p = self.new_param(v) + cdef Slvs_Entity e = Slvs_MakePoint2d(self.eh(), self.g, wp.h, u_p, v_p) + self.entity_list.push_back(e) + + return Entity.create(&e, 2) + + cpdef Entity add_point_3d(self, double x, double y, double z): + """Add 3D point.""" + cdef Slvs_hParam x_p = self.new_param(x) + cdef Slvs_hParam y_p = self.new_param(y) + cdef Slvs_hParam z_p = self.new_param(z) + cdef Slvs_Entity e = Slvs_MakePoint3d(self.eh(), self.g, x_p, y_p, z_p) + self.entity_list.push_back(e) + + return Entity.create(&e, 3) + + cpdef Entity add_normal_2d(self, Entity wp): + """Add a 2D normal.""" + if wp is None or not wp.is_work_plane(): + raise TypeError(f"{wp} is not a work plane") + + cdef Slvs_Entity e = Slvs_MakeNormal2d(self.eh(), self.g, wp.h) + self.entity_list.push_back(e) + + return Entity.create(&e, 0) + + cpdef Entity add_normal_3d(self, double qw, double qx, double qy, double qz): + """Add a 3D normal.""" + cdef Slvs_hParam w_p = self.new_param(qw) + cdef Slvs_hParam x_p = self.new_param(qx) + cdef Slvs_hParam y_p = self.new_param(qy) + cdef Slvs_hParam z_p = self.new_param(qz) + self.entity_list.push_back(Slvs_MakeNormal3d(self.eh(), self.g, w_p, x_p, y_p, z_p)) + return Entity.create(&self.entity_list.back(), 4) + + cpdef Entity add_distance(self, double d, Entity wp): + """Add a 2D distance.""" + if wp is None or not wp.is_work_plane(): + raise TypeError(f"{wp} is not a work plane") + + cdef Slvs_hParam d_p = self.new_param(d) + self.entity_list.push_back(Slvs_MakeDistance(self.eh(), self.g, wp.h, d_p)) + return Entity.create(&self.entity_list.back(), 1) + + cpdef Entity add_line_2d(self, Entity p1, Entity p2, Entity wp): + """Add a 2D line.""" + if wp is None or not wp.is_work_plane(): + raise TypeError(f"{wp} is not a work plane") + if p1 is None or not p1.is_point_2d(): + raise TypeError(f"{p1} is not a 2d point") + if p2 is None or not p2.is_point_2d(): + raise TypeError(f"{p2} is not a 2d point") + + self.entity_list.push_back(Slvs_MakeLineSegment(self.eh(), self.g, wp.h, p1.h, p2.h)) + return Entity.create(&self.entity_list.back(), 0) + + cpdef Entity add_line_3d(self, Entity p1, Entity p2): + """Add a 3D line.""" + if p1 is None or not p1.is_point_3d(): + raise TypeError(f"{p1} is not a 3d point") + if p2 is None or not p2.is_point_3d(): + raise TypeError(f"{p2} is not a 3d point") + + self.entity_list.push_back(Slvs_MakeLineSegment(self.eh(), self.g, SLVS_FREE_IN_3D, p1.h, p2.h)) + return Entity.create(&self.entity_list.back(), 0) + + cpdef Entity add_cubic(self, Entity p1, Entity p2, Entity p3, Entity p4, Entity wp): + """Add a 2D cubic.""" + if wp is None or not wp.is_work_plane(): + raise TypeError(f"{wp} is not a work plane") + if p1 is None or not p1.is_point_2d(): + raise TypeError(f"{p1} is not a 2d point") + if p2 is None or not p2.is_point_2d(): + raise TypeError(f"{p2} is not a 2d point") + if p3 is None or not p3.is_point_2d(): + raise TypeError(f"{p3} is not a 2d point") + if p4 is None or not p4.is_point_2d(): + raise TypeError(f"{p4} is not a 2d point") + + self.entity_list.push_back(Slvs_MakeCubic(self.eh(), self.g, wp.h, p1.h, p2.h, p3.h, p4.h)) + return Entity.create(&self.entity_list.back(), 0) + + cpdef Entity add_arc(self, Entity nm, Entity ct, Entity start, Entity end, Entity wp): + """Add an 2D arc.""" + if wp is None or not wp.is_work_plane(): + raise TypeError(f"{wp} is not a work plane") + if nm is None or not nm.is_normal_3d(): + raise TypeError(f"{nm} is not a 3d normal") + if ct is None or not ct.is_point_2d(): + raise TypeError(f"{ct} is not a 2d point") + if start is None or not start.is_point_2d(): + raise TypeError(f"{start} is not a 2d point") + if end is None or not end.is_point_2d(): + raise TypeError(f"{end} is not a 2d point") + + self.entity_list.push_back(Slvs_MakeArcOfCircle(self.eh(), self.g, wp.h, nm.h, ct.h, start.h, end.h)) + return Entity.create(&self.entity_list.back(), 0) + + cpdef Entity add_circle(self, Entity nm, Entity ct, Entity radius, Entity wp): + """Add a 2D circle.""" + if wp is None or not wp.is_work_plane(): + raise TypeError(f"{wp} is not a work plane") + if nm is None or not nm.is_normal_3d(): + raise TypeError(f"{nm} is not a 3d normal") + if ct is None or not ct.is_point_2d(): + raise TypeError(f"{ct} is not a 2d point") + if radius is None or not radius.is_distance(): + raise TypeError(f"{radius} is not a distance") + + self.entity_list.push_back(Slvs_MakeCircle(self.eh(), self.g, wp.h, ct.h, nm.h, radius.h)) + return Entity.create(&self.entity_list.back(), 0) + + cpdef Entity add_work_plane(self, Entity origin, Entity nm): + """Add a 3D work plane.""" + if origin is None or origin.t != SLVS_E_POINT_IN_3D: + raise TypeError(f"{origin} is not a 3d point") + if nm is None or nm.t != SLVS_E_NORMAL_IN_3D: + raise TypeError(f"{nm} is not a 3d normal") + + self.entity_list.push_back(Slvs_MakeWorkplane(self.eh(), self.g, origin.h, nm.h)) + return Entity.create(&self.entity_list.back(), 0) + + cpdef void add_constraint( + self, + Constraint c_type, + Entity wp, + double v, + Entity p1, + Entity p2, + Entity e1, + Entity e2, + Entity e3 = _E_NONE, + Entity e4 = _E_NONE, + int other = 0, + int other2 = 0 + ): + """Add customized constraint.""" + if wp is None or not wp.is_work_plane(): + raise TypeError(f"{wp} is not a work plane") + + cdef Entity e + for e in (p1, p2): + if e is None or not (e.is_none() or e.is_point()): + raise TypeError(f"{e} is not a point") + for e in (e1, e2, e3, e4): + if e is None: + raise TypeError(f"{e} is not a entity") + + self.sys.constraints += 1 + cdef Slvs_Constraint c + c.h = self.sys.constraints + c.group = self.g + c.type = c_type + c.wrkpl = wp.h + c.valA = v + c.ptA = p1.h + c.ptB = p2.h + c.entityA = e1.h + c.entityB = e2.h + c.entityC = e3.h + c.entityD = e4.h + c.other = other + c.other2 = other2 + self.cons_list.push_back(c) + + ##### + # Constraint methods. + ##### + + cpdef void coincident(self, Entity e1, Entity e2, Entity wp = _E_FREE_IN_3D): + """Coincident two entities.""" + cdef Constraint t + if e1.is_point() and e2.is_point(): + self.add_constraint(POINTS_COINCIDENT, wp, 0., e1, e2, _E_NONE, _E_NONE) + elif e1.is_point() and e2.is_work_plane() and wp is _E_FREE_IN_3D: + self.add_constraint(PT_IN_PLANE, e2, 0., e1, _E_NONE, e2, _E_NONE) + elif e1.is_point() and (e2.is_line() or e2.is_circle()): + if e2.is_line(): + t = PT_ON_LINE + else: + t = PT_ON_CIRCLE + self.add_constraint(t, wp, 0., e1, _E_NONE, e2, _E_NONE) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {wp}") + + cpdef void distance( + self, + Entity e1, + Entity e2, + double value, + Entity wp = _E_FREE_IN_3D + ): + """Distance constraint between two entities.""" + if value == 0.: + self.coincident(e1, e2, wp) + return + + if e1.is_point() and e2.is_point(): + self.add_constraint(PT_PT_DISTANCE, wp, value, e1, e2, _E_NONE, _E_NONE) + elif e1.is_point() and e2.is_work_plane() and wp is _E_FREE_IN_3D: + self.add_constraint(PT_PLANE_DISTANCE, e2, value, e1, _E_NONE, e2, _E_NONE) + elif e1.is_point() and e2.is_line(): + self.add_constraint(PT_LINE_DISTANCE, wp, value, e1, _E_NONE, e2, _E_NONE) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {wp}") + + cpdef void equal(self, Entity e1, Entity e2, Entity wp = _E_FREE_IN_3D): + """Equal constraint between two entities.""" + if e1.is_line() and e2.is_line(): + self.add_constraint(EQUAL_LENGTH_LINES, wp, 0., _E_NONE, _E_NONE, e1, e2) + elif e1.is_line() and (e2.is_arc() or e2.is_circle()): + self.add_constraint(EQUAL_LINE_ARC_LEN, wp, 0., _E_NONE, _E_NONE, e1, e2) + elif (e1.is_arc() or e1.is_circle()) and (e2.is_arc() or e2.is_circle()): + self.add_constraint(EQUAL_RADIUS, wp, 0., _E_NONE, _E_NONE, e1, e2) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {wp}") + + cpdef void equal_included_angle( + self, + Entity e1, + Entity e2, + Entity e3, + Entity e4, + Entity wp + ): + """Constraint that line 1 and line 2, line 3 and line 4 + must have same included angle. + """ + if wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + + if e1.is_line_2d() and e2.is_line_2d() and e3.is_line_2d() and e4.is_line_2d(): + self.add_constraint(EQUAL_ANGLE, wp, 0., _E_NONE, _E_NONE, e1, e2, e3, e4) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {e3}, {e4}, {wp}") + + cpdef void equal_point_to_line( + self, + Entity e1, + Entity e2, + Entity e3, + Entity e4, + Entity wp + ): + """Constraint that point 1 and line 1, point 2 and line 2 + must have same distance. + """ + if wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + + if e1.is_point_2d() and e2.is_line_2d() and e3.is_point_2d() and e4.is_line_2d(): + self.add_constraint(EQ_PT_LN_DISTANCES, wp, 0., e1, e3, e2, e4) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {e3}, {e4}, {wp}") + + cpdef void ratio(self, Entity e1, Entity e2, double value, Entity wp): + """The ratio constraint between two lines.""" + if wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + + if e1.is_line_2d() and e2.is_line_2d(): + self.add_constraint(EQ_PT_LN_DISTANCES, wp, value, _E_NONE, _E_NONE, e1, e2) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {wp}") + + cpdef void symmetric( + self, + Entity e1, + Entity e2, + Entity e3 = _E_NONE, + Entity wp = _E_FREE_IN_3D + ): + """Symmetric constraint between two points.""" + if e1.is_point_3d() and e2.is_point_3d() and e3.is_work_plane() and wp is _E_FREE_IN_3D: + self.add_constraint(SYMMETRIC, wp, 0., e1, e2, e3, _E_NONE) + elif e1.is_point_2d() and e2.is_point_2d() and e3.is_work_plane() and wp is _E_FREE_IN_3D: + self.add_constraint(SYMMETRIC, e3, 0., e1, e2, e3, _E_NONE) + elif e1.is_point_2d() and e2.is_point_2d() and e3.is_line_2d(): + if wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + self.add_constraint(SYMMETRIC_LINE, wp, 0., e1, e2, e3, _E_NONE) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {e3}, {wp}") + + cpdef void symmetric_h(self, Entity e1, Entity e2, Entity wp): + """Symmetric constraint between two points with horizontal line.""" + if wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + + if e1.is_point_2d() and e2.is_point_2d(): + self.add_constraint(SYMMETRIC_HORIZ, wp, 0., e1, e2, _E_NONE, _E_NONE) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {wp}") + + cpdef void symmetric_v(self, Entity e1, Entity e2, Entity wp): + """Symmetric constraint between two points with vertical line.""" + if wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + + if e1.is_point_2d() and e2.is_point_2d(): + self.add_constraint(SYMMETRIC_VERT, wp, 0., e1, e2, _E_NONE, _E_NONE) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {wp}") + + cpdef void midpoint( + self, + Entity e1, + Entity e2, + Entity wp = _E_FREE_IN_3D + ): + """Midpoint constraint between a point and a line.""" + if e1.is_point() and e2.is_line(): + self.add_constraint(AT_MIDPOINT, wp, 0., e1, _E_NONE, e2, _E_NONE) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {wp}") + + cpdef void horizontal(self, Entity e1, Entity wp): + """Horizontal constraint of a 2d point.""" + if wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + + if e1.is_line_2d(): + self.add_constraint(HORIZONTAL, wp, 0., _E_NONE, _E_NONE, e1, _E_NONE) + else: + raise TypeError(f"unsupported entities: {e1}, {wp}") + + cpdef void vertical(self, Entity e1, Entity wp): + """Vertical constraint of a 2d point.""" + if wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + + if e1.is_line_2d(): + self.add_constraint(VERTICAL, wp, 0., _E_NONE, _E_NONE, e1, _E_NONE) + else: + raise TypeError(f"unsupported entities: {e1}, {wp}") + + cpdef void diameter(self, Entity e1, double value, Entity wp): + """Diameter constraint of a circular entities.""" + if wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + + if e1.is_arc() or e1.is_circle(): + self.add_constraint(DIAMETER, wp, value, _E_NONE, _E_NONE, e1, _E_NONE) + else: + raise TypeError(f"unsupported entities: {e1}, {wp}") + + cpdef void same_orientation(self, Entity e1, Entity e2): + """Equal orientation constraint between two 3d normals.""" + if e1.is_normal_3d() and e2.is_normal_3d(): + self.add_constraint(SAME_ORIENTATION, _E_FREE_IN_3D, 0., _E_NONE, _E_NONE, e1, e2) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}") + + cpdef void angle(self, Entity e1, Entity e2, double value, Entity wp, bint inverse = False): + """Degrees angle constraint between two 2d lines.""" + if wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + + if e1.is_line_2d() and e2.is_line_2d(): + self.add_constraint(ANGLE, wp, value, _E_NONE, _E_NONE, + e1, e2, _E_NONE, _E_NONE, inverse) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {wp}") + + cpdef void perpendicular(self, Entity e1, Entity e2, Entity wp, bint inverse = False): + """Perpendicular constraint between two 2d lines.""" + if wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + + if e1.is_line_2d() and e2.is_line_2d(): + self.add_constraint(PERPENDICULAR, wp, 0., _E_NONE, _E_NONE, + e1, e2, _E_NONE, _E_NONE, inverse) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {wp}") + + cpdef void parallel(self, Entity e1, Entity e2, Entity wp = _E_FREE_IN_3D): + """Parallel constraint between two lines.""" + if e1.is_line() and e2.is_line(): + self.add_constraint(PARALLEL, wp, 0., _E_NONE, _E_NONE, e1, e2) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {wp}") + + cpdef void tangent(self, Entity e1, Entity e2, Entity wp = _E_FREE_IN_3D): + """Parallel constraint between two entities.""" + if e1.is_arc() and e2.is_line_2d(): + if wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + self.add_constraint(ARC_LINE_TANGENT, wp, 0., _E_NONE, _E_NONE, e1, e2) + elif e1.is_cubic() and e2.is_line_3d() and wp is _E_FREE_IN_3D: + self.add_constraint(CUBIC_LINE_TANGENT, wp, 0., _E_NONE, _E_NONE, e1, e2) + elif (e1.is_arc() or e1.is_cubic()) and (e2.is_arc() or e2.is_cubic()): + if (e1.is_arc() or e2.is_arc()) and wp is _E_FREE_IN_3D: + raise ValueError("this is a 2d constraint") + self.add_constraint(CURVE_CURVE_TANGENT, wp, 0., _E_NONE, _E_NONE, e1, e2) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}, {wp}") + + cpdef void distance_proj(self, Entity e1, Entity e2, double value): + """Projected distance constraint between two 3d points.""" + if e1.is_point_3d() and e2.is_point_3d(): + self.add_constraint(CURVE_CURVE_TANGENT, _E_FREE_IN_3D, value, e1, e2, _E_NONE, _E_NONE) + else: + raise TypeError(f"unsupported entities: {e1}, {e2}") + + cpdef void dragged(self, Entity e1, Entity wp = _E_FREE_IN_3D): + """Dragged constraint of a point.""" + if e1.is_point(): + self.add_constraint(WHERE_DRAGGED, wp, 0., e1, _E_NONE, _E_NONE, _E_NONE) + else: + raise TypeError(f"unsupported entities: {e1}, {wp}") diff --git a/cython/requirements.txt b/cython/requirements.txt new file mode 100644 index 00000000..f6629e02 --- /dev/null +++ b/cython/requirements.txt @@ -0,0 +1 @@ +cython diff --git a/cython/setup.py b/cython/setup.py new file mode 100644 index 00000000..dd44e7c8 --- /dev/null +++ b/cython/setup.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +"""Compile the Cython libraries of Python-Solvespace.""" + +__author__ = "Yuan Chang" +__copyright__ = "Copyright (C) 2016-2019" +__license__ = "GPLv3+" +__email__ = "pyslvs@gmail.com" + +import os +import re +import codecs +from setuptools import setup, Extension, find_packages +from platform import system +from distutils import sysconfig + +here = os.path.abspath(os.path.dirname(__file__)) +include_path = '../include/' +src_path = '../src/' +platform_path = src_path + 'platform/' +ver = sysconfig.get_config_var('VERSION') +lib = sysconfig.get_config_var('BINDIR') + + +def read(*parts): + with codecs.open(os.path.join(here, *parts), 'r') as f: + return f.read() + + +def get_version(*file_paths): + doc = read(*file_paths) + m1 = re.search(r"^set\(solvespace_VERSION_MAJOR (\d)\)", doc, re.M) + m2 = re.search(r"^set\(solvespace_VERSION_MINOR (\d)\)", doc, re.M) + if m1 and m2: + return f"{m1.group(1)}.{m2.group(1)}" + raise RuntimeError("Unable to find version string.") + + +macros = [ + ('_hypot', 'hypot'), + ('M_PI', 'PI'), # C++ 11 + ('ISOLATION_AWARE_ENABLED', None), + ('LIBRARY', None), + ('EXPORT_DLL', None), + ('_CRT_SECURE_NO_WARNINGS', None), +] + +compile_args = [ + '-O3', + '-Wno-cpp', + '-g', + '-Wno-write-strings', + '-fpermissive', + '-fPIC', + '-std=c++11', +] + +sources = [ + 'python_solvespace/' + 'slvs.pyx', + src_path + 'util.cpp', + src_path + 'entity.cpp', + src_path + 'expr.cpp', + src_path + 'constrainteq.cpp', + src_path + 'constraint.cpp', + src_path + 'system.cpp', + src_path + 'lib.cpp', +] + +if system() == 'Windows': + # Avoid compile error with CYTHON_USE_PYLONG_INTERNALS. + # https://github.com/cython/cython/issues/2670#issuecomment-432212671 + macros.append(('MS_WIN64', None)) + # Disable format warning + compile_args.append('-Wno-format') + + # Solvespace arguments + macros.append(('WIN32', None)) + + # Platform sources + sources.append(platform_path + 'w32util.cpp') + sources.append(platform_path + 'platform.cpp') +else: + sources.append(platform_path + 'unixutil.cpp') + +setup( + name="python_solvespace", + version=get_version('..', 'CMakeLists.txt'), + author=__author__, + author_email=__email__, + description="Python library of Solvespace", + long_description=read("README.md"), + url="https://github.com/solvespace/solvespace", + packages=find_packages(exclude=('tests',)), + ext_modules=[Extension( + "slvs", + sources, + language="c++", + include_dirs=[include_path, src_path, platform_path], + define_macros=macros, + extra_compile_args=compile_args + )], + python_requires=">=3.6", + setup_requires=[ + 'setuptools', + 'wheel', + 'cython', + ], + classifiers=[ + "Programming Language :: Python :: 3", + "Programming Language :: Cython", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Operating System :: OS Independent", + ] +) diff --git a/cython/tests/__init__.py b/cython/tests/__init__.py new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/cython/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/cython/tests/test_slvs.py b/cython/tests/test_slvs.py new file mode 100644 index 00000000..6e78a1d4 --- /dev/null +++ b/cython/tests/test_slvs.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- + +"""This module will test the functions of Python-Solvespace.""" + +__author__ = "Yuan Chang" +__copyright__ = "Copyright (C) 2016-2019" +__license__ = "GPLv3+" +__email__ = "pyslvs@gmail.com" + +import unittest +from unittest import TestCase +from math import radians +from slvs import ResultFlag, SolverSystem, make_quaternion + + +class CoreTest(TestCase): + + def test_crank_rocker(self): + """Crank rocker example.""" + sys = SolverSystem() + wp = sys.create_2d_base() + p0 = sys.add_point_2d(0, 0, wp) + sys.dragged(p0, wp) + p1 = sys.add_point_2d(90, 0, wp) + sys.dragged(p1, wp) + line0 = sys.add_line_2d(p0, p1, wp) + p2 = sys.add_point_2d(20, 20, wp) + p3 = sys.add_point_2d(0, 10, wp) + p4 = sys.add_point_2d(30, 20, wp) + sys.distance(p2, p3, 40, wp) + sys.distance(p2, p4, 40, wp) + sys.distance(p3, p4, 70, wp) + + sys.distance(p0, p3, 35, wp) + sys.distance(p1, p4, 70, wp) + line1 = sys.add_line_2d(p0, p3, wp) + sys.angle(line0, line1, 45, wp) + + result_flag = sys.solve() + self.assertEqual(result_flag, ResultFlag.OKAY) + x, y = sys.params(p2.params) + self.assertAlmostEqual(39.54852, x, 4) + self.assertAlmostEqual(61.91009, y, 4) + + def test_involute(self): + """Involute example.""" + r = 10 + angle = 45 + + sys = SolverSystem() + wp = sys.create_2d_base() + p0 = sys.add_point_2d(0, 0, wp) + sys.dragged(p0, wp) + + p1 = sys.add_point_2d(0, 10, wp) + sys.distance(p0, p1, r, wp) + line0 = sys.add_line_2d(p0, p1, wp) + + p2 = sys.add_point_2d(10, 10, wp) + line1 = sys.add_line_2d(p1, p2, wp) + sys.distance(p1, p2, r * radians(angle), wp) + sys.perpendicular(line0, line1, wp, False) + + p3 = sys.add_point_2d(10, 0, wp) + sys.dragged(p3, wp) + line_base = sys.add_line_2d(p0, p3, wp) + sys.angle(line0, line_base, angle, wp) + + result_flag = sys.solve() + self.assertEqual(result_flag, ResultFlag.OKAY) + x, y = sys.params(p2.params) + self.assertAlmostEqual(12.62467, x, 4) + self.assertAlmostEqual(1.51746, y, 4) + + def test_jansen_linkage(self): + """Jansen's linkage example.""" + sys = SolverSystem() + wp = sys.create_2d_base() + p0 = sys.add_point_2d(0, 0, wp) + sys.dragged(p0, wp) + + p1 = sys.add_point_2d(0, 20, wp) + sys.distance(p0, p1, 15, wp) + line0 = sys.add_line_2d(p0, p1, wp) + + p2 = sys.add_point_2d(-38, -7.8, wp) + sys.dragged(p2, wp) + p3 = sys.add_point_2d(-50, 30, wp) + p4 = sys.add_point_2d(-70, -15, wp) + sys.distance(p2, p3, 41.5, wp) + sys.distance(p3, p4, 55.8, wp) + sys.distance(p2, p4, 40.1, wp) + + p5 = sys.add_point_2d(-50, -50, wp) + p6 = sys.add_point_2d(-10, -90, wp) + p7 = sys.add_point_2d(-20, -40, wp) + sys.distance(p5, p6, 65.7, wp) + sys.distance(p6, p7, 49.0, wp) + sys.distance(p5, p7, 36.7, wp) + + sys.distance(p1, p3, 50, wp) + sys.distance(p1, p7, 61.9, wp) + + p8 = sys.add_point_2d(20, 0, wp) + line_base = sys.add_line_2d(p0, p8, wp) + sys.angle(line0, line_base, 45, wp) + + result_flag = sys.solve() + self.assertEqual(result_flag, ResultFlag.OKAY) + x, y = sys.params(p2.params) + self.assertAlmostEqual(-38, x, 4) + self.assertAlmostEqual(-7.8, y, 4) + + def test_nut_cracker(self): + """Nut cracker example.""" + h0 = 0.5 + b0 = 0.75 + r0 = 0.25 + n1 = 1.5 + n2 = 2.3 + l0 = 3.25 + + sys = SolverSystem() + wp = sys.create_2d_base() + p0 = sys.add_point_2d(0, 0, wp) + sys.dragged(p0, wp) + + p1 = sys.add_point_2d(2, 2, wp) + p2 = sys.add_point_2d(2, 0, wp) + line0 = sys.add_line_2d(p0, p2, wp) + sys.horizontal(line0, wp) + + line1 = sys.add_line_2d(p1, p2, wp) + p3 = sys.add_point_2d(b0 / 2, h0, wp) + sys.dragged(p3, wp) + sys.distance(p3, line1, r0, wp) + sys.distance(p0, p1, n1, wp) + sys.distance(p1, p2, n2, wp) + + result_flag = sys.solve() + self.assertEqual(result_flag, ResultFlag.OKAY) + x, _ = sys.params(p2.params) + ans_min = x - b0 / 2 + ans_max = l0 - r0 - b0 / 2 + self.assertAlmostEqual(1.01576, ans_min, 4) + self.assertAlmostEqual(2.625, ans_max, 4) + + def test_pydemo(self): + """ + Some sample code for slvs.dll. We draw some geometric entities, provide + initial guesses for their positions, and then constrain them. The solver + calculates their new positions, in order to satisfy the constraints. + + Copyright 2008-2013 Jonathan Westhues. + Copyright 2016-2017 Yuan Chang [pyslvs@gmail.com] Python-Solvespace bundled. + + An example of a constraint in 2d. In our first group, we create a workplane + along the reference frame's xy plane. In a second group, we create some + entities in that group and dimension them. + """ + sys = SolverSystem() + sys.set_group(1) + + # First, we create our workplane. Its origin corresponds to the origin + # of our base frame (x y z) = (0 0 0) + p101 = sys.add_point_3d(0, 0, 0) + # and it is parallel to the xy plane, so it has basis vectors (1 0 0) + # and (0 1 0). + qw, qx, qy, qz = make_quaternion(1, 0, 0, 0, 1, 0) + n102 = sys.add_normal_3d(qw, qx, qy, qz) + wp200 = sys.add_work_plane(p101, n102) + + # Now create a second group. We'll solve group 2, while leaving group 1 + # constant; so the workplane that we've created will be locked down, + # and the solver can't move it. + sys.set_group(2) + p301 = sys.add_point_2d(10, 20, wp200) + p302 = sys.add_point_2d(20, 10, wp200) + + # And we create a line segment with those endpoints. + l400 = sys.add_line_2d(p301, p302, wp200) + + # Now three more points. + p303 = sys.add_point_2d(100, 120, wp200) + p304 = sys.add_point_2d(120, 110, wp200) + p305 = sys.add_point_2d(115, 115, wp200) + + # And arc, centered at point 303, starting at point 304, ending at + # point 305. + a401 = sys.add_arc(n102, p303, p304, p305, wp200) + + # Now one more point, and a distance + p306 = sys.add_point_2d(200, 200, wp200) + d307 = sys.add_distance(30, wp200) + + # And a complete circle, centered at point 306 with radius equal to + # distance 307. The normal is 102, the same as our workplane. + c402 = sys.add_circle(n102, p306, d307, wp200) + + # The length of our line segment is 30.0 units. + sys.distance(p301, p302, 30, wp200) + + # And the distance from our line segment to the origin is 10.0 units. + sys.distance(p101, l400, 10, wp200) + + # And the line segment is vertical. + sys.vertical(l400, wp200) + + # And the distance from one endpoint to the origin is 15.0 units. + sys.distance(p301, p101, 15, wp200) + + # The arc and the circle have equal radius. + sys.equal(a401, c402, wp200) + + # The arc has radius 17.0 units. + sys.diameter(a401, 17 * 2, wp200) + + # If the solver fails, then ask it to report which constraints caused + # the problem. + + # And solve. + result_flag = sys.solve() + self.assertEqual(result_flag, ResultFlag.OKAY) + x, y = sys.params(p301.params) + self.assertAlmostEqual(10, x, 4) + self.assertAlmostEqual(11.18030, y, 4) + x, y = sys.params(p302.params) + self.assertAlmostEqual(10, x, 4) + self.assertAlmostEqual(-18.81966, y, 4) + x, y = sys.params(p303.params) + self.assertAlmostEqual(101.11418, x, 4) + self.assertAlmostEqual(119.04153, y, 4) + x, y = sys.params(p304.params) + self.assertAlmostEqual(116.47661, x, 4) + self.assertAlmostEqual(111.76171, y, 4) + x, y = sys.params(p305.params) + self.assertAlmostEqual(117.40922, x, 4) + self.assertAlmostEqual(114.19676, y, 4) + x, y = sys.params(p306.params) + self.assertAlmostEqual(200, x, 4) + self.assertAlmostEqual(200, y, 4) + x, = sys.params(d307.params) + self.assertAlmostEqual(17, x, 4) + self.assertEqual(6, sys.dof()) + + +if __name__ == '__main__': + unittest.main()