""" The ice against what it must not touch: the moored vessel and the station's rooms. And the new meshes' own state. blender -b --factory-startup Anchorage_Station_exterior.blend --python build/check_site.py vessel no triangle of the floe or of a serac may cut a triangle of the vessel; the smallest gap is given rooms the ice must lie under every kit piece that reaches below floor level (Relay Control's sunken ring) meshes of every new mesh: quads / triangles, open edges, edges shared by more than two faces, UVs """ import bpy import numpy as np from mathutils import Vector from mathutils.bvhtree import BVHTree sc = bpy.context.scene dg = bpy.context.evaluated_depsgraph_get() def world_mesh(me, mw): co = np.empty(len(me.vertices) * 3) me.vertices.foreach_get("co", co) m = np.array(mw) w = co.reshape(-1, 3) @ m[:3, :3].T + m[:3, 3] return [tuple(v) for v in w], [tuple(p.vertices) for p in me.polygons] ice = [ob for ob in sc.objects if ob.type == "MESH" and ob.get("anchorage_surroundings")] floe = bpy.data.objects.get("ANCH_Ice_Mass") if floe is None: raise RuntimeError("no ANCH_Ice_Mass in this file") ice_bvh = [] # against the vessel: everything that stands round the station, and the outpost's own armour and roof works near = ice + [ob for ob in sc.objects if ob.type == "MESH" and not ob.get("anchorage_surroundings") and str(ob.get("anchorage_module", "")).startswith("outpost")] for ob in near: v, f = world_mesh(ob.data, ob.matrix_world) ice_bvh.append((ob.name, BVHTree.FromPolygons(v, f), v)) fv, ff = world_mesh(floe.data, floe.matrix_world) floe_bvh = BVHTree.FromPolygons(fv, ff) # ---- the vessel -------------------------------------------------------------------------------------------- cuts = 0 gap = (1e9, None, None) n_ship = 0 ship_bvh = [] for inst in dg.object_instances: ob = inst.object if ob.type != "MESH" or not inst.is_instance or inst.parent is None or "CANTICLE" not in inst.parent.name: continue n_ship += 1 v, f = world_mesh(ob.data, inst.matrix_world) bvh = BVHTree.FromPolygons(v, f) for name, ib, iv in ice_bvh: pairs = ib.overlap(bvh) if pairs: cuts += len(pairs) print("[site] CUT: %s cuts the vessel's %s in %d triangle pairs" % (name, ob.name, len(pairs))) # the smallest gap between this part and the floe, from every point of the part (and, further down, from every # point of the floe's rim to the vessel: a gap can be smallest between a point of one and a face of the other) for p in v: loc, nrm, idx, d = floe_bvh.find_nearest(Vector(p), 3.0) if loc is not None and d < gap[0]: gap = (d, ob.name, tuple(round(float(c), 2) for c in p)) ship_bvh.append(bvh) back = (1e9, None) for p in fv: if p[1] > -33.0: continue # only the floe's south side lies near the vessel for bvh in ship_bvh: loc, nrm, idx, d = bvh.find_nearest(Vector(p), 3.0) if loc is not None and d < back[0]: back = (d, tuple(round(float(c), 2) for c in p)) print("[site] vessel: %d parts checked against %d ice objects: %d cuts; smallest gap %.2f m from the vessel's points " "(%s at %s), %.2f m from the floe's points (at %s)" % (n_ship, len(ice_bvh), cuts, gap[0], gap[1], gap[2], back[0], back[1])) # ---- the rooms --------------------------------------------------------------------------------------------- low = 0 worst = (1e9, None) poke = 0 for ob in sc.objects: if ob.type != "MESH" or not ob.data.get("ue_mesh") or ob.get("anchorage_surroundings") or ob.get("ue_decal_material") is not None: continue if not ob.get("anchorage_zone") or min((ob.matrix_world @ Vector(c)).z for c in ob.bound_box) > -0.45: continue low += 1 v, f = world_mesh(ob.data, ob.matrix_world) for p in v: if p[2] > -0.40: continue hit = floe_bvh.ray_cast(Vector((p[0], p[1], p[2] + 0.001)), Vector((0, 0, -1)), 30.0) if hit[0] is None: # nothing below this point: the ice is above it, or it is outside the floe up = floe_bvh.ray_cast(Vector((p[0], p[1], p[2])), Vector((0, 0, 1)), 5.0) if up[0] is not None and up[0].z < 0.0: poke += 1 if poke <= 5: print("[site] ICE ABOVE A ROOM PIECE: %s at (%.2f %.2f %.2f), ice top there %.2f" % (ob.name, p[0], p[1], p[2], up[0].z)) continue d = p[2] - hit[0].z if d < worst[0]: worst = (d, "%s at (%.2f %.2f %.2f)" % (ob.data.name, p[0], p[1], p[2])) print("[site] rooms: %d kit pieces reach below floor level; points with ice above them: %d; the ice is at least %.2f m under them (%s)" % ( low, poke, worst[0], worst[1])) # ---- what stands on the ice stands on ice ------------------------------------------------------------------ # seracs, blocks and drift ice: under the middle and the four corners of each one's footprint there must be ice, # and its top (not the cliff or the sea): the floe's edge wanders, and nothing may hang over it off = 0 n_on = 0 site_off = 0 n_site = 0 for ob in ice: if ob is floe: continue if str(ob.get("anchorage_module", "")).startswith("outpost"): # the outpost's site pieces: each one's own place must be on the ice (the sea buoys are meant to float) if ob.get("anchorage_custom_mesh") or "buoy" in ob.data.name.lower() and "life" not in ob.data.name.lower(): continue n_site += 1 p = ob.matrix_world.translation hit = floe_bvh.ray_cast(Vector((p.x, p.y, 30.0)), Vector((0, 0, -1)), 60.0) if hit[0] is None or hit[0].z < -1.2: site_off += 1 print("[site] OFF THE ICE: %s at (%.1f %.1f)" % (ob.name, p.x, p.y)) continue pts = [ob.matrix_world @ Vector(c) for c in ob.bound_box] x0, x1 = min(p.x for p in pts), max(p.x for p in pts) y0, y1 = min(p.y for p in pts), max(p.y for p in pts) n_on += 1 for x, y in (((x0 + x1) / 2, (y0 + y1) / 2), (x0, y0), (x1, y0), (x1, y1), (x0, y1)): hit = floe_bvh.ray_cast(Vector((x, y, 30.0)), Vector((0, 0, -1)), 60.0) if hit[0] is None or hit[0].z < -1.2 or hit[1].z < 0.6: off += 1 print("[site] OVER THE EDGE: %s at (%.1f %.1f): its footprint reaches (%.1f %.1f), where there is %s" % ( ob.name, (x0 + x1) / 2, (y0 + y1) / 2, x, y, "no ice" if hit[0] is None else "the cliff or the moat")) break print("[site] on the ice: %d seracs, blocks and patches of drift ice; %d reach over the floe's edge" % (n_on, off)) print("[site] the outpost's site: %d pieces placed on the ice; %d of them not over ice" % (n_site, site_off)) # ---- the floe's size, and the ice coolers on their pads ------------------------------------------------------ fx = [p[0] for p in fv] fy = [p[1] for p in fv] print("[site] the floe: %.1f x %.1f m (x %.1f..%.1f, y %.1f..%.1f), %d faces" % ( max(fx) - min(fx), max(fy) - min(fy), min(fx), max(fx), min(fy), max(fy), len(ff))) # the coolers only: the furnishing's props (build/furnish.py) are instances of Fab models too coolers = [ob for ob in sc.objects if ob.type == "EMPTY" and ob.get("fab_prop") == "scifiEngine_005" and ob.instance_collection is not None] c_bad, c_worst = 0, 0.0 for ob in coolers: # the platform's footprint (the model's "COOLER base..." parts), in the world pts = [ob.matrix_world @ (o.matrix_world @ Vector(c)) for o in ob.instance_collection.all_objects if o.type == "MESH" and o.name.startswith("COOLER base") for c in o.bound_box] if not pts: print("[site] COOLER %s: no platform in its model" % ob.name) c_bad += 1 continue zf = min(p.z for p in pts) x0, x1 = min(p.x for p in pts), max(p.x for p in pts) y0, y1 = min(p.y for p in pts), max(p.y for p in pts) for x, y in (((x0 + x1) / 2, (y0 + y1) / 2), (x0, y0), (x1, y0), (x1, y1), (x0, y1)): hit = floe_bvh.ray_cast(Vector((x, y, 30.0)), Vector((0, 0, -1)), 60.0) gapz = 99.0 if hit[0] is None else abs(hit[0].z - zf) c_worst = max(c_worst, gapz) if gapz > 0.03: c_bad += 1 print("[site] COOLER %s: under (%.1f %.1f) the ice is %s its platform's foot (z %.2f)" % ( ob.name, x, y, "missing" if hit[0] is None else "%.2f m off" % gapz, zf)) print("[site] ice coolers: %d on the floe, each platform's corners and middle on level ice (worst %.3f m); %d faults" % ( len(coolers), c_worst if coolers else 0.0, c_bad)) # ---- the walls stand in the ice ---------------------------------------------------------------------------- # every outer wall of the shell goes down below the top of the ice; where the ice falls away (at the floe's edge, in # the moat) the foot of a wall must not come out of it foot = (1e9, None) bare = 0 n_foot = 0 for ob in sc.objects: if ob.type != "MESH" or not ob.get("anchorage_custom_mesh") or ob.get("anchorage_surroundings"): continue if not ob.name.startswith(("ANCH_Shell_", "ANCH_Decks")): continue v, f = world_mesh(ob.data, ob.matrix_world) feet = {} for p in v: # the lowest point of the shell at every place it has one key = (round(p[0], 2), round(p[1], 2)) if key not in feet or p[2] < feet[key][2]: feet[key] = p for p in feet.values(): if p[2] > -0.75: continue # this one does not go into the ice at all (roofs, ledges) hit = floe_bvh.ray_cast(Vector((p[0], p[1], 3.0)), Vector((0, 0, -1)), 30.0) if hit[0] is None: bare += 1 if bare <= 5: print("[site] WALL FOOT OVER WATER: %s at (%.2f %.2f %.2f)" % (ob.name, p[0], p[1], p[2])) continue n_foot += 1 cover = hit[0].z - p[2] if cover < foot[0]: foot = (cover, "%s at (%.2f %.2f %.2f), ice top %.2f" % (ob.name, p[0], p[1], p[2], hit[0].z)) print("[site] walls: %d wall feet checked, %d with no ice under them; the ice covers every foot by at least %.2f m (%s)" % ( n_foot, bare, foot[0], foot[1])) # ---- the new meshes ---------------------------------------------------------------------------------------- for ob in sorted(sc.objects, key=lambda o: o.name): if ob.type != "MESH" or not ob.get("anchorage_custom_mesh"): continue me = ob.data sizes = {} for p in me.polygons: sizes[len(p.vertices)] = sizes.get(len(p.vertices), 0) + 1 use = {} for p in me.polygons: for k in p.edge_keys: use[k] = use.get(k, 0) + 1 open_e = sum(1 for n in use.values() if n == 1) multi = sum(1 for n in use.values() if n > 2) zero = sum(1 for p in me.polygons if p.area < 1e-8) first_inner = me.get("anchorage_inner_from") print("[site] mesh %-22s %5d faces (%s)%s, %d tris; open edges %d, edges with more than 2 faces %d, zero-area faces %d; uv %s; materials %s" % ( ob.name, len(me.polygons), ", ".join("%d-gons %d" % (k, v) for k, v in sorted(sizes.items())), ", half of them the inner skin" if first_inner is not None else "", sum(len(p.vertices) - 2 for p in me.polygons), open_e, multi, zero, "yes" if me.uv_layers else "NO", [m.get("ue_material", "?").split("/")[-1] for m in me.materials]))