# Grouper: objects, rig, sockets, normals, collision, report (exec'd by build_grouper.py last). # Hidden faces go (critique round 1 m10): the fittings' faces wholly deeper than 15 mm under the skin (was 60 mm), and the hull # grid's faces that lie wholly inside the pods' nose and tail pieces (where the lobe sinks into the hull) or under the # collar's walls (between its bore and its skirt). # depth under the ACTUAL skin: the base skin's depth plus the level of the nearest grid vertex (the waist band is 30 mm # recessed, the grooves' floors 50 mm, the frame band 25 mm proud): a vent on the band is 24 mm under the base skin but on # the band's face from mathutils import kdtree as _kdt _KD = _kdt.KDTree(len(SKIN_PTS)) _LVS = list(SKIN_LV) # (round 3) the skin's own vertices and levels (gr_skin_build.py) for _q, _p in enumerate(SKIN_PTS): _KD.insert(_p, _q) _KD.balance() def depth_actual(p): d = inside_depth(p) if d < 0: return d co, idx, dist = _KD.find(Vector(p)) return d + _LVS[idx] REPORT["pruned_faces"] = prune_inside(att, depth_actual, keep_depth=0.015) def _in_pod_piece(p): x = ximg(p.y) if not (735.5 <= x <= 843.0) or 769.5 < x < 830.6: return False d = math.hypot(abs(p.x) - POD_L, p.z - POD_Z) return d < pod_r_true(min(x, 843.0)) - 0.008 # (critique round 2, M1: the skin under the collar is no longer pruned face by face; its opening is the grid's clean round # hole r 0.512, and the collar closes over it down to its skirt on the skin) _dead = [f for f in hull.bm.faces if all(_in_pod_piece(v.co) for v in f.verts)] bmesh.ops.delete(hull.bm, geom=_dead, context='FACES') _loose = [v for v in hull.bm.verts if not v.link_faces] if _loose: bmesh.ops.delete(hull.bm, geom=_loose, context='VERTS') REPORT["pruned_hull_faces"] = len(_dead) # critique round 2 (m3): a quad whose two triangles differ by more than 45 deg (either diagonal) is split into its two # triangles along the diagonal that bends less (the critique's fallback "where re-pinning cannot be done"): the pod/hull # fillet's groove walls, the frame band's corners round the front glass, the duct ring's first band at the stern, the collar # pockets' chamfers. The rest of every mesh stays quads. def split_twisted(bm, limit=45.0): fixed, alt = [], [] for f in bm.faces: if len(f.verts) != 4: continue a_, b_, c_, d_ = [v.co for v in f.verts] n1, n2 = (b_ - a_).cross(c_ - a_), (c_ - a_).cross(d_ - a_) n3, n4 = (b_ - a_).cross(d_ - a_), (c_ - b_).cross(d_ - b_) if min(n.length for n in (n1, n2, n3, n4)) < 1e-14: continue t02, t13 = math.degrees(n1.angle(n2)), math.degrees(n3.angle(n4)) if max(t02, t13) <= limit: continue (fixed if t02 <= t13 else alt).append(f) if fixed: bmesh.ops.triangulate(bm, faces=fixed, quad_method='FIXED', ngon_method='BEAUTY') if alt: bmesh.ops.triangulate(bm, faces=alt, quad_method='ALTERNATE', ngon_method='BEAUTY') return len(fixed) + len(alt) REPORT["twisted_quads_split"] = {nm: split_twisted(mb_.bm) for nm, mb_ in (("skin", hull), ("frames", frames), ("collar", coll_mb), ("attached", att))} def finish(mb, origin=None): return mb.build(C_ROOT, parent=root, recalc=False, origin=origin) def pivot_at(o, p): o.matrix_parent_inverse.identity() o.location = p o_hull = finish(hull) o_frames = finish(frames) o_coll = finish(coll_mb) o_att = finish(att) o_hfx = finish(hinge_fx) o_dst = finish(door_static) o_glass = finish(glass) o_cab = finish(cab) o_hatch = finish(hatch, origin=HATCH_HINGE); pivot_at(o_hatch, HATCH_HINGE) o_rotor = finish(rotor, origin=ROT_C); pivot_at(o_rotor, ROT_C) o_ant = finish(antenna, origin=A_FOOT); pivot_at(o_ant, A_FOOT) o_doors = {} for sg, (mbd, ax) in DOORS.items(): od = finish(mbd, origin=ax) pivot_at(od, ax) o_doors[sg] = od def drive(o, path, index, prop, expr, lo, hi, desc, default=0.0): if prop not in root.keys(): root[prop] = default root.id_properties_ui(prop).update(min=lo, max=hi, description=desc) fc = o.driver_add(path, index) dr = fc.driver dr.type = 'SCRIPTED' v = dr.variables.new(); v.name = "p"; v.type = 'SINGLE_PROP' v.targets[0].id = root; v.targets[0].data_path = '["%s"]' % prop dr.expression = expr # the rig: simple-expression drivers (the user's Blender runs no Python in files) drive(o_hatch, "rotation_euler", 0, "hatch_open_deg", "p*0.0174533", 0.0, HATCH_MAX, "hatch in the docking collar: 0 shut .. %d deg open, hinged at its forward edge (lifts its aft edge)" % HATCH_MAX) for sg, od in o_doors.items(): tag = "l" if sg > 0 else "r" drive(od, "rotation_euler", 2, "door_%s_deg" % tag, ("p" if sg > 0 else "-p") + "*0.0174533", 0.0, DOOR["open_deg"][1], "%s side door: 0 shut .. 95 deg open, vertical hinge at its aft edge" % ("port" if sg > 0 else "starboard")) drive(o_rotor, "rotation_euler", 1, "rotor_turns", "p*6.283185", -1e6, 1e6, "stern thruster rotor: turns about the long axis") drive(o_ant, "rotation_euler", 0, "antenna_fold_deg", "-p*0.0174533", 0.0, 70.0, "antenna: 0 as drawn (raked 23 deg aft) .. 70 deg folded aft onto the deck (fold it for docking)") root["lights"] = 1.0 root.id_properties_ui("lights").update(min=0.0, max=1.0, description="emission of the strips, lamps, scanner and screens (surfaces pass)") o_hatch["hinge_axis"] = "X" o_ant["hinge_axis"] = "X" for od in o_doors.values(): od["hinge_axis"] = "Z" o_rotor["spin_axis"] = "Y" # sockets for the game DOOR_C = {sg: on_side(sg, 0.5 * (DOOR_YA + DOOR_YB), 0.5 * (Zi(DOOR["y"][0]) + Zi(DOOR["y"][1])), 0.30)[0] for sg in (1, -1)} SOCK = {"SOCKET_Clamp": Vector((0.0, Y_CL, 1.94)), "SOCKET_Seat_L_Eye": SEAT_EYES[1], "SOCKET_Seat_R_Eye": SEAT_EYES[-1], "SOCKET_Hatch": Vector((0.0, Y_CL, 1.45)), "SOCKET_Thrust": Vector((0.0, Y_LIP + 0.05, DUCT_AX.z)), "SOCKET_Scanner": SCANNER_C, "SOCKET_Door_L": DOOR_C[1], "SOCKET_Door_R": DOOR_C[-1]} for p_, sg in LAMP_HEADS: SOCK["SOCKET_Lamp_" + ("L" if sg > 0 else "R")] = p_ for p_, sg in POD_LAMPS: SOCK["SOCKET_PodLamp_" + ("L" if sg > 0 else "R")] = p_ for p_, sg in DRUM_LAMPS: SOCK["SOCKET_DrumLamp_" + ("L" if sg > 0 else "R")] = p_ for nm, p in SOCK.items(): e = bpy.data.objects.new(nm, None) C_SOCK.objects.link(e) e.parent = root e.location = p e.empty_display_size = 0.15 # (round 3) the parts' triangles before they are joined (the budget: the hull LOD0 about 60k) REPORT["hull_parts_tris"] = {o_.name: sum(len(p_.vertices) - 2 for p_ in o_.data.polygons) for o_ in (o_hull, o_frames, o_coll, o_att, o_hfx, o_dst)} # one static exterior mesh: skin, frames, collar, fittings, hinge brackets (glass, cabin and the moving parts stay apart) with bpy.context.temp_override(active_object=o_hull, selected_editable_objects=[o_hull, o_frames, o_coll, o_att, o_hfx, o_dst]): bpy.ops.object.join() o_hull.name = "SM_Grouper_Hull" # the skin's normals from the analytic surface: faces the hull pass flagged on_skin take, at each corner, the normal the # build stored on the vertex (an_x/an_y/an_z); steps, frames, sleeves and fittings keep their own me = o_hull.data cn = [Vector(c.vector) for c in me.corner_normals] fl = me.attributes.get("on_skin") an = [me.attributes.get(n) for n in ("an_x", "an_y", "an_z")] n_set = 0 if fl is not None and all(a is not None for a in an): onf = [d.value for d in fl.data] ax_ = [[d.value for d in a.data] for a in an] lv_ = [l.vertex_index for l in me.loops] for p in me.polygons: if onf[p.index] != 1: continue for li in range(p.loop_start, p.loop_start + p.loop_total): vi = lv_[li] n = Vector((ax_[0][vi], ax_[1][vi], ax_[2][vi])) if n.length > 0.5 and n.dot(p.normal) > 0.5: cn[li] = n.normalized() n_set += 1 me.normals_split_custom_set(cn) for nm in ("on_skin", "an_x", "an_y", "an_z", "crisp"): # build-time data: not exported a = me.attributes.get(nm) if a is not None: me.attributes.remove(a) REPORT["custom_normal_corners"] = n_set exec(open(os.path.join(HERE, "gr_collision.py")).read()) REPORT["ucx"] = REPORT_UCX allp = [] for o in C_ROOT.all_objects: if o.type == 'MESH': allp += [o.matrix_world @ v.co for v in o.data.vertices] mn = Vector((min(p.x for p in allp), min(p.y for p in allp), min(p.z for p in allp))) mx = Vector((max(p.x for p in allp), max(p.y for p in allp), max(p.z for p in allp))) REPORT["bounds_min"] = [round(c, 3) for c in mn] REPORT["bounds_max"] = [round(c, 3) for c in mx] REPORT["dimensions"] = [round(c, 3) for c in (mx - mn)] REPORT["audit"] = [audit(o) for o in C_ROOT.all_objects if o.type == 'MESH'] # the cabin stays inside: no vertex of it (the lining itself excepted) within 5 mm of the skin (or of the glass, sunk 22 mm) _lin = o_cab.data.attributes.get("is_lining") _isl = [d.value for d in _lin.data] if _lin is not None else [0] * len(o_cab.data.vertices) _out = [o_cab.matrix_world @ v.co for v in o_cab.data.vertices if not _isl[v.index]] _bad = [p for p in _out if inside_depth(p) < GLASS_D + 0.005] if _lin is not None: o_cab.data.attributes.remove(_lin) REPORT["cabin_verts_near_or_outside_skin"] = len(_bad) REPORT["cabin_worst_points"] = [[round(c, 3) for c in p] for p in sorted(_bad, key=lambda q: inside_depth(q))[:8]] REPORT["build_seconds"] = round(time.time() - t_start, 2) json.dump(REPORT, open(os.path.join(HERE, "build_report.json"), "w"), indent=1, default=str) # (round 3) the skin's structure and the openings' outlines for build/gr_topo_check.py if "SKIN_STRUCT" in globals(): SKIN_STRUCT["trims"] = {k: [[round(c, 6) for c in p_] for p_ in v["p"]] for k, v in TRIM_OUTLINES.items()} SKIN_STRUCT["trim_normals"] = {k: [[round(c, 5) for c in p_] for p_ in v["n"]] for k, v in TRIM_OUTLINES.items()} json.dump(SKIN_STRUCT, open(os.path.join(HERE, "checks", "skin_struct.json"), "w")) for ob in C_ROOT.all_objects: # drivers made in this run can stay flagged invalid until re-set if ob.animation_data: for fc in ob.animation_data.drivers: fc.driver.expression = fc.driver.expression fc.driver.is_valid = True