"""7 面図のイラストから起こしたマスコットロボット。

構成は「角丸ボックスの頭と胴」＋「関節球と丸棒でつないだ四肢」。
顔・胸・頭頂の色パーツは、頭／胴の外形を太らせた形と細らせた形で挟み込んで
（`_patch`）表面に沿った薄板として切り出している。こうすると丸みの式を
一度書くだけで済み、パーツが本体に埋まったり浮いたりしない。

単位はすべて mm。正面は −Y 方向、ロボットから見た右手側が +X。
"""

from __future__ import annotations

import math
from functools import lru_cache

import cadquery as cq

# 基準寸法: 卓上に置くマスコットとして全高 120 mm を仮定し、
# 正面図のピクセル比（全高 400 px）を 0.30 mm/px で mm に換算した。

# ---- 頭 ----------------------------------------------------------------
HEAD_W = 58.0          # 正面図 195 px
HEAD_D = 52.0          # 側面図から。幅よりわずかに浅い
HEAD_H = 48.0          # 正面図 170 px
HEAD_R = 18.0          # 角丸半径。大きく取ってドーム状にする
HEAD_Z = 80.0          # 頭の中心高さ

# ---- 顔（黒いバイザー面）------------------------------------------------
VISOR_W = 46.0
VISOR_H = 34.0
VISOR_R = 11.0         # バイザーの角丸
VISOR_DZ = -3.0        # 頭中心よりわずかに下げる
VISOR_T = 3.0          # 板厚。頭側には同じ深さのくぼみができる
VISOR_PROUD = 0.4      # 頭の外形からの出っ張り

FACE_T = 1.0           # 目・口・ほおの浮き出し厚

EYE_PITCH = 23.0       # 左右の目の間隔
EYE_W = 9.0
EYE_H = 11.0
EYE_DZ = 2.0           # バイザー中心からの上げ量

MOUTH_R = 7.0          # 笑い口の円弧半径
MOUTH_T = 1.6          # 線の太さ
MOUTH_DZ = -2.0        # 円弧の中心。線は下半分だけ使う

CHEEK_DIA = 8.5
CHEEK_X = 18.0
CHEEK_DZ = -6.0

# ---- 耳（側頭部の円盤）--------------------------------------------------
EAR_DIA = 22.0
EAR_PROUD = 4.0        # 頭の外形からの出っ張り
EAR_DY = 1.0
EAR_DZ = -3.0

# ---- 頭頂のライン ------------------------------------------------------
STRIPE_W = 18.0
STRIPE_L = 34.0
STRIPE_R = 8.0
STRIPE_DY = -3.0       # やや前寄り
STRIPE_T = 2.5
STRIPE_PROUD = 0.5

# ---- アンテナ ----------------------------------------------------------
ANT_ROD_DIA = 2.8
ANT_ROD_LEN = 10.0
ANT_BALL_DIA = 9.0
ANT_DY = -10.0         # 上面図に合わせて頭頂ラインの前寄りに立てる

# ---- 胴 ----------------------------------------------------------------
BODY_W = 46.0          # 正面図 155 px
BODY_D = 34.0
BODY_H = 38.0
BODY_R = 14.0
BODY_Z = 37.0          # 中心高さ。上端 56 が頭の下端に一致する
NECK_DIA = 22.0        # 頭と胴をつなぐ首。外からはほぼ見えない

# ---- 胸のパネルと星 ----------------------------------------------------
PANEL_W = 28.0
PANEL_H = 22.0
PANEL_R = 6.0
PANEL_DZ = 1.0
PANEL_T = 2.5
PANEL_PROUD = 0.5
STAR_R = 8.0           # 星の外接半径
STAR_WAIST = 0.48      # くぼみ側の半径比。小さいほど鋭い星になる
STAR_T = 1.2

# ---- 腕 ----------------------------------------------------------------
# 関節位置（右腕）。左腕は YZ 面で鏡像にする。
SHOULDER = (21.0, -1.0, 50.0)
ELBOW = (27.0, -3.0, 33.0)
WRIST = (29.0, -5.0, 22.0)
PALM = (30.0, -6.0, 18.0)

JOINT_DIA = 9.5        # 肩・肘の関節球
UPPER_ARM_DIA = 8.0
FOREARM_DIA = 7.5
PALM_DIA = 9.0
FINGER_DIA = 2.8
FINGER_LEN = 6.5
FINGER_SPREAD = 3.0    # 3 本の指を前後に振る間隔
FINGER_REACH = 2.0     # 指先を前へ出す量

# ---- 脚と靴 ------------------------------------------------------------
LEG_X = 12.0           # 左右の脚の中心
LEG_DIA = 16.0
HIP_Z = 23.0
ANKLE_Z = 10.5

FOOT_W = 18.0
FOOT_L = 24.0
FOOT_H = 13.0
FOOT_R = 5.5
FOOT_DY = -4.0         # つま先側へずらす
SOLE_H = 3.5           # 靴底（濃色）の厚み
TOE_L = 9.0            # つま先キャップ（水色）の長さ

# ---- 色 ----------------------------------------------------------------
WHITE = cq.Color(0.96, 0.97, 0.98)
CYAN = cq.Color(0.35, 0.76, 0.92)
BLACK = cq.Color(0.14, 0.16, 0.19)
YELLOW = cq.Color(0.98, 0.83, 0.28)
PINK = cq.Color(0.97, 0.68, 0.72)


# ---- 補助 --------------------------------------------------------------
def _rounded_box(w: float, d: float, h: float, r: float, grow: float) -> cq.Workplane:
    """原点中心の角丸ボックス。grow だけ全方向に太らせる（負なら細らせる）。"""
    return (
        cq.Workplane("XY")
        .box(w + 2 * grow, d + 2 * grow, h + 2 * grow)
        .edges()
        .fillet(r + grow)
    )


@lru_cache(maxsize=None)
def _head(grow: float = 0.0) -> cq.Workplane:
    """頭の外形。表面パーツの切り出しで繰り返し使うのでキャッシュする。"""
    return _rounded_box(HEAD_W, HEAD_D, HEAD_H, HEAD_R, grow).translate((0, 0, HEAD_Z))


@lru_cache(maxsize=None)
def _torso(grow: float = 0.0) -> cq.Workplane:
    """胴の外形。"""
    return _rounded_box(BODY_W, BODY_D, BODY_H, BODY_R, grow).translate((0, 0, BODY_Z))


def _patch(region: cq.Workplane, shell, proud: float, thickness: float) -> cq.Workplane:
    """柱状の region を、shell の表面に沿った薄板として切り出す。

    外形を +proud 太らせた形で外側を止め、そこから thickness ぶん内側を抜く。
    proud を正にすると表面から浮き、0 にすると面一（つらいち）になる。
    """
    return region.intersect(shell(proud)).cut(shell(proud - thickness))


def _front_face() -> cq.Workplane:
    """正面向きの特徴を描くためのワークプレーン。

    XZ 平面は面内が (X, Z)、押し出し方向が −Y なので、そのまま前向きの柱になる。
    """
    return cq.Workplane("XZ")


def _rod(p1, p2, dia: float) -> cq.Workplane:
    """2 点を結ぶ丸棒。関節を座標で置けるので姿勢を作りやすい。"""
    a, b = cq.Vector(*p1), cq.Vector(*p2)
    axis = b - a
    mid = (a + b) * 0.5
    return (
        cq.Workplane("XY")
        .cylinder(axis.Length, dia / 2, direct=axis)
        .translate(mid.toTuple())
    )


def _ball(p, dia: float) -> cq.Workplane:
    return cq.Workplane("XY").sphere(dia / 2).translate(p)


def _both_sides(part: cq.Workplane) -> cq.Workplane:
    """右半身のパーツを左右対称にする。"""
    return part.union(part.mirror("YZ"))


# ---- 頭まわり ----------------------------------------------------------
def _visor() -> cq.Workplane:
    """顔の黒い面。頭の前半分だけを通る柱から切り出す（後頭部に回さない）。"""
    region = (
        _front_face()
        .center(0, HEAD_Z + VISOR_DZ)
        .rect(VISOR_W, VISOR_H)
        .extrude(HEAD_D)
        .edges("|Y")
        .fillet(VISOR_R)
    )
    return _patch(region, _head, VISOR_PROUD, VISOR_T)


def _face_patch(region: cq.Workplane) -> cq.Workplane:
    """バイザーの上に乗る薄板（目・口・ほお）。"""
    return _patch(region, _head, VISOR_PROUD + FACE_T, FACE_T)


def _eyes() -> cq.Workplane:
    z = HEAD_Z + VISOR_DZ + EYE_DZ
    return _face_patch(
        _front_face()
        .pushPoints([(-EYE_PITCH / 2, z), (EYE_PITCH / 2, z)])
        .ellipse(EYE_W / 2, EYE_H / 2)
        .extrude(HEAD_D)
    )


def _mouth() -> cq.Workplane:
    """円環の下半分を笑い口にする。"""
    z = HEAD_Z + VISOR_DZ + MOUTH_DZ
    ring = (
        _front_face()
        .center(0, z)
        .circle(MOUTH_R)
        .circle(MOUTH_R - MOUTH_T)
        .extrude(HEAD_D)
    )
    lower_half = (
        _front_face()
        .center(0, z - MOUTH_R / 2)
        .rect(2 * MOUTH_R, MOUTH_R)
        .extrude(HEAD_D)
    )
    return _face_patch(ring.intersect(lower_half))


def _cheeks() -> cq.Workplane:
    z = HEAD_Z + VISOR_DZ + CHEEK_DZ
    return _face_patch(
        _front_face()
        .pushPoints([(-CHEEK_X, z), (CHEEK_X, z)])
        .circle(CHEEK_DIA / 2)
        .extrude(HEAD_D)
    )


def _ears() -> cq.Workplane:
    """側頭部の円盤。頭の外に出た分だけを残す。"""
    ear = (
        cq.Workplane("YZ", origin=(HEAD_W / 2 - EAR_DIA / 4, EAR_DY, HEAD_Z + EAR_DZ))
        .circle(EAR_DIA / 2)
        .extrude(EAR_DIA / 4 + EAR_PROUD)
        .edges(">X")
        .fillet(2.5)
        .cut(_head(0.0))
    )
    return _both_sides(ear)


def _crown_stripe() -> cq.Workplane:
    """頭頂を前後に走る帯。"""
    region = (
        cq.Workplane("XY", origin=(0, STRIPE_DY, HEAD_Z))
        .rect(STRIPE_W, STRIPE_L)
        .extrude(HEAD_H)
        .edges("|Z")
        .fillet(STRIPE_R)
    )
    return _patch(region, _head, STRIPE_PROUD, STRIPE_T)


def _antenna() -> tuple[cq.Workplane, cq.Workplane]:
    """支柱と先端の球を返す。支柱は頭頂の帯の表面から生やす。"""
    base_z = HEAD_Z + HEAD_H / 2 - 2.0
    rod = (
        cq.Workplane("XY", origin=(0, ANT_DY, base_z))
        .circle(ANT_ROD_DIA / 2)
        .extrude(ANT_ROD_LEN)
        .cut(_head(STRIPE_PROUD))
    )
    ball = _ball((0, ANT_DY, base_z + ANT_ROD_LEN + ANT_BALL_DIA / 2 - 1.5), ANT_BALL_DIA)
    return rod, ball


# ---- 胴まわり ----------------------------------------------------------
def _chest_panel() -> cq.Workplane:
    region = (
        _front_face()
        .center(0, BODY_Z + PANEL_DZ)
        .rect(PANEL_W, PANEL_H)
        .extrude(BODY_D)
        .edges("|Y")
        .fillet(PANEL_R)
    )
    return _patch(region, _torso, PANEL_PROUD, PANEL_T)


def _star() -> cq.Workplane:
    """胸パネルの上に乗る 5 稜星。外周と谷の半径を交互に並べる。"""
    points = []
    for i in range(10):
        angle = math.radians(90.0 + i * 36.0)
        radius = STAR_R if i % 2 == 0 else STAR_R * STAR_WAIST
        points.append((radius * math.cos(angle), radius * math.sin(angle)))

    region = (
        _front_face()
        .center(0, BODY_Z + PANEL_DZ)
        .polyline(points)
        .close()
        .extrude(BODY_D)
    )
    return _patch(region, _torso, PANEL_PROUD + STAR_T, STAR_T)


# ---- 四肢 --------------------------------------------------------------
def _arm() -> tuple[cq.Workplane, cq.Workplane, cq.Workplane]:
    """右腕を（白い腕, 水色の関節, 黒い手）で返す。"""
    joints = _ball(SHOULDER, JOINT_DIA).union(_ball(ELBOW, JOINT_DIA))

    hand = _ball(PALM, PALM_DIA)
    for i in (-1, 0, 1):
        tip = (PALM[0], PALM[1] + i * FINGER_SPREAD - FINGER_REACH, PALM[2] - FINGER_LEN)
        hand = hand.union(_rod(PALM, tip, FINGER_DIA)).union(_ball(tip, FINGER_DIA))

    arm = _rod(SHOULDER, ELBOW, UPPER_ARM_DIA).union(_rod(ELBOW, WRIST, FOREARM_DIA))
    return arm.cut(joints).cut(hand), joints.cut(_torso(0.0)), hand


def _leg() -> cq.Workplane:
    """右脚。靴の中まで伸ばして胴とつなぐ。"""
    return _rod((LEG_X, 0, ANKLE_Z), (LEG_X, 0, HIP_Z), LEG_DIA)


def _foot() -> tuple[cq.Workplane, cq.Workplane, cq.Workplane]:
    """右足を（白い甲, 水色のつま先, 濃色の靴底）で返す。"""
    boot = (
        cq.Workplane("XY", origin=(LEG_X, FOOT_DY, FOOT_H / 2))
        .box(FOOT_W, FOOT_L, FOOT_H)
        .edges()
        .fillet(FOOT_R)
    )
    below_sole = cq.Workplane("XY", origin=(LEG_X, FOOT_DY, SOLE_H / 2)).box(
        FOOT_W * 2, FOOT_L * 2, SOLE_H
    )
    toe_side = cq.Workplane(
        "XY", origin=(LEG_X, FOOT_DY - FOOT_L / 2 + TOE_L / 2, FOOT_H)
    ).box(FOOT_W * 2, TOE_L, FOOT_H * 2)

    upper = boot.cut(below_sole)
    return upper.cut(toe_side), upper.intersect(toe_side), boot.intersect(below_sole)


# ---- 組み立て ----------------------------------------------------------
def build() -> cq.Assembly:
    visor = _visor()
    eyes, mouth, cheeks = _eyes(), _mouth(), _cheeks()
    stripe, ears = _crown_stripe(), _ears()
    ant_rod, ant_ball = _antenna()
    panel, star = _chest_panel(), _star()

    arm, joints, hand = _arm()
    foot, toe, sole = _foot()

    neck = cq.Workplane("XY", origin=(0, 0, BODY_Z + BODY_H / 2)).cylinder(12.0, NECK_DIA / 2)

    # 白い本体。表面パーツが入る分（バイザー・帯・胸パネル）はくぼみとして抜く。
    shell = (
        _head(0.0)
        .union(_torso(0.0))
        .union(neck)
        .union(_both_sides(arm))
        .union(_both_sides(_leg()))
        .union(_both_sides(foot))
        .cut(visor)
        .cut(stripe)
        .cut(panel)
    )

    trim = (
        stripe.union(ears)
        .union(panel)
        .union(ant_rod)
        .union(eyes)
        .union(mouth)
        .union(_both_sides(joints))
        .union(_both_sides(toe))
    )

    return (
        cq.Assembly(name="robot-mascot")
        .add(shell, name="shell", color=WHITE)
        .add(trim, name="trim", color=CYAN)
        .add(visor.union(_both_sides(hand)).union(_both_sides(sole)), name="face", color=BLACK)
        .add(ant_ball.union(star), name="accents", color=YELLOW)
        .add(cheeks, name="cheeks", color=PINK)
    )


if __name__ == "__main__":
    cq.exporters.export(build().toCompound(), "preview.stl")
