"""粘土のような簡略化した看護師（大城さん）を、形の式（距離の場）から作る。
python3 nurse_build.py <out_dir> [--vox 0.0025]
前が -y、上が z、単位は m。身長 1.65 m。
出力：nurse_clay.blend・nurse_oshiro.fbx・nurse_report.json（部品ごとの三角形の数・骨の名前）
FBX は Unity 向け（顔が Unity の +Z、上が +Y）。骨は Unity の Humanoid の名前（Hips・LeftUpperArm など）。
口は5つの形（smile・flat・worried・talk・surprised）を別の部品で入れてあり、Unity で表示を切り替える。
"""
import math
import os
import sys

import bpy  # noqa: I001  bpy を先に読む
import numpy as np
from skimage.measure import marching_cubes

OUT = sys.argv[1]
os.makedirs(OUT, exist_ok=True)
VOX = float(sys.argv[sys.argv.index("--vox") + 1]) if "--vox" in sys.argv else 0.0025


# ---------- 形の式 ----------
def length(v):
    return np.sqrt(np.sum(v * v, axis=0))


def ellipsoid(p, c, r):
    q = (p - np.array(c).reshape(3, 1)) / np.array(r).reshape(3, 1)
    k0 = length(q)
    k1 = length(q / np.array(r).reshape(3, 1))
    return k0 * (k0 - 1.0) / np.maximum(k1, 1e-9)


def capsule(p, a, b, ra, rb):
    a = np.array(a, float).reshape(3, 1)
    b = np.array(b, float).reshape(3, 1)
    ba = b - a
    h = np.clip(np.sum((p - a) * ba, axis=0) / np.sum(ba * ba), 0, 1)
    return length(p - a - ba * h) - (ra + (rb - ra) * h)


def smin(a, b, k):
    h = np.clip(0.5 + 0.5 * (b - a) / k, 0, 1)
    return b + (a - b) * h - k * h * (1 - h)


def smax(a, b, k):
    return -smin(-a, -b, k)


def union(ds, k):
    d = ds[0]
    for e in ds[1:]:
        d = smin(d, e, k)
    return d


def shell(d, t):
    return np.maximum(d, -(d + t))


# ---------- 寸法 ----------
HC = np.array([0.0, 0.0, 1.516])          # 頭の中心


def H(x, y, z):
    return (HC + np.array([x, y, z])).tolist()


SH = 0.150                                  # 肩の関節の横の位置
ARM_OUT = math.radians(9)                   # 腕の開き
ARM_FWD = math.radians(4)


def arm_points(side):
    s = 1 if side > 0 else -1
    sh = np.array([s * SH, 0.01, 1.300])
    d1 = np.array([s * math.sin(ARM_OUT), -math.sin(ARM_FWD), -math.cos(ARM_OUT)])
    d1 /= np.linalg.norm(d1)
    el = sh + d1 * 0.250
    d2 = np.array([s * math.sin(ARM_OUT * 0.6), -math.sin(math.radians(14)), -1.0])
    d2 /= np.linalg.norm(d2)
    wr = el + d2 * 0.220
    return sh, el, wr, d1, d2


def hand_geo(side):
    """手の形の骨組み（手首・手のひら・指の関節の位置）。形と骨の両方で使う。
    u＝手首から指先の向き、n＝手のひらの向き（体のほう）、w＝親指のほう（前）"""
    s = 1 if side > 0 else -1
    _, _, wr, _, u = arm_points(side)
    n = np.array([-s, 0.0, 0.0])            # 手のひらは体のほうを向く
    n = n - u * np.dot(n, u)
    n /= np.linalg.norm(n)
    w = np.cross(u, n) * s                  # 前（-y）向きにそろえる
    if w[1] > 0:
        w = -w
    base = wr + u * 0.085
    fingers = {}
    for name, off, ln, r in (("Index", 0.026, 0.070, 0.0105), ("Middle", 0.009, 0.078, 0.0108),
                             ("Ring", -0.009, 0.074, 0.0105), ("Little", -0.025, 0.060, 0.0095)):
        a = base + w * off
        d_1 = u * math.cos(math.radians(14)) + n * math.sin(math.radians(14))
        b = a + d_1 * ln * 0.52
        d_2 = u * math.cos(math.radians(38)) + n * math.sin(math.radians(38))
        c = b + d_2 * ln * 0.48
        fingers[name] = (a, b, c, r)
    ta = wr + u * 0.03 + w * 0.024 + n * 0.006
    td = u * 0.55 + w * 0.65 + n * 0.45
    td /= np.linalg.norm(td)
    tb = ta + td * 0.04
    td2 = u * 0.75 + w * 0.3 + n * 0.6
    td2 /= np.linalg.norm(td2)
    tc = tb + td2 * 0.032
    return {"wr": wr, "u": u, "n": n, "w": w, "palm_end": base, "fingers": fingers, "thumb": (ta, tb, tc)}


def palm(p, side):
    g = hand_geo(side)
    wr, u, w = g["wr"], g["u"], g["w"]
    ds = [ellipsoid(p, (wr + u * 0.048).tolist(), (0.024, 0.024, 0.024))]
    # 手のひらは平たい楕円。軸に沿って並べた小さい楕円で作る
    for t in (0.02, 0.045, 0.07):
        c = wr + u * t
        ds.append(capsule(p, (c - w * 0.022).tolist(), (c + w * 0.022).tolist(), 0.019, 0.019))
    return union(ds, 0.010)


def finger_segs(p, side):
    """指の節ごとの形：{骨の名前の後ろ半分: 形}"""
    g = hand_geo(side)
    out = {}
    for name, (a, b, c, r) in g["fingers"].items():
        out[name + "Proximal"] = capsule(p, a.tolist(), b.tolist(), r, r * 0.95)
        out[name + "Intermediate"] = capsule(p, b.tolist(), c.tolist(), r * 0.95, r * 0.88)
    ta, tb, tc = g["thumb"]
    out["ThumbProximal"] = capsule(p, ta.tolist(), tb.tolist(), 0.0135, 0.012)
    out["ThumbIntermediate"] = capsule(p, tb.tolist(), tc.tolist(), 0.012, 0.0105)
    return out


def hand(p, side):
    return union([palm(p, side)] + list(finger_segs(p, side).values()), 0.010)


def head_solid(p):
    cr = ellipsoid(p, H(0, 0, 0), (0.098, 0.106, 0.108))
    jaw = ellipsoid(p, H(0, -0.010, -0.055), (0.080, 0.082, 0.072))
    d = smin(cr, jaw, 0.03)
    cheeks = [ellipsoid(p, H(s * 0.044, -0.055, -0.045), (0.040, 0.036, 0.035)) for s in (-1, 1)]
    d = union([d] + cheeks, 0.03)
    nose = ellipsoid(p, H(0, -0.099, -0.026), (0.011, 0.011, 0.014))
    d = smin(d, nose, 0.010)
    ears = [ellipsoid(p, H(s * 0.096, 0.008, -0.020), (0.014, 0.021, 0.029)) for s in (-1, 1)]
    return union([d] + ears, 0.008)


def skin(p):
    ds = [head_solid(p)]
    ds.append(capsule(p, (0, 0.012, 1.30), (0, 0.006, 1.45), 0.044, 0.041))
    ds.append(ellipsoid(p, (0, 0.012, 1.27), (0.10, 0.066, 0.08)))
    for sd in (-1, 1):   # 首の付け根から肩へ（服の内側を肌で埋める）
        ds.append(capsule(p, (sd * 0.045, 0.015, 1.335), (sd * 0.12, 0.015, 1.31), 0.024, 0.022))
    d = union(ds, 0.03)
    arms = []
    for side in (-1, 1):
        sh, el, wr, _, _ = arm_points(side)
        a = smin(capsule(p, sh.tolist(), el.tolist(), 0.040, 0.037),
                 capsule(p, el.tolist(), wr.tolist(), 0.036, 0.029), 0.02)
        arms.append(smin(a, hand(p, side), 0.012))
    d = union([d] + arms, 0.01)
    # 袖の中に隠れる腕の付け根の肌は作らない（腕を上げて服がのびたとき、肌が服から突き出ないように）
    for side in (-1, 1):
        sh, _, _, d1, _ = arm_points(side)
        q = p - sh.reshape(3, 1)
        proj = np.sum(q * d1.reshape(3, 1), axis=0)
        radial = length(q - d1.reshape(3, 1) * proj)
        inside = np.maximum(np.maximum(-0.12 - proj, proj - 0.134), radial - 0.065)
        d = smax(d, -inside, 0.005)
    return d


def hairline(p):
    """頭の中心から見た向きごとの、髪の下の端の高さ（頭の中心からの差）。"""
    x = p[0] - HC[0]
    y = p[1] - (HC[1] + 0.005)
    th = np.degrees(np.arctan2(x, -y))
    a = np.abs(th)
    z = np.interp(a, [0, 35, 58, 68, 78, 88, 104, 120, 180],
                  [0.056, 0.046, 0.030, 0.000, -0.012, 0.024, 0.024, -0.080, -0.100])
    # 横に流した前髪：右（+x）が上がり、左（-x）へ下がる
    front = np.clip(np.cos(np.radians(th)), 0, 1) ** 2
    z = z + 0.025 * np.sin(np.radians(th)) * front
    return z + HC[2]


def hair(p):
    s = ellipsoid(p, H(0, 0.010, 0.006), (0.110, 0.118, 0.120))
    nape = ellipsoid(p, H(0, 0.040, -0.062), (0.090, 0.078, 0.070))
    s = smin(s, nape, 0.03)
    d = smax(s, hairline(p) - p[2], 0.010)
    # 耳の前に下りる短い毛束
    bun = ellipsoid(p, H(0, 0.116, -0.088), (0.047, 0.041, 0.044))
    return smin(d, bun, 0.012)


def top(p):
    rr = 0.068
    c = np.array([0.0, 0.010, 1.085]).reshape(3, 1)
    b = np.array([0.158 - rr, 0.098 - rr, 0.30 - rr]).reshape(3, 1)
    qq = np.abs(p - c) - b
    d = length(np.maximum(qq, 0)) + np.minimum(qq.max(axis=0), 0) - rr
    # 首から肩へのなで肩の線
    ax = np.abs(p[0])
    slope = (p[2] - (1.372 - 0.25 * np.clip(ax - 0.05, 0, 0.09) - 0.16 * np.maximum(ax - 0.14, 0))) / 1.03
    d = smax(d, slope, 0.06)
    bust = [ellipsoid(p, (s * 0.060, -0.056, 1.175), (0.058, 0.055, 0.052)) for s in (-1, 1)]
    d = union([d] + bust, 0.045)
    outer = d
    d = shell(d, 0.007)
    d = smax(d, 0.80 - p[2], 0.004)                         # すそ
    neck = np.sqrt(p[0] ** 2 + (p[1] - 0.010) ** 2) - 0.051
    vee = np.maximum(p[1] + 0.02, (1.288 + 1.15 * np.abs(p[0])) - p[2]) / 1.5
    cut = smin(np.maximum(neck, 1.30 - p[2]), vee, 0.02)    # 丸い首まわりと V を角なしでつなぐ
    d = smax(d, -cut, 0.009)                                 # えりぐり（後ろは丸く、前は V）
    # ポケット：表面に貼った薄い板（胸の左に1つ、すその左右に1つずつ）
    for cx, cz, hw, hh in ((0.068, 1.19, 0.034, 0.038), (0.088, 0.875, 0.046, 0.046), (-0.088, 0.875, 0.046, 0.046)):
        rect = np.maximum(np.abs(p[0] - cx) - hw, np.abs(p[2] - cz) - hh)
        layer = np.maximum(outer - 0.0035, -outer - 0.004)
        d = np.minimum(d, np.maximum(np.maximum(rect, layer), p[1]))
    return d


def sleeves(p):
    """袖は胴とつなげず、別の部品にする（胴とつなげると、腕を動かしたとき継ぎ目が裂ける）。
    袖の上の丸みは胴の肩の中に入っていて、腕を上げても袖の筒がそのまま腕といっしょに回る"""
    sl = []
    for side in (-1, 1):
        sh, el, _, d1, _ = arm_points(side)
        # 袖の上の丸みは胴の肩より低くして、肩の線から飛び出さないようにする
        c = capsule(p, (sh + d1 * 0.035).tolist(), (sh + d1 * 0.2).tolist(), 0.051, 0.051)
        c = shell(c, 0.006)
        proj = np.sum((p - sh.reshape(3, 1)) * d1.reshape(3, 1), axis=0)
        sl.append(smax(c, proj - 0.14, 0.004))
    return np.minimum(sl[0], sl[1])


def pants(p):
    ds = [ellipsoid(p, (0, 0.012, 0.86), (0.148, 0.086, 0.10))]
    for s in (-1, 1):
        ds.append(capsule(p, (s * 0.080, 0.01, 0.82), (s * 0.095, 0.0, 0.08), 0.074, 0.062))
    d = union(ds, 0.03)
    return smax(d, 0.075 - p[2], 0.004)


def shoes(p):
    ds = []
    for s in (-1, 1):
        ds.append(ellipsoid(p, (s * 0.096, -0.035, 0.028), (0.048, 0.115, 0.052)))
    d = union(ds, 0.01)
    return smax(d, -p[2], 0.006)


def surface_y(x, z):
    """顔の正面の表面の y を、前から光線を進めて求める。"""
    y = -0.2
    for _ in range(200):
        d = float(head_solid(np.array([[x], [y], [z]]))[0])
        if d < 1e-5:
            break
        y += max(d * 0.8, 1e-5)
    return y


EYE_Z = HC[2] - 0.012


def eye_center(side):
    x = side * 0.038
    return np.array([x, surface_y(x, EYE_Z) + 0.0035, EYE_Z])


def eyes(p):
    ds = []
    for s in (-1, 1):
        x, yc, z = eye_center(s)
        # ハコブくんの縦長の丸い目に寄せる：縦のカプセル（側面がまっすぐ）と楕円の中間の形
        k = 0.0118 / 0.0062
        ps = p.copy()
        ps[1] = (p[1] - yc) * k + yc
        pill = capsule(ps, (x, yc, z - 0.0072), (x, yc, z + 0.0072), 0.0118, 0.0118) / k
        ell = ellipsoid(p, (x, yc, z), (0.0128, 0.0062, 0.0188))
        ds.append(0.65 * pill + 0.35 * ell)
    return union(ds, 0.002)


def brow_points(side):
    xs = np.linspace(0.022, 0.056, 8)
    pts = [(side * x0, 0.0275 + 0.003 * (1 - ((x0 - 0.037) / 0.019) ** 2)) for x0 in xs]
    q = [(x0, surface_y(x0, HC[2] + z0) + 0.0012, HC[2] + z0) for x0, z0 in pts]
    rs = [0.0021 + 0.0005 * (1 - abs(x0 - 0.034) / 0.022) for x0 in xs]
    return q, rs


def brows(p):
    ds = []
    for s in (-1, 1):
        q, rs = brow_points(s)
        for i in range(len(q) - 1):
            ds.append(capsule(p, q[i], q[i + 1], rs[i], rs[i + 1]))
    return union(ds, 0.002)


MOUTH_Z = HC[2] - 0.068


def mouth_line(p, half_w, curve, z0=MOUTH_Z, r=0.0017):
    """横の線の口。curve＞0 で端が上がる（笑顔）、＜0 で端が下がる（困り顔）"""
    xs = np.linspace(-half_w, half_w, 13)
    q = []
    for x0 in xs:
        z = z0 + curve * (x0 / half_w) ** 2
        q.append((x0, surface_y(x0, z) + 0.0012, z))
    ds = [capsule(p, q[i], q[i + 1], r, r) for i in range(len(q) - 1)]
    return union(ds, 0.002)


def mouth_open(p, rx, rz, flat_top=True, z0=MOUTH_Z):
    """開いた口（話す・驚く）。flat_top で上がまっすぐ・下が丸い形"""
    y = surface_y(0.0, z0)
    zc = z0 + (rz * 0.35 if flat_top else 0.0)
    d = ellipsoid(p, (0.0, y + 0.0028, zc), (rx, 0.0045, rz))
    if flat_top:
        d = smax(d, p[2] - (zc - rz * 0.05), 0.0015)
    return d


MOUTHS = {   # 口の形（Unity で表示を切り替える。既定は smile）
    "smile": lambda p: mouth_line(p, 0.015, 0.006),
    "flat": lambda p: mouth_line(p, 0.012, 0.0),
    "worried": lambda p: mouth_line(p, 0.011, -0.004, z0=MOUTH_Z - 0.002),
    "talk": lambda p: mouth_open(p, 0.011, 0.0085),
    "surprised": lambda p: mouth_open(p, 0.0065, 0.0085, flat_top=False),
}


# ---------- 格子で面にする ----------
def mesh_of(fn, lo, hi, vox):
    lo, hi = np.array(lo, float) + vox * 0.37, np.array(hi, float)   # 格子の点が平らな面にちょうど乗らないようにずらす
    n = np.ceil((hi - lo) / vox).astype(int) + 1
    xs = lo[0] + np.arange(n[0]) * vox
    ys = lo[1] + np.arange(n[1]) * vox
    zs = lo[2] + np.arange(n[2]) * vox
    vol = np.empty((n[0], n[1], n[2]), np.float32)
    gx, gy = np.meshgrid(xs, ys, indexing="ij")
    step = 24
    for k0 in range(0, n[2], step):
        zz = zs[k0:k0 + step]
        P = np.stack([np.repeat(gx[..., None], len(zz), 2), np.repeat(gy[..., None], len(zz), 2),
                      np.broadcast_to(zz, (n[0], n[1], len(zz)))]).reshape(3, -1)
        vol[:, :, k0:k0 + step] = fn(P).reshape(n[0], n[1], len(zz))
    v, f, _, _ = marching_cubes(vol, 0.0, spacing=(vox, vox, vox))
    return v + lo, f


def to_blender(name, v, f, col, rough=0.6, sss=0.0, ratio=None):
    me = bpy.data.meshes.new(name)
    me.from_pydata(v.tolist(), [], f[:, ::-1].tolist())
    me.update()
    ob = bpy.data.objects.new(name, me)
    bpy.context.scene.collection.objects.link(ob)
    if ratio:
        m = ob.modifiers.new("dec", "DECIMATE")
        m.ratio = ratio
        bpy.context.view_layer.objects.active = ob
        bpy.ops.object.modifier_apply(modifier="dec")
    for poly in ob.data.polygons:
        poly.use_smooth = True
    mat = bpy.data.materials.new(name + "_Mat")
    mat.use_nodes = True
    b = mat.node_tree.nodes["Principled BSDF"]
    b.inputs["Base Color"].default_value = (*[(c / 255) ** 2.2 for c in col], 1)
    b.inputs["Roughness"].default_value = rough
    if sss:
        b.inputs["Subsurface Weight"].default_value = sss
        b.inputs["Subsurface Radius"].default_value = (0.02, 0.008, 0.005)
    ob.data.materials.append(mat)
    print(name, len(ob.data.polygons), "faces")
    return ob


SKIN = (232, 180, 142)
HAIR = (38, 30, 27)
ROYAL = (44, 84, 170)
WHITE = (238, 238, 234)
DARK = (44, 34, 30)
LIP = (150, 72, 66)

# ---------- 骨 ----------
# Unity の Humanoid がそのまま読み取れる名前にする（Left＝本人の左＝+x。前は -y）。
# 目・眉の骨は Humanoid に入れない追加の骨（まばたき・眉の上げ下げをスクリプトで動かす）。
def leg_points(side):
    s = 1 if side > 0 else -1
    hip = np.array([s * 0.085, 0.010, 0.860])
    knee = np.array([s * 0.090, 0.000, 0.470])
    ankle = np.array([s * 0.095, 0.004, 0.085])
    ball = np.array([s * 0.096, -0.075, 0.025])
    tip = np.array([s * 0.096, -0.140, 0.022])
    return hip, knee, ankle, ball, tip


FWD = (0, -1, 0)


def bone_layout():
    """{骨の名前: (根元, 先, 親, 骨の z 軸を向ける向き)}"""
    B = {
        "Hips": ((0, 0.012, 0.90), (0, 0.012, 0.98), None, FWD),
        "Spine": ((0, 0.012, 0.98), (0, 0.012, 1.11), "Hips", FWD),
        "Chest": ((0, 0.012, 1.11), (0, 0.012, 1.30), "Spine", FWD),
        "Neck": ((0, 0.012, 1.31), (0, 0.006, 1.42), "Chest", FWD),
        "Head": ((0, 0.006, 1.42), (0, 0.0, 1.62), "Neck", FWD),
    }
    for side, L, tag in ((1, "Left", "L"), (-1, "Right", "R")):
        s = side
        sh, el, wr, _, _ = arm_points(side)
        g = hand_geo(side)
        n = g["n"]
        B[L + "Shoulder"] = ((s * 0.030, 0.012, 1.325), sh, "Chest", FWD)
        B[L + "UpperArm"] = (sh, el, L + "Shoulder", FWD)
        B[L + "LowerArm"] = (el, wr, L + "UpperArm", FWD)
        B[L + "Hand"] = (wr, g["palm_end"], L + "LowerArm", n)
        for fname, (a, b, c, _) in g["fingers"].items():
            B[L + fname + "Proximal"] = (a, b, L + "Hand", n)
            B[L + fname + "Intermediate"] = (b, c, L + fname + "Proximal", n)
        ta, tb, tc = g["thumb"]
        B[L + "ThumbProximal"] = (ta, tb, L + "Hand", n)
        B[L + "ThumbIntermediate"] = (tb, tc, L + "ThumbProximal", n)
        hip, knee, ankle, ball, tip = leg_points(side)
        B[L + "UpperLeg"] = (hip, knee, "Hips", FWD)
        B[L + "LowerLeg"] = (knee, ankle, L + "UpperLeg", FWD)
        B[L + "Foot"] = (ankle, ball, L + "LowerLeg", (0, 0, 1))
        B[L + "Toes"] = (ball, tip, L + "Foot", (0, 0, 1))
        ec = eye_center(side)
        B["Eye_" + tag] = (ec, ec + np.array([0, -0.015, 0]), "Head", (0, 0, 1))
        q, _ = brow_points(side)
        bc = np.mean(np.array(q), axis=0)
        B["Brow_" + tag] = (bc, bc + np.array([0, -0.012, 0]), "Head", (0, 0, 1))
    return B


def band(p, z0, z1):
    return np.maximum(z0 - p[2], p[2] - z1)


def regions(kind, p):
    """部品の種類ごとに、骨ごとの「その骨の受け持つ形までの距離」を返す。重みはここから決める"""
    R = {}
    if kind == "skin":
        R["Head"] = head_solid(p)
        R["Neck"] = capsule(p, (0, 0.012, 1.30), (0, 0.006, 1.45), 0.044, 0.041)
        R["Chest"] = ellipsoid(p, (0, 0.012, 1.27), (0.10, 0.066, 0.08))
        for side, L in ((1, "Left"), (-1, "Right")):
            sh, el, wr, _, _ = arm_points(side)
            R[L + "Shoulder"] = capsule(p, (side * 0.045, 0.015, 1.335), (side * 0.12, 0.015, 1.31), 0.024, 0.022)
            R[L + "UpperArm"] = capsule(p, sh.tolist(), el.tolist(), 0.040, 0.037)
            R[L + "LowerArm"] = capsule(p, el.tolist(), wr.tolist(), 0.036, 0.029)
            R[L + "Hand"] = palm(p, side)
            for k, v in finger_segs(p, side).items():
                R[L + k] = v
    elif kind == "top":
        rr = 0.068
        c = np.array([0.0, 0.010, 1.085]).reshape(3, 1)
        b = np.array([0.158 - rr, 0.098 - rr, 0.30 - rr]).reshape(3, 1)
        qq = np.abs(p - c) - b
        torso = length(np.maximum(qq, 0)) + np.minimum(qq.max(axis=0), 0) - rr
        R["Hips"] = np.maximum(torso, band(p, 0.60, 0.94))
        R["Spine"] = np.maximum(torso, band(p, 0.94, 1.10))
        R["Chest"] = np.maximum(torso, band(p, 1.10, 1.45))
        for side, L in ((1, "Left"), (-1, "Right")):
            sh, _, _, _, _ = arm_points(side)
            R[L + "Shoulder"] = capsule(p, (side * 0.045, 0.012, 1.33), sh.tolist(), 0.045, 0.045)
    elif kind == "pants":
        R["Hips"] = ellipsoid(p, (0, 0.012, 0.86), (0.148, 0.086, 0.10))
        for side, L in ((1, "Left"), (-1, "Right")):
            hip, knee, ankle, _, _ = leg_points(side)
            R[L + "UpperLeg"] = capsule(p, hip.tolist(), knee.tolist(), 0.074, 0.066)
            R[L + "LowerLeg"] = capsule(p, knee.tolist(), ankle.tolist(), 0.066, 0.062)
    elif kind == "shoes":
        for side, L in ((1, "Left"), (-1, "Right")):
            _, _, ankle, ball, tip = leg_points(side)
            R[L + "Foot"] = capsule(p, ankle.tolist(), ball.tolist(), 0.05, 0.05)
            R[L + "Toes"] = capsule(p, ball.tolist(), tip.tolist(), 0.045, 0.04)
    return R


def tau_of(bone):
    if any(k in bone for k in ("Index", "Middle", "Ring", "Little")):
        return 0.0022
    if "Thumb" in bone:
        return 0.0025
    if bone.endswith("Hand"):
        return 0.005
    if bone in ("Hips", "Spine", "Chest"):
        return 0.018
    if "Leg" in bone or "Foot" in bone or "Toes" in bone:
        return 0.018
    return 0.011


def weight_mesh(ob, kind):
    P = np.array([v.co for v in ob.data.vertices]).T          # 根は原点・回転なしなので形の座標そのまま
    R = regions(kind, P)
    names = list(R)
    D = np.stack([R[k] for k in names])
    dmin = D.min(axis=0)
    W = np.stack([np.exp(-np.maximum(D[i] - dmin, 0) / tau_of(k)) for i, k in enumerate(names)])
    W = W / W.sum(axis=0)
    top4 = np.argsort(-W, axis=0)[:4]
    groups = {k: ob.vertex_groups.new(name=k) for k in names}
    for vi in range(P.shape[1]):
        idx = top4[:, vi]
        w = W[idx, vi]
        w = w / w.sum()
        for j, ww in zip(idx, w):
            if ww > 0.01:
                groups[names[j]].add([vi], float(ww), "REPLACE")


def weight_sleeves(ob):
    """袖はまるごと上腕の骨に付ける（付け根を肩の骨と分け合うと、腕を上げた時に袖の山がゆがんだ）。
    袖の山は肩の関節のすぐ外にあるので、腕と一緒に回しても肩から離れない"""
    for side, L in ((1, "Left"), (-1, "Right")):
        g = ob.vertex_groups.new(name=L + "UpperArm")
        g.add([v.index for v in ob.data.vertices if v.co.x * side > 0], 1.0, "REPLACE")


def weight_whole(ob, bone):
    g = ob.vertex_groups.new(name=bone)
    g.add(list(range(len(ob.data.vertices))), 1.0, "REPLACE")


def weight_by_side(ob, prefix):
    for tag, sgn in (("L", 1), ("R", -1)):
        g = ob.vertex_groups.new(name=prefix + tag)
        g.add([v.index for v in ob.data.vertices if v.co.x * sgn > 0], 1.0, "REPLACE")


def build_armature():
    from mathutils import Vector
    arm = bpy.data.armatures.new("Nurse_Oshiro_Rig")
    rig = bpy.data.objects.new("Nurse_Oshiro", arm)          # 根：床の中心
    bpy.context.scene.collection.objects.link(rig)
    bpy.context.view_layer.objects.active = rig
    bpy.ops.object.mode_set(mode="EDIT")
    B = bone_layout()
    eb = {}
    for name, (h, t, _, _) in B.items():
        b = arm.edit_bones.new(name)
        b.head = Vector([float(x) for x in h])
        b.tail = Vector([float(x) for x in t])
        eb[name] = b
    for name, (_, _, parent, zdir) in B.items():
        if parent:
            eb[name].parent = eb[parent]
        eb[name].align_roll(Vector([float(x) for x in zdir]))
    bpy.ops.object.mode_set(mode="OBJECT")
    return rig


SKIN = (232, 180, 142)
HAIR = (38, 30, 27)
ROYAL = (44, 84, 170)
WHITE = (238, 238, 234)
DARK = (44, 34, 30)
LIP = (150, 72, 66)
MOUTH_IN = (120, 48, 46)

bpy.ops.wm.read_factory_settings(use_empty=True)
FACE_LO, FACE_HI = (-0.08, -0.14, HC[2] - 0.04), (0.08, -0.06, HC[2] + 0.05)
MOUTH_LO, MOUTH_HI = (-0.03, -0.13, HC[2] - 0.095), (0.03, -0.07, HC[2] - 0.05)
# (名前, 形, 範囲, 格子, 色, 粗さ, 肌の透け, 三角形を残す割合, 重みの付け方)
parts = [
    ("Skin", skin, (-0.34, -0.18, 0.58), (0.34, 0.15, 1.67), VOX, SKIN, 0.55, 0.12, 0.07, ("mesh", "skin")),
    ("Hair", hair, (-0.15, -0.16, 1.32), (0.15, 0.21, 1.69), VOX, HAIR, 0.5, 0, 0.06, ("whole", "Head")),
    ("Top", top, (-0.30, -0.15, 0.78), (0.30, 0.15, 1.40), VOX, ROYAL, 0.75, 0, 0.025, ("mesh", "top")),
    ("Sleeves", sleeves, (-0.30, -0.12, 1.08), (0.30, 0.12, 1.40), VOX, ROYAL, 0.75, 0, 0.05, ("sleeves", None)),
    ("Pants", pants, (-0.20, -0.13, 0.05), (0.20, 0.14, 1.0), VOX * 1.4, ROYAL, 0.75, 0, 0.04, ("mesh", "pants")),
    ("Shoes", shoes, (-0.17, -0.17, -0.01), (0.17, 0.10, 0.11), VOX, WHITE, 0.5, 0, 0.05, ("mesh", "shoes")),
    ("Eyes", eyes, FACE_LO, FACE_HI, 0.0008, DARK, 0.35, 0, 0.15, ("side", "Eye_")),
    ("Brows", brows, FACE_LO, FACE_HI, 0.0008, DARK, 0.35, 0, 0.15, ("side", "Brow_")),
]
for k, fn in MOUTHS.items():
    col = MOUTH_IN if k in ("talk", "surprised") else LIP
    parts.append(("Mouth_" + k, fn, MOUTH_LO, MOUTH_HI, 0.0006, col, 0.5, 0, 0.3, ("whole", "Head")))

rig = build_armature()
tris = {}
for name, fn, lo, hi, vox, col, rough, sss, ratio, how in parts:
    v, f = mesh_of(fn, lo, hi, vox)
    ob = to_blender("Nurse_" + name, v, f, col, rough, sss, ratio)
    if how[0] == "mesh":
        weight_mesh(ob, how[1])
    elif how[0] == "sleeves":
        weight_sleeves(ob)
    elif how[0] == "whole":
        weight_whole(ob, how[1])
    else:
        weight_by_side(ob, how[1])
    ob.parent = rig
    mod = ob.modifiers.new("Armature", "ARMATURE")
    mod.object = rig
    if name.startswith("Mouth_") and name != "Mouth_smile":
        ob.hide_render = True                                  # 既定の顔は笑顔。ほかの口は隠しておく
        ob.hide_viewport = True
    tris[ob.name] = sum(len(p.vertices) - 2 for p in ob.data.polygons)
tris["_total_triangles"] = sum(tris.values())
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT, "nurse_clay.blend"))
for ob in bpy.data.objects:      # FBX には隠した口も入れる（Unity で切り替える）
    ob.hide_viewport = False
bpy.ops.export_scene.fbx(
    filepath=os.path.join(OUT, "nurse_oshiro.fbx"), use_selection=False, object_types={"ARMATURE", "MESH"},
    apply_unit_scale=True, apply_scale_options="FBX_SCALE_ALL",
    axis_forward="-Z", axis_up="Y", bake_space_transform=False,
    mesh_smooth_type="FACE", use_mesh_modifiers=False, use_triangles=True,
    add_leaf_bones=False, primary_bone_axis="Y", secondary_bone_axis="X", armature_nodetype="NULL",
    bake_anim=False, path_mode="STRIP", embed_textures=False,
)
import json  # noqa: E402
json.dump({"height_m": 1.65, "triangles": tris, "bones": list(bone_layout())},
          open(os.path.join(OUT, "nurse_report.json"), "w"), ensure_ascii=False, indent=1)
print(json.dumps(tris, ensure_ascii=False))
