# -*- coding: utf-8 -*-
# FreeCAD Macro: Parametric 3" Through-Bore Spindle Cartridge (Robust Spreadsheet Init)
#
# Generates:
#   - Spindle (hollow shaft + nose flange + bearing journal turn-downs)
#   - Housing (cartridge body + front mounting flange + bearing bores + inner relief)
#   - Bearing placeholders (2x front, 1x rear) as simple rings/envelopes
#
# Workflow:
#   1) Run macro once -> creates Spreadsheet "SPINDLE_PARAMS" with defaults + creates geometry.
#   2) Edit Spreadsheet aliases (Value column) -> re-run macro -> rebuilds.
#
# Notes:
#   - Uses placeholder bearing envelope dimensions; replace with real catalog dimensions.
#   - Journal turn-downs follow bearing bore values (cone bore). Real bearings require shoulders, fillets, undercuts.
#
# Units:
#   - Spreadsheet values are in inches.
#   - Internally converted to mm (FreeCAD's native).

import FreeCAD as App
import Part

DOC = App.ActiveDocument
if DOC is None:
    DOC = App.newDocument("SpindleCartridge")

MM_PER_IN = 25.4

def inch(x):
    return float(x) * MM_PER_IN

def get_or_create_spreadsheet(name="SPINDLE_PARAMS"):
    ss = DOC.getObject(name)
    if ss is None:
        ss = DOC.addObject("Spreadsheet::Sheet", name)
    return ss

def safe_set_alias(ss, cell, alias):
    try:
        ss.setAlias(cell, alias)
    except Exception:
        # Some FreeCAD builds may reject duplicates; ignore safely
        pass

def safe_set_comment(ss, cell, comment):
    try:
        ss.setComment(cell, comment)
    except Exception:
        pass

def ss_set(ss, cell, value, alias=None, comment=None):
    ss.set(cell, str(value))
    if alias:
        safe_set_alias(ss, cell, alias)
    if comment:
        safe_set_comment(ss, cell, comment)

def ss_get(ss, alias, default):
    try:
        return float(ss.get(alias))
    except Exception:
        return float(default)

def purge_generated(prefix="GEN_"):
    # Remove prior generated objects
    todel = []
    for obj in list(DOC.Objects):
        if obj.Name.startswith(prefix):
            todel.append(obj.Name)
    for name in todel:
        DOC.removeObject(name)

def add_part_feature(name, shape):
    obj = DOC.addObject("Part::Feature", name)
    obj.Shape = shape
    return obj

# -------------------------
# Spreadsheet init (robust)
# -------------------------
ss = get_or_create_spreadsheet("SPINDLE_PARAMS")

# Detect first run by checking for a required alias
try:
    ss.get("bore_id_in")
    first_run = False
except Exception:
    first_run = True

if first_run:
    # Header
    ss_set(ss, "A1", "Parameter")
    ss_set(ss, "B1", "Value (in)")

    # Spindle geometry
    ss_set(ss, "A3", "bore_id_in");             ss_set(ss, "B3",  3.000, "bore_id_in", "Spindle through-bore ID (clear feed-through)")
    ss_set(ss, "A4", "spindle_len_in");         ss_set(ss, "B4", 20.000, "spindle_len_in", "Overall spindle length")
    ss_set(ss, "A5", "nose_flange_od_in");      ss_set(ss, "B5",  6.000, "nose_flange_od_in", "Nose flange OD (for chuck/backplate)")
    ss_set(ss, "A6", "nose_flange_thk_in");     ss_set(ss, "B6",  1.000, "nose_flange_thk_in", "Nose flange thickness")
    ss_set(ss, "A7", "spindle_body_od_in");     ss_set(ss, "B7",  5.000, "spindle_body_od_in", "Spindle OD in bearing journal region")

    # Bearing envelopes (placeholders; replace with real catalog dims)
    # Front TRB pair (two bearings)
    ss_set(ss, "A10", "front_brg_bore_in");     ss_set(ss, "B10", 3.937, "front_brg_bore_in", "Front bearing bore (cone bore). Example: 100mm = 3.937")
    ss_set(ss, "A11", "front_brg_od_in");       ss_set(ss, "B11", 7.090, "front_brg_od_in", "Front bearing OD (cup OD). Example: 180mm = 7.09")
    ss_set(ss, "A12", "front_brg_w_in");        ss_set(ss, "B12", 1.930, "front_brg_w_in", "Front bearing width (each), placeholder")

    # Rear bearing (DGB or TRB, placeholder)
    ss_set(ss, "A14", "rear_brg_bore_in");      ss_set(ss, "B14", 3.150, "rear_brg_bore_in", "Rear bearing bore. Example: 80mm = 3.150")
    ss_set(ss, "A15", "rear_brg_od_in");        ss_set(ss, "B15", 6.693, "rear_brg_od_in", "Rear bearing OD. Example: 170mm = 6.693")
    ss_set(ss, "A16", "rear_brg_w_in");         ss_set(ss, "B16", 1.540, "rear_brg_w_in", "Rear bearing width, placeholder")

    # Axial layout (inches, from spindle nose face)
    ss_set(ss, "A20", "front_setback_in");      ss_set(ss, "B20", 1.250, "front_setback_in", "Distance from nose flange back face to first front bearing face")
    ss_set(ss, "A21", "front_pair_gap_in");     ss_set(ss, "B21", 0.500, "front_pair_gap_in", "Spacer gap between the 2 front bearings (placeholder)")
    ss_set(ss, "A22", "rear_brg_from_front_in");ss_set(ss, "B22", 9.000, "rear_brg_from_front_in", "Distance from first front bearing face to rear bearing face")

    # Housing geometry
    ss_set(ss, "A30", "housing_wall_in");       ss_set(ss, "B30", 0.750, "housing_wall_in", "Housing wall thickness added around max bearing OD")
    ss_set(ss, "A31", "housing_len_in");        ss_set(ss, "B31",16.000, "housing_len_in", "Housing length")
    ss_set(ss, "A32", "mount_flange_od_in");    ss_set(ss, "B32", 9.000, "mount_flange_od_in", "Front mounting flange OD")
    ss_set(ss, "A33", "mount_flange_thk_in");   ss_set(ss, "B33", 0.750, "mount_flange_thk_in", "Front mounting flange thickness")
    ss_set(ss, "A34", "front_bore_fit_clear_in");ss_set(ss, "B34",0.002, "front_bore_fit_clear_in", "Model-only clearance added to front bearing OD for housing bore")
    ss_set(ss, "A35", "rear_bore_fit_clear_in"); ss_set(ss, "B35",0.002, "rear_bore_fit_clear_in", "Model-only clearance added to rear bearing OD for housing bore")

DOC.recompute()

# -------------------------
# Read parameters
# -------------------------
bore_id_in              = ss_get(ss, "bore_id_in", 3.0)
spindle_len_in          = ss_get(ss, "spindle_len_in", 20.0)
nose_flange_od_in       = ss_get(ss, "nose_flange_od_in", 6.0)
nose_flange_thk_in      = ss_get(ss, "nose_flange_thk_in", 1.0)
spindle_body_od_in      = ss_get(ss, "spindle_body_od_in", 5.0)

front_brg_bore_in       = ss_get(ss, "front_brg_bore_in", 3.937)
front_brg_od_in         = ss_get(ss, "front_brg_od_in", 7.09)
front_brg_w_in          = ss_get(ss, "front_brg_w_in", 1.93)

rear_brg_bore_in        = ss_get(ss, "rear_brg_bore_in", 3.15)
rear_brg_od_in          = ss_get(ss, "rear_brg_od_in", 6.693)
rear_brg_w_in           = ss_get(ss, "rear_brg_w_in", 1.54)

front_setback_in        = ss_get(ss, "front_setback_in", 1.25)
front_pair_gap_in       = ss_get(ss, "front_pair_gap_in", 0.50)
rear_brg_from_front_in  = ss_get(ss, "rear_brg_from_front_in", 9.0)

housing_wall_in         = ss_get(ss, "housing_wall_in", 0.75)
housing_len_in          = ss_get(ss, "housing_len_in", 16.0)
mount_flange_od_in      = ss_get(ss, "mount_flange_od_in", 9.0)
mount_flange_thk_in     = ss_get(ss, "mount_flange_thk_in", 0.75)
front_bore_fit_clear_in = ss_get(ss, "front_bore_fit_clear_in", 0.002)
rear_bore_fit_clear_in  = ss_get(ss, "rear_bore_fit_clear_in", 0.002)

# -------------------------
# Axial positions (Z axis)
# Convention:
#   Z=0 at spindle nose front face
#   Spindle extends in +Z direction to rear
# -------------------------
nose_flange_back_z = inch(nose_flange_thk_in)

front_brg1_z = nose_flange_back_z + inch(front_setback_in)
front_brg2_z = front_brg1_z + inch(front_brg_w_in) + inch(front_pair_gap_in)
rear_brg_z   = front_brg1_z + inch(rear_brg_from_front_in)

# -------------------------
# Rebuild geometry
# -------------------------
purge_generated("GEN_")

# -------------------------
# Spindle
# -------------------------
spindle_r = inch(spindle_body_od_in / 2.0)
spindle_len = inch(spindle_len_in)

# Base spindle body cylinder
spindle_body = Part.makeCylinder(spindle_r, spindle_len, App.Vector(0, 0, 0))

# Nose flange (fused at front)
flange_r = inch(nose_flange_od_in / 2.0)
flange_thk = inch(nose_flange_thk_in)
flange = Part.makeCylinder(flange_r, flange_thk, App.Vector(0, 0, 0))

spindle_outer = spindle_body.fuse(flange)

# Through bore
bore_r = inch(bore_id_in / 2.0)
bore = Part.makeCylinder(bore_r, spindle_len, App.Vector(0, 0, 0))
spindle_outer = spindle_outer.cut(bore)

# Journal turn-downs (simplified)
front_journal_od = front_brg_bore_in
rear_journal_od  = rear_brg_bore_in
front_journal_r  = inch(front_journal_od / 2.0)
rear_journal_r   = inch(rear_journal_od / 2.0)

def turn_down(shape, z0_mm, length_in, target_r_mm):
    length_mm = inch(length_in)
    if target_r_mm >= spindle_r:
        return shape
    outer = Part.makeCylinder(spindle_r, length_mm, App.Vector(0, 0, z0_mm))
    inner = Part.makeCylinder(target_r_mm, length_mm, App.Vector(0, 0, z0_mm))
    sleeve = outer.cut(inner)  # material outside target radius
    return shape.cut(sleeve)

spindle_outer = turn_down(spindle_outer, front_brg1_z, front_brg_w_in, front_journal_r)
spindle_outer = turn_down(spindle_outer, front_brg2_z, front_brg_w_in, front_journal_r)
spindle_outer = turn_down(spindle_outer, rear_brg_z,   rear_brg_w_in,  rear_journal_r)

add_part_feature("GEN_Spindle", spindle_outer)

# -------------------------
# Bearing placeholders (rings)
# -------------------------
def bearing_ring(name, bore_in, od_in, w_in, z0_mm):
    od_r = inch(od_in / 2.0)
    id_r = inch(bore_in / 2.0)
    w_mm = inch(w_in)
    outer = Part.makeCylinder(od_r, w_mm, App.Vector(0, 0, z0_mm))
    inner = Part.makeCylinder(id_r, w_mm, App.Vector(0, 0, z0_mm))
    ring = outer.cut(inner)
    add_part_feature(name, ring)

bearing_ring("GEN_FrontBearing1", front_brg_bore_in, front_brg_od_in, front_brg_w_in, front_brg1_z)
bearing_ring("GEN_FrontBearing2", front_brg_bore_in, front_brg_od_in, front_brg_w_in, front_brg2_z)
bearing_ring("GEN_RearBearing",   rear_brg_bore_in,  rear_brg_od_in,  rear_brg_w_in,  rear_brg_z)

# -------------------------
# Housing (cartridge)
# -------------------------
max_brg_od_in = max(front_brg_od_in, rear_brg_od_in)
housing_od_in = max_brg_od_in + 2.0 * housing_wall_in
housing_r = inch(housing_od_in / 2.0)
housing_len = inch(housing_len_in)

# Main housing cylinder
housing_outer = Part.makeCylinder(housing_r, housing_len, App.Vector(0, 0, 0))

# Front mounting flange
mf_r = inch(mount_flange_od_in / 2.0)
mf_thk = inch(mount_flange_thk_in)
mount_flange = Part.makeCylinder(mf_r, mf_thk, App.Vector(0, 0, 0))
housing_outer = housing_outer.fuse(mount_flange)

# Bearing bores (model-only clearance)
front_bore_r = inch((front_brg_od_in + front_bore_fit_clear_in) / 2.0)
rear_bore_r  = inch((rear_brg_od_in  + rear_bore_fit_clear_in)  / 2.0)

front_bore1 = Part.makeCylinder(front_bore_r, inch(front_brg_w_in), App.Vector(0, 0, front_brg1_z))
front_bore2 = Part.makeCylinder(front_bore_r, inch(front_brg_w_in), App.Vector(0, 0, front_brg2_z))
rear_bore   = Part.makeCylinder(rear_bore_r,  inch(rear_brg_w_in),  App.Vector(0, 0, rear_brg_z))

housing = housing_outer.cut(front_bore1).cut(front_bore2).cut(rear_bore)

# Inner relief for rotating spindle clearance (visualization; not a production shoulder stack)
clear_r = spindle_r + inch(0.10)  # 0.10" radial clearance
inner_relief = Part.makeCylinder(clear_r, housing_len, App.Vector(0, 0, 0))
housing = housing.cut(inner_relief)

add_part_feature("GEN_Housing", housing)

DOC.recompute()
print("Spindle cartridge generated. Edit SPINDLE_PARAMS aliases and rerun to rebuild.")
