""" Geometry queries on the open station for placing props (build/furnish.py): rays and box clashes against what the scene shows, instances included (the ice coolers are collection instances), with one BVH per mesh built when first needed and every query narrowed to the objects whose world box it touches. g = Geo(skip=lambda ob: ...) # objects to leave out (the props being placed, helpers) g.ray(origin, direction, dist) -> (distance, point, normal, object name, material name) or None g.floor(x, y, z_from, below=3.0) -> (z, normal, name) of the first surface under (x, y) or None g.clash(matrix, lo, hi, pad) -> names of the meshes that cut into the box lo..hi (local) at matrix """ import math import bpy from mathutils import Matrix, Vector from mathutils.bvhtree import BVHTree def _aabb(pts): return (Vector((min(p.x for p in pts), min(p.y for p in pts), min(p.z for p in pts))), Vector((max(p.x for p in pts), max(p.y for p in pts), max(p.z for p in pts)))) def box_corners(lo, hi): return [Vector((x, y, z)) for x in (lo.x, hi.x) for y in (lo.y, hi.y) for z in (lo.z, hi.z)] BOX_FACES = [(0, 1, 3, 2), (4, 6, 7, 5), (0, 4, 5, 1), (2, 3, 7, 6), (0, 2, 6, 4), (1, 5, 7, 3)] class Geo(object): def __init__(self, skip=None, depsgraph=None): self.dg = depsgraph or bpy.context.evaluated_depsgraph_get() self.items = [] self._bvh = {} for inst in self.dg.object_instances: ob = inst.object if ob.type != "MESH": continue orig = ob.original if orig.hide_render or (skip and skip(orig)): continue if inst.is_instance and inst.parent is not None and skip and skip(inst.parent.original): continue mw = inst.matrix_world.copy() lo, hi = _aabb([mw @ Vector(c) for c in ob.bound_box]) key = orig.name if ob.modifiers else "ME:" + orig.data.name name = orig.name if not inst.is_instance else "%s (in %s)" % (orig.name, inst.parent.original.name) # the iterator's objects are temporary: keep the original and look its evaluated copy up when needed self.items.append((key, orig, mw, mw.inverted_safe(), lo, hi, name)) def mesh_of(self, orig): """The evaluated mesh of an original object (to_mesh_clear() it after use), or None.""" ev = orig.evaluated_get(self.dg) try: return ev, ev.to_mesh() except RuntimeError: # an evaluated object without geometry return ev, None def _tree(self, key, ob): t = self._bvh.get(key) if t is None: ev, me = self.mesh_of(ob) if me is None: self._bvh[key] = t = (None, [], []) return t verts = [v.co.copy() for v in me.vertices] polys = [tuple(p.vertices) for p in me.polygons] mats = [p.material_index for p in me.polygons] names = [s.material.name if s.material else "" for s in ev.material_slots] ev.to_mesh_clear() t = (BVHTree.FromPolygons(verts, polys, all_triangles=False) if polys else None, mats, names) self._bvh[key] = t return t def ray(self, origin, direction, dist=50.0): o = Vector(origin) d = Vector(direction).normalized() end = o + d * dist slo, shi = _aabb([o, end]) best = None for key, ob, mw, inv, lo, hi, name in self.items: if hi.x < slo.x or lo.x > shi.x or hi.y < slo.y or lo.y > shi.y or hi.z < slo.z or lo.z > shi.z: continue tree, mats, mnames = self._tree(key, ob) if tree is None: continue ol = inv @ o dl = (inv.to_3x3() @ d) ln = dl.length if ln < 1e-12: continue hit = tree.ray_cast(ol, dl / ln, dist * ln) if hit[0] is None: continue pw = mw @ hit[0] t = (pw - o).length if best is None or t < best[0]: nw = (inv.to_3x3().transposed() @ hit[1]).normalized() mi = mats[hit[2]] if hit[2] is not None and hit[2] < len(mats) else 0 best = (t, pw, nw, name, mnames[mi] if mi < len(mnames) else "") return best def floor(self, x, y, z_from, below=3.0): h = self.ray((x, y, z_from), (0, 0, -1), below) return None if h is None else (h[1].z, h[2], h[3]) def clash(self, matrix, lo, hi, pad=0.0, ignore=()): """Meshes whose faces cut into the box (lo..hi in the box's own space, shrunk by pad) placed at matrix, or that lie wholly inside it.""" lo = Vector(lo) + Vector((pad, pad, pad)) hi = Vector(hi) - Vector((pad, pad, pad)) cw = [matrix @ c for c in box_corners(lo, hi)] blo, bhi = _aabb(cw) out = [] for key, ob, mw, inv, ilo, ihi, name in self.items: if ihi.x < blo.x or ilo.x > bhi.x or ihi.y < blo.y or ilo.y > bhi.y or ihi.z < blo.z or ilo.z > bhi.z: continue if any(name.startswith(i) for i in ignore): continue tree = self._tree(key, ob)[0] if tree is None: continue local = [inv @ c for c in cw] box = BVHTree.FromPolygons(local, BOX_FACES, all_triangles=False) if tree.overlap(box): out.append(name) continue # wholly inside: the item's box inside ours (tested in the box's frame) binv = matrix.inverted_safe() ic = [binv @ (mw @ Vector(c)) for c in ob.evaluated_get(self.dg).bound_box] il, ih = _aabb(ic) if il.x >= lo.x and il.y >= lo.y and il.z >= lo.z and ih.x <= hi.x and ih.y <= hi.y and ih.z <= hi.z: out.append(name) return out def yaw_matrix(x, y, z, yaw_deg): return Matrix.Translation((x, y, z)) @ Matrix.Rotation(math.radians(yaw_deg), 4, "Z")