2082 lines
65 KiB
Python
2082 lines
65 KiB
Python
'''
|
|
Copyright (C) 2023 CG Cookie
|
|
http://cgcookie.com
|
|
hello@cgcookie.com
|
|
|
|
Created by Jonathan Denning, Jonathan Williamson
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
'''
|
|
|
|
import re
|
|
import random
|
|
from math import sqrt, acos, cos, sin, floor, ceil, isinf, sqrt, pi, isnan
|
|
from typing import List
|
|
from itertools import chain
|
|
|
|
import gpu
|
|
from mathutils import Matrix, Vector, Quaternion
|
|
from bmesh.types import BMVert
|
|
from mathutils.geometry import intersect_line_plane, intersect_point_tri
|
|
|
|
from .colors import colorname_to_color
|
|
from .decorators import stats_wrapper, blender_version_wrapper
|
|
from .profiler import profiler
|
|
|
|
from ..terminal import term_printer
|
|
|
|
'''
|
|
The types below wrap the mathutils.Vector class, distinguishing among the
|
|
different types of geometric entities that are typically represented using
|
|
a vanilla Vector.
|
|
'''
|
|
|
|
|
|
float_inf = float('inf')
|
|
zero_threshold = 0.0000001
|
|
|
|
|
|
class Entity2D:
|
|
def is_2D(self):
|
|
return True
|
|
|
|
def is_3D(self):
|
|
return False
|
|
|
|
|
|
class Entity3D:
|
|
def is_2D(self):
|
|
return False
|
|
|
|
def is_3D(self):
|
|
return True
|
|
|
|
|
|
class VecUtils(Vector):
|
|
@classmethod
|
|
def new_from_vector(cls, v):
|
|
return cls(v) if v else None
|
|
|
|
def normalize(self):
|
|
super().normalize()
|
|
return self
|
|
|
|
def as_vector(self):
|
|
return Vector(self)
|
|
|
|
def from_vector(self, v):
|
|
self.x, self.y, self.z = v
|
|
|
|
def perpendicular_direction(self):
|
|
q0 = Quaternion(Vector((42, 1.618034, 2.71828)), 1.5707963)
|
|
q1 = Quaternion(Vector((1.41421, 2, 1.73205)), -1.5707963)
|
|
v = q1 * q0 * self
|
|
return Direction(self.cross(v))
|
|
|
|
def cross(self, other):
|
|
t = type(other)
|
|
if t is Vector:
|
|
return Vec(super().cross(other))
|
|
if t is Vec or t is Direction or t is Normal:
|
|
return Vec(super().cross(Vector(other)))
|
|
assert False, 'unhandled type of other: %s (%s)' % (str(other), str(t))
|
|
|
|
|
|
class Vec2D(Vector, Entity2D):
|
|
@stats_wrapper
|
|
def __init__(self, *args, **kwargs):
|
|
Vector.__init__(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return '<Vec2D (%0.4f, %0.4f)>' % (self.x, self.y)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def as_vector(self):
|
|
return Vector(self)
|
|
|
|
def from_vector(self, v):
|
|
self.x, self.y = v
|
|
|
|
def project(self, other):
|
|
''' returns the projection of self onto other '''
|
|
olen2 = other.length_squared
|
|
if olen2 <= zero_threshold: return Vec2D((0,0))
|
|
return (self.dot(other) / olen2) * other
|
|
|
|
|
|
class Vec(VecUtils, Entity3D):
|
|
@stats_wrapper
|
|
def __init__(self, *args, **kwargs):
|
|
Vector.__init__(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return '<Vec (%0.4f, %0.4f, %0.4f)>' % (self.x, self.y, self.z)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def project(self, other):
|
|
''' returns the projection of self onto other '''
|
|
olen2 = other.length_squared
|
|
if olen2 <= zero_threshold: return Vec3D((0,0,0))
|
|
return (self.dot(other) / olen2) * other
|
|
|
|
@staticmethod
|
|
def average(vecs):
|
|
ax, ay, az, ac = 0, 0, 0, 0
|
|
for v in vecs:
|
|
vx,vy,vz = v
|
|
ax,ay,az,ac = ax+vx,ay+vy,az+vz,ac+1
|
|
return Vec((ax / ac, ay / ac, az / ac)) if ac else Vec((0, 0, 0))
|
|
|
|
|
|
class Index2D:
|
|
def __init__(self, i, j):
|
|
self._i = i
|
|
self._j = j
|
|
def __iter__(self): yield from (self._i, self._j)
|
|
@property
|
|
def i(self): return self._i
|
|
@i.setter
|
|
def i(self, i): self._i = i
|
|
@property
|
|
def j(self): return self._j
|
|
@j.setter
|
|
def j(self, j): self._j = j
|
|
def update(self, i=None, j=None, i_off=None, j_off=None):
|
|
if i is not None: self._i = i
|
|
if j is not None: self._j = j
|
|
if i_off is not None: self._i += i_off
|
|
if j_off is not None: self._j += j_off
|
|
def to_tuple(self): return (self._i, self._j)
|
|
|
|
|
|
class Point2D(Vector, Entity2D):
|
|
@stats_wrapper
|
|
def __init__(self, *args, **kwargs):
|
|
Vector.__init__(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return '<Point2D (%0.4f, %0.4f)>' % (self.x, self.y)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def __iter__(self):
|
|
return iter([self.x, self.y])
|
|
|
|
def __add__(self, other):
|
|
t = type(other)
|
|
if t is Direction2D:
|
|
return Point2D((self.x + other.x, self.y + other.y))
|
|
if t is Vector or t is Vec2D:
|
|
return Point2D((self.x + other.x, self.y + other.y))
|
|
if t is RelPoint2D:
|
|
return Point2D((self.x + other.x, self.y + other.y))
|
|
assert False, "unhandled type of other: %s (%s)" % (str(other), str(t))
|
|
|
|
def __radd__(self, other):
|
|
return self.__add__(other)
|
|
|
|
def __sub__(self, other):
|
|
t = type(other)
|
|
if t is Vector or t is Vec2D:
|
|
return Point2D((self.x - other.x, self.y - other.y))
|
|
elif t is Point2D:
|
|
return Vec2D((self.x - other.x, self.y - other.y))
|
|
elif t is RelPoint2D:
|
|
return Point2D((self.x - other.x, self.y - other.y))
|
|
assert False, "unhandled type of other: %s (%s)" % (str(other), str(t))
|
|
|
|
def distance_squared_to(self, other) -> float:
|
|
return (self.x - other.x)**2 + (self.y - other.y)**2
|
|
|
|
def distance_to(self, other) -> float:
|
|
return sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
|
|
|
|
def as_vector(self):
|
|
return Vector(self)
|
|
|
|
def from_vector(self, v):
|
|
self.x, self.y = v
|
|
|
|
@staticmethod
|
|
def average(points):
|
|
x, y, c = 0, 0, 0
|
|
for p in points:
|
|
x += p.x
|
|
y += p.y
|
|
c += 1
|
|
return Point2D((x / c, y / c)) if c else Point2D((0, 0))
|
|
|
|
@staticmethod
|
|
def weighted_average(weight_points):
|
|
x, y, c = 0, 0, 0
|
|
for w, p in weight_points:
|
|
x += p.x * w
|
|
y += p.y * w
|
|
c += w
|
|
return Point2D((x / c, y / c)) if c else Point2D((0, 0))
|
|
|
|
|
|
class RelPoint2D(Vector, Entity2D):
|
|
@stats_wrapper
|
|
def __init__(self, *args, **kwargs):
|
|
Vector.__init__(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return '<RelPoint2D (%0.4f, %0.4f)>' % (self.x, self.y)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def __iter__(self):
|
|
return iter([self.x, self.y])
|
|
|
|
def __add__(self, other):
|
|
t = type(other)
|
|
if t is Direction2D:
|
|
return RelPoint2D((self.x + other.x, self.y + other.y))
|
|
if t is Vector or t is Vec2D:
|
|
return RelPoint2D((self.x + other.x, self.y + other.y))
|
|
if t is RelPoint2D:
|
|
return RelPoint2D((self.x + other.x, self.y, + other.y))
|
|
if t is Point2D:
|
|
return Point2D((self.x + other.x, self.y + other.y))
|
|
assert False, "unhandled type of other: %s (%s)" % (str(other), str(t))
|
|
|
|
def __radd__(self, other):
|
|
return self.__add__(other)
|
|
|
|
def __sub__(self, other):
|
|
t = type(other)
|
|
if t is Vector or t is Vec2D:
|
|
return RelPoint2D((self.x - other.x, self.y - other.y))
|
|
elif t is Point2D or t is RelPoint2D:
|
|
return Vec2D((self.x - other.x, self.y - other.y))
|
|
assert False, "unhandled type of other: %s (%s)" % (str(other), str(t))
|
|
|
|
def distance_squared_to(self, other) -> float:
|
|
return (self.x - other.x)**2 + (self.y - other.y)**2
|
|
|
|
def distance_to(self, other) -> float:
|
|
return sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
|
|
|
|
def as_vector(self):
|
|
return Vector(self)
|
|
|
|
def from_vector(self, v):
|
|
self.x, self.y = v
|
|
|
|
@staticmethod
|
|
def average(points):
|
|
x, y, c = 0, 0, 0
|
|
for p in points:
|
|
x += p.x
|
|
y += p.y
|
|
c += 1
|
|
return RelPoint2D((x / c, y / c)) if c else RelPoint2D((0, 0))
|
|
|
|
@staticmethod
|
|
def weighted_average(weight_points):
|
|
x, y, c = 0, 0, 0
|
|
for w, p in weight_points:
|
|
x += p.x * w
|
|
y += p.y * w
|
|
c += w
|
|
return RelPoint2D((x / c, y / c)) if c else RelPoint2D((0, 0))
|
|
RelPoint2D.ZERO = RelPoint2D((0,0))
|
|
|
|
|
|
class Point(Vector, Entity3D):
|
|
@classmethod
|
|
def new_from_vector(cls, v):
|
|
return cls(v) if v else None
|
|
|
|
@stats_wrapper
|
|
def __init__(self, *args, **kwargs):
|
|
Vector.__init__(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return '<Point (%0.4f, %0.4f, %0.4f)>' % (self.x, self.y, self.z)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def __add__(self, other):
|
|
t = type(other)
|
|
if t is Direction or t is Normal:
|
|
return Point((
|
|
self.x + other.x,
|
|
self.y + other.y,
|
|
self.z + other.z
|
|
))
|
|
if t is Vector or t is Vec:
|
|
return Point((
|
|
self.x + other.x,
|
|
self.y + other.y,
|
|
self.z + other.z
|
|
))
|
|
assert False, "unhandled type of other: %s (%s)" % (str(other), str(t))
|
|
|
|
def __radd__(self, other):
|
|
return self.__add__(other)
|
|
|
|
def __sub__(self, other):
|
|
t = type(other)
|
|
if t is Vector or t is Vec:
|
|
return Point((
|
|
self.x - other.x,
|
|
self.y - other.y,
|
|
self.z - other.z
|
|
))
|
|
elif t is Point:
|
|
return Vec((
|
|
self.x - other.x,
|
|
self.y - other.y,
|
|
self.z - other.z
|
|
))
|
|
assert False, "unhandled type of other: %s (%s)" % (str(other), str(t))
|
|
|
|
def as_vector(self):
|
|
return Vector(self)
|
|
|
|
def from_vector(self, v):
|
|
self.x, self.y, self.z = v
|
|
|
|
@staticmethod
|
|
def average(points):
|
|
x, y, z, c = 0, 0, 0, 0
|
|
for p in points:
|
|
x += p.x
|
|
y += p.y
|
|
z += p.z
|
|
c += 1
|
|
return Point((x / c, y / c, z / c)) if c else Point((0, 0, 0))
|
|
|
|
@staticmethod
|
|
def weighted_average(weight_points):
|
|
x, y, z, c = 0, 0, 0, 0
|
|
for w, p in weight_points:
|
|
x += p.x * w
|
|
y += p.y * w
|
|
z += p.z * w
|
|
c += w
|
|
return Point((x / c, y / c, z / c)) if c else Point((0, 0, 0))
|
|
|
|
class Direction2D(Vector, Entity2D):
|
|
@stats_wrapper
|
|
def __init__(self, t=None):
|
|
if t is not None:
|
|
self.from_vector(t)
|
|
|
|
def __str__(self):
|
|
return '<Direction2D (%0.4f, %0.4f)>' % (self.x, self.y)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def __mul__(self, other):
|
|
t = type(other)
|
|
if t is float or t is int:
|
|
return Vec2D((other * self.x, other * self.y))
|
|
assert False, "unhandled type of other: %s (%s)" % (str(other), str(t))
|
|
|
|
def __rmul__(self, other):
|
|
return self.__mul__(other)
|
|
|
|
def reverse(self):
|
|
self.x *= -1
|
|
self.y *= -1
|
|
return self
|
|
|
|
def normalize(self):
|
|
super().normalize()
|
|
return self
|
|
|
|
def as_vector(self):
|
|
return Vector(self)
|
|
|
|
def from_vector(self, v):
|
|
self.x, self.y = v
|
|
self.normalize()
|
|
|
|
|
|
class Direction(VecUtils, Entity3D):
|
|
@stats_wrapper
|
|
def __init__(self, t=None):
|
|
if t is not None:
|
|
self.from_vector(t)
|
|
|
|
def __str__(self):
|
|
return '<Direction (%0.4f, %0.4f, %0.4f)>' % (self.x, self.y, self.z)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def __mul__(self, other):
|
|
t = type(other)
|
|
if t is float or t is int:
|
|
return Vector((other * self.x, other * self.y, other * self.z))
|
|
assert False, "unhandled type of other: %s (%s)" % (str(other), str(t))
|
|
|
|
def __rmul__(self, other):
|
|
return self.__mul__(other)
|
|
|
|
def reverse(self):
|
|
self.x *= -1
|
|
self.y *= -1
|
|
self.z *= -1
|
|
return self
|
|
|
|
def angleBetween(self, other):
|
|
return acos(mid(-1, 1, self.dot(other.normalized())))
|
|
|
|
def from_vector(self, v):
|
|
super().from_vector(v)
|
|
self.normalize()
|
|
|
|
@classmethod
|
|
def uniform(cls):
|
|
# http://corysimon.github.io/articles/uniformdistn-on-sphere/
|
|
theta = random.uniform(0, pi*2)
|
|
phi = acos(random.uniform(-1, 1))
|
|
x = sin(phi) * cos(theta)
|
|
y = sin(phi) * sin(theta)
|
|
z = cos(phi)
|
|
return cls((x,y,z))
|
|
Direction.X = Direction((1,0,0))
|
|
Direction.Y = Direction((0,1,0))
|
|
Direction.Z = Direction((0,0,1))
|
|
|
|
|
|
class Normal(VecUtils, Entity3D):
|
|
@stats_wrapper
|
|
def __init__(self, t=None):
|
|
if t is not None:
|
|
self.from_vector(t)
|
|
|
|
def __str__(self):
|
|
return '<Normal (%0.4f, %0.4f, %0.4f)>' % (self.x, self.y, self.z)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def __mul__(self, other):
|
|
t = type(other)
|
|
if t is float or t is int:
|
|
return Vector((other * self.x, other * self.y, other * self.z))
|
|
assert False, "unhandled type of other: %s (%s)" % (str(other), str(t))
|
|
|
|
def __rmul__(self, other):
|
|
return self.__mul__(other)
|
|
|
|
def from_vector(self, v):
|
|
super().from_vector(v)
|
|
self.normalize()
|
|
|
|
@staticmethod
|
|
def average(normals):
|
|
v, c = Vector(), 0
|
|
for n in normals:
|
|
v += n
|
|
c += 1
|
|
return Normal(v) if c else v
|
|
|
|
|
|
class Color(Vector):
|
|
@staticmethod
|
|
def from_ints(r, g, b, a=255):
|
|
return Color((r/255.0, g/255.0, b/255.0, a/255.0))
|
|
|
|
@staticmethod
|
|
def as_vec4(c):
|
|
if type(c) in {float, int}: return Vector((c, c, c, 1.0))
|
|
if len(c) == 3: return Vector((*c, 1.0))
|
|
return Vector(c)
|
|
|
|
@staticmethod
|
|
def HSL(hsl):
|
|
# https://en.wikipedia.org/wiki/HSL_and_HSV
|
|
# 0 <= H < 1 (circular), 0 <= S <= 1, 0 <= L <= 1
|
|
if len(hsl) == 3: h,s,l,a = *hsl, 1.0
|
|
else: h,s,l,a = hsl
|
|
|
|
h = (h % 1) * 6
|
|
s = clamp(s, 0, 1)
|
|
l = clamp(l, 0, 1)
|
|
a = clamp(a, 0, 1)
|
|
|
|
c = (1 - abs(2 * l - 1)) * s
|
|
x = c * (1 - abs(h % 2 - 1))
|
|
m = l - c / 2
|
|
|
|
if h < 1: r,g,b = c,x,0
|
|
elif h < 2: r,g,b = x,c,0
|
|
elif h < 3: r,g,b = 0,c,x
|
|
elif h < 4: r,g,b = 0,x,c
|
|
elif h < 5: r,g,b = x,0,c
|
|
else: r,g,b = c,0,x
|
|
|
|
r += m
|
|
g += m
|
|
b += m
|
|
|
|
return Color((r, g, b, a))
|
|
|
|
@property
|
|
def r(self): return self.x
|
|
@r.setter
|
|
def r(self, v): self.x = v
|
|
|
|
@property
|
|
def g(self): return self.y
|
|
@g.setter
|
|
def g(self, v): self.y = v
|
|
|
|
@property
|
|
def b(self): return self.z
|
|
@b.setter
|
|
def b(self, v): self.z = v
|
|
|
|
@property
|
|
def a(self): return self.w
|
|
@a.setter
|
|
def a(self, v): self.w = v
|
|
|
|
@property
|
|
def hsl(self):
|
|
# https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
|
|
# 0 <= H < 1 (circular), 0 <= S <= 1, 0 <= L <= 1
|
|
r, g, b = self.x, self.y, self.z
|
|
x_max, x_min = max(r, g, b), min(r, g, b)
|
|
c = x_max - x_min
|
|
l = (x_max + x_min) / 2.0
|
|
h = 0
|
|
if c > 0:
|
|
if x_max == r: h = (60 / 360) * (((g - b) / c) % 6)
|
|
elif x_max == g: h = (60 / 360) * (((b - r) / c) + 2)
|
|
else: h = (60 / 360) * (((r - g) / c) + 4)
|
|
s = (x_max - l) / min(l, 1 - l) if 0 < l < 1 else 0
|
|
return (h, s, l)
|
|
|
|
def rotated_hue(self, hue_add):
|
|
h,s,l = self.hsl
|
|
return Color.HSL((h + hue_add, s, l))
|
|
|
|
def __str__(self):
|
|
# return '<Color (%0.4f, %0.4f, %0.4f, %0.4f)>' % (self.r, self.g, self.b, self.a)
|
|
return 'Color(%0.2f, %0.2f, %0.2f, %0.2f)' % (self.r, self.g, self.b, self.a)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def __mul__(self, other):
|
|
t = type(other)
|
|
if t is float or t is int:
|
|
return Color((other * self.r, other * self.g, other * self.b, self.a))
|
|
if t is Color:
|
|
return Color((self.r * other.r, self.g * other.g, self.b * other.b, self.a * other.a))
|
|
assert False, "unhandled type of other: %s (%s)" % (str(other), str(t))
|
|
|
|
def __rmul__(self, other):
|
|
return self.__mul__(other)
|
|
|
|
def as_vector(self):
|
|
return Vector(self)
|
|
|
|
def from_vector(self, v):
|
|
if len(v) == 3: self.r, self.g, self.b = v
|
|
else: self.r, self.g, self.b, self.a = v
|
|
|
|
# set colornames in Color, ex: Color.white, Color.black, Color.transparent
|
|
for colorname in colorname_to_color.keys():
|
|
c = colorname_to_color[colorname]
|
|
c = (c[0]/255, c[1]/255, c[2]/255, 1.0 if len(c)==3 else c[3])
|
|
setattr(Color, colorname, Color(c))
|
|
|
|
|
|
class Ray(Entity3D):
|
|
__slots__ = ['o', 'd', 'max']
|
|
|
|
@staticmethod
|
|
def from_segment(a: Point, b: Point):
|
|
v = b - a
|
|
dist = v.length
|
|
return Ray(a, v / dist, max_dist=dist)
|
|
|
|
@stats_wrapper
|
|
def __init__(
|
|
self,
|
|
o: Point,
|
|
d: Direction,
|
|
min_dist: float=0.0,
|
|
max_dist: float=float_inf
|
|
):
|
|
# sys.float_info.max
|
|
o, d = Point(o), Direction(d)
|
|
self.o = o + min_dist * d
|
|
self.d = d
|
|
if max_dist == float_inf:
|
|
self.max = max_dist
|
|
else:
|
|
om = o + max_dist * d
|
|
self.max = (self.o - om).length
|
|
|
|
def __str__(self):
|
|
return '<Ray (%0.4f, %0.4f, %0.4f)->(%0.4f, %0.4f, %0.4f)>' % (
|
|
self.o.x, self.o.y, self.o.z,
|
|
self.d.x, self.d.y, self.d.z
|
|
)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def eval(self, t: float):
|
|
v = self.d * clamp(t, 0.0, self.max)
|
|
return self.o + v
|
|
|
|
@classmethod
|
|
def from_screenspace(cls, pos: Vector):
|
|
# convert pos in screenspace to ray
|
|
pass
|
|
|
|
|
|
class Plane(Entity3D):
|
|
@classmethod
|
|
def from_points(cls, p0: Point, p1: Point, p2: Point):
|
|
o = Point((
|
|
(p0.x + p1.x + p2.x) / 3,
|
|
(p0.y + p1.y + p2.y) / 3,
|
|
(p0.z + p1.z + p2.z) / 3
|
|
))
|
|
n = Normal((p1 - p0).cross(p2 - p0)).normalize()
|
|
return cls(o, n)
|
|
|
|
def __init__(self, o: Point, n: Normal):
|
|
self.o = o
|
|
self.n = n
|
|
self.d = o.dot(n)
|
|
|
|
def __str__(self):
|
|
return '<Plane (%0.4f, %0.4f, %0.4f), (%0.4f, %0.4f, %0.4f)>' % (
|
|
self.o.x, self.o.y, self.o.z,
|
|
self.n.x, self.n.y, self.n.z
|
|
)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def side(self, p: Point, threshold=zero_threshold):
|
|
d = (p - self.o).dot(self.n)
|
|
if abs(d) < threshold:
|
|
return 0
|
|
return -1 if d < 0 else 1
|
|
|
|
def distance_to(self, p: Point):
|
|
return abs((p - self.o).dot(self.n))
|
|
|
|
def signed_distance_to(self, p: Point):
|
|
return (p - self.o).dot(self.n)
|
|
|
|
def project(self, p: Point):
|
|
return p + self.n * (self.o - p).dot(self.n)
|
|
|
|
def polygon_intersects(self, points: List[Point]):
|
|
return abs(sum(self.side(p) for p in points)) != len(points)
|
|
|
|
@stats_wrapper
|
|
def triangle_intersect(self, points: List[Point]):
|
|
return abs(sum(self.side(p) for p in points)) != 3
|
|
|
|
def line_intersection(self, p0:Point, p1:Point):
|
|
v01 = p1 - p0
|
|
if v01.dot(self.n) == 0: return None
|
|
l = Direction(v01)
|
|
d = (self.o - p0).dot(self.n) / l.dot(self.n)
|
|
return p0 + l * d
|
|
#return intersect_line_plane(p0, p1, self.o, self.n)
|
|
|
|
# @profiler.function
|
|
def triangle_intersection(self, points: List[Point]):
|
|
assert len(points) == 3, f'triangle intersection on non triangle ({len(points)=})'
|
|
s0, s1, s2 = map(self.side, points)
|
|
if abs(s0 + s1 + s2) == 3:
|
|
return [] # all points on same side of plane
|
|
p0, p1, p2 = map(Point, points)
|
|
if s0 == 0 or s1 == 0 or s2 == 0: # at least one point on plane
|
|
# handle if all points in plane
|
|
if s0 == 0 and s1 == 0 and s2 == 0:
|
|
return [(p0, p1), (p1, p2), (p2, p0)]
|
|
# handle if two points in plane
|
|
if s0 == 0 and s1 == 0:
|
|
return [(p0, p1)]
|
|
if s1 == 0 and s2 == 0:
|
|
return [(p1, p2)]
|
|
if s2 == 0 and s0 == 0:
|
|
return [(p2, p0)]
|
|
# one point on plane, two on same side
|
|
if s0 == 0 and s1 == s2:
|
|
return [(p0, p0)]
|
|
if s1 == 0 and s2 == s0:
|
|
return [(p1, p1)]
|
|
if s2 == 0 and s0 == s1:
|
|
return [(p2, p2)]
|
|
# one point on plane, other two on different sides
|
|
# pass through and catch this case below
|
|
# two points on one side, one point on the other
|
|
p01 = self.line_intersection(p0, p1)
|
|
p12 = self.line_intersection(p1, p2)
|
|
p20 = self.line_intersection(p2, p0)
|
|
if s0 == 0:
|
|
return [(p0, p12)]
|
|
if s1 == 0:
|
|
return [(p1, p20)]
|
|
if s2 == 0:
|
|
return [(p2, p01)]
|
|
if s0 != s1 and s0 != s2 and p01 and p20:
|
|
return [(p01, p20)]
|
|
if s1 != s0 and s1 != s2 and p01 and p12:
|
|
return [(p01, p12)]
|
|
if s2 != s0 and s2 != s1 and p12 and p20:
|
|
return [(p12, p20)]
|
|
print(f'{self.o=} {self.n=} {self.d=}')
|
|
print(f'{p0=} {s0=}')
|
|
print(f'{p1=} {s1=}')
|
|
print(f'{p2=} {s2=}')
|
|
print(f'{p01=} {p12=} {p20=}')
|
|
assert False
|
|
|
|
@stats_wrapper
|
|
def edge_intersect(self, points: List[Point]):
|
|
return abs(sum(self.side(p) for p in points)) != 2
|
|
|
|
# @profiler.function
|
|
def edge_clamp(self, points: List[Point]):
|
|
s0, s1 = map(self.side, points)
|
|
if abs(s0 + s1) == 2:
|
|
return [] # points on same side
|
|
p0, p1 = map(Point, points)
|
|
if s0 == 0 and s1 == 0:
|
|
return [(p0, p1)]
|
|
if s0 == 0:
|
|
return [(p0, p0)]
|
|
if s1 == 0:
|
|
return [(p1, p1)]
|
|
p01 = self.line_intersection(p0, p1)
|
|
return [(p01, p01)]
|
|
|
|
def edge_intersection(self, p0:Point, p1:Point, threshold=zero_threshold):
|
|
s0, s1 = self.side(p0,threshold=threshold), self.side(p1,threshold=threshold)
|
|
if s0 == 0: return Point(p0) # p0 is on plane
|
|
if s1 == 0: return Point(p1) # p1 is on plane
|
|
if s0 == s1: return None # points on same side
|
|
# points on opposite sides of plane, might be parallel to plane...
|
|
return self.line_intersection(p0, p1)
|
|
|
|
def edge_crosses(self, points):
|
|
p0, p1 = points
|
|
s0, s1 = self.side(p0), self.side(p1)
|
|
return (s0 == 0 and s1 == 0) or s0 != s1
|
|
|
|
def edge_coplanar(self, points):
|
|
p0, p1 = points
|
|
return self.side(p0) == 0 and self.side(p1) == 0
|
|
|
|
|
|
class Frame:
|
|
@staticmethod
|
|
def from_plane(plane: Plane, x: Direction=None, y: Direction=None):
|
|
return Frame(plane.o, x=x, y=y, z=Direction(plane.n))
|
|
|
|
@stats_wrapper
|
|
def __init__(
|
|
self,
|
|
o: Point,
|
|
x: Direction=None,
|
|
y: Direction=None,
|
|
z: Direction=None
|
|
):
|
|
c = (1 if x else 0) + (1 if y else 0) + (1 if z else 0)
|
|
assert c != 0, "Must specify at least one direction"
|
|
if c == 1:
|
|
if x:
|
|
y = Direction((-x.x + 3.14, x.y + 42, x.z - 1.61))
|
|
z = Direction(x.cross(y))
|
|
y = Direction(z.cross(x))
|
|
elif y:
|
|
x = Direction((-y.x + 3.14, y.y + 42, y.z - 1.61))
|
|
z = Direction(x.cross(y))
|
|
x = Direction(y.cross(z))
|
|
else:
|
|
x = Direction((-z.x + 3.14, z.y + 42, z.z - 1.61))
|
|
y = Direction(-x.cross(z))
|
|
x = Direction(y.cross(z))
|
|
elif c >= 2:
|
|
if x and y:
|
|
z = Direction(x.cross(y))
|
|
y = Direction(z.cross(x))
|
|
x = Direction(y.cross(z))
|
|
elif x and z:
|
|
y = Direction(z.cross(x))
|
|
x = Direction(y.cross(z))
|
|
z = Direction(x.cross(y))
|
|
else:
|
|
x = Direction(y.cross(z))
|
|
y = Direction(z.cross(x))
|
|
z = Direction(z)
|
|
|
|
self.o = Point(o)
|
|
self.x = x
|
|
self.y = y
|
|
self.z = z
|
|
|
|
self.fn_l2w_typed = {
|
|
Vec: self.l2w_vector,
|
|
Point: self.l2w_point,
|
|
Normal: self.l2w_normal,
|
|
Vector: self.l2w_vector,
|
|
Direction: self.l2w_direction,
|
|
# Ray: self.l2w_ray,
|
|
# Plane: self.l2w_plane,
|
|
# BMVert: self.l2w_bmvert,
|
|
}
|
|
self.fn_w2l_typed = {
|
|
Vec: self.w2l_vector,
|
|
Point: self.w2l_point,
|
|
Normal: self.w2l_normal,
|
|
Vector: self.w2l_vector,
|
|
Direction: self.w2l_direction,
|
|
# Ray: self.w2l_ray,
|
|
# Plane: self.w2l_plane,
|
|
# BMVert: self.w2l_bmvert,
|
|
}
|
|
|
|
def __str__(self):
|
|
s = '(%0.4f, %0.4f, %0.4f)'
|
|
return '<Frame %s, %s, %s, %s>' % (
|
|
s % (self.o.x, self.o.y, self.o.z),
|
|
s % (self.x.x, self.x.y, self.x.z),
|
|
s % (self.y.x, self.y.y, self.y.z),
|
|
s % (self.z.x, self.z.y, self.z.z)
|
|
)
|
|
|
|
def _dot_fns(self):
|
|
return self.x.dot, self.y.dot, self.z.dot
|
|
|
|
def _dots(self, v):
|
|
return (self.x.dot(v), self.y.dot(v), self.z.dot(v))
|
|
|
|
def _mults(self, v):
|
|
return self.x * v.x + self.y * v.y + self.z * v.z
|
|
|
|
def l2w_typed(self, data):
|
|
''' dispatched conversion '''
|
|
t = type(data)
|
|
assert t in self.fn_l2w_typed, "unhandled type of data: %s (%s)" % (
|
|
str(data), str(type(data))
|
|
)
|
|
return self.fn_l2w_typed[t](data)
|
|
|
|
def w2l_typed(self, data):
|
|
''' dispatched conversion '''
|
|
t = type(data)
|
|
assert t in self.fn_w2l_typed, "unhandled type of data: %s (%s)" % (
|
|
str(data), str(type(data))
|
|
)
|
|
return self.fn_w2l_typed[t](data)
|
|
|
|
def w2l_point(self, p: Point) -> Point:
|
|
return Point(self._dots(p - self.o))
|
|
|
|
def l2w_point(self, p: Point) -> Point:
|
|
return Point(self.o + self._mults(p))
|
|
|
|
def w2l_vector(self, v: Vector) -> Vec:
|
|
return Vec(self._dots(v))
|
|
|
|
def l2w_vector(self, v: Vector) -> Vec:
|
|
return Vec(self._mults(v))
|
|
|
|
def w2l_direction(self, d: Direction) -> Direction:
|
|
return Direction(self._dots(d)).normalize()
|
|
|
|
def l2w_direction(self, d: Direction) -> Direction:
|
|
return Direction(self._mults(d)).normalize()
|
|
|
|
def w2l_normal(self, n: Normal) -> Normal:
|
|
return Normal(self._dots(n)).normalize()
|
|
|
|
def l2w_normal(self, n: Normal) -> Normal:
|
|
return Normal(self._mults(n)).normalize()
|
|
|
|
def w2l_frame(self, f):
|
|
o = self.w2l_point(f.o)
|
|
x = self.w2l_direction(f.x)
|
|
y = self.w2l_direction(f.y)
|
|
z = self.w2l_direction(f.z)
|
|
return Frame(o=o, x=x, y=y, z=z)
|
|
|
|
def l2w_frame(self, f):
|
|
o = self.l2w_point(f.o)
|
|
x = self.l2w_direction(f.x)
|
|
y = self.l2w_direction(f.y)
|
|
z = self.l2w_direction(f.z)
|
|
return Frame(o=o, x=x, y=y, z=z)
|
|
|
|
def rotate_about_z(self, radians: float):
|
|
c, s = cos(radians), sin(radians)
|
|
x, y = self.x, self.y
|
|
self.x = x * c + y * s
|
|
self.y = -x * s + y * c
|
|
|
|
|
|
class XForm:
|
|
@staticmethod
|
|
def Scale(factor):
|
|
if type(factor) in {int, float}:
|
|
x = y = z = factor
|
|
else:
|
|
x, y, z = factor
|
|
return XForm(rows=((
|
|
(x, 0, 0, 0),
|
|
(0, y, 0, 0),
|
|
(0, 0, z, 0),
|
|
(0, 0, 0, 1),
|
|
)))
|
|
|
|
@staticmethod
|
|
def get_mats(mx: Matrix):
|
|
smat, d = str(mx), XForm.get_mats.__dict__
|
|
if smat not in d:
|
|
m = {
|
|
'mx_p': None, 'imx_p': None,
|
|
'mx_d': None, 'imx_d': None,
|
|
'mx_n': None, 'imx_n': None
|
|
}
|
|
m[ 'mx_p'] = Matrix(mx)
|
|
m[ 'mx_t'] = mx.transposed()
|
|
m['imx_p'] = mx.inverted_safe()
|
|
m[ 'mx_d'] = mx.to_3x3()
|
|
m['imx_d'] = m['mx_d'].inverted_safe()
|
|
m[ 'mx_n'] = m['imx_d'].transposed()
|
|
m['imx_n'] = m['mx_d'].transposed()
|
|
d[smat] = m
|
|
return d[smat]
|
|
|
|
@stats_wrapper
|
|
def __init__(self, mx: Matrix=None, *, rows=None):
|
|
if mx is None:
|
|
mx = Matrix()
|
|
elif type(mx) is not Matrix:
|
|
mx = Matrix(rows)
|
|
self.assign(mx)
|
|
|
|
def assign(self, mx):
|
|
if type(mx) is XForm:
|
|
return self.assign(mx.mx_p)
|
|
|
|
mats = XForm.get_mats(mx)
|
|
self.mx_p, self.imx_p = mats['mx_p'], mats['imx_p']
|
|
self.mx_d, self.imx_d = mats['mx_d'], mats['imx_d']
|
|
self.mx_n, self.imx_n = mats['mx_n'], mats['imx_n']
|
|
self.mx_t = mats['mx_t']
|
|
|
|
self.fn_l2w_typed = {
|
|
Ray: self.l2w_ray,
|
|
Vec: self.l2w_vector,
|
|
Plane: self.l2w_plane,
|
|
Point: self.l2w_point,
|
|
BMVert: self.l2w_bmvert,
|
|
Normal: self.l2w_normal,
|
|
Vector: self.l2w_vector,
|
|
Direction: self.l2w_direction,
|
|
}
|
|
self.fn_w2l_typed = {
|
|
Ray: self.w2l_ray,
|
|
Vec: self.w2l_vector,
|
|
Plane: self.w2l_plane,
|
|
Point: self.w2l_point,
|
|
BMVert: self.w2l_bmvert,
|
|
Normal: self.w2l_normal,
|
|
Vector: self.w2l_vector,
|
|
Direction: self.w2l_direction,
|
|
}
|
|
return self
|
|
|
|
def __str__(self):
|
|
v = tuple(x for r in self.mx_p for x in r)
|
|
return '<XForm (%0.4f, %0.4f, %0.4f, %0.4f)\n' \
|
|
' (%0.4f, %0.4f, %0.4f, %0.4f)\n' \
|
|
' (%0.4f, %0.4f, %0.4f, %0.4f)\n' \
|
|
' (%0.4f, %0.4f, %0.4f, %0.4f)>' % v
|
|
|
|
def __repr__(self):
|
|
v = tuple(x for r in self.mx_p for x in r)
|
|
return 'XForm(((%f, %f, %f, %f),\n' \
|
|
' (%f, %f, %f, %f),\n' \
|
|
' (%f, %f, %f, %f),\n' \
|
|
' (%f, %f, %f, %f)))' % v
|
|
|
|
@property
|
|
def matrix(self):
|
|
return Matrix(self.mx_p)
|
|
@matrix.setter
|
|
def matrix(self, mx):
|
|
self.assign(mx)
|
|
|
|
def __mul__(self, other):
|
|
t = type(other)
|
|
if t is XForm:
|
|
return XForm(self.mx_p * other.mx_p)
|
|
if t is Matrix:
|
|
return XForm(self.mx_p * other)
|
|
return self.l2w_typed(other)
|
|
|
|
def __imul__(self, other):
|
|
other_mx = other.mx_p if type(other) is XForm else other
|
|
self.assign(self.mx_p * other_mx)
|
|
|
|
def __truediv__(self, other):
|
|
return self.w2l_typed(other)
|
|
|
|
def __iter__(self):
|
|
for v in self.mx_p:
|
|
yield v
|
|
|
|
def to_frame(self):
|
|
o = Point(self.mx_p @ Point((0, 0, 0)))
|
|
x = Direction(self.mx_d @ Direction((1, 0, 0)))
|
|
y = Direction(self.mx_d @ Direction((0, 1, 0)))
|
|
z = Direction(self.mx_d @ Direction((0, 0, 1)))
|
|
return Frame(o=o, x=x, y=y, z=z)
|
|
|
|
def l2w_typed(self, data):
|
|
''' dispatched conversion '''
|
|
t = type(data)
|
|
assert t in self.fn_l2w_typed, "unhandled type of data: %s (%s)" % (
|
|
str(data), str(type(data))
|
|
)
|
|
return self.fn_l2w_typed[t](data)
|
|
|
|
def w2l_typed(self, data):
|
|
''' dispatched conversion '''
|
|
t = type(data)
|
|
assert t in self.fn_w2l_typed, "unhandled type of data: %s (%s)" % (
|
|
str(data), str(type(data))
|
|
)
|
|
return self.fn_w2l_typed[t](data)
|
|
|
|
def l2w_point(self, p: Point) -> Point:
|
|
#return Point(self.mx_p @ p)
|
|
v = self.mx_p @ Vector((p.x, p.y, p.z, 1.0))
|
|
return Point(v.xyz / v.w)
|
|
|
|
def w2l_point(self, p: Point) -> Point:
|
|
# return Point(self.imx_p @ p)
|
|
v = self.imx_p @ Vector((p.x, p.y, p.z, 1.0))
|
|
return Point(v.xyz / v.w)
|
|
|
|
def l2w_direction(self, d: Direction) -> Direction: return Direction(self.mx_d.to_3x3() @ d)
|
|
def w2l_direction(self, d: Direction) -> Direction: return Direction(self.imx_d.to_3x3() @ d)
|
|
def l2w_normal(self, n: Normal) -> Normal: return Normal(self.mx_n.to_3x3() @ n)
|
|
def w2l_normal(self, n: Normal) -> Normal: return Normal(self.imx_n.to_3x3() @ n)
|
|
def l2w_vector(self, v: Vector) -> Vec: return Vec(self.mx_d.to_3x3() @ v)
|
|
def w2l_vector(self, v: Vector) -> Vec: return Vec(self.imx_d.to_3x3() @ v)
|
|
|
|
def l2w_ray(self, ray: Ray) -> Ray:
|
|
o = self.l2w_point(ray.o)
|
|
d = self.l2w_direction(ray.d)
|
|
if ray.max == float('inf'):
|
|
l1 = ray.max
|
|
else:
|
|
l1 = (o - self.l2w_point(ray.o + ray.max * ray.d)).length
|
|
return Ray(o=o, d=d, max_dist=l1)
|
|
|
|
def w2l_ray(self, ray: Ray) -> Ray:
|
|
o = self.w2l_point(ray.o)
|
|
d = self.w2l_direction(ray.d)
|
|
if ray.max == float('inf'):
|
|
l1 = ray.max
|
|
else:
|
|
l1 = (o - self.w2l_point(ray.o + ray.max * ray.d)).length
|
|
return Ray(o=o, d=d, max_dist=l1)
|
|
|
|
def l2w_plane(self, plane: Plane) -> Plane:
|
|
return Plane(o=self.l2w_point(plane.o), n=self.l2w_normal(plane.n))
|
|
|
|
def w2l_plane(self, plane: Plane) -> Plane:
|
|
return Plane(o=self.w2l_point(plane.o), n=self.w2l_normal(plane.n))
|
|
|
|
def l2w_bmvert(self, bmv: BMVert) -> Point: return Point(self.mx_p @ bmv.co)
|
|
def w2l_bmvert(self, bmv: BMVert) -> Point: return Point(self.imx_p @ bmv.co)
|
|
|
|
@staticmethod
|
|
def to_gpubuffer(mat):
|
|
return gpu.types.Buffer('FLOAT', [len(mat), len(mat)], mat)
|
|
|
|
def to_gpubuffer_Model(self):
|
|
return self.to_gpubuffer(self.mx_p)
|
|
|
|
def to_gpubuffer_Inverse(self):
|
|
return self.to_gpubuffer(self.imx_p)
|
|
|
|
def to_gpubuffer_Normal(self):
|
|
return self.to_gpubuffer(self.mx_n)
|
|
|
|
|
|
class BBox:
|
|
@stats_wrapper
|
|
def __init__(self, from_object=None, from_bmverts=None, from_coords=None, xform_point=None):
|
|
if not any([from_object, from_bmverts, from_coords]):
|
|
nan = float('nan')
|
|
self.min = None
|
|
self.max = None
|
|
self.mx, self.my, self.mz = nan, nan, nan
|
|
self.Mx, self.My, self.Mz = nan, nan, nan
|
|
self.min_dim = nan
|
|
self.max_dim = nan
|
|
return
|
|
|
|
if from_object: from_coords = [Point(c) for c in from_object.bound_box]
|
|
elif from_bmverts: from_coords = [bmv.co for bmv in from_bmverts]
|
|
elif from_coords: from_coords = list(from_coords)
|
|
|
|
if xform_point: from_coords = [xform_point(co) for co in from_coords]
|
|
|
|
mx, Mx = min(x for (x,y,z) in from_coords), max(x for (x,y,z) in from_coords)
|
|
my, My = min(y for (x,y,z) in from_coords), max(y for (x,y,z) in from_coords)
|
|
mz, Mz = min(z for (x,y,z) in from_coords), max(z for (x,y,z) in from_coords)
|
|
self.min = Point((mx, my, mz))
|
|
self.max = Point((Mx, My, Mz))
|
|
self.mx, self.my, self.mz = mx, my, mz
|
|
self.Mx, self.My, self.Mz = Mx, My, Mz
|
|
self.min_dim = min(self.size_x, self.size_y, self.size_z)
|
|
self.max_dim = max(self.size_x, self.size_y, self.size_z)
|
|
self._corners = [
|
|
Point((self.mx, self.my, self.mz)),
|
|
Point((self.mx, self.my, self.Mz)),
|
|
Point((self.mx, self.My, self.mz)),
|
|
Point((self.mx, self.My, self.Mz)),
|
|
Point((self.Mx, self.my, self.mz)),
|
|
Point((self.Mx, self.my, self.Mz)),
|
|
Point((self.Mx, self.My, self.mz)),
|
|
Point((self.Mx, self.My, self.Mz)),
|
|
]
|
|
|
|
@property
|
|
def corners(self): yield from self._corners
|
|
|
|
@property
|
|
def size_x(self): return self.Mx - self.mx
|
|
@property
|
|
def size_y(self): return self.My - self.my
|
|
@property
|
|
def size_z(self): return self.Mz - self.mz
|
|
|
|
@staticmethod
|
|
def merge(boxes, *args):
|
|
if type(boxes) is BBox: boxes = [boxes]
|
|
if args: boxes = boxes + args
|
|
return BBox(from_coords=[Point(p) for b in boxes for p in [
|
|
(b.mx, b.my, b.mz),
|
|
(b.Mx, b.My, b.Mz)
|
|
]])
|
|
|
|
def __str__(self):
|
|
s = '(%0.4f, %0.4f, %0.4f)'
|
|
return '<BBox %s, %s>' % (
|
|
s % (self.mx, self.my, self.mz),
|
|
s % (self.Mx, self.My, self.Mz),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def Point_within(self, point: Point, margin=0):
|
|
if not self.min or not self.max: return False
|
|
return all(
|
|
m - margin <= v and v <= M + margin
|
|
for (v, m, M) in zip(point, self.min, self.max)
|
|
)
|
|
|
|
def closest_Point(self, point:Point):
|
|
return Point((
|
|
clamp(point.x, self.mx, self.Mx),
|
|
clamp(point.y, self.my, self.My),
|
|
clamp(point.z, self.mz, self.Mz),
|
|
))
|
|
|
|
def farthest_Point(self, point:Point):
|
|
cx, cy, cz = (self.mx + self.Mx) / 2, (self.my + self.My) / 2, (self.mz + self.Mz) / 2
|
|
return Point((
|
|
self.mx if point.x > cx else self.Mx,
|
|
self.my if point.y > cy else self.My,
|
|
self.mz if point.z > cz else self.Mz,
|
|
))
|
|
|
|
def get_min_dimension(self):
|
|
return self.min_dim
|
|
|
|
def get_max_dimension(self):
|
|
return self.max_dim
|
|
|
|
class BBox2D:
|
|
def __init__(self, pts=None):
|
|
self._count = 0
|
|
nan = float('nan')
|
|
self.mx, self.my = nan, nan
|
|
self.Mx, self.My = nan, nan
|
|
self.insert_points(pts)
|
|
|
|
def __str__(self): return f'<BBox2D ({self.mx}, {self.my}) ({self.Mx}, {self.My})>'
|
|
def __repr__(self): return self.__str__()
|
|
|
|
def insert_points(self, pts):
|
|
if not pts: return
|
|
for pt in pts: self.insert(pt)
|
|
def insert(self, pt:Point2D):
|
|
if not pt: return
|
|
(x, y) = pt
|
|
if self._count == 0:
|
|
self.mx, self.my = x, y
|
|
self.Mx, self.My = x, y
|
|
else:
|
|
self.mx, self.my = min(self.mx, x), min(self.my, y)
|
|
self.Mx, self.My = max(self.Mx, x), max(self.My, y)
|
|
self._count += 1
|
|
|
|
@property
|
|
def count(self): return self._count
|
|
@property
|
|
def min(self): return Point2D((self.mx, self.my))
|
|
@property
|
|
def max(self): return Point2D((self.Mx, self.My))
|
|
@property
|
|
def size_x(self): return (self.Mx - self.mx)
|
|
@property
|
|
def size_y(self): return (self.My - self.my)
|
|
@property
|
|
def min_dim(self): return min(self.size_x, self.size_y)
|
|
@property
|
|
def max_dim(self): return max(self.size_x, self.size_y)
|
|
|
|
def get_min_dimension(self): return self.min_dim
|
|
def get_max_dimension(self): return self.max_dim
|
|
|
|
@property
|
|
def corners(self):
|
|
yield from [
|
|
Point2D((self.mx, self.my)),
|
|
Point2D((self.mx, self.My)),
|
|
Point2D((self.Mx, self.My)),
|
|
Point2D((self.Mx, self.my)),
|
|
]
|
|
|
|
def Point2D_within(self, point: Point2D, *, margin=0):
|
|
if self._count == 0: return False
|
|
(x, y) = point
|
|
return (
|
|
self.mx - margin <= x <= self.Mx + margin and
|
|
self.my - margin <= y <= self.My + margin
|
|
)
|
|
|
|
def closest_Point(self, point:Point2D):
|
|
if self._count == 0: Point2D((float('nan'), float('nan')))
|
|
x, y = point
|
|
return Point2D((
|
|
clamp(x, self.mx, self.Mx),
|
|
clamp(y, self.my, self.My),
|
|
))
|
|
|
|
def farthest_Point(self, point:Point2D):
|
|
if self._count == 0: Point2D((float('nan'), float('nan')))
|
|
cx, cy = (self.mx + self.Mx) / 2, (self.my + self.My) / 2
|
|
x, y = point
|
|
return Point2D((
|
|
self.mx if x > cx else self.Mx,
|
|
self.my if y > cy else self.My,
|
|
))
|
|
|
|
|
|
|
|
class Size1D:
|
|
def __init__(self, **kwargs):
|
|
self._length = kwargs.get('length', kwargs.get('l', None))
|
|
self._min = kwargs.get('min', None)
|
|
self._max = kwargs.get('max', None)
|
|
|
|
@property
|
|
def length(self):
|
|
if self._length is None: return None
|
|
v = self._length
|
|
if self._min is not None: v = max(v, self._min)
|
|
if self._max is not None: v = min(v, self._max)
|
|
return v
|
|
@length.setter
|
|
def length(self, l):
|
|
self._length = l
|
|
|
|
@property
|
|
def _min(self): return self.__min
|
|
@_min.setter
|
|
def _min(self, v): self.__min = v
|
|
|
|
@property
|
|
def max(self): return self._max
|
|
@max.setter
|
|
def max(self, v): self._max = v
|
|
|
|
|
|
class Size2D:
|
|
def __init__(self, **kwargs):
|
|
self._width = kwargs.get('width', kwargs.get('w', None))
|
|
self._height = kwargs.get('height', kwargs.get('h', None))
|
|
self._min_width = kwargs.get('min_width', 0)
|
|
self._min_height = kwargs.get('min_height', 0)
|
|
self._max_width = kwargs.get('max_width', None)
|
|
self._max_height = kwargs.get('max_height', None)
|
|
|
|
def __iter__(self):
|
|
return iter([self._width, self._height])
|
|
def __str__(self):
|
|
ret = '<Size2D'
|
|
if self._min_width is not None: ret += ' min_width=%f' % self._min_width
|
|
if self._width is not None: ret += ' width=%f' % self._width
|
|
if self._max_width is not None: ret += ' max_width=%f' % self._max_width
|
|
if self._min_height is not None: ret += ' min_height=%f' % self._min_height
|
|
if self._height is not None: ret += ' height=%f' % self._height
|
|
if self._max_height is not None: ret += ' max_height=%f' % self._max_height
|
|
ret += '>'
|
|
return ret
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def __eq__(self, other):
|
|
if type(other) is not Size2D: return False
|
|
if self._width != other._width: return False
|
|
if self._min_width != other._min_width: return False
|
|
if self._max_width != other._max_width: return False
|
|
if self._height != other._height: return False
|
|
if self._min_height != other._min_height: return False
|
|
if self._max_height != other._max_height: return False
|
|
return True
|
|
|
|
def clone(self):
|
|
return Size2D(
|
|
width=self._width, min_width=self._min_width, max_width=self._max_width,
|
|
height=self._height, min_height=self._min_height, max_height=self._max_height,
|
|
)
|
|
|
|
def clamp_width(self, w):
|
|
if w is None: return None
|
|
if self._min_width is not None: w = max(w, self._min_width)
|
|
if self._max_width is not None: w = min(w, self._max_width)
|
|
return w
|
|
|
|
def clamp_height(self, h):
|
|
if h is None: return None
|
|
if self._min_height is not None: h = max(h, self._min_height)
|
|
if self._max_height is not None: h = min(h, self._max_height)
|
|
return h
|
|
|
|
def clamp_size(self, w, h):
|
|
return self.clamp_width(w), self.clamp_height(h)
|
|
|
|
@property
|
|
def width(self): return self.clamp_width(self._width)
|
|
@width.setter
|
|
def width(self, v): self._width = v
|
|
|
|
@property
|
|
def height(self): return self.clamp_height(self._height)
|
|
@height.setter
|
|
def height(self, v): self._height = v
|
|
|
|
@property
|
|
def size(self): return (self.width, self.height)
|
|
|
|
@property
|
|
def min_width(self): return self._min_width
|
|
@min_width.setter
|
|
def min_width(self, v): self._min_width = v
|
|
|
|
@property
|
|
def min_height(self): return self._min_height
|
|
@min_height.setter
|
|
def min_height(self, v): self._min_height = v
|
|
|
|
@property
|
|
def max_width(self): return self._max_width
|
|
@max_width.setter
|
|
def max_width(self, v): self._max_width = v
|
|
|
|
@property
|
|
def max_height(self): return self._max_height
|
|
@max_height.setter
|
|
def max_height(self, v): self._max_height = v
|
|
|
|
def biggest_width(self):
|
|
if self._max_width is not None: return self._max_width
|
|
if self._width is not None: return self._width
|
|
return self._min_width
|
|
def biggest_height(self):
|
|
if self._max_height is not None: return self._max_height
|
|
if self._height is not None: return self._height
|
|
return self._min_height
|
|
|
|
def smallest_width(self):
|
|
if self._min_width is not None: return self._min_width
|
|
if self._width is not None: return self._width
|
|
return self._max_width
|
|
def smallest_height(self):
|
|
if self._min_height is not None: return self._min_height
|
|
if self._height is not None: return self._height
|
|
return self._max_height
|
|
|
|
def get_width_midmaxmin(self):
|
|
if self._width is not None: return self._width
|
|
if self._max_width is not None: return self._max_width
|
|
return self._min_width
|
|
def get_height_midmaxmin(self):
|
|
if self._height is not None: return self._height
|
|
if self._max_height is not None: return self._max_height
|
|
return self._min_height
|
|
|
|
def set_all_widths(self, v):
|
|
self._width = self._min_width = self._max_width = v
|
|
def set_all_heights(self, v):
|
|
self._height = self._min_height = self._max_height = v
|
|
|
|
def update_min_width(self, v):
|
|
self._min_width = v if self._min_width is None else min(self._min_width, v)
|
|
def update_min_height(self, v):
|
|
self._min_height = v if self._min_height is None else min(self._min_height, v)
|
|
def update_max_width(self, v):
|
|
self._max_width = v if self._max_width is None else max(self._max_width, v)
|
|
def update_max_height(self, v):
|
|
self._max_height = v if self._max_height is None else max(self._max_height, v)
|
|
|
|
def add_width(self, v):
|
|
self._width = v if self._width is None else self._width + v
|
|
def add_height(self, v):
|
|
self._height = v if self._height is None else self._height + v
|
|
def add_min_width(self, v):
|
|
self._min_width = v if self._min_width is None else self._min_width + v
|
|
def add_min_height(self, v):
|
|
self._min_height = v if self._min_height is None else self._min_height + v
|
|
def add_max_width(self, v):
|
|
self._max_width = v if self._max_width is None else self._max_width + v
|
|
def add_max_height(self, v):
|
|
self._max_height = v if self._max_height is None else self._max_height + v
|
|
|
|
def sub_all_widths(self, v):
|
|
if self._width is not None: self._width = max(0, self._width - v)
|
|
if self._min_width is not None: self._min_width = max(0, self._min_width - v)
|
|
if self._max_width is not None: self._max_width = max(0, self._max_width - v)
|
|
def sub_all_heights(self, v):
|
|
if self._height is not None: self._height = max(0, self._height - v)
|
|
if self._min_height is not None: self._min_height = max(0, self._min_height - v)
|
|
if self._max_height is not None: self._max_height = max(0, self._max_height - v)
|
|
|
|
|
|
class Box2D:
|
|
'''
|
|
WARNING: this class does not prevent right < left or top < bottom!
|
|
NOTE: y increases up and x increases left (matches OpenGL)
|
|
'''
|
|
def __init__(self, **kwargs):
|
|
self.set(**kwargs)
|
|
|
|
def set(self, **kwargs):
|
|
# gather position and size info from kwargs
|
|
left, right = kwargs.get('left', None), kwargs.get('right', None)
|
|
top, bottom = kwargs.get('top', None), kwargs.get('bottom', None)
|
|
width, height = kwargs.get('width', None), kwargs.get('height', None)
|
|
# composite specification
|
|
topleft, topright = kwargs.get('topleft', None), kwargs.get('topright', None)
|
|
bottomleft, bottomright = kwargs.get('bottomleft', None), kwargs.get('bottomright', None)
|
|
size = kwargs.get('size', None)
|
|
# relative positioning
|
|
#above, below = kwargs.get('above', None), kwargs.get('below', None)
|
|
#toleft, toright = kwargs.get('toleft', None), kwargs.get('toright', None)
|
|
#parent = kwargs.get('parent', None) # if None, pos is abs; ow rel
|
|
|
|
# unpack composite specs
|
|
if size is not None: width,height = size
|
|
if topleft is not None: left,top = topleft
|
|
if topright is not None: right,top = topright
|
|
if bottomleft is not None: left,bottom = bottomleft
|
|
if bottomright is not None: right,bottom = bottomright
|
|
|
|
# make sure that caller sent all info needed to create Box2D instance
|
|
ln,rn,wn = left is not None, right is not None, width is not None
|
|
tn,bn,hn = top is not None, bottom is not None, height is not None
|
|
assert ln or rn, "Box2D: left and right cannot both be None"
|
|
assert tn or bn, "Box2D: top and bottom cannot both be None"
|
|
assert (ln and rn) or wn, "Box2D: must specify either both left and right or width"
|
|
assert (tn and bn) or hn, "Box2D: must specify either both top and bottom or height"
|
|
if ln and rn and wn: assert width == right - left + 1, "Box2D: left (%f), right (%f), and width (%f) do not agree" % (left, right, width)
|
|
if tn and bn and hn: assert height == top - bottom + 1, "Box2D: top (%f), bottom (%f), and height (%f) do not agree" % (top, bottom, height)
|
|
|
|
# set properties
|
|
self._left = left if ln else right - (width - 1)
|
|
self._right = right if rn else left + (width - 1)
|
|
self._width = width if wn else right - left + 1
|
|
self._top = top if tn else bottom + (height - 1)
|
|
self._bottom = bottom if bn else top - (height - 1)
|
|
self._height = height if hn else top - bottom + 1
|
|
|
|
@property
|
|
def left(self):
|
|
return self._left
|
|
@left.setter
|
|
def left(self, v):
|
|
''' sets left side to v, keeps right '''
|
|
self._left = v
|
|
self._width = self._right - self._left + 1
|
|
def move_left(self, v):
|
|
''' moves left side to v, keeps width '''
|
|
self._left = v
|
|
self._right = self._left + self._width - 1
|
|
|
|
@property
|
|
def right(self):
|
|
return self._right
|
|
@right.setter
|
|
def right(self, v):
|
|
''' sets right side to v, keeps left '''
|
|
self._right = v
|
|
self._width = self._right - self._left + 1
|
|
def move_right(self, v):
|
|
''' moves right side to v, keeps width '''
|
|
self._right = v
|
|
self._left = self._right - self._width + 1
|
|
|
|
@property
|
|
def bottom(self):
|
|
return self._bottom
|
|
@bottom.setter
|
|
def bottom(self, v):
|
|
''' sets bottom side to v, keeps top '''
|
|
self._bottom = v
|
|
self._height = self._top - self._bottom + 1
|
|
def move_bottom(self, v):
|
|
''' moves bottom side to v, keeps height '''
|
|
self._bottom = v
|
|
self._top = self._bottom + self._height - 1
|
|
|
|
@property
|
|
def top(self):
|
|
return self._top
|
|
@top.setter
|
|
def top(self, v):
|
|
''' sets top side to v, keeps bottom '''
|
|
self._top = v
|
|
self._height = self._top - self._bottom + 1
|
|
def move_top(self, v):
|
|
''' moves top side to v, keeps height '''
|
|
self._top = v
|
|
self._bottom = self._top - self._height + 1
|
|
|
|
@property
|
|
def topleft(self):
|
|
return (self._left, self._top)
|
|
@topleft.setter
|
|
def topleft(self, lt):
|
|
self._left, self._top = lt
|
|
self._width = self._right - self._left + 1
|
|
self._height = self._top - self._bottom + 1
|
|
|
|
@property
|
|
def topright(self):
|
|
return (self._right, self._top)
|
|
@topright.setter
|
|
def topright(self, rt):
|
|
self._right, self._top = rt
|
|
self._width = self._right - self._left + 1
|
|
self._height = self._top - self._bottom + 1
|
|
|
|
@property
|
|
def bottomleft(self):
|
|
return (self._left, self._bottom)
|
|
@bottomleft.setter
|
|
def bottomleft(self, lb):
|
|
self._left, self._bottom = lb
|
|
self._width = self._right - self._left + 1
|
|
self._height = self._top - self._bottom + 1
|
|
|
|
@property
|
|
def bottomright(self):
|
|
return (self._right, self._bottom)
|
|
@bottomright.setter
|
|
def bottomright(self, rb):
|
|
self._right, self._bottom = rb
|
|
self._width = self._right - self._left + 1
|
|
self._height = self._top - self._bottom + 1
|
|
|
|
@property
|
|
def width(self):
|
|
return self._width
|
|
|
|
@property
|
|
def height(self):
|
|
return self._height
|
|
|
|
@property
|
|
def size(self):
|
|
return Size2D(width=self._width, height=self._height)
|
|
|
|
def overlap(self, that:'Box2D'):
|
|
''' do self and that overlap? '''
|
|
if self._left > that._right: return False
|
|
if that._left > self._right: return False
|
|
if self._bottom > that._top: return False
|
|
if that._bottom > self._top: return False
|
|
return True
|
|
|
|
def point_inside(self, point:Point2D):
|
|
''' is given point inside self? '''
|
|
x,y = point
|
|
if x < self._left or x > self._right: return False
|
|
if y < self._bottom or y > self._top: return False
|
|
return True
|
|
|
|
def new_neighbor(self, rellocation, padding=0, **kwargs):
|
|
''' create new Box2D that neighbors self '''
|
|
box = Box2D(**kwargs)
|
|
if rellocation in {'above'}:
|
|
box.move_bottom(self._top + padding + 1)
|
|
elif rellocation in {'below'}:
|
|
box.move_top(self._bottom - padding - 1)
|
|
elif rellocation in {'left', 'toleft'}:
|
|
box.move_right(self._left - padding - 1)
|
|
elif rellocation in {'right', 'toright'}:
|
|
box.move_left(self._right + padding + 1)
|
|
else:
|
|
assert False, 'Unhandled relative location: %s' % rellocation
|
|
return box
|
|
|
|
# (bbox) intersect, union, difference
|
|
# copy
|
|
|
|
|
|
class NumberUnit:
|
|
val_fn = {
|
|
'%': lambda num,base,_base: (num / 100.0) * float(base if base is not None else _base if _base is not None else 1),
|
|
'px': lambda num,base,_base: num,
|
|
'pt': lambda num,base,_base: num,
|
|
'': lambda num,base,_base: num,
|
|
}
|
|
|
|
def __init__(self, num, unit, base=None):
|
|
self._num = float(num)
|
|
self._unit = unit
|
|
self._base = base
|
|
|
|
@property
|
|
def unit(self): return self._unit
|
|
|
|
# def __str__(self): return '<NumberUnit num=%f unit=%s>' % (self._num, str(self._unit))
|
|
def __str__(self): return f'{self._num}{self._unit or "?"}'
|
|
|
|
def __repr__(self): return self.__str__()
|
|
|
|
def __float__(self): return self.val()
|
|
|
|
def val(self, base=None):
|
|
fn = NumberUnit.val_fn.get(self._unit, None)
|
|
assert fn, f'Unhandled unit "{self._unit}"'
|
|
return fn(self._num, base, self._base)
|
|
|
|
def __add__(self, other):
|
|
assert type(other) is NumberUnit, f'Unhandled type for add: {other} ({type(other)})'
|
|
assert self._unit == other._unit, f'Unhandled unit for add: {self} ({self._unit}) != {other} ({other._unit})'
|
|
return NumberUnit(self._num + other_num, self._unit, self._base)
|
|
|
|
def __radd__(self, other):
|
|
assert type(other) is NumberUnit, f'Unhandled type for add: {other} ({type(other)})'
|
|
assert self._unit == other._unit, f'Unhandled unit for add: {self} ({self._unit}) != {other} ({other._unit})'
|
|
return NumberUnit(self._num + other_num, self._unit, self._base)
|
|
|
|
def __mul__(self, other):
|
|
assert type(other) in {float, int}
|
|
return NumberUnit(self._num * other, self._unit, self._base)
|
|
|
|
def __div__(self, other):
|
|
assert type(other) in {float, int}
|
|
return NumberUnit(self._num / other, self._unit, self._base)
|
|
|
|
NumberUnit.zero = NumberUnit(0, 'px')
|
|
|
|
|
|
|
|
def floor_if_finite(v):
|
|
return v if v is None or isinf(v) else floor(v)
|
|
def ceil_if_finite(v):
|
|
return v if v is None or isinf(v) else ceil(v)
|
|
|
|
|
|
|
|
multipliers = {
|
|
'k': 1_000,
|
|
'm': 1_000_000,
|
|
'b': 1_000_000_000,
|
|
}
|
|
def convert_numstr_num(numstr):
|
|
if type(numstr) is not str: return numstr
|
|
m = re.match(r'(?P<num>\d+(?P<dec>[.]\d+)?)(?P<mult>[kmb])?', numstr)
|
|
num = int(m.group('num')) if not m.group('dec') else float(m.group('num'))
|
|
if m.group('mult'):
|
|
num *= multipliers[m.group('mult')]
|
|
return num
|
|
|
|
|
|
def has_inverse(mat):
|
|
smat, d = str(mat), has_inverse.__dict__
|
|
if smat not in d:
|
|
if len(d) > 1000: d.clear()
|
|
try:
|
|
_ = mat.inverted()
|
|
d[smat] = True
|
|
except:
|
|
d[smat] = False
|
|
return d[smat]
|
|
|
|
def invert_matrix(mat):
|
|
smat, d = str(mat), invert_matrix.__dict__
|
|
if smat not in d:
|
|
if len(d) > 1000: d.clear()
|
|
d[smat] = mat.inverted_safe()
|
|
return d[smat]
|
|
|
|
def matrix_normal(mat):
|
|
smat, d = str(mat), matrix_normal.__dict__
|
|
if smat not in d:
|
|
if len(d) > 1000: d.clear()
|
|
d[smat] = invert_matrix(mat).transposed().to_3x3()
|
|
return d[smat]
|
|
|
|
|
|
def rotate2D(point:Point2D, theta:float, *, origin:Point2D=None):
|
|
c,s = cos(theta),sin(theta)
|
|
x,y = point
|
|
if origin is None:
|
|
return Point2D((
|
|
x*c - y*s,
|
|
x*s + y*c,
|
|
))
|
|
ox,oy = origin
|
|
x -= ox
|
|
y -= oy
|
|
return Point2D((
|
|
ox + (x*c - y*s),
|
|
oy + (x*s + y*c),
|
|
))
|
|
|
|
|
|
def get_path_length(verts):
|
|
'''
|
|
sum up the length of a string of vertices
|
|
'''
|
|
if len(verts) < 2:
|
|
return 0
|
|
l_tot = 0
|
|
for i in range(0,len(verts)-1):
|
|
d = verts[i+1] - verts[i]
|
|
l_tot += d.length
|
|
return l_tot
|
|
|
|
def space_evenly_on_path(verts, edges, segments, shift = 0, debug = False): #prev deved for Open Dental CAD
|
|
'''
|
|
Gives evenly spaced location along a string of verts
|
|
Assumes that nverts > nsegments
|
|
Assumes verts are ORDERED along path
|
|
Assumes edges are ordered coherently
|
|
Yes these are lazy assumptions, but the way I build my data
|
|
guarantees these assumptions so deal with it.
|
|
|
|
args:
|
|
verts - list of vert locations type Mathutils.Vector
|
|
eds - list of index pairs type tuple(integer) eg (3,5).
|
|
should look like this though [(0,1),(1,2),(2,3),(3,4),(4,0)]
|
|
segments - number of segments to divide path into
|
|
shift - for cyclic verts chains, shifting the verts along
|
|
the loop can provide better alignment with previous
|
|
loops. This should be -1 to 1 representing a percentage of segment length.
|
|
Eg, a shift of .5 with 8 segments will shift the verts 1/16th of the loop length
|
|
|
|
return
|
|
new_verts - list of new Vert Locations type list[Mathutils.Vector]
|
|
'''
|
|
|
|
if len(verts) < 2:
|
|
print('this is crazy, there are not enough verts to do anything!')
|
|
return verts
|
|
|
|
if segments >= len(verts):
|
|
print('more segments requested than original verts')
|
|
|
|
|
|
#determine if cyclic or not, first vert same as last vert
|
|
if 0 in edges[-1]:
|
|
cyclic = True
|
|
|
|
else:
|
|
cyclic = False
|
|
#zero out the shift in case the vert chain insn't cyclic
|
|
if shift != 0: #not PEP but it shows that we want shift = 0
|
|
print('not shifting because this is not a cyclic vert chain')
|
|
shift = 0
|
|
|
|
#calc_length
|
|
arch_len = 0
|
|
cumulative_lengths = [0]#TODO, make this the right size and dont append
|
|
for i in range(0,len(verts)-1):
|
|
v0 = verts[i]
|
|
v1 = verts[i+1]
|
|
V = v1-v0
|
|
arch_len += V.length
|
|
cumulative_lengths.append(arch_len)
|
|
|
|
if cyclic:
|
|
v0 = verts[-1]
|
|
v1 = verts[0]
|
|
V = v1-v0
|
|
arch_len += V.length
|
|
cumulative_lengths.append(arch_len)
|
|
#print(cumulative_lengths)
|
|
|
|
#identify vert indicies of import
|
|
#this will be the largest vert which lies at
|
|
#no further than the desired fraction of the curve
|
|
|
|
#initialze new vert array and seal the end points
|
|
if cyclic:
|
|
new_verts = [[None]]*(segments)
|
|
#new_verts[0] = verts[0]
|
|
|
|
else:
|
|
new_verts = [[None]]*(segments + 1)
|
|
new_verts[0] = verts[0]
|
|
new_verts[-1] = verts[-1]
|
|
|
|
|
|
n = 0 #index to save some looping through the cumulative lengths list
|
|
#now we are leaving it 0 becase we may end up needing the beginning of the loop last
|
|
#and if we are subdividing, we may hit the same cumulative lenght several times.
|
|
#for now, use the slow and generic way, later developsomething smarter.
|
|
for i in range(0,segments- 1 + cyclic * 1):
|
|
desired_length_raw = (i + 1 + cyclic * -1)/segments * arch_len + shift * arch_len / segments
|
|
#print('the length we desire for the %i segment is %f compared to the total length which is %f' % (i, desired_length_raw, arch_len))
|
|
#like a mod function, but for non integers?
|
|
if desired_length_raw > arch_len:
|
|
desired_length = desired_length_raw - arch_len
|
|
elif desired_length_raw < 0:
|
|
desired_length = arch_len + desired_length_raw #this is the end, + a negative number
|
|
else:
|
|
desired_length = desired_length_raw
|
|
|
|
#find the original vert with the largets legnth
|
|
#not greater than the desired length
|
|
#I used to set n = J after each iteration
|
|
for j in range(n, len(verts)+1):
|
|
|
|
if cumulative_lengths[j] > desired_length:
|
|
#print('found a greater length at vert %i' % j)
|
|
#this was supposed to save us some iterations so that
|
|
#we don't have to start at the beginning each time....
|
|
#if j >= 1:
|
|
#n = j - 1 #going one back allows us to space multiple verts on one edge
|
|
#else:
|
|
#n = 0
|
|
break
|
|
|
|
extra = desired_length - cumulative_lengths[j-1]
|
|
if j == len(verts):
|
|
new_verts[i + 1 + cyclic * -1] = verts[j-1] + extra * (verts[0]-verts[j-1]).normalized()
|
|
else:
|
|
new_verts[i + 1 + cyclic * -1] = verts[j-1] + extra * (verts[j]-verts[j-1]).normalized()
|
|
|
|
eds = []
|
|
|
|
for i in range(0,len(new_verts)-1):
|
|
eds.append((i,i+1))
|
|
if cyclic:
|
|
#close the loop
|
|
eds.append((i+1,0))
|
|
if debug:
|
|
print(cumulative_lengths)
|
|
print(arch_len)
|
|
print(eds)
|
|
|
|
return new_verts, eds
|
|
|
|
|
|
|
|
def delta_angles(vec_about, l_vecs):
|
|
'''
|
|
will find the difference betwen each element and the next element in the list
|
|
this is a foward difference. Eg delta[n] = item[n+1] - item[n]
|
|
|
|
deltas should add up to 2*pi
|
|
'''
|
|
|
|
v0 = l_vecs[0]
|
|
l_angles = [0] + [vector_angle_between(v0,v1,vec_about) for v1 in l_vecs[1:]]
|
|
|
|
L = len(l_angles)
|
|
|
|
deltas = [l_angles[n + 1] - l_angles[n] for n in range(0, L-1)] + [2*math.pi - l_angles[-1]]
|
|
return deltas
|
|
|
|
|
|
# https://rosettacode.org/wiki/Determine_if_two_triangles_overlap#C.2B.2B
|
|
def triangle2D_det(p0, p1, p2):
|
|
return p0.x * (p1.y - p2.y) + p1.x * (p2.y - p0.y) + p2.x * (p0.y - p1.y)
|
|
|
|
|
|
def triangle2D_boundary_collision_check(p0, p1, p2, eps):
|
|
return triangle2D_det(p0, p1, p2) < eps
|
|
|
|
|
|
def triangle2D_collision_check(p0, p1, p2, eps):
|
|
return triangle2D_det(p0, p1, p2) <= eps
|
|
|
|
|
|
def triangle2D_overlap(triangle0, triangle1, eps=0.0):
|
|
# XXX: needs testing!
|
|
_chk = triangle2D_collision_check
|
|
|
|
def chk(e0, e1, p0, p1, p2):
|
|
c = _chk(e0, e1, p0, eps)
|
|
c &= _chk(e0, e1, p1, eps)
|
|
c &= _chk(e0, e1, p2, eps)
|
|
return c
|
|
|
|
def chk_edges(a0, a1, a2, b0, b1, b2):
|
|
c = chk(a0, a1, b0, b1, b2)
|
|
c |= chk(a1, a2, b0, b1, b2)
|
|
c |= chk(a2, a0, b0, b1, b2)
|
|
return c
|
|
|
|
a0, a1, a2 = triangle0
|
|
b0, b1, b2 = triangle1
|
|
h0 = chk_edges(a0, a1, a2, b0, b1, b2)
|
|
h1 = chk_edges(b0, b1, b2, a0, a1, a2)
|
|
return not (h0 or h1)
|
|
|
|
|
|
def triangle2D_area(p0, p1, p2):
|
|
a = Vector((p0.x, p0.y, 0.0))
|
|
b = Vector((p1.x, p1.y, 0.0))
|
|
c = Vector((p2.x, p2.y, 0.0))
|
|
return (b - a).cross(c - a).length / 2
|
|
|
|
|
|
def segment2D_intersection(a0, a1, b0, b1):
|
|
p = intersection2d_line_line(a0, a1, b0, b1)
|
|
if not p: return None
|
|
(ax0, ay0), (ax1, ay1) = a0, a1
|
|
(bx0, by0), (bx1, by1) = b0, b1
|
|
px, py = p
|
|
if px < min(ax0, ax1) or px < min(bx0, bx1): return None
|
|
if px > max(ax0, ax1) or px > max(bx0, bx1): return None
|
|
if py < min(ay0, ay1) or py < min(by0, by1): return None
|
|
if py > max(ay0, ay1) or py > max(by0, by1): return None
|
|
return p
|
|
|
|
|
|
def clamp(v, min_v, max_v):
|
|
return max(min_v, min(max_v, v))
|
|
|
|
|
|
def mid(v0, v1, v2):
|
|
v0,v1 = min(v0,v1),max(v0,v1)
|
|
v1,v2 = min(v1,v2),max(v1,v2)
|
|
v0,v1 = min(v0,v1),max(v0,v1)
|
|
return v1
|
|
|
|
|
|
def intersection2d_line_line(p0, p1, p2, p3):
|
|
# https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
|
|
x0,y0 = p0
|
|
x1,y1 = p1
|
|
x2,y2 = p2
|
|
x3,y3 = p3
|
|
tn = (x0 - x2) * (y2 - y3) - (y0 - y2) * (x2 - x3)
|
|
td = (x0 - x1) * (y2 - y3) - (y0 - y1) * (x2 - x3)
|
|
if td == 0: return None
|
|
t = tn / td
|
|
return (x0 + t * (x1 - x0), y0 + t * (y1 - y0))
|
|
|
|
|
|
def closest2d_point_line(pt:Point2D, p0:Point2D, p1:Point2D):
|
|
d = Direction2D(p1 - p0)
|
|
v = Vec2D(pt - p0)
|
|
u = d * d.dot(v)
|
|
return Point2D(p0 + u)
|
|
|
|
def closest2d_point_segment(pt:Point2D, p0:Point2D, p1:Point2D):
|
|
dv = Vec2D(p1 - p0)
|
|
ld = dv.length
|
|
if abs(ld) <= 0.00001: return p0
|
|
dd = dv / ld
|
|
v = Vec2D(pt - p0)
|
|
p = Point2D(p0 + dd * clamp(dd.dot(v), 0, ld))
|
|
p.freeze()
|
|
return p
|
|
|
|
def closest_point_segment(pt:Point, p0:Point, p1:Point):
|
|
dv = Vec(p1 - p0)
|
|
ld = dv.length
|
|
if abs(ld) <= 0.00001: return p0
|
|
dd = dv / ld
|
|
v = Vec(pt - p0)
|
|
p = Point(p0 + dd * clamp(dd.dot(v), 0, ld))
|
|
p.freeze()
|
|
return p
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# run tests
|
|
p0 = Point((1, 2, 3))
|
|
p1 = Point((0, 0, 1))
|
|
v0 = Vec((1, 0, 0))
|
|
r = Ray(p0, v0)
|
|
mxt = XForm(Matrix.Translation((1, 2, 3)))
|
|
mxr = XForm(Matrix.Rotation(0.1, 4, Vector((0, 0, 1))))
|
|
mxtr = mxt * mxr
|
|
|
|
print('')
|
|
|
|
print(p1 - p0)
|
|
print(p0 + v0)
|
|
print(p0 * 2) # should be able to do this??
|
|
print(p0.copy())
|
|
print(r)
|
|
|
|
print("%s => %s" % (v0, mxt * v0))
|
|
print("%s => %s" % (p0, mxt * p0))
|
|
|
|
print("%s => %s" % (v0, mxr * v0))
|
|
print("%s => %s" % (p0, mxr * p0))
|
|
|
|
print('%s => %s => %s' % (r, mxtr * r, mxtr / (mxtr * r)))
|
|
|
|
print(mxr)
|
|
print(mxr.mx_p)
|