''' 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 . ''' 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 '' % (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 '' % (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 '' % (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 '' % (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 '' % (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 '' % (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 '' % (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 '' % (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 '' % (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 '(%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 '' % ( 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 '' % ( 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 '' % 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 '' % ( 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'' 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 = ' 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 '' % (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\d+(?P[.]\d+)?)(?P[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)