225 lines
9.0 KiB
Python
225 lines
9.0 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, isfinite
|
|
from typing import List
|
|
from itertools import chain
|
|
from concurrent.futures import ProcessPoolExecutor
|
|
|
|
import gpu
|
|
from mathutils import Matrix, Vector, Quaternion
|
|
from bmesh.types import BMVert
|
|
from mathutils.geometry import intersect_line_plane, intersect_point_tri
|
|
|
|
from .maths import zero_threshold, BBox2D, Point2D, clamp, Vec2D, Vec, mid
|
|
|
|
from .colors import colorname_to_color
|
|
from .decorators import stats_wrapper, blender_version_wrapper
|
|
from .profiler import profiler, time_it
|
|
|
|
from ..terminal import term_printer
|
|
|
|
|
|
class SimpleVert:
|
|
def __init__(self, co):
|
|
self.co = co
|
|
self.normal = Vec((0, 0, 0))
|
|
self.is_valid = True
|
|
|
|
class SimpleEdge:
|
|
def __init__(self, verts):
|
|
self.verts = verts
|
|
self.p0 = verts[0].co
|
|
self.p1 = verts[1].co
|
|
self.v01 = self.p1 - self.p0
|
|
self.l = self.v01.length
|
|
self.d01 = self.v01 / max(self.l, zero_threshold)
|
|
self.is_valid = True
|
|
def closest(self, p):
|
|
v0p = p - self.p0
|
|
d = self.d01.dot(v0p)
|
|
return self.p0 + self.d01 * mid(d, 0, self.l)
|
|
|
|
class Accel2D:
|
|
margin = 0.001
|
|
DEBUG = False
|
|
|
|
# @staticmethod
|
|
# def simple_verts(label, lco, Point_to_Point2Ds):
|
|
# verts = [ SimpleVert(co) for co in lco ]
|
|
# return Accel2D(label, verts, [], [], Point_to_Point2Ds)
|
|
|
|
@staticmethod
|
|
def simple_edges(label, edges, Point_to_Point2Ds):
|
|
edges = [ SimpleEdge(( SimpleVert(co0), SimpleVert(co1) )) for (co0, co1) in edges ]
|
|
verts = [ co for e in edges for co in e.verts ]
|
|
return Accel2D(label, verts, edges, [], Point_to_Point2Ds)
|
|
|
|
def _insert_edge(self, edge):
|
|
pts_list = zip(*[ self.Point_to_Point2Ds(v.co, v.normal) for v in edge.verts ])
|
|
for co0, co1 in pts_list:
|
|
(i0, j0), (i1, j1) = self.compute_ij(co0), self.compute_ij(co1)
|
|
mini, minj, maxi, maxj = min(i0, i1), min(j0, j1), max(i0, i1), max(j0, j1)
|
|
for i in range(mini, maxi + 1):
|
|
for j in range(minj, maxj + 1):
|
|
self._put((i, j), edge)
|
|
|
|
# @profiler.function
|
|
def __init__(self, label, verts, edges, faces, Point_to_Point2Ds):
|
|
self.verts = list(verts) if verts else []
|
|
self.edges = list(edges) if edges else []
|
|
self.faces = list(faces) if faces else []
|
|
self.Point_to_Point2Ds = Point_to_Point2Ds
|
|
|
|
vert_type, edge_type, face_type = ( type(elems[0] if elems else None) for elems in [self.verts, self.edges, self.faces] )
|
|
self._is_vert = lambda elem: isinstance(elem, vert_type)
|
|
self._is_edge = lambda elem: isinstance(elem, edge_type)
|
|
self._is_face = lambda elem: isinstance(elem, face_type)
|
|
self.bins = {}
|
|
|
|
# collect all involved pts so we can find bbox
|
|
with time_it('collect', enabled=Accel2D.DEBUG):
|
|
bbox = BBox2D()
|
|
with time_it('collect verts', enabled=Accel2D.DEBUG):
|
|
bbox.insert_points(pt for v in verts for pt in Point_to_Point2Ds(v.co, v.normal))
|
|
with time_it('collect edges and faces', enabled=Accel2D.DEBUG):
|
|
bbox.insert_points(
|
|
pt
|
|
for ef in chain(edges, faces)
|
|
for ef_pts in zip(*[Point_to_Point2Ds(v.co, v.normal) for v in ef.verts])
|
|
for pt in ef_pts
|
|
)
|
|
if bbox.count == 0:
|
|
bbox.insert(Point2D((0,0)))
|
|
|
|
tot_points = len(self.verts) + 2 * len(self.edges) + sum(len(f.verts) for f in self.faces)
|
|
|
|
self.min = Point2D((bbox.mx - self.margin, bbox.my - self.margin))
|
|
self.max = Point2D((bbox.Mx + self.margin, bbox.My + self.margin))
|
|
self.size = self.max - self.min # includes margin
|
|
self.sizex, self.sizey = self.size
|
|
self.minx, self.miny = self.min
|
|
self.bin_len = ceil(sqrt(tot_points) + 0.1)
|
|
|
|
# Accel2D.debug variables
|
|
tot_inserted = 0
|
|
max_spread = (1, 1, 1)
|
|
|
|
# inserting verts
|
|
with time_it('insert verts', enabled=Accel2D.DEBUG):
|
|
for v in verts:
|
|
for pt in Point_to_Point2Ds(v.co, v.normal):
|
|
tot_inserted += 1
|
|
i, j = self.compute_ij(pt)
|
|
self._put((i, j), v)
|
|
|
|
# inserting edges and faces
|
|
with time_it('insert edges and faces', enabled=Accel2D.DEBUG):
|
|
for e in edges:
|
|
self._insert_edge(e)
|
|
for ef in faces:
|
|
ef_pts_list = zip(*[Point_to_Point2Ds(v.co, v.normal) for v in ef.verts])
|
|
for ef_pts in ef_pts_list:
|
|
tot_inserted += 1
|
|
bbox2 = BBox2D((self.compute_ij(pt) for pt in ef_pts))
|
|
mini, minj, maxi, maxj = int(bbox2.mx), int(bbox2.my), int(bbox2.Mx), int(bbox2.My)
|
|
sizei, sizej = maxi - mini + 1, maxj - minj + 1
|
|
if (spread := sizei*sizej) > max_spread[0]: max_spread = (spread, sizei, sizej)
|
|
for i in range(mini, maxi + 1):
|
|
for j in range(minj, maxj + 1):
|
|
self._put((i, j), ef)
|
|
|
|
if Accel2D.DEBUG:
|
|
# debug reporting
|
|
def get_index(s, v, m, M): return clamp(int(len(s) * (v - m) / max(1, M - m)), 0, len(s) - 1)
|
|
fill_max = max((len(b) for b in self.bins.values()), default=0)
|
|
fill_min = min((len(b) for b in self.bins.values()), default=0)
|
|
distribution = [0] * min(100, self.bin_len * self.bin_len)
|
|
for b in self.bins.values():
|
|
distribution[get_index(distribution, len(b), fill_min, fill_max)] += 1
|
|
filling_max = max(distribution)
|
|
chars = '_▁▂▃▄▅▆▇█' # https://en.wikipedia.org/wiki/Block_Elements
|
|
def get_char(v): return chars[get_index(chars, v, 0, filling_max)] if v else ' '
|
|
distribution = ''.join(get_char(v) for v in distribution)
|
|
term_printer.boxed(
|
|
f'Counts: v={len(self.verts)} e={len(self.edges)} f={len(self.faces)}',
|
|
f' total pts={tot_points}, bbox ins={bbox.count}, accel ins={tot_inserted}',
|
|
f'Size: min={self.min}, max={self.max} size={self.size}',
|
|
f'Bins: {self.bin_len}x{self.bin_len} non-zero={len(self.bins)}/{self.bin_len*self.bin_len} ({100*len(self.bins)/(self.bin_len*self.bin_len):0.0f}%)',
|
|
f'Inserts: total={tot_inserted}, max spread={max_spread}',
|
|
f'Fill: {fill_min} [{distribution}] {fill_max}',
|
|
title=f'Accel2D: {label}', color='black', highlight='green',
|
|
)
|
|
|
|
# @profiler.function
|
|
def compute_ij(self, v2d):
|
|
bl = self.bin_len
|
|
return (
|
|
clamp(int(bl * (v2d.x - self.minx) / self.sizex), 0, bl - 1),
|
|
clamp(int(bl * (v2d.y - self.miny) / self.sizey), 0, bl - 1)
|
|
)
|
|
|
|
def _put(self, ij, o):
|
|
# assert 0 <= ij[0] < self.bin_len and 0 <= ij[1] < self.bin_len, f'{ij} is outside {self.bin_len}x{self.bin_len}'
|
|
if ij in self.bins: self.bins[ij].add(o)
|
|
else: self.bins[ij] = { o }
|
|
|
|
def _get(self, ij):
|
|
return self.bins[ij] if ij in self.bins else set()
|
|
|
|
# @profiler.function
|
|
def clean_invalid(self):
|
|
self.bins = {
|
|
t: {o for o in objs if o.is_valid}
|
|
for (t, objs) in self.bins.items()
|
|
}
|
|
|
|
# @profiler.function
|
|
def get(self, v2d, within, *, fn_filter=None):
|
|
if v2d is None or not (isfinite(v2d.x) and isfinite(v2d.y)): return set()
|
|
delta = Vec2D((within, within))
|
|
p0, p1 = v2d - delta, v2d + delta
|
|
i0, j0 = self.compute_ij(p0)
|
|
i1, j1 = self.compute_ij(p1)
|
|
ret = {
|
|
elem
|
|
for i in range(i0, i1+1)
|
|
for j in range(j0, j1+1)
|
|
for elem in self._get((i, j))
|
|
if elem.is_valid and (fn_filter is None or fn_filter(elem))
|
|
}
|
|
return ret
|
|
|
|
# @profiler.function
|
|
def get_verts(self, v2d, within):
|
|
return self.get(v2d, within, fn_filter=self._is_vert)
|
|
|
|
# @profiler.function
|
|
def get_edges(self, v2d, within):
|
|
return self.get(v2d, within, fn_filter=self._is_edge)
|
|
|
|
# @profiler.function
|
|
def get_faces(self, v2d, within):
|
|
return self.get(v2d, within, fn_filter=self._is_face)
|
|
|