""" The bridge's own shell seen from inside: two things that showed there and how they are put right. (The user, 2026-10-12: "fix the bridge ceiling slot and seam lines too".) 1. THE CEILING SLOT. Under the roof pod the hull's crest is hidden by a Mask modifier ('LAYOUT mask pod opening'), because the corridor's cars stand taller than the crest. The mask takes whole faces, and the hull's faces are about a metre long there: the hidden strip ran on to x 32.18, 1.17 m past the bridge face of the aft bulkhead (x 31.01). In the bridge that was a slot in the ceiling, 8.2 m wide, looking up into the pod. end_hull_opening_at() gives the hull an edge across that strip inside the bulkhead's thickness and ends the hidden strip there: from the bulkhead forward the crest is whole again. Nothing is deleted; the mask stays, but now it ends on an edge that was cut for it instead of wherever a face happened to end. 2. THE SEAM LINES. The hull's outer trims are bevelled curves that lie half in the skin, so their inner half stood 1 to 4 cm inside the hull. Where the hull's inner face is the room's wall, that is, in the bridge, they showed as dark lines. seat_on_skin() gives such a curve a Shrinkwrap modifier (nearest surface point, snap mode Outside): only the vertices that are under the skin move, onto the skin plus 2 mm. What shows outside does not change, the curve stays a curve. refine_poly() first adds points to long straight pieces of a POLY spline (same path), so that no piece sags under the skin between two points. Everything is measured against the hull as Blender draws it (shipfit.drawn_tree). """ import math import bmesh import bpy from mathutils import Vector import shipfit SEAT = "Seat on the skin (nothing of it inside the hull)" # --------------------------------------------------------------------------------------------------------------- def end_hull_opening_at(hull, x_cut, modifier="LAYOUT mask pod opening", old_suffix=" (before the ceiling slot fix)"): """Ends the hidden strip of `modifier` (a Mask by vertex group, inverted) at the plane x = x_cut. The faces of the strip that the plane crosses get an edge along it: a vertex on every edge between two of them, the faces split from vertex to vertex. The strip's outermost face on each side is split from its one new vertex to its aft outer corner instead, so that the edge it shares with the visible skin beside the strip stays whole (no vertex is put on a face that is not part of this). Then nothing forward of the plane is in the mask's group any more. Works on a copy of the mesh; the object gets the copy, the mesh from before is kept under its name plus old_suffix with a fake user. Returns a dict of what was done.""" mask = hull.modifiers[modifier] assert mask.type == "MASK" and mask.invert_vertex_group, "expected an inverted Mask modifier" gi = hull.vertex_groups[mask.vertex_group].index thr = mask.threshold old = hull.data mw = hull.matrix_world def X(v): return (mw @ v.co).x bm = bmesh.new() bm.from_mesh(old) dl = bm.verts.layers.deform.verify() def in_group(v): return v[dl].get(gi, 0.0) > thr hidden = [f for f in bm.faces if any(in_group(v) for v in f.verts)] info = {"hidden_before": len(hidden), "faces_before": len(bm.faces), "verts_before": len(bm.verts)} row = [f for f in hidden if min(X(v) for v in f.verts) < x_cut - 0.02 and max(X(v) for v in f.verts) > x_cut + 0.02] if not row: bm.free() info["note"] = "no hidden face crosses x = %.3f: nothing to do" % x_cut return info rowset = set(row) new_verts = set() for e in list(set(e for f in row for e in f.edges)): if len(e.link_faces) != 2 or not all(lf in rowset for lf in e.link_faces): continue # the strip's own outline: left whole xa, xb = X(e.verts[0]), X(e.verts[1]) if (xa - x_cut) * (xb - x_cut) >= 0.0: continue ne, nv = bmesh.utils.edge_split(e, e.verts[0], (x_cut - xa) / (xb - xa)) new_verts.add(nv) splits = {"across": 0, "to_a_corner": 0} for f in row: fv = list(f.verts) nv = [v for v in fv if v in new_verts] if len(nv) == 2: bmesh.utils.face_split(f, nv[0], nv[1]) splits["across"] += 1 elif len(nv) == 1: i = fv.index(nv[0]) beside = (fv[i - 1], fv[(i + 1) % len(fv)]) far = [v for v in fv if v is not nv[0] and v not in beside] bmesh.utils.face_split(f, nv[0], min(far, key=X)) splits["to_a_corner"] += 1 else: bm.free() raise RuntimeError("a face of the strip has %d new vertices: the strip is not a plain band here" % len(nv)) out_of_group = 0 for v in bm.verts: if gi in v[dl] and X(v) > x_cut - 1e-4: del v[dl][gi] out_of_group += 1 bm.normal_update() hidden_after = [f for f in bm.faces if any(in_group(v) for v in f.verts)] info.update({"faces_cut": len(row), "new_vertices": len(new_verts), "splits": splits, "taken_out_of_the_group": out_of_group, "hidden_after": len(hidden_after), "hidden_reaches_x": round(max(X(v) for f in hidden_after for v in f.verts), 3), "faces_after": len(bm.faces), "verts_after": len(bm.verts), "ngons": sum(1 for f in bm.faces if len(f.verts) > 4) - 0}) new = old.copy() bm.to_mesh(new) bm.free() name = old.name old.name = name + old_suffix old.use_fake_user = True new.name = name hull.data = new info["mesh"], info["kept"] = new.name, old.name return info def compare_normals(old, new): """Largest angle (degrees) between the drawn normals of the two meshes on faces that did not change (same index, same corners). A cut must not disturb the shading of the rest of a hull with custom normals.""" worst, n = 0.0, 0 oc, nc = old.corner_normals, new.corner_normals for po in old.polygons: if po.index >= len(new.polygons): break pn = new.polygons[po.index] if po.loop_total != pn.loop_total: continue if any((old.vertices[a].co - new.vertices[b].co).length > 1e-6 for a, b in zip(po.vertices, pn.vertices)): continue n += 1 for k in range(po.loop_total): a, b = oc[po.loop_start + k].vector, nc[pn.loop_start + k].vector d = max(-1.0, min(1.0, a.dot(b))) ang = math.degrees(math.acos(d)) if ang > worst: worst = ang return {"faces_compared": n, "largest_angle_deg": round(worst, 4)} # --------------------------------------------------------------------------------------------------------------- def refine_poly(ob, max_len, where=None): """Adds points along the straight pieces of POLY splines that are longer than max_len (and for which where(a, b) is true, world points), so that the pieces are at most max_len long. The path does not change. Returns the number of points added.""" cu = ob.data mw = ob.matrix_world added = 0 for sp in list(cu.splines): if sp.type != "POLY": continue pts = [(Vector(p.co), p.tilt, p.radius, p.weight_softbody) for p in sp.points] n = len(pts) out = [] last = n if sp.use_cyclic_u else n - 1 for i in range(n): out.append(pts[i]) if i >= last: break a, b = pts[i], pts[(i + 1) % n] wa, wb = mw @ a[0].to_3d(), mw @ b[0].to_3d() length = (wb - wa).length if length <= max_len or (where is not None and not where(wa, wb)): continue k = int(math.ceil(length / max_len)) for j in range(1, k): t = j / k out.append((a[0].lerp(b[0], t), a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t, a[3] + (b[3] - a[3]) * t)) if len(out) == n: continue ns = cu.splines.new("POLY") ns.points.add(len(out) - 1) for p, (co, tilt, radius, w) in zip(ns.points, out): p.co, p.tilt, p.radius, p.weight_softbody = co, tilt, radius, w for attr in ("use_cyclic_u", "use_smooth", "material_index", "resolution_u", "order_u", "tilt_interpolation", "radius_interpolation", "use_endpoint_u"): try: setattr(ns, attr, getattr(sp, attr)) except Exception: # noqa: BLE001 pass cu.splines.remove(sp) added += len(out) - n return added def seat_on_skin(ob, target, offset=0.002): """Shrinkwrap, nearest surface point, snap mode Outside, on the bevelled geometry (not on the spline).""" m = ob.modifiers.get(SEAT) or ob.modifiers.new(SEAT, "SHRINKWRAP") m.target = target m.wrap_method = "NEAREST_SURFACEPOINT" m.wrap_mode = "OUTSIDE" m.offset = offset m.use_apply_on_spline = False return m def under_the_skin(ob, tree, spacing=0.02, region=None): """An object's evaluated geometry sampled along its edges every `spacing`: how many samples lie under the skin of `tree` (a drawn_tree), and how deep. region(q) limits the count to part of the ship.""" dg = bpy.context.evaluated_depsgraph_get() ev = ob.evaluated_get(dg) m = ev.to_mesh() mw = ob.matrix_world co = [mw @ v.co for v in m.vertices] n, inside, deep, at = 0, 0, 0.0, None for e in m.edges: a, b = co[e.vertices[0]], co[e.vertices[1]] k = max(1, int(math.ceil((b - a).length / spacing))) for j in range(k + 1): q = a.lerp(b, j / k) if region is not None and not region(q): continue n += 1 loc, nrm, idx, dd = tree.find_nearest(q) s = (q - loc).dot(nrm) if s < -0.0002: inside += 1 if s < deep: deep, at = s, (round(q.x, 2), round(q.y, 2), round(q.z, 2)) out = {"verts": len(co), "samples": n, "under_the_skin": inside, "deepest_mm": round(-deep * 1000, 2), "at": at} ev.to_mesh_clear() return out # the trims that showed inside the bridge on 2026-10-12 (seen from eight points on the bridge deck) BRIDGE_TRIMS = ("Hull | transverse assembly seam.003", "Cowl | belt panel seam", "Cowl | belt panel seam.001", "Hull | longitudinal deck seam", "Hull | longitudinal deck seam.001", "Hull | longitudinal deck seam.002", "Hull | longitudinal deck seam.003", "Hull | recessed grab slot.001", "Hull | recessed grab slot.003") def in_bridge(q): return q.x >= 31.0 and q.z >= 3.1 def fix_bridge_trims(hull, names=BRIDGE_TRIMS, offset=0.002, max_len=0.17): """seat_on_skin for each named trim; long straight pieces near the bridge are refined first. Returns per trim what was under the skin in the bridge before and after.""" tree = shipfit.drawn_tree(hull) report = {} for n in names: ob = bpy.data.objects.get(n) if ob is None or ob.type != "CURVE": report[n] = "not found" continue before = under_the_skin(ob, tree, region=in_bridge) added = refine_poly(ob, max_len, where=lambda a, b: max(a.x, b.x) >= 30.3) seat_on_skin(ob, hull, offset) bpy.context.view_layer.update() after = under_the_skin(ob, tree, region=in_bridge) whole = under_the_skin(ob, tree) report[n] = {"points_added": added, "in_the_bridge_before": [before["under_the_skin"], before["deepest_mm"]], "in_the_bridge_after": [after["under_the_skin"], after["deepest_mm"]], "whole_length_after": [whole["under_the_skin"], whole["deepest_mm"], whole["at"]], "verts": whole["verts"]} return report