"""Grouper: a Tender after the user's blueprint sheet (reference/user_blueprint.jpg, "DEEP SEA EXPLORER - SUBTUB CLASS, ORTHOGRAPHIC BLUEPRINT DS-ST-001"), 2026-10-22: "create me 3d model of this tender from this blueprint exactly", with the user's concept render (reference/user_concept.jpg, "Check this picture as a referance") for the look. "Grouper" is a working name; the user has not named it. The blueprint rules the shape (SIDE first), the concept the look. Every number is in gr_measure.py (the Designer's), the design in design/DESIGN.md. Axes as the Kazah and every Tender: 1 unit = 1 m, nose to -Y, port +X, Z up, origin on the centre line 9 cm behind the docking clamp's axis. No booleans; quads (a few triangles only where a cap must close). The hull is one quad grid whose columns are pinned to the measured lines (gr_hull.py); openings are regions of whole grid faces with swept trims; fittings are lofted or revolved quads with quad caps, pruned where they sink into the hull. Runs headless (run_build_headless.py); it rebuilds only its own collections. GROUPER_STAGES (environment): a comma list to stop early, e.g. "hull" (default: everything)""" import bpy, bmesh, math, os, sys, json, time from mathutils import Vector, Matrix, Euler HERE = r"D:\HADAL - The Descent\models\tender-grouper\build" exec(open(os.path.join(HERE, "gr_lib.py")).read()) exec(open(os.path.join(HERE, "gr_measure.py")).read()) OUT = os.path.dirname(HERE) t_start = time.time() REPORT = {} PFX = "GROUPER | " STAGES = os.environ.get("GROUPER_STAGES", "all") for cname in (PFX + "Tender (exported)", PFX + "Collision (UCX)", PFX + "Sockets"): c = bpy.data.collections.get(cname) if c: for o in list(c.all_objects): bpy.data.objects.remove(o) bpy.data.collections.remove(c) for me in list(bpy.data.meshes): if me.users == 0: bpy.data.meshes.remove(me) # ================================================================== materials (flat looks; the surfaces pass dresses them). # The concept rules the look (gr_measure.py MATERIALS): dark gunmetal, near-black titanium frames, clear acrylic, cyan strips. GUN = MAT("M_Grouper_Gunmetal", (0.10, 0.11, 0.12), metal=0.9, rough=0.36, coat=0.2) BAND = MAT("M_Grouper_Band", (0.07, 0.075, 0.08), metal=0.85, rough=0.45) FRAME = MAT("M_Grouper_FrameDark", (0.035, 0.04, 0.045), metal=1.0, rough=0.34) ACRYL = MAT("M_Grouper_Acrylic", (0.80, 0.92, 0.92), rough=0.03) STRIP = MAT("M_Grouper_Strip", (0.02, 0.05, 0.06), rough=0.2, emit=(0.0, 0.85, 1.0), emit_str=4.0) TRACE = MAT("M_Grouper_Trace", (0.02, 0.05, 0.06), rough=0.3, emit=(0.0, 0.85, 1.0), emit_str=2.5) LAMPW = MAT("M_Grouper_LampWhite", (0.9, 0.9, 0.88), rough=0.2, emit=(1.0, 0.95, 0.85), emit_str=8.0) SCAN = MAT("M_Grouper_ScannerLens", (0.25, 0.55, 0.85), rough=0.1, emit=(0.25, 0.65, 1.0), emit_str=5.0) DUCTG = MAT("M_Grouper_DuctGlow", (0.04, 0.06, 0.07), metal=0.6, rough=0.4, emit=(0.0, 0.85, 1.0), emit_str=1.5) RECESS = MAT("M_Grouper_Recess", (0.02, 0.02, 0.02), rough=0.6) MARK = MAT("M_Grouper_Marking", (0.75, 0.77, 0.78), rough=0.4) CABL = MAT("M_Grouper_Cabin", (0.12, 0.12, 0.13), rough=0.6) CABF = MAT("M_Grouper_CabinFloor", (0.08, 0.085, 0.09), rough=0.6, metal=0.6) SEAT = MAT("M_Grouper_Seat", (0.08, 0.08, 0.085), rough=0.55) SCREEN = MAT("M_Grouper_Screen", (0.01, 0.03, 0.04), rough=0.15, emit=(0.1, 0.85, 1.0), emit_str=3.0) CABD = MAT("M_Grouper_CabinDark", (0.03, 0.035, 0.04), metal=0.9, rough=0.35) STEEL = MAT("M_Grouper_Steel", (0.45, 0.47, 0.5), metal=1.0, rough=0.3) CABGLOW = MAT("M_Grouper_CabinGlow", (0.02, 0.05, 0.06), rough=0.3, emit=(0.0, 0.85, 1.0), emit_str=1.2) # the cabin's # dim ceiling strips (critique round 1 M6) ACRYLD = MAT("M_Grouper_AcrylicDark", (0.10, 0.13, 0.14), rough=0.08) # the porthole: smoked acrylic (critique m3) for m in (ACRYL, ACRYLD): b = m.node_tree.nodes["Principled BSDF"] b.inputs["Transmission Weight"].default_value = 1.0 b.inputs["IOR"].default_value = 1.49 C_ROOT = get_coll(PFX + "Tender (exported)") C_COL = get_coll(PFX + "Collision (UCX)") C_SOCK = get_coll(PFX + "Sockets") root = bpy.data.objects.new("Grouper_Root", None) C_ROOT.objects.link(root) root.empty_display_type = 'ARROWS' ORDER = ["gr_shape.py", "gr_hull.py", "gr_parts.py", "gr_clamp.py", "gr_fittings.py", "gr_doors.py", "gr_cabin.py", "gr_finish.py"] if STAGES == "hull": ORDER = ["gr_shape.py", "gr_hull.py", "gr_finish_min.py"] for _stage in ORDER: _t0 = time.time() exec(open(os.path.join(HERE, _stage)).read()) REPORT.setdefault("stage_seconds", {})[_stage] = round(time.time() - _t0, 2) print("STAGE", _stage, round(time.time() - _t0, 2), flush=True)