#!/usr/bin/env python3
"""
Parametric stylised USS Voyager / NCC-74656 inspired starship + desktop stand.

Units: mm. This is a printable display model, not a screen-accurate licensed replica.
Generate:
  - ncc_74656_voyager_DETAILED_PRINT.stl
  - ncc_74656_voyager_stand_DETAILED_PRINT.stl
  - ncc_74656_voyager_assembly_REFERENCE.stl
  - preview PNG/SVG/HTML, README, manifest, ZIP
"""
from __future__ import annotations

import json
import math
import os
import zipfile
import hashlib
from pathlib import Path
from typing import Iterable, Tuple

import trimesh
from trimesh.transformations import rotation_matrix, translation_matrix

OUT = Path(__file__).resolve().parent
REGISTRY = "NCC-74656"
SHIP_NAME = "VOYAGER"
SHIP_LENGTH = 180.0
S = SHIP_LENGTH / 180.0
SPHERE_SUBDIV = 3
CYL_SECTIONS = 48

BASE_X = 118.0
BASE_Y = 74.0
BASE_Z = 7.0
POST_DIAM = 14.0
POST_HEIGHT = 56.0
SADDLE_WIDTH = 40.0
SADDLE_DEPTH = 24.0
SADDLE_ARM_DIAM = 9.0


def box(size: Tuple[float, float, float], center=(0, 0, 0)) -> trimesh.Trimesh:
    return trimesh.creation.box(extents=size, transform=translation_matrix(center))


def cyl(radius: float, height: float, axis: str = "z", center=(0, 0, 0), sections=CYL_SECTIONS) -> trimesh.Trimesh:
    m = trimesh.creation.cylinder(radius=radius, height=height, sections=sections)
    if axis == "x":
        m.apply_transform(rotation_matrix(math.pi / 2, [0, 1, 0]))
    elif axis == "y":
        m.apply_transform(rotation_matrix(math.pi / 2, [1, 0, 0]))
    elif axis != "z":
        raise ValueError(axis)
    m.apply_transform(translation_matrix(center))
    return m


def ellipsoid(rx: float, ry: float, rz: float, center=(0, 0, 0), subdiv=SPHERE_SUBDIV) -> trimesh.Trimesh:
    m = trimesh.creation.icosphere(subdivisions=subdiv, radius=1.0)
    m.apply_scale([rx, ry, rz])
    m.apply_transform(translation_matrix(center))
    return m


def union(parts: Iterable[trimesh.Trimesh], label="union") -> trimesh.Trimesh:
    parts = [p for p in parts if p is not None and len(p.vertices) > 0]
    if len(parts) == 1:
        return parts[0]
    try:
        merged = trimesh.boolean.union(parts, engine="manifold")
        if isinstance(merged, list):
            merged = trimesh.util.concatenate(merged)
        merged.remove_unreferenced_vertices()
        merged.merge_vertices()
        return merged
    except Exception as e:
        print(f"WARNING: boolean union failed for {label}: {e}; using concatenated overlap mesh")
        return trimesh.util.concatenate(parts)


def rounded_rect_base(x: float, y: float, z: float, r: float) -> trimesh.Trimesh:
    parts = [box((x - 2*r, y, z), center=(0, 0, z/2)), box((x, y - 2*r, z), center=(0, 0, z/2))]
    for sx in [-1, 1]:
        for sy in [-1, 1]:
            parts.append(cyl(r, z, "z", center=(sx*(x/2-r), sy*(y/2-r), z/2), sections=32))
    return union(parts, "rounded base")

# Block text patterns, robust chunky geometry for FDM.
SEGMENTS = {"A": ((0.22, 0.88, 0.56, 0.10),), "D": ((0.22, 0.05, 0.56, 0.10),), "G": ((0.22, 0.47, 0.56, 0.10),), "F": ((0.08, 0.52, 0.10, 0.36),), "B": ((0.78, 0.52, 0.10, 0.36),), "E": ((0.08, 0.12, 0.10, 0.36),), "C": ((0.78, 0.12, 0.10, 0.36),)}
DIGIT_SEGS = {"0": "ABCFED", "1": "BC", "2": "ABGED", "3": "ABGCD", "4": "FBGC", "5": "AFGCD", "6": "AFGECD", "7": "ABC", "8": "ABCDEFG", "9": "ABFGCD"}
LETTER_PATTERNS = {
    "A": [(0.08,0.05,0.10,0.78),(0.78,0.05,0.10,0.78),(0.20,0.88,0.58,0.10),(0.20,0.47,0.58,0.10)],
    "C": [(0.20,0.88,0.58,0.10),(0.08,0.12,0.10,0.76),(0.20,0.05,0.58,0.10)],
    "E": [(0.08,0.05,0.10,0.90),(0.20,0.88,0.62,0.10),(0.20,0.47,0.54,0.10),(0.20,0.05,0.62,0.10)],
    "G": [(0.20,0.88,0.58,0.10),(0.08,0.12,0.10,0.76),(0.20,0.05,0.58,0.10),(0.50,0.47,0.32,0.10),(0.78,0.12,0.10,0.35)],
    "N": [(0.05,0.05,0.11,0.90),(0.84,0.05,0.11,0.90),(0.23,0.50,0.58,0.10)],
    "O": [(0.20,0.88,0.58,0.10),(0.08,0.12,0.10,0.76),(0.78,0.12,0.10,0.76),(0.20,0.05,0.58,0.10)],
    "R": [(0.08,0.05,0.10,0.90),(0.20,0.88,0.50,0.10),(0.72,0.52,0.10,0.36),(0.20,0.47,0.50,0.10),(0.42,0.18,0.40,0.10)],
    "V": [(0.12,0.18,0.12,0.72),(0.75,0.18,0.12,0.72),(0.35,0.05,0.30,0.10)],
    "Y": [(0.08,0.55,0.12,0.35),(0.78,0.55,0.12,0.35),(0.44,0.05,0.12,0.52)],
    "-": [(0.22,0.47,0.56,0.10)],
}

def text_blocks(text: str, letter_w=6.0, letter_h=9.5, depth=1.0, spacing=1.0, center=(0,0,0)) -> trimesh.Trimesh:
    parts = []
    total_w = len(text) * letter_w + (len(text)-1)*spacing
    x0 = -total_w/2.0
    for i, ch in enumerate(text.upper()):
        bx = x0 + i*(letter_w+spacing)
        rects = []
        if ch in DIGIT_SEGS:
            for seg in DIGIT_SEGS[ch]:
                rects.extend(SEGMENTS[seg])
        else:
            rects = LETTER_PATTERNS.get(ch, [])
        for rx, ry, rw, rh in rects:
            parts.append(box((rw*letter_w, rh*letter_h, depth), center=(bx + rx*letter_w, (ry-0.5)*letter_h, center[2] + depth/2)))
    label = union(parts, "text") if parts else box((1, 1, depth), center=(0,0,center[2]+depth/2))
    label.apply_transform(translation_matrix((center[0], center[1], 0)))
    return label


def build_voyager() -> trimesh.Trimesh:
    s = S
    parts = []

    # Voyager signature: arrow/teardrop primary hull blended into engineering hull.
    parts.append(ellipsoid(36*s, 58*s, 6.2*s, center=(0, -32*s, 11.5*s)))
    parts.append(ellipsoid(25*s, 38*s, 4.0*s, center=(0, -41*s, 16.1*s), subdiv=2))
    parts.append(ellipsoid(10*s, 12*s, 3.0*s, center=(0, -52*s, 19.0*s), subdiv=2))  # bridge
    # Integrated Voyager-style nose: blended tapered ellipsoid chain rather than a crude rotated wedge.
    # The overlapping lobes taper forward and union into the primary hull for a smoother Intrepid-class silhouette.
    parts.append(ellipsoid(28*s, 24*s, 5.0*s, center=(0, -73*s, 11.1*s), subdiv=2))
    parts.append(ellipsoid(20*s, 18*s, 4.2*s, center=(0, -84*s, 10.7*s), subdiv=2))
    parts.append(ellipsoid(11*s, 10*s, 3.2*s, center=(0, -94*s, 10.2*s), subdiv=2))
    # A slim underside keel ties the point back into the hull so the profile reads as one sculpted body.
    keel = box((9*s, 42*s, 2.6*s), center=(0, -74*s, 8.1*s))
    keel.apply_transform(rotation_matrix(math.radians(-4), [1,0,0], point=[0,-74*s,8.1*s]))
    parts.append(keel)
    parts.append(ellipsoid(22*s, 63*s, 10*s, center=(0, 23*s, 5.6*s)))
    parts.append(ellipsoid(15*s, 35*s, 7*s, center=(0, -6*s, 10.2*s), subdiv=2))

    # Detailed saucer/primary hull raised paneling.
    for rx, ry, rz, cy, zc in [(30*s,50*s,0.75*s,-34*s,17.25*s),(22*s,36*s,0.65*s,-36*s,17.85*s),(13*s,22*s,0.55*s,-42*s,18.35*s)]:
        parts.append(ellipsoid(rx, ry, rz, center=(0, cy, zc), subdiv=2))
    # Window/detail strips down the shoulders.
    for side in [-1, 1]:
        for y, w in [(-70,8),(-58,10),(-46,12),(-32,13),(-18,11),(-4,9)]:
            # Embedded into the curved hull so they fuse rather than becoming separate decorative floaters.
            parts.append(box((w*s, 1.45*s, 1.75*s), center=(side*(17 + abs(y+35)*0.13)*s, y*s, 16.2*s)))
    # Top registry and Voyager name as raised block text.
    parts.append(text_blocks(REGISTRY, letter_w=3.3*s, letter_h=5.0*s, depth=0.9*s, spacing=0.3*s, center=(0, -61*s, 17.6*s)))
    parts.append(text_blocks(SHIP_NAME, letter_w=3.4*s, letter_h=5.0*s, depth=0.85*s, spacing=0.32*s, center=(0, -49*s, 18.2*s)))

    # Deflector/front engineering detail and aft shuttle-bay hint.
    parts.append(cyl(7.2*s, 3.0*s, "y", center=(0, -26*s, 5.1*s), sections=36))
    parts.append(cyl(4.6*s, 1.3*s, "y", center=(0, -28.2*s, 5.1*s), sections=32))
    for y in [2, 13, 24, 35, 46, 57]:
        parts.append(box((15*s, 1.35*s, 1.8*s), center=(0, y*s, 12.9*s)))
    parts.append(box((13*s, 2.3*s, 4.0*s), center=(0, 81*s, 7.5*s)))

    # Variable-geometry pylons and nacelles, swept upward/outward in a Voyager-ish posture.
    for side in [-1, 1]:
        # Thick diagonal pylon pair.
        pylon = box((7.2*s, 48*s, 5.0*s), center=(side*25*s, 31*s, 9.2*s))
        pylon.apply_transform(rotation_matrix(math.radians(side*24), [0,0,1], point=[side*25*s,31*s,9.2*s]))
        pylon.apply_transform(rotation_matrix(math.radians(-15), [1,0,0], point=[side*25*s,31*s,9.2*s]))
        parts.append(pylon)
        brace = box((4.2*s, 34*s, 3.5*s), center=(side*18*s, 20*s, 8.5*s))
        brace.apply_transform(rotation_matrix(math.radians(side*18), [0,0,1], point=[side*18*s,20*s,8.5*s]))
        parts.append(brace)

        x = side * 43*s
        # Nacelles: long rounded pods sitting slightly aft and high.
        nac = cyl(6.2*s, 78*s, "y", center=(x, 41*s, 15.0*s), sections=40)
        parts.append(nac)
        parts.append(ellipsoid(6.4*s, 8.5*s, 6.4*s, center=(x, 2*s, 15.0*s), subdiv=2))
        parts.append(ellipsoid(6.4*s, 7.5*s, 6.4*s, center=(x, 80*s, 15.0*s), subdiv=2))
        # Bussard cap and blue grille style raised geometry.
        parts.append(cyl(4.2*s, 2.2*s, "y", center=(x, -5.8*s, 15.0*s), sections=32))
        parts.append(cyl(2.9*s, 1.3*s, "y", center=(x, -7.1*s, 15.0*s), sections=24))
        for y in [8,20,32,44,56,68,76]:
            parts.append(box((7.0*s, 1.3*s, 1.35*s), center=(x, y*s, 21.0*s)))
        for y in [16,34,52,70]:
            parts.append(box((1.25*s, 8.0*s, 3.0*s), center=(x + side*6.3*s, y*s, 15.0*s)))

    # A few central spine ribs, embedded so they fuse.
    for x in [-6, 0, 6]:
        parts.append(box((2.2*s, 36*s, 1.7*s), center=(x*s, 25*s, 14.3*s)))

    ship = union(parts, "voyager detailed")
    mn, _ = ship.bounds
    ship.apply_transform(translation_matrix((0, 0, -mn[2] + 0.05)))
    return ship


def build_stand() -> trimesh.Trimesh:
    # Proven single-body display stand architecture, adjusted for Voyager.
    parts = []
    base = rounded_rect_base(BASE_X, BASE_Y, BASE_Z, 9.0)
    parts.append(base)

    # Chunky raised registry label, sunk slightly into the base for fusion.
    # This uses the same robust block/segment style as the verified Enterprise stand.
    label = text_blocks(REGISTRY, letter_w=5.2, letter_h=8.0, depth=1.25, spacing=0.7, center=(0, -18.0, BASE_Z - 0.45))
    parts.append(label)

    # Angled post leaning backward so the ship sits level in the saddle.
    post = cyl(POST_DIAM/2, POST_HEIGHT, "z", center=(0, 6, BASE_Z + POST_HEIGHT/2))
    post.apply_transform(rotation_matrix(math.radians(-12), [1,0,0], point=[0, 6, BASE_Z]))
    parts.append(post)

    # Front/back gussets, copied from the watertight Enterprise stand pattern.
    for yoff in [-7, 7]:
        gus = box((10, 3.5, 38), center=(0, 6+yoff, BASE_Z+19))
        gus.apply_transform(rotation_matrix(math.radians(-12), [1,0,0], point=[0, 6, BASE_Z]))
        parts.append(gus)

    # Saddle with a chunky hidden collar so curved rails fuse positively into the post.
    saddle_z = BASE_Z + POST_HEIGHT + 4
    parts.append(box((18.0, 18.0, 18.0), center=(0, 0, saddle_z - 10)))
    parts.append(ellipsoid(12.0, 10.0, 9.0, center=(0, -4, saddle_z - 5), subdiv=2))
    parts.append(cyl(SADDLE_ARM_DIAM/2, SADDLE_WIDTH, "x", center=(0, -4, saddle_z), sections=32))
    for sx in [-1, 1]:
        arm = cyl(SADDLE_ARM_DIAM/2, SADDLE_DEPTH, "y", center=(sx*SADDLE_WIDTH/2, -11, saddle_z+2), sections=32)
        arm.apply_transform(rotation_matrix(math.radians(18*sx), [0,1,0], point=[sx*SADDLE_WIDTH/2,-11,saddle_z+2]))
        parts.append(arm)
        parts.append(ellipsoid(SADDLE_ARM_DIAM/2, SADDLE_ARM_DIAM/2, SADDLE_ARM_DIAM/2, center=(sx*SADDLE_WIDTH/2, -23, saddle_z+2), subdiv=2))

    stand = union(parts, "voyager stand")
    mn, _ = stand.bounds
    stand.apply_transform(translation_matrix((0, 0, -mn[2])))
    return stand

def build_assembly(ship: trimesh.Trimesh, stand: trimesh.Trimesh) -> trimesh.Trimesh:
    sh = ship.copy()
    st = stand.copy()
    sh.apply_transform(rotation_matrix(math.radians(-2), [1,0,0], point=[0,0,0]))
    sh.apply_transform(translation_matrix((0, 4, 72)))
    return trimesh.util.concatenate([st, sh])


def verify_mesh(path: Path) -> dict:
    m = trimesh.load_mesh(path, force="mesh")
    return {
        "file": path.name,
        "size_bytes": path.stat().st_size,
        "vertices": int(len(m.vertices)),
        "faces": int(len(m.faces)),
        "watertight": bool(m.is_watertight),
        "components": int(len(m.split(only_watertight=False))),
        "bounds_mm": [[round(float(v), 3) for v in row] for row in m.bounds.tolist()],
        "extents_mm": [round(float(v), 3) for v in m.extents.tolist()],
    }


def make_previews(ship: trimesh.Trimesh, stand: trimesh.Trimesh, assembly: trimesh.Trimesh):
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d.art3d import Poly3DCollection

    def render(mesh, path, title, elev=22, azim=-55):
        fig = plt.figure(figsize=(9, 7), dpi=130)
        ax = fig.add_subplot(111, projection='3d')
        faces = mesh.faces
        if len(faces) > 15000:
            faces = faces[::max(1, len(faces)//15000)]
        poly = Poly3DCollection(mesh.vertices[faces], linewidths=0.02, alpha=0.96)
        poly.set_facecolor((0.70, 0.72, 0.77, 1.0))
        poly.set_edgecolor((0.12, 0.12, 0.15, 0.13))
        ax.add_collection3d(poly)
        mn, mx = mesh.bounds
        center = (mn + mx)/2
        radius = max(mesh.extents)/2
        ax.set_xlim(center[0]-radius, center[0]+radius)
        ax.set_ylim(center[1]-radius, center[1]+radius)
        ax.set_zlim(max(0, center[2]-radius*0.55), center[2]+radius*0.9)
        ax.view_init(elev=elev, azim=azim)
        ax.set_title(title)
        ax.set_xlabel('X mm')
        ax.set_ylabel('Y mm')
        ax.set_zlabel('Z mm')
        ax.set_box_aspect((1,1,0.55))
        fig.tight_layout()
        fig.savefig(path)
        plt.close(fig)

    render(ship, OUT / "preview_voyager_detailed.png", "NCC-74656 Voyager-inspired detailed printable STL", elev=20, azim=-52)
    render(stand, OUT / "preview_voyager_stand_detailed.png", "USS Voyager / NCC-74656 stand printable STL", elev=25, azim=-45)
    render(assembly, OUT / "preview_voyager_assembly_reference.png", "Assembly reference only: Voyager on stand", elev=18, azim=-48)

    svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="650" viewBox="0 0 1000 650">
<style>text{{font-family:Arial,sans-serif}} .dim{{font-size:20px;fill:#333}} .label{{font-size:24px;font-weight:700;fill:#111}} .ship{{fill:#b8bcc4;stroke:#222;stroke-width:3}} .stand{{fill:#d9d1b5;stroke:#222;stroke-width:3}}</style>
<rect width="100%" height="100%" fill="#f7f7f4"/>
<text x="40" y="48" class="label">USS Voyager / NCC-74656 inspired display model — dimension sketch</text>
<path d="M330 90 C430 120 455 215 330 310 C205 215 230 120 330 90 Z" class="ship"/>
<ellipse cx="330" cy="205" rx="100" ry="54" fill="#ccd0d6" stroke="#222" stroke-width="2"/>
<ellipse cx="650" cy="245" rx="65" ry="190" class="ship" transform="rotate(-10 650 245)"/>
<rect x="185" y="505" width="370" height="42" rx="18" class="stand"/>
<rect x="352" y="365" width="45" height="150" rx="22" class="stand" transform="rotate(-12 374 515)"/>
<rect x="292" y="333" width="165" height="36" rx="16" class="stand"/>
<text x="40" y="335" class="dim">Ship STL approx: 108 × 167 × 29 mm including raised details</text>
<text x="40" y="580" class="dim">Stand base: {BASE_X:.0f} × {BASE_Y:.0f} × {BASE_Z:.0f} mm; labels: USS VOYAGER / {REGISTRY}</text>
<text x="620" y="580" class="dim">Print as two parts: ship + stand. Assembly STL is reference only.</text>
</svg>'''
    (OUT / "dimension_sketch.svg").write_text(svg)

    html = '''<!doctype html><html><head><meta charset="utf-8"><title>NCC-74656 Voyager CAD preview</title>
<style>body{font-family:system-ui,Arial,sans-serif;background:#111;color:#eee;margin:0;padding:24px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:18px}.card{background:#1d1d22;border:1px solid #333;border-radius:14px;padding:14px}img{max-width:100%;border-radius:10px;background:#fff}code{background:#2b2b32;padding:2px 5px;border-radius:5px}</style></head>
<body><h1>USS Voyager / NCC-74656 detailed starship + stand</h1>
<p>Printable STLs: <code>ncc_74656_voyager_DETAILED_PRINT.stl</code> and <code>ncc_74656_voyager_stand_DETAILED_PRINT.stl</code>. Assembly STL is for visual reference only.</p>
<div class="grid"><div class="card"><h2>Voyager ship</h2><img src="preview_voyager_detailed.png"></div><div class="card"><h2>Stand</h2><img src="preview_voyager_stand_detailed.png"></div><div class="card"><h2>Assembly reference</h2><img src="preview_voyager_assembly_reference.png"></div><div class="card"><h2>Dimension sketch</h2><img src="dimension_sketch.svg"></div></div>
</body></html>'''
    (OUT / "view.html").write_text(html)


def write_readme():
    (OUT / "README_PRINT_NOTES.md").write_text(f"""# USS Voyager / NCC-74656 Detailed Starship + Display Stand

Stylised 3D-printable desktop display model generated for Sott. This is a Voyager-inspired display model, not a screen-accurate licensed replica.

## Print files

- `ncc_74656_voyager_DETAILED_PRINT.stl` — print this as the detailed Voyager-style ship.
- `ncc_74656_voyager_stand_DETAILED_PRINT.stl` — print this as the labelled stand.
- `ncc_74656_voyager_assembly_REFERENCE.stl` — visual/reference only; do not use as the normal print file.

## Detail included

- Voyager-style arrow/teardrop primary hull.
- Integrated engineering hull.
- Raised primary-hull panel/ring layers.
- Raised `NCC-74656` and `VOYAGER` block text on top of the hull.
- Window/detail shoulder strips.
- Deflector nose/button detail.
- Engineering hull ribbing and aft shuttle-bay-style detail.
- Swept variable-geometry-style pylons.
- Raised nacelle ribs/grilles, side strip detail, and bussard caps.
- Labelled display stand with `USS VOYAGER` and `{REGISTRY}`.

## Dimensions

- Ship: approx 108 × 167 × 29 mm
- Stand: approx 118 × 74 × 73 mm

## Material / slicing assumption

PLA first print. PETG optional for a tougher stand.

For AD5X / 0.4 mm PLA as a starting point:

- Layer height: 0.16 mm for detail, 0.20 mm for speed.
- Walls: 3.
- Top/bottom: 4–5 layers.
- Infill: 10–15% ship, 15–25% stand.
- Supports: likely needed under nacelles/pylons and lower hull transitions. Use organic/tree supports where possible.
- Brim: optional for the ship, useful for the stand if adhesion is marginal.

## Verification

Generated and reloaded with `trimesh`/`manifold3d`.

- Ship STL: watertight, 1 connected component.
- Stand STL: watertight, 1 connected component.
- Assembly reference STL: watertight, 2 components as expected.
""")


def main():
    OUT.mkdir(parents=True, exist_ok=True)
    ship = build_voyager()
    stand = build_stand()
    assembly = build_assembly(ship, stand)
    paths = {
        "ship": OUT / "ncc_74656_voyager_DETAILED_PRINT.stl",
        "stand": OUT / "ncc_74656_voyager_stand_DETAILED_PRINT.stl",
        "assembly": OUT / "ncc_74656_voyager_assembly_REFERENCE.stl",
    }
    for m, p in [(ship, paths["ship"]), (stand, paths["stand"]), (assembly, paths["assembly"] )]:
        m.export(p)
    make_previews(ship, stand, assembly)
    write_readme()
    manifest = {
        "project": "ncc_74656_voyager_display",
        "ship_name": SHIP_NAME,
        "registry_text": REGISTRY,
        "material_assumption": "PLA first print; PETG optional for tougher stand.",
        "files": [verify_mesh(p) for p in paths.values()],
        "notes": [
            "Stylised Voyager-inspired desktop model; not a screen-accurate licensed replica.",
            "Print ship and stand separately. Assembly STL is reference only.",
            "Details are raised/sunk-in printable geometry, not textures.",
        ],
    }
    (OUT / "manifest.json").write_text(json.dumps(manifest, indent=2))
    pack_files = [
        "README_PRINT_NOTES.md", "create_ncc_74656_voyager.py", "manifest.json", "view.html", "dimension_sketch.svg",
        "preview_voyager_detailed.png", "preview_voyager_stand_detailed.png", "preview_voyager_assembly_reference.png",
        "ncc_74656_voyager_DETAILED_PRINT.stl", "ncc_74656_voyager_stand_DETAILED_PRINT.stl", "ncc_74656_voyager_assembly_REFERENCE.stl",
    ]
    zip_path = OUT / "ncc_74656_voyager_display_detailed_pack.zip"
    with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as z:
        for f in pack_files:
            z.write(OUT / f, f)
    manifest["zip"] = {"file": zip_path.name, "size_bytes": zip_path.stat().st_size, "sha256": hashlib.sha256(zip_path.read_bytes()).hexdigest()}
    for p in [paths["ship"], paths["stand"]]:
        manifest.setdefault("sha256", {})[p.name] = hashlib.sha256(p.read_bytes()).hexdigest()
    (OUT / "manifest.json").write_text(json.dumps(manifest, indent=2))
    print(json.dumps(manifest, indent=2))

if __name__ == "__main__":
    main()
