# Grouper: objects, rig, sockets, normals, collision, report (exec'd by build_grouper.py last). REPORT["pruned_faces"] = prune_inside(att, inside_depth, keep_depth=0.06) 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 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 # 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) 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