import cadquery as cq
import json
import math
from moldforge_core.cad.assembly_tree import ComponentKind, Registry

# ============================================
# MoldWise AI — compositional mold_builder output
# ============================================
FRAME_W = 246.0
FRAME_H = 246.0

_bs_T_CLAMP_FIXED  = 36
_bs_T_CLAMP_MOVING = 36
_bs_T_CAVITY       = 56
_bs_T_CORE         = 56
_bs_T_SUPPORT      = 46
_bs_T_SPACER       = 96
_bs_T_EJECT_RET    = 22
_bs_T_EJECT        = 27
_bs_GUIDE_PIN_DIA  = 24.0
CLAMP_FIXED_PLATE_KNARR_PATH      = "/data/knarr/01_plates/K/10010_clamp_plate_with_locating_ring_recess_width_salient/10010_246x246x36-Toolox33.step"
CLAMP_MOVING_PLATE_KNARR_PATH     = "/data/knarr/01_plates/K/10010_clamp_plate_with_locating_ring_recess_width_salient/10010_246x246x36-Toolox33.step"
CAVITY_PLATE_KNARR_PATH           = "/data/knarr/01_plates/K/10020_mould_plate_with_reference_edges/10020_246x246x56-Toolox33.step"
CORE_PLATE_KNARR_PATH             = "/data/knarr/01_plates/K/10020_mould_plate_with_reference_edges/10020_246x246x56-Toolox33.step"
SUPPORT_PLATE_KNARR_PATH          = "/data/knarr/01_plates/K/10030_backing_plate/10030_246x246x46-1.1730.step"
EJECTOR_PLATE_KNARR_PATH          = "/data/knarr/01_plates/K/10070_ejector_base_plate/10070_246x296x22-1.1730.step"
EJECTOR_RETAINER_PLATE_KNARR_PATH = "/data/knarr/01_plates/K/10060_ejector_retaining_plate/10060_246x346x17-Toolox33.step"
GUIDE_PILLAR_KNARR_PATH           = "/data/knarr/05_centering_and_guiding_elements/04_guide_pillars/51001_guide_pillar_with_collar/51001_24x140.step"
GUIDE_BUSHING_KNARR_PATH          = "/data/knarr/05_centering_and_guiding_elements/03_guide_pillars_and_guide_bushes/51110_guide_bush_with_centering_collar/51110_24x56.step"
SUPPORT_PILLAR_KNARR_PATH         = "/data/knarr/05_centering_and_guiding_elements/08_other_components/51513_support_pillar_with_guiding_diameter/51513_36-50.step"
SCREW_TOP_KNARR_PATH              = "/data/knarr/04_general_parts_for_tooling/03_fasteners/40031_cylinder_head_screw_with_hexagon_socket/40031_M12-90.step"
SCREW_BOT_KNARR_PATH              = "/data/knarr/04_general_parts_for_tooling/03_fasteners/40031_cylinder_head_screw_with_hexagon_socket/40031_M12-100.step"
DOWEL_PIN_KNARR_PATH              = "/data/knarr/04_general_parts_for_tooling/03_fasteners/42511_dowel_pin/42511_10-120.step"
_bs_GUIDE_PIN_POS = [(-(FRAME_W/2)+35, -(FRAME_H/2)+35), ((FRAME_W/2)-35, -(FRAME_H/2)+35), (-(FRAME_W/2)+35, (FRAME_H/2)-35), ((FRAME_W/2)-35, (FRAME_H/2)-35)]
_cr_SPRUE_DIA = 6.0
_cr_SPRUE_BODY_DIA = 20.0
_cr_LOC_RING_OD = 125.0
_cr_GATE_W = 3.0
_cr_GATE_D = 1.5
_cr_GATE_X = 5.563881322536972
_cr_GATE_Y = -10.846816424948287
SPRUE_BUSHING_KNARR_PATH = "/data/knarr/06_injection_lettering_marking/03_injection/62408_sprue_bushing_without_radius/62408_12-22-2.5-0.5.step"
LOCATING_RING_KNARR_PATH = "/data/knarr/05_centering_and_guiding_elements/07_locating_rings/50100_locating_ring_with_bore/50100_125x12x36.step"
EJECTOR_PINS = [(-35.5, -4.0, 2.5), (-27.5, -4.0, 2.5), (-35.5, 4.0, 2.5), (-27.5, 4.0, 2.5), (27.5, -4.0, 2.5), (35.5, -4.0, 2.5), (27.5, 4.0, 2.5), (35.5, 4.0, 2.5)]
EJECTOR_PIN_KNARR_PATHS = json.loads("[\"/data/knarr/03_ejection_demoulding/01_ejector_pins/33111_ejector_pin/33111_2.5-100.step\", \"/data/knarr/03_ejection_demoulding/01_ejector_pins/33111_ejector_pin/33111_2.5-100.step\", \"/data/knarr/03_ejection_demoulding/01_ejector_pins/33111_ejector_pin/33111_2.5-100.step\", \"/data/knarr/03_ejection_demoulding/01_ejector_pins/33111_ejector_pin/33111_2.5-100.step\", \"/data/knarr/03_ejection_demoulding/01_ejector_pins/33111_ejector_pin/33111_2.5-100.step\", \"/data/knarr/03_ejection_demoulding/01_ejector_pins/33111_ejector_pin/33111_2.5-100.step\", \"/data/knarr/03_ejection_demoulding/01_ejector_pins/33111_ejector_pin/33111_2.5-100.step\", \"/data/knarr/03_ejection_demoulding/01_ejector_pins/33111_ejector_pin/33111_2.5-100.step\"]")
_re_RETURN_POS = [[-61.5, -61.5], [61.5, -61.5], [-61.5, 61.5], [61.5, 61.5]]
_re_PIN_DIA = 16.0
_re_PIN_LEN = 154.0
_re_SPRING_OD = 32.0
_re_SPRING_FREE_L = 70.5
RETURN_PIN_KNARR_PATH = "/data/knarr/03_ejection_demoulding/06_ejector_pins_shouldered/36111_ejector_pin_shouldered/36111_4-400-200.step"
RETURN_SPRING_KNARR_PATH = "/data/knarr/04_general_parts_for_tooling/04_System compression springs/40202_System compression spring/40202_32-76.step"
_cool_DIA        = 12.0
_cool_N_CHANNELS = 5
_cool_PITCH_Y    = 30.0
_cool_ORING_ID   = 12.0
_cool_ORING_CORD = 2.0
COOLING_ORING_KNARR_PATH = "/data/knarr/07_cooling_tempering/02_deflection_elements_and_accessories_and_aids/70098_o-ring_seal/70098_12-2.step"
COOLING_SCREW_PLUG_KNARR_PATH = "/data/knarr/07_cooling_tempering/02_deflection_elements_and_accessories_and_aids/70094_screw_plug_with_hexagon_socket_conical/70094_M11x1.step"
_pt_PARTING_Z_MM = -6.4
_pt_SHRINKAGE = 1.005
_pt_MATERIAL_FAMILY = "PP"
_pt_PART_STEP_PATH = "/data/renders/uploads/20260408_143611_1571 detal.stp"
_pt_CAVITY_POSITIONS = [[0.0, 0.0]]
_sl_PIN_D = 20.0
_sl_PIN_L = 150.0
ANGLE_PIN_KNARR_PATH = "/data/knarr/02_slide_systems/05_Slanted_slides/224090_angle_pin/224090_16.step"

def make_plate(width, height, thickness, name="plate", knarr_path=""):
    if knarr_path:
        try:
            plate = cq.importers.importStep(knarr_path)
            print(f"[KNARR] plate {name} from {knarr_path}")
            return plate
        except Exception as _knarr_err:
            print(f"[KNARR] plate {name} import failed: {_knarr_err}; using primitive")
    return (
        cq.Workplane("XY")
        .box(width, height, thickness, centered=(True, True, False))
        .edges("|Z")
        .fillet(2)
    )

def drill_bores(plate, positions, dia, thickness):
    for _bx, _by in positions:
        _bore = (
            cq.Workplane("XY")
            .cylinder(thickness + 2, dia / 2, centered=(True, True, False))
            .translate((_bx, _by, -1))
        )
        try:
            plate = plate.cut(_bore)
        except Exception as _err:
            print(f"[bore] skip at ({_bx},{_by}) dia={dia}: {_err}")
    return plate

def drill_pocket(plate, x, y, dia, depth, thickness):
    # Pocket open on top face (Z=thickness). Cut from z=thickness-depth upward.
    _pocket = (
        cq.Workplane("XY")
        .cylinder(depth + 1, dia / 2, centered=(True, True, False))
        .translate((x, y, thickness - depth))
    )
    try:
        return plate.cut(_pocket)
    except Exception as _err:
        print(f"[pocket] skip at ({x},{y}) dia={dia}: {_err}")
        return plate



# ============================================
# base_stack — plates + guides + dowels + screws
# ============================================
_bs_clamp_moving = make_plate(FRAME_W, FRAME_H, _bs_T_CLAMP_MOVING, "clamp_moving", CLAMP_MOVING_PLATE_KNARR_PATH)
_bs_ejector_plate = make_plate(FRAME_W-20, FRAME_H-20, _bs_T_EJECT, "ejector_plate", EJECTOR_PLATE_KNARR_PATH)
_bs_ejector_retainer = make_plate(FRAME_W-20, FRAME_H-20, _bs_T_EJECT_RET, "ejector_retainer", EJECTOR_RETAINER_PLATE_KNARR_PATH)
_bs_support_plate = make_plate(FRAME_W, FRAME_H, _bs_T_SUPPORT, "support_plate", SUPPORT_PLATE_KNARR_PATH)
_bs_core_plate = make_plate(FRAME_W, FRAME_H, _bs_T_CORE, "core_plate", CORE_PLATE_KNARR_PATH)
_bs_cavity_plate = make_plate(FRAME_W, FRAME_H, _bs_T_CAVITY, "cavity_plate", CAVITY_PLATE_KNARR_PATH)
_bs_clamp_fixed = make_plate(FRAME_W, FRAME_H, _bs_T_CLAMP_FIXED, "clamp_fixed", CLAMP_FIXED_PLATE_KNARR_PATH)

# Z stack
_bs_z_clamp_moving = 0
_bs_z_eject = _bs_T_CLAMP_MOVING
_bs_z_eject_ret = _bs_z_eject + _bs_T_EJECT
_bs_z_spacer = _bs_z_eject_ret + _bs_T_EJECT_RET
_bs_z_support = _bs_z_spacer + _bs_T_SPACER
_bs_z_core = _bs_z_support + _bs_T_SUPPORT
_bs_z_cavity = _bs_z_core + _bs_T_CORE
_bs_z_clamp_fixed = _bs_z_cavity + _bs_T_CAVITY

# Guide pillars
_bs_guide_body = None
if GUIDE_PILLAR_KNARR_PATH:
    try:
        _bs_guide_body = cq.importers.importStep(GUIDE_PILLAR_KNARR_PATH)
        print(f"[KNARR] guide pillar from {GUIDE_PILLAR_KNARR_PATH}")
    except Exception as _knarr_err:
        print(f"[KNARR] guide pillar import failed: {_knarr_err}; using primitive")
        _bs_guide_body = None
if _bs_guide_body is not None:
    # KNARR blanks ship long + HORIZONTAL (cut-to-length). Reorient
    # the long axis to Z, centre XY, base z=0 — or discard an
    # implausible catalog match so the primitive is used instead.
    try:
        _bs_gpn_bb = _bs_guide_body.val().BoundingBox()
        _bs_gpn_dims = sorted([_bs_gpn_bb.xlen, _bs_gpn_bb.ylen, _bs_gpn_bb.zlen])
        if _bs_gpn_dims[2] > ((_bs_T_CORE + _bs_T_CAVITY + 20)) * 1.5 or _bs_gpn_dims[1] > (_bs_GUIDE_PIN_DIA) * 1.8:
            _bs_guide_body = None
        else:
            if _bs_gpn_bb.xlen >= _bs_gpn_bb.ylen and _bs_gpn_bb.xlen >= _bs_gpn_bb.zlen:
                _bs_guide_body = _bs_guide_body.rotate((0, 0, 0), (0, 1, 0), 90)
            elif _bs_gpn_bb.ylen >= _bs_gpn_bb.xlen and _bs_gpn_bb.ylen >= _bs_gpn_bb.zlen:
                _bs_guide_body = _bs_guide_body.rotate((0, 0, 0), (1, 0, 0), -90)
            _bs_gpn_bb2 = _bs_guide_body.val().BoundingBox()
            _bs_guide_body = _bs_guide_body.translate((
                -(_bs_gpn_bb2.xmin + _bs_gpn_bb2.xmax) / 2.0,
                -(_bs_gpn_bb2.ymin + _bs_gpn_bb2.ymax) / 2.0,
                -_bs_gpn_bb2.zmin))
    except Exception as _bs_gpn_err:
        print(f"[KNARR] guide pillar normalise failed: {_bs_gpn_err}")
        _bs_guide_body = None
_bs_guide_pins = []
for _bs_gx, _bs_gy in _bs_GUIDE_PIN_POS:
    if _bs_guide_body is not None:
        _bs_pin = _bs_guide_body
    else:
        _bs_pin = (
            cq.Workplane("XY")
            .circle(_bs_GUIDE_PIN_DIA/2)
            .extrude(_bs_T_CORE + _bs_T_CAVITY + 20)
        )
    _bs_guide_pins.append((_bs_pin, _bs_gx, _bs_gy))

# Guide bushings (Track B Step 4) — KNARR 51110 with collar.
# Mating pillar in cavity_plate; primitive fallback is a hollow
# cylinder of the same OD/ID/length so STEP shows a real mating
# part not just a drilled hole.
_bs_bushing_body = None
if GUIDE_BUSHING_KNARR_PATH:
    try:
        _bs_bushing_body = cq.importers.importStep(GUIDE_BUSHING_KNARR_PATH)
        print(f"[KNARR] guide bushing from {GUIDE_BUSHING_KNARR_PATH}")
    except Exception as _knarr_err:
        print(f"[KNARR] guide bushing import failed: {_knarr_err}; using primitive")
        _bs_bushing_body = None
if _bs_bushing_body is not None:
    # KNARR blanks ship long + HORIZONTAL (cut-to-length). Reorient
    # the long axis to Z, centre XY, base z=0 — or discard an
    # implausible catalog match so the primitive is used instead.
    try:
        _bs_gbn_bb = _bs_bushing_body.val().BoundingBox()
        _bs_gbn_dims = sorted([_bs_gbn_bb.xlen, _bs_gbn_bb.ylen, _bs_gbn_bb.zlen])
        if _bs_gbn_dims[2] > (_bs_T_CAVITY) * 1.5 or _bs_gbn_dims[1] > ((_bs_GUIDE_PIN_DIA + 4)) * 1.8:
            _bs_bushing_body = None
        else:
            if _bs_gbn_bb.xlen >= _bs_gbn_bb.ylen and _bs_gbn_bb.xlen >= _bs_gbn_bb.zlen:
                _bs_bushing_body = _bs_bushing_body.rotate((0, 0, 0), (0, 1, 0), 90)
            elif _bs_gbn_bb.ylen >= _bs_gbn_bb.xlen and _bs_gbn_bb.ylen >= _bs_gbn_bb.zlen:
                _bs_bushing_body = _bs_bushing_body.rotate((0, 0, 0), (1, 0, 0), -90)
            _bs_gbn_bb2 = _bs_bushing_body.val().BoundingBox()
            _bs_bushing_body = _bs_bushing_body.translate((
                -(_bs_gbn_bb2.xmin + _bs_gbn_bb2.xmax) / 2.0,
                -(_bs_gbn_bb2.ymin + _bs_gbn_bb2.ymax) / 2.0,
                -_bs_gbn_bb2.zmin))
    except Exception as _bs_gbn_err:
        print(f"[KNARR] guide bushing normalise failed: {_bs_gbn_err}")
        _bs_bushing_body = None
_bs_guide_bushings = []
for _bs_gx, _bs_gy in _bs_GUIDE_PIN_POS:
    if _bs_bushing_body is not None:
        _bs_bushing = _bs_bushing_body
    else:
        # Primitive: hollow cylinder OD = pin_d + 4mm, ID = pin_d + 0.05mm
        _bs_bushing_outer = (
            cq.Workplane("XY")
            .circle(_bs_GUIDE_PIN_DIA/2 + 2.0)
            .extrude(_bs_T_CAVITY)
        )
        _bs_bushing_inner = (
            cq.Workplane("XY")
            .circle(_bs_GUIDE_PIN_DIA/2 + 0.05)
            .extrude(_bs_T_CAVITY)
        )
        _bs_bushing = _bs_bushing_outer.cut(_bs_bushing_inner)
    _bs_guide_bushings.append((_bs_bushing, _bs_gx, _bs_gy))

# Spacer block — two rails at frame edges
_bs_spacer_left = (
    cq.Workplane("XY")
    .box(40, FRAME_H, _bs_T_SPACER, centered=(True, True, False))
    .translate((-FRAME_W / 2 + 20, 0, 0))
)
_bs_spacer_right = (
    cq.Workplane("XY")
    .box(40, FRAME_H, _bs_T_SPACER, centered=(True, True, False))
    .translate((FRAME_W / 2 - 20, 0, 0))
)
_bs_spacer_block = _bs_spacer_left.union(_bs_spacer_right)

# Support pillars — 4 corners inside spacer area
_bs_sp_knarr = None
if SUPPORT_PILLAR_KNARR_PATH:
    try:
        _bs_sp_knarr = cq.importers.importStep(SUPPORT_PILLAR_KNARR_PATH)
        print(f"[KNARR] support pillar from {SUPPORT_PILLAR_KNARR_PATH}")
    except Exception as _knarr_err:
        print(f"[KNARR] support pillar import failed: {_knarr_err}; using primitive")
        _bs_sp_knarr = None
_bs_sp_margin = 45
_bs_sp_positions = [
    (FRAME_W/2 - _bs_sp_margin, FRAME_H/2 - _bs_sp_margin),
    (-FRAME_W/2 + _bs_sp_margin, FRAME_H/2 - _bs_sp_margin),
    (-FRAME_W/2 + _bs_sp_margin, -FRAME_H/2 + _bs_sp_margin),
    (FRAME_W/2 - _bs_sp_margin, -FRAME_H/2 + _bs_sp_margin),
]
_bs_support_pillars = []
for _bs_spx, _bs_spy in _bs_sp_positions:
    if _bs_sp_knarr is not None:
        _bs_sp = _bs_sp_knarr
    else:
        _bs_sp = (
            cq.Workplane("XY")
            .circle(30 / 2)
            .extrude(_bs_T_SPACER - _bs_T_EJECT_RET - _bs_T_EJECT)
        )
    _bs_support_pillars.append((_bs_sp, _bs_spx, _bs_spy))

# Mounting screws — 4 corners × 2 levels
_bs_sc_off_x = FRAME_W / 2 - 25
_bs_sc_off_y = FRAME_H / 2 - 25
_bs_sc_corners = [
    (-_bs_sc_off_x, -_bs_sc_off_y),
    (_bs_sc_off_x, -_bs_sc_off_y),
    (-_bs_sc_off_x, _bs_sc_off_y),
    (_bs_sc_off_x, _bs_sc_off_y),
]
_bs_screw_top = None
if SCREW_TOP_KNARR_PATH:
    try:
        _bs_screw_top = cq.importers.importStep(SCREW_TOP_KNARR_PATH)
    except Exception as _knarr_err:
        print(f"[KNARR] screw top import failed: {_knarr_err}; using primitive")
        _bs_screw_top = None
_bs_screw_bot = None
if SCREW_BOT_KNARR_PATH:
    try:
        _bs_screw_bot = cq.importers.importStep(SCREW_BOT_KNARR_PATH)
    except Exception as _knarr_err:
        print(f"[KNARR] screw bot import failed: {_knarr_err}; using primitive")
        _bs_screw_bot = None
_bs_screws_top = []
for _bs_scx, _bs_scy in _bs_sc_corners:
    if _bs_screw_top is not None:
        _bs_screw = _bs_screw_top
    else:
        _bs_screw_len = _bs_T_CLAMP_FIXED + _bs_T_CAVITY
        _bs_screw = (
            cq.Workplane("XY")
            .cylinder(_bs_screw_len, 6, centered=(True, True, False))
            .union(
                cq.Workplane("XY")
                .cylinder(12, 9, centered=(True, True, False))
                .translate((0, 0, _bs_screw_len - 12))
            )
        )
    _bs_screws_top.append((_bs_screw, _bs_scx, _bs_scy))
_bs_screws_bot = []
for _bs_scx, _bs_scy in _bs_sc_corners:
    if _bs_screw_bot is not None:
        _bs_screw = _bs_screw_bot
    else:
        _bs_screw_len = _bs_T_CORE + _bs_T_SUPPORT
        _bs_screw = (
            cq.Workplane("XY")
            .cylinder(_bs_screw_len, 6, centered=(True, True, False))
        )
    _bs_screws_bot.append((_bs_screw, _bs_scx, _bs_scy))

# Dowel pins — 2 diagonal positions between cavity and core
_bs_dp_off_x = FRAME_W / 2 - 40
_bs_dp_off_y = FRAME_H / 2 - 40
_bs_dp_positions = [
    (_bs_dp_off_x, _bs_dp_off_y),
    (-_bs_dp_off_x, -_bs_dp_off_y),
]
_bs_dowel = None
if DOWEL_PIN_KNARR_PATH:
    try:
        _bs_dowel = cq.importers.importStep(DOWEL_PIN_KNARR_PATH)
    except Exception as _knarr_err:
        print(f"[KNARR] dowel pin import failed: {_knarr_err}; using primitive")
        _bs_dowel = None
_bs_dowels = []
for _bs_dpx, _bs_dpy in _bs_dp_positions:
    if _bs_dowel is not None:
        _bs_d = _bs_dowel
    else:
        _bs_dp_len = _bs_T_CAVITY + _bs_T_CORE
        _bs_d = (
            cq.Workplane("XY")
            .cylinder(_bs_dp_len, 5, centered=(True, True, False))
        )
    _bs_dowels.append((_bs_d, _bs_dpx, _bs_dpy))

# Insulation plate — 10mm on top of clamp_fixed
_bs_insul = (
    cq.Workplane("XY").box(FRAME_W, FRAME_H, 10, centered=(True, True, False))
)


# ============================================
# Plate bore cuts — P1B.3
# ============================================
_bs_SCREW_TOP_DIA = 14   # M12 through-hole + counterbore Ø18 is in legacy
_bs_SCREW_CBORE_DIA = 20
_bs_SCREW_CBORE_D = 12
_bs_DOWEL_DIA = 10.1
_bs_LOC_RING_OD = 125.0
_bs_LOC_RING_DEPTH = 12.0
_bs_SPRUE_TOP_DIA = 16.0  # sprue seat on cavity top face

# Dowel pin positions — match _bs_dp_positions
_bs_DOWEL_POS = [
    (FRAME_W / 2 - 40, FRAME_H / 2 - 40),
    (-(FRAME_W / 2 - 40), -(FRAME_H / 2 - 40)),
]
# Screw corner positions — match _bs_sc_corners
_bs_SCREW_POS = [
    (-(FRAME_W / 2 - 25), -(FRAME_H / 2 - 25)),
    ((FRAME_W / 2 - 25), -(FRAME_H / 2 - 25)),
    (-(FRAME_W / 2 - 25), (FRAME_H / 2 - 25)),
    ((FRAME_W / 2 - 25), (FRAME_H / 2 - 25)),
]

# Guide pin bores — 5 plates
_bs_clamp_fixed  = drill_bores(
    _bs_clamp_fixed,  _bs_GUIDE_PIN_POS, _bs_GUIDE_PIN_DIA + 0.1, _bs_T_CLAMP_FIXED)
_bs_cavity_plate = drill_bores(
    _bs_cavity_plate, _bs_GUIDE_PIN_POS, _bs_GUIDE_PIN_DIA + 0.1, _bs_T_CAVITY)
_bs_core_plate   = drill_bores(
    _bs_core_plate,   _bs_GUIDE_PIN_POS, _bs_GUIDE_PIN_DIA + 0.1, _bs_T_CORE)
_bs_support_plate = drill_bores(
    _bs_support_plate, _bs_GUIDE_PIN_POS, _bs_GUIDE_PIN_DIA + 0.1, _bs_T_SUPPORT)
_bs_clamp_moving = drill_bores(
    _bs_clamp_moving, _bs_GUIDE_PIN_POS, _bs_GUIDE_PIN_DIA + 0.1, _bs_T_CLAMP_MOVING)

# Dowel pin bores — cavity + core
_bs_cavity_plate = drill_bores(
    _bs_cavity_plate, _bs_DOWEL_POS, _bs_DOWEL_DIA, _bs_T_CAVITY)
_bs_core_plate   = drill_bores(
    _bs_core_plate,   _bs_DOWEL_POS, _bs_DOWEL_DIA, _bs_T_CORE)

# Screw-top bores — clamp_fixed (counterbore) + cavity (through)
for _bs_sx, _bs_sy in _bs_SCREW_POS:
    _bs_clamp_fixed = drill_pocket(
        _bs_clamp_fixed, _bs_sx, _bs_sy, _bs_SCREW_CBORE_DIA, _bs_SCREW_CBORE_D, _bs_T_CLAMP_FIXED)
_bs_clamp_fixed  = drill_bores(
    _bs_clamp_fixed,  _bs_SCREW_POS, _bs_SCREW_TOP_DIA, _bs_T_CLAMP_FIXED)
_bs_cavity_plate = drill_bores(
    _bs_cavity_plate, _bs_SCREW_POS, _bs_SCREW_TOP_DIA, _bs_T_CAVITY)

# Screw-bot bores — core (counterbore) + support (through)
for _bs_sx, _bs_sy in _bs_SCREW_POS:
    _bs_core_plate = drill_pocket(
        _bs_core_plate, _bs_sx, _bs_sy, _bs_SCREW_CBORE_DIA, _bs_SCREW_CBORE_D, _bs_T_CORE)
_bs_core_plate    = drill_bores(
    _bs_core_plate,    _bs_SCREW_POS, _bs_SCREW_TOP_DIA, _bs_T_CORE)
_bs_support_plate = drill_bores(
    _bs_support_plate, _bs_SCREW_POS, _bs_SCREW_TOP_DIA, _bs_T_SUPPORT)

# Sprue seat in cavity plate (centered)
_bs_cavity_plate = drill_bores(
    _bs_cavity_plate, [(0, 0)], _bs_SPRUE_TOP_DIA, _bs_T_CAVITY)

# Locating ring recess in clamp_fixed (top face pocket)
_bs_clamp_fixed = drill_pocket(
    _bs_clamp_fixed, 0, 0, _bs_LOC_RING_OD, _bs_LOC_RING_DEPTH, _bs_T_CLAMP_FIXED)



# ============================================
# feed.cold_runner — sprue bushing + locating ring
# ============================================
_cr_sprue = None
if SPRUE_BUSHING_KNARR_PATH:
    try:
        _cr_sprue = cq.importers.importStep(SPRUE_BUSHING_KNARR_PATH)
        print(f"[KNARR] sprue bushing from {SPRUE_BUSHING_KNARR_PATH}")
    except Exception as _knarr_err:
        print(f"[KNARR] sprue bushing import failed: {_knarr_err}; using primitive")
        _cr_sprue = None
if _cr_sprue is None:
    _cr_sprue_len = _bs_T_CAVITY + _bs_T_CLAMP_FIXED
    _cr_sprue_body = (
        cq.Workplane("XY")
        .cylinder(_cr_sprue_len, _cr_SPRUE_BODY_DIA/2,
                  centered=(True, True, False))
    )
    _cr_sprue_head = (
        cq.Workplane("XY")
        .cylinder(12, (_cr_SPRUE_BODY_DIA+20)/2,
                  centered=(True, True, False))
        .translate((0, 0, _cr_sprue_len))
    )
    _cr_sprue_bore = (
        cq.Workplane("XY")
        .cylinder(_cr_sprue_len + 13, _cr_SPRUE_DIA/2,
                  centered=(True, True, False))
    )
    _cr_sprue = (
        _cr_sprue_body.union(_cr_sprue_head).cut(_cr_sprue_bore, clean=False)
    )

_cr_loc_ring = None
if LOCATING_RING_KNARR_PATH:
    try:
        _cr_loc_ring = cq.importers.importStep(LOCATING_RING_KNARR_PATH)
        print(f"[KNARR] locating ring from {LOCATING_RING_KNARR_PATH}")
    except Exception as _knarr_err:
        print(f"[KNARR] locating ring import failed: {_knarr_err}; using primitive")
        _cr_loc_ring = None
if _cr_loc_ring is None:
    _cr_loc_ring_body = (
        cq.Workplane("XY")
        .cylinder(12, _cr_LOC_RING_OD/2,
                  centered=(True, True, False))
    )
    _cr_loc_ring_bore = (
        cq.Workplane("XY")
        .cylinder(13, 22,
                  centered=(True, True, False))
    )
    _cr_loc_ring = _cr_loc_ring_body.cut(_cr_loc_ring_bore, clean=False)

# Edge gate — rectangular box at parting plane
_cr_gate = (
    cq.Workplane("XY")
    .box(_cr_GATE_W, 2, _cr_GATE_D, centered=(True, True, False))
)


# ============================================
# ejection.pin — N ejector pins
# ============================================
_pe_ejector_pins = []
for _pe_i, (_pe_ex, _pe_ey, _pe_edia) in enumerate(EJECTOR_PINS):
    _pe_knarr = (
        EJECTOR_PIN_KNARR_PATHS[_pe_i]
        if _pe_i < len(EJECTOR_PIN_KNARR_PATHS) else ""
    )
    _pe_pin = None
    if _pe_knarr:
        try:
            _pe_pin = cq.importers.importStep(_pe_knarr)
        except Exception as _pe_err:
            print(f"[KNARR] ejector pin import failed: {_pe_err}")
            _pe_pin = None
    if _pe_pin is None:
        _pe_len = _bs_T_CORE + _bs_T_SUPPORT
        _pe_body = (
            cq.Workplane("XY")
            .cylinder(_pe_len, _pe_edia/2,
                      centered=(True, True, False))
        )
        _pe_head = (
            cq.Workplane("XY")
            .cylinder(5, (_pe_edia + 4)/2,
                      centered=(True, True, False))
        )
        _pe_pin = _pe_head.union(_pe_body.translate((0, 0, 5)))
    _pe_ejector_pins.append((_pe_pin, _pe_ex, _pe_ey))


# ============================================
# ejection.return_assembly — 4 return pins + 4 springs
# ============================================
# Return-pin bores — sliding fit H7 (pin_d + 0.1) through
# ejector_retainer / support_plate / core_plate.
_re_bore_dia = _re_PIN_DIA + 0.1
_bs_ejector_retainer = drill_bores(
    _bs_ejector_retainer, _re_RETURN_POS, _re_bore_dia,
    _bs_T_EJECT_RET)
_bs_support_plate = drill_bores(
    _bs_support_plate, _re_RETURN_POS, _re_bore_dia,
    _bs_T_SUPPORT)
_bs_core_plate = drill_bores(
    _bs_core_plate, _re_RETURN_POS, _re_bore_dia,
    _bs_T_CORE)

_re_pins = []
_re_springs = []
for _re_x, _re_y in _re_RETURN_POS:
    # ---- return pin --------------------------------------
    _re_pin = None
    if RETURN_PIN_KNARR_PATH:
        try:
            _re_pin = cq.importers.importStep(RETURN_PIN_KNARR_PATH)
        except Exception as _re_err:
            print(f"[KNARR] return pin import failed: {_re_err}")
            _re_pin = None
    if _re_pin is not None:
        # KNARR blanks ship long + HORIZONTAL (cut-to-length). Reorient
        # the long axis to Z, centre XY, base z=0 — or discard an
        # implausible catalog match so the primitive is used instead.
        try:
            _re_bb = _re_pin.val().BoundingBox()
            _re_dims = sorted([_re_bb.xlen, _re_bb.ylen, _re_bb.zlen])
            if _re_dims[2] > (_re_PIN_LEN) * 1.5 or _re_dims[1] > (_re_PIN_DIA) * 1.8:
                _re_pin = None
            else:
                if _re_bb.xlen >= _re_bb.ylen and _re_bb.xlen >= _re_bb.zlen:
                    _re_pin = _re_pin.rotate((0, 0, 0), (0, 1, 0), 90)
                elif _re_bb.ylen >= _re_bb.xlen and _re_bb.ylen >= _re_bb.zlen:
                    _re_pin = _re_pin.rotate((0, 0, 0), (1, 0, 0), -90)
                _re_bb2 = _re_pin.val().BoundingBox()
                _re_pin = _re_pin.translate((
                    -(_re_bb2.xmin + _re_bb2.xmax) / 2.0,
                    -(_re_bb2.ymin + _re_bb2.ymax) / 2.0,
                    -_re_bb2.zmin))
        except Exception as _re_err:
            print(f"[KNARR] return pin normalise failed: {_re_err}")
            _re_pin = None
    if _re_pin is None:
        _re_pin_total_l = _re_PIN_LEN
        _re_pin_shaft = (
            cq.Workplane("XY")
            .cylinder(_re_pin_total_l, _re_PIN_DIA / 2,
                      centered=(True, True, False))
        )
        # head shoulder (sits in ejector retainer pocket)
        _re_pin_head = (
            cq.Workplane("XY")
            .cylinder(5, (_re_PIN_DIA + 8) / 2,
                      centered=(True, True, False))
        )
        _re_pin = _re_pin_head.union(
            _re_pin_shaft.translate((0, 0, 5))
        )
    _re_pins.append((_re_pin, _re_x, _re_y))
    # ---- return spring -----------------------------------
    _re_spring = None
    if RETURN_SPRING_KNARR_PATH:
        try:
            _re_spring = cq.importers.importStep(
                RETURN_SPRING_KNARR_PATH)
        except Exception as _re_err:
            print(f"[KNARR] return spring import failed: {_re_err}")
            _re_spring = None
    if _re_spring is not None:
        # KNARR blanks ship long + HORIZONTAL (cut-to-length). Reorient
        # the long axis to Z, centre XY, base z=0 — or discard an
        # implausible catalog match so the primitive is used instead.
        try:
            _re_sp_bb = _re_spring.val().BoundingBox()
            _re_sp_dims = sorted([_re_sp_bb.xlen, _re_sp_bb.ylen, _re_sp_bb.zlen])
            if _re_sp_dims[2] > (_re_SPRING_FREE_L) * 1.5 or _re_sp_dims[1] > (_re_SPRING_OD) * 1.8:
                _re_spring = None
            else:
                if _re_sp_bb.xlen >= _re_sp_bb.ylen and _re_sp_bb.xlen >= _re_sp_bb.zlen:
                    _re_spring = _re_spring.rotate((0, 0, 0), (0, 1, 0), 90)
                elif _re_sp_bb.ylen >= _re_sp_bb.xlen and _re_sp_bb.ylen >= _re_sp_bb.zlen:
                    _re_spring = _re_spring.rotate((0, 0, 0), (1, 0, 0), -90)
                _re_sp_bb2 = _re_spring.val().BoundingBox()
                _re_spring = _re_spring.translate((
                    -(_re_sp_bb2.xmin + _re_sp_bb2.xmax) / 2.0,
                    -(_re_sp_bb2.ymin + _re_sp_bb2.ymax) / 2.0,
                    -_re_sp_bb2.zmin))
        except Exception as _re_sp_err:
            print(f"[KNARR] return spring normalise failed: {_re_sp_err}")
            _re_spring = None
    if _re_spring is None:
        # primitive: hollow cylinder representing the coil OD
        _re_outer = (
            cq.Workplane("XY")
            .cylinder(_re_SPRING_FREE_L, _re_SPRING_OD / 2,
                      centered=(True, True, False))
        )
        _re_inner = (
            cq.Workplane("XY")
            .cylinder(_re_SPRING_FREE_L + 1,
                      (_re_SPRING_OD - 4) / 2,
                      centered=(True, True, False))
        )
        try:
            _re_spring = _re_outer.cut(_re_inner)
        except Exception:
            _re_spring = _re_outer  # fallback to solid coil
    _re_springs.append((_re_spring, _re_x, _re_y))


# ============================================
# cooling — visualisation solids (no bore cuts here)
# ============================================
# Straight cooling channels (4x) — parallel to X axis
_cool_channels = []
_cool_ch_len = FRAME_W - 40
for _cool_ci in range(_cool_N_CHANNELS):
    _cool_y = (_cool_ci - (_cool_N_CHANNELS - 1) / 2) * _cool_PITCH_Y
    _cool_cyl = (
        cq.Workplane("XY")
        .cylinder(_cool_ch_len, _cool_DIA / 2)
        .rotateAboutCenter((0, 1, 0), 90)
    )
    _cool_channels.append((_cool_cyl, _cool_y))

# U-turns (3x) — connect adjacent channels at alternating frame edges
_cool_uturns = []
for _cool_ui in range(_cool_N_CHANNELS - 1):
    _cool_y1 = (_cool_ui - (_cool_N_CHANNELS - 1) / 2) * _cool_PITCH_Y
    _cool_y2 = (_cool_ui + 1 - (_cool_N_CHANNELS - 1) / 2) * _cool_PITCH_Y
    _cool_uy_mid = (_cool_y1 + _cool_y2) / 2
    _cool_uturn_len = abs(_cool_y2 - _cool_y1)
    _cool_ux = (FRAME_W / 2 - 25) * (1 if _cool_ui % 2 == 0 else -1)
    _cool_uturn = (
        cq.Workplane("XY")
        .cylinder(_cool_uturn_len, _cool_DIA / 2)
        .rotateAboutCenter((1, 0, 0), 90)
    )
    _cool_uturns.append((_cool_uturn, _cool_ux, _cool_uy_mid))

# Inlet + outlet markers — G1/8 fitting bosses on +Y/-Y frame edge
# (G1/8 BSP: major 9.728mm, pitch 0.907mm — toolmaker taps it)
_cool_marker_y_top = FRAME_H / 2 - 10
_cool_marker_y_bot = -FRAME_H / 2 + 10
_cool_FITTING_BOSS_OD = 15.0  # boss OD (sealing surface)
_cool_FITTING_THREAD_D = 9.728  # G1/8 BSP major diameter
_cool_FITTING_DEPTH = 12.0  # threaded section depth
_cool_FITTING_CHAMFER = 2.0  # tap relief chamfer
# Build a single fitting profile (boss + threaded core +
# entrance chamfer), reused for inlet and outlet.
_cool_fitting_outer = (
    cq.Workplane("XY")
    .cylinder(_cool_FITTING_DEPTH + 3, _cool_FITTING_BOSS_OD / 2)
)
_cool_fitting_thread_core = (
    cq.Workplane("XY")
    .workplane(offset=-_cool_FITTING_DEPTH / 2)
    .cylinder(_cool_FITTING_DEPTH, _cool_FITTING_THREAD_D / 2)
)
# Subtract the thread-relief chamfer at the boss face.
_cool_fitting_chamfer = (
    cq.Workplane("XY")
    .workplane(offset=_cool_FITTING_DEPTH / 2 - _cool_FITTING_CHAMFER)
    .add(
        cq.Solid.makeCone(
            _cool_FITTING_THREAD_D / 2 + _cool_FITTING_CHAMFER,
            _cool_FITTING_THREAD_D / 2,
            _cool_FITTING_CHAMFER,
        )
    )
)
_cool_inlet = (
    _cool_fitting_outer
    .cut(_cool_fitting_thread_core)
    .cut(_cool_fitting_chamfer)
)
_cool_outlet = _cool_inlet  # identical fitting on opposite edge

# Cooling-fitting O-rings (KNARR 70098) — sit under the boss
# collar between brass fitting and plate face.
_cool_oring_inlet = None
if COOLING_ORING_KNARR_PATH:
    try:
        _cool_oring_inlet = cq.importers.importStep(
            COOLING_ORING_KNARR_PATH)
    except Exception as _cool_err:
        print(f"[KNARR] cooling O-ring import failed: {_cool_err}")
        _cool_oring_inlet = None
if _cool_oring_inlet is None:
    # Primitive torus fallback (visual placeholder — BOM still
    # carries the 70098 part number).
    try:
        _cool_oring_inlet = (
            cq.Workplane("XY")
            .add(cq.Solid.makeTorus(
                _cool_ORING_ID / 2 + _cool_ORING_CORD / 2,
                _cool_ORING_CORD / 2))
        )
    except Exception:
        # Older OCC may not expose makeTorus — fall back to a
        # thin ring (revolved square cross-section).
        _cool_oring_inlet = (
            cq.Workplane("XY")
            .cylinder(_cool_ORING_CORD,
                      _cool_ORING_ID / 2 + _cool_ORING_CORD)
            .cut(
                cq.Workplane("XY")
                .cylinder(_cool_ORING_CORD + 1,
                          _cool_ORING_ID / 2)
            )
        )
_cool_oring_outlet = _cool_oring_inlet

_cool_z = _bs_z_cavity + _bs_T_CAVITY / 2



# ============================================
# parting — split part at parting Z, boolean into plates
# ============================================
try:
    from moldforge_core.cad.ecip.splitting import split_at_parting_plane
    from moldforge_core.cad.ecip.types import ECIPSolid

    _pt_raw = cq.importers.importStep(_pt_PART_STEP_PATH).val()
    if hasattr(_pt_raw, 'Solids'):
        _pt_solids = _pt_raw.Solids()
        if len(_pt_solids) == 1:
            _pt_raw = _pt_solids[0]
    _pt_scaled = _pt_raw.scale(_pt_SHRINKAGE)

    _pt_sbb = _pt_scaled.BoundingBox()
    _pt_ecip = ECIPSolid(
        shape=cq.Workplane("XY").newObject([_pt_scaled]),
        name="part",
        bounding_box=(
            _pt_sbb.xmin, _pt_sbb.ymin, _pt_sbb.zmin,
            _pt_sbb.xmax, _pt_sbb.ymax, _pt_sbb.zmax,
        ),
        volume=_pt_scaled.Volume(),
        face_count=len(_pt_scaled.Faces()),
    )
    _pt_pz = _pt_PARTING_Z_MM
    if _pt_pz == 0.0:
        _pt_pz = (_pt_sbb.zmin + _pt_sbb.zmax) / 2
        print(
            f"[parting] PARTING_Z=0, using mid-height {_pt_pz:.1f}mm"
        )

    _pt_cav_half, _pt_core_half = split_at_parting_plane(_pt_ecip, _pt_pz)
    _pt_cav_solid = _pt_cav_half.shape.val()
    _pt_core_solid = _pt_core_half.shape.val()

    # Material-aware draft — best-effort, skip silently on failure
    try:
        from moldforge_core.cad.ecip.draft import (
            apply_draft_to_half, recommended_draft_deg,
        )
        _pt_draft = recommended_draft_deg(_pt_MATERIAL_FAMILY)
        _pt_cav_solid = apply_draft_to_half(
            _pt_cav_solid, draft_deg=_pt_draft,
        )
        _pt_core_solid = apply_draft_to_half(
            _pt_core_solid, draft_deg=_pt_draft,
        )
    except Exception as _pt_draft_err:
        print(f"[parting] draft skipped: {_pt_draft_err}")

    _pt_cav_bb = _pt_cav_solid.BoundingBox()
    _pt_core_bb = _pt_core_solid.BoundingBox()

    _pt_impressions = []  # (name, wp, cx, cy)
    for _pt_cx, _pt_cy in _pt_CAVITY_POSITIONS:
        _pt_dx = _pt_cx - (_pt_cav_bb.xmin + _pt_cav_bb.xmax) / 2
        _pt_dy = _pt_cy - (_pt_cav_bb.ymin + _pt_cav_bb.ymax) / 2
        _pt_cav_wp = (
            cq.Workplane("XY")
            .add(_pt_cav_solid)
            .translate((_pt_dx, _pt_dy, -_pt_cav_bb.zmin))
        )
        try:
            _bs_cavity_plate = _bs_cavity_plate.cut(
                _pt_cav_wp, clean=False,
            )
        except Exception as _pt_cav_err:
            print(f"[parting] cavity cut failed: {_pt_cav_err}")
        _pt_impressions.append(
            ("cavity_impression", _pt_cav_wp, _pt_cx, _pt_cy)
        )

        _pt_dx2 = _pt_cx - (_pt_core_bb.xmin + _pt_core_bb.xmax) / 2
        _pt_dy2 = _pt_cy - (_pt_core_bb.ymin + _pt_core_bb.ymax) / 2
        _pt_core_wp = (
            cq.Workplane("XY")
            .add(_pt_core_solid)
            .translate(
                (_pt_dx2, _pt_dy2, _bs_T_CORE - _pt_core_bb.zmax)
            )
        )
        try:
            _bs_core_plate = _bs_core_plate.cut(
                _pt_core_wp, clean=False,
            )
        except Exception as _pt_core_err:
            print(f"[parting] core cut failed: {_pt_core_err}")
        _pt_impressions.append(
            ("core_impression", _pt_core_wp, _pt_cx, _pt_cy)
        )
except Exception as _pt_err:
    print(f"[parting] failed — generating frame without impression: {_pt_err}")
    _pt_impressions = []


# ============================================
# undercut.slides — pockets + angle pins + K2528 slider bodies
# ============================================
# --- Slide 1: direction=(1, 0, 0) ---
_sl_off_x = 1 * (FRAME_W / 2 - 17.0 / 2)
_sl_off_y = 0 * (FRAME_H / 2 - 17.0 / 2)
try:
    _sl_pocket = (
        cq.Workplane("XY")
        .box(17.0, 27.0, _bs_T_CAVITY,
              centered=(True, True, False))
        .translate((_sl_off_x, _sl_off_y, 0))
    )
    _bs_cavity_plate = _bs_cavity_plate.cut(_sl_pocket)
except Exception as _sl_err:
    print(f"[slide 1] pocket cut failed: {_sl_err}")
try:
    _sl_pin_bore = (
        cq.Workplane("XZ")
        .transformed(
            offset=cq.Vector(
                _sl_off_x, 0, 0,
            ),
            rotate=cq.Vector(0, -20, 0),
        )
        .circle(_sl_PIN_D / 2)
        .extrude(_sl_PIN_L)
    )
    _bs_cavity_plate = _bs_cavity_plate.cut(_sl_pin_bore)
except Exception as _sl_pin_err:
    print(f"[slide 1] angle pin bore failed: {_sl_pin_err}")
_sl_body_1 = (
    cq.Workplane("XY")
    .box(16.8, 26.8, _bs_T_CAVITY - 2,
          centered=(True, True, False))
    .translate((_sl_off_x, _sl_off_y, 0))
)


# ============================================
# ASSEMBLY (hierarchical)
# ============================================
_reg = Registry(name="mold_builder_assembly")

_reg.add("clamp_moving", _bs_clamp_moving.translate((0,0,_bs_z_clamp_moving)), ComponentKind.MOVING_PLATE)
_reg.add("ejector_plate", _bs_ejector_plate.translate((0,0,_bs_z_eject)), ComponentKind.EJECTION_PLATE)
_reg.add("ejector_retainer", _bs_ejector_retainer.translate((0,0,_bs_z_eject_ret)), ComponentKind.EJECTION_PLATE)
_reg.add("support_plate", _bs_support_plate.translate((0,0,_bs_z_support)), ComponentKind.MOVING_PLATE)
_reg.add("core_plate", _bs_core_plate.translate((0,0,_bs_z_core)), ComponentKind.MOVING_PLATE)
_reg.add("cavity_plate", _bs_cavity_plate.translate((0,0,_bs_z_cavity)), ComponentKind.FIXED_PLATE)
_reg.add("clamp_fixed", _bs_clamp_fixed.translate((0,0,_bs_z_clamp_fixed)), ComponentKind.FIXED_PLATE)
for _bs_i, (_bs_pin, _bs_gx, _bs_gy) in enumerate(_bs_guide_pins):
            _reg.add(f"guide_pin_{_bs_i}", _bs_pin.translate((_bs_gx, _bs_gy, _bs_z_support)), ComponentKind.GUIDE)
for _bs_bi, (_bs_bsh, _bs_bgx, _bs_bgy) in enumerate(_bs_guide_bushings):
            _reg.add(f"guide_bushing_{_bs_bi}", _bs_bsh.translate((_bs_bgx, _bs_bgy, _bs_z_cavity)), ComponentKind.GUIDE)
_reg.add("spacer_block", _bs_spacer_block.translate((0, 0, _bs_z_spacer)), ComponentKind.MOVING_PLATE)
for _bs_spi, (_bs_sp, _bs_spx, _bs_spy) in enumerate(_bs_support_pillars):
    _reg.add(f"support_pillar_{_bs_spi}", _bs_sp.translate((_bs_spx, _bs_spy, _bs_z_spacer + _bs_T_EJECT_RET + _bs_T_EJECT)), ComponentKind.MOVING_PLATE)
for _bs_sti, (_bs_screw, _bs_scx, _bs_scy) in enumerate(_bs_screws_top):
    _reg.add(f"screw_top_{_bs_sti}", _bs_screw.translate((_bs_scx, _bs_scy, _bs_z_cavity)), ComponentKind.FIXED_PLATE)
for _bs_sbi, (_bs_screw, _bs_scx, _bs_scy) in enumerate(_bs_screws_bot):
    _reg.add(f"screw_bot_{_bs_sbi}", _bs_screw.translate((_bs_scx, _bs_scy, _bs_z_support)), ComponentKind.MOVING_PLATE)
for _bs_di, (_bs_d, _bs_dpx, _bs_dpy) in enumerate(_bs_dowels):
    _reg.add(f"dowel_pin_{_bs_di}", _bs_d.translate((_bs_dpx, _bs_dpy, _bs_z_core)), ComponentKind.GUIDE)
_reg.add("insulation_plate", _bs_insul.translate((0, 0, _bs_z_clamp_fixed + _bs_T_CLAMP_FIXED + 17)), ComponentKind.FIXED_PLATE)
_reg.add("sprue_bushing", _cr_sprue.translate((0, 0, _bs_z_cavity)), ComponentKind.FEED_SYSTEM)
_reg.add("locating_ring", _cr_loc_ring.translate((0, 0, _bs_z_clamp_fixed + _bs_T_CLAMP_FIXED)), ComponentKind.FEED_SYSTEM)
_reg.add("gate_0", _cr_gate.translate((_cr_GATE_X, _cr_GATE_Y, _bs_z_cavity)), ComponentKind.FEED_SYSTEM)
for _pe_i, (_pe_pin, _pe_ex, _pe_ey) in enumerate(_pe_ejector_pins):
    _reg.add(f"ejector_pin_{_pe_i}", _pe_pin.translate((_pe_ex, _pe_ey, _bs_z_eject)), ComponentKind.EJECTOR_PIN)
for _re_pi, (_re_pin, _re_px, _re_py) in enumerate(_re_pins):
    _reg.add(f"return_pin_{_re_pi}", _re_pin.translate((_re_px, _re_py, _bs_z_eject_ret)), ComponentKind.EJECTION_PLATE)
for _re_si, (_re_spr, _re_sx, _re_sy) in enumerate(_re_springs):
    _reg.add(f"return_spring_{_re_si}", _re_spr.translate((_re_sx, _re_sy, _bs_z_eject_ret + _bs_T_EJECT_RET)), ComponentKind.EJECTION_PLATE)
for _cool_ri, (_cool_cyl, _cool_y) in enumerate(_cool_channels):
    _reg.add(f"cooling_channel_{_cool_ri}", _cool_cyl.translate((0, _cool_y, _cool_z)), ComponentKind.COOLING)
for _cool_ui2, (_cool_u, _cool_ux, _cool_uy) in enumerate(_cool_uturns):
    _reg.add(f"cooling_uturn_{_cool_ui2}", _cool_u.translate((_cool_ux, _cool_uy, _cool_z)), ComponentKind.COOLING)
_reg.add("cooling_inlet", _cool_inlet.translate((0, _cool_marker_y_top, _cool_z)), ComponentKind.COOLING)
_reg.add("cooling_outlet", _cool_outlet.translate((0, _cool_marker_y_bot, _cool_z)), ComponentKind.COOLING)
_reg.add("cooling_oring_inlet", _cool_oring_inlet.translate((0, _cool_marker_y_top, _cool_z + 6)), ComponentKind.COOLING)
_reg.add("cooling_oring_outlet", _cool_oring_outlet.translate((0, _cool_marker_y_bot, _cool_z + 6)), ComponentKind.COOLING)
_reg.add("slide_1_K2528", _sl_body_1.translate((0, 0, _bs_z_cavity)), ComponentKind.SLIDER)

assy = _reg.build()
assy.save("mold_assembly.step")
assy.save("mold_assembly.glb")
print(f"compositional pipeline: {len(_reg._parts)} components")
