"""Build a reviewable night-vision experiment from the synthetic thick eye.

Optional dependencies: matplotlib and Pillow. Images are computed from ray
energy, never image-generated or painted rings. Lamp contributions are traced
at their own field angles. Background uses a local, shift-invariant PSF.
"""

from dataclasses import replace
import json
from pathlib import Path
import platform

import numpy as np

from .halo import _convolve_centered
from .optics import Config, simulate
from .thick_eye import Eye, PATHS, calibrate_eye, disk_samples, retinal_histogram, sag, trace_eye

COLORS=["#75ddca","#ed8cce","#ffc36e","#d3a0ff","#ef7e78","#a9bacb"]
LABELS=["晶体材料主光路","透明孔通路","光学区外通路","孔壁交互通路","光学区侧壁通路","未植入晶体"]
EXPOSURES=[.25,.5,1.,2.,4.]
LAMPS=[{"field_deg":[-.7,-.55],"flux":1.,"diameter_deg":.05},
       {"field_deg":[.7,-.55],"flux":1.,"diameter_deg":.05},
       {"field_deg":[-2.2,1.4],"flux":.55,"diameter_deg":.05},
       {"field_deg":[2.2,1.4],"flux":.55,"diameter_deg":.05}]


def variants(base):
    return [
        ("preop","术前对照",replace(base,implant=False),"同一长眼轴，未植入 ICL。光线尚未在视网膜上正确聚焦。"),
        ("night","术后 · 大瞳孔",base,"6.5 mm 瞳孔、5.8 mm 合成光学区。部分外侧光线未获得同样矫正，在主像周围形成环带。"),
        ("small","术后 · 小瞳孔",replace(base,pupil_mm=3.),"只改变瞳孔为 3.0 mm，保持眼轴、晶体与光源不变。检查外侧环带是否消失。"),
        ("wide","术后 · 较大光学区",replace(base,optic_mm=7.2),"只把合成光学区扩大为 7.2 mm。不是临床选型建议，而是验证边缘通路的干预。"),
        ("noport","术后 · 无中央孔对照",replace(base,port_mm=0.),"将透明孔填为同样的晶体材料，保持其余曲面不变。用于判断大环是否来自孔。"),
        ("decenter","术后 · 晶体偏心",replace(base,icl_decenter_x_mm=.35),"晶体沿 x 方向移动 0.35 mm。光学区覆盖和折射方向共同改变，可能出现偏移及不均匀环带。"),
    ]


def _airy(eye,n,fov):
    config=Config(pupil_mm=eye.pupil_mm,eye_power_d=0.,icl_power_d=0.,optic_mm=8.6,
                  grid_n=512,padding=4,output_n=n,angular_pixel_arcsec=fov/n*3600.)
    return simulate(config)["psf"]


def _finite_emitter(n,fov,diameter):
    axis=(np.arange(n)-n//2)*fov/n
    x,y=np.meshgrid(axis,axis)
    kernel=(x*x+y*y<=(diameter/2)**2).astype(float)
    return kernel/kernel.sum()


def _smooth_maps(maps,kernel):
    output=[]
    for image in maps:
        if image.sum()>0:output.append(_convolve_centered(image,kernel))
        else:output.append(image.copy())
    return np.asarray(output)


def _background(n,fov):
    """An explicitly synthetic radiance chart resembling a night road."""
    from PIL import Image,ImageDraw
    im=Image.new("L",(n,n),0);draw=ImageDraw.Draw(im)
    horizon=int(.49*n)
    # Pixel intensities are illustrative radiance, not photograph/lux data.
    draw.polygon([(0,n),(int(.39*n),horizon),(int(.61*n),horizon),(n,n)],fill=3)
    for edge in (-1,1):
        draw.line([(n//2+int(edge*.085*n),horizon),(n//2+int(edge*.43*n),n)],fill=36,width=max(1,n//420))
    for t in [.22,.38,.58,.8]:
        y0=int(horizon+t*(n-horizon));y1=int(horizon+(t+.06)*(n-horizon))
        draw.line([(n//2,y0),(n//2,y1)],fill=44,width=max(1,int(t*5)))
    draw.rectangle([int(.75*n),int(.43*n),int(.86*n),int(.49*n)],outline=48,width=2)
    draw.rectangle([int(.79*n),int(.49*n),int(.795*n),int(.65*n)],fill=8)
    for side in (-1,1):
        px=int(n/2+side*(2.2/fov*n));top=int(n/2-1.4/fov*n)
        draw.line([(px,top),(px,int(.62*n))],fill=8,width=max(1,n//500))
    # Display drawing is flipped here into the model's +y-up row convention.
    return np.flipud(np.asarray(im,dtype=float)/255.*2e-5)


def _display(linear,exposure=1.):
    value=np.clip(np.log1p(np.maximum(linear,0.)*1e6*exposure)/np.log1p(1e6*.03),0.,1.)
    return np.uint8(np.flipud(value)*255)


def _radial_by_path(rays,field=(0.,0.)):
    edges=np.linspace(0,3.,241)
    rr=np.linalg.norm(rays["view_deg"]-np.asarray(field),axis=1)
    curves=[]
    for i in range(len(PATHS)):
        k=(rays["path_id"]==i)&(rays["weights"]>0)
        energy=np.histogram(rr[k],bins=edges,weights=rays["weights"][k])[0]
        curves.append(energy/(np.pi*(edges[1:]**2-edges[:-1]**2)))
    return .5*(edges[:-1]+edges[1:]),np.asarray(curves)


def _ring_summary(rays):
    k=(rays["path_id"]==2)&(rays["weights"]>0)
    if not k.any():return None
    rr=np.linalg.norm(rays["view_deg"][k],axis=1)
    order=np.argsort(rr);rr=rr[order];ww=rays["weights"][k][order]
    cdf=np.cumsum(ww)/ww.sum()
    q=np.interp([.1,.5,.9],cdf,rr)
    return {"mechanism":"outside_optic_ray_bundle", "radius_quantiles_deg":q.tolist(),
            "power_per_incident_beam":float(ww.sum()),
            "power_fraction_retina":float(ww.sum()/rays["weights"].sum()),
            "definition":"radial distance from fixation for on-axis source; energy quantiles, not fitted perceived-ring radius"}


def _plot_setup():
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    from matplotlib import font_manager
    font=next((p for p in [Path("/System/Library/Fonts/PingFang.ttc"),Path("/System/Library/Fonts/STHeiti Light.ttc")] if p.exists()),None)
    if font is not None:
        font_manager.fontManager.addfont(str(font))
        plt.rcParams["font.family"]=font_manager.FontProperties(fname=str(font)).get_name()
    plt.rcParams.update({"axes.unicode_minus":False,"figure.facecolor":"#111a21","axes.facecolor":"#111a21",
        "axes.edgecolor":"#61717d","axes.labelcolor":"#d6e1e8","xtick.color":"#a9bacb","ytick.color":"#a9bacb",
        "text.color":"#e8f0f6","savefig.facecolor":"#111a21","font.size":10})
    return plt


def plot_paths(eye,output):
    plt=_plot_setup()
    xy=np.column_stack((np.linspace(-4.3,4.3,141)*1e-3,np.zeros(141)))
    rays=trace_eye(eye,xy=xy,keep_paths=True)
    history=np.array(rays["history"])
    fig,axes=plt.subplots(1,2,figsize=(13,4.3),gridspec_kw={"width_ratios":[2.5,1.]})
    def draw(ax,zoom=False):
        for i in range(len(xy)):
            if rays["weights"][i]<=0:continue
            path=rays["path_id"][i]
            if path in (3,4) and not zoom:continue
            points=history[:,i,:]*1e3
            ax.plot(points[:,2],points[:,0],color=COLORS[path],alpha=.4 if not zoom else .6,lw=.6)
        specs=[(eye.cornea_front_mm,eye.cornea_front_radius_mm,eye.cornea_front_q,"#b4e3ef",4.),
               (eye.cornea_front_mm+eye.cornea_thickness_mm,eye.cornea_back_radius_mm,eye.cornea_back_q,"#b4e3ef",4.),
               (eye.lens_front_mm,eye.lens_front_radius_mm,eye.lens_front_q,"#c4d5b2",3.6),
               (eye.lens_back_mm,eye.lens_back_radius_mm,eye.lens_back_q,"#c4d5b2",3.6)]
        for z,r,q,color,extent in specs:
            yy=np.linspace(-extent,extent,300)
            ax.plot(z+sag(np.abs(yy),r,q),yy,color=color,lw=1.5)
        if eye.implant:
            for sign in (-1,1):
                yy=np.linspace(eye.port_mm/2,eye.optic_mm/2,200)*sign
                front=eye.icl_front_mm+sag(np.abs(yy),eye.icl_front_radius_mm)
                back=eye.icl_front_mm+eye.icl_thickness_mm+sag(np.abs(yy),eye.icl_back_radius_mm)
                ax.fill_betweenx(yy,front,back,color="#75ddca",alpha=.35)
                ax.plot(front,yy,color="#75ddca",lw=1.3);ax.plot(back,yy,color="#75ddca",lw=1.3)
                h=eye.port_mm/2*sign
                ax.plot([eye.icl_front_mm+sag(abs(h),eye.icl_front_radius_mm),eye.icl_front_mm+eye.icl_thickness_mm+sag(abs(h),eye.icl_back_radius_mm)],[h,h],color="#ed8cce",lw=1.8)
        for sign in (-1,1):ax.plot([eye.iris_mm]*2,[sign*eye.pupil_mm/2,sign*4.2],color="#e9c997",lw=4)
        ax.axvline(eye.retina_mm,color="#ef9d91",lw=2);ax.axhline(0,color="#728390",lw=.5,alpha=.6)
        ax.set_xlabel("轴向位置 z / mm");ax.set_ylabel("横向位置 x / mm");ax.grid(alpha=.08)
        if zoom:
            ax.set_xlim(3.05,4.35);ax.set_ylim(-.7,.7);ax.set_title("ICL 与透明孔局部放大")
        else:
            ax.set_xlim(-1,eye.retina_mm+.5);ax.set_ylim(-4.5,4.5);ax.set_title("实际计算光路 · 分色显示最终路径类别")
            for x,y,text in [(0.,4.1,"角膜"),(4.5,3.5,"天然晶状体"),(eye.retina_mm-2.,3.8,"视网膜")]:ax.text(x,y,text,fontsize=9)
    draw(axes[0]);draw(axes[1],True)
    fig.tight_layout();fig.savefig(output/"ray-paths.png",dpi=170);fig.savefig(output/"ray-paths.svg");plt.close(fig)
    np.savez_compressed(output/"ray-paths.npz",positions_m=history,path_id=rays["path_id"],weights=rays["weights"])


def plot_profiles(records,output):
    plt=_plot_setup()
    fig,axes=plt.subplots(1,2,figsize=(12,4.3))
    night=records["night"]
    rr=np.array(night["radial_deg"]);curves=np.array(night["radial_power_density"])
    for i in [0,1,2,3]:
        label=LABELS[i]+("（低样本）" if i==3 else "")
        if curves[i].max()>0:axes[0].plot(rr,np.maximum(curves[i],1e-8),label=label,color=COLORS[i])
    axes[0].set_yscale("log");axes[0].set_ylim(1e-6,1e3);axes[0].set_xlim(0,2.)
    axes[0].set_title("同一轴上点光源：路径类别分解")
    for key,color,label in [("night","#ffc36e","大瞳孔"),("small","#75ddca","小瞳孔"),("wide","#9ebcf0","较大光学区"),("noport","#ed8cce","无中央孔")]:
        data=records[key]
        curve=np.array(data["radial_power_density"]).sum(axis=0)
        axes[1].plot(data["radial_deg"],np.maximum(curve,1e-8),color=color,label=label,alpha=.85)
    axes[1].set_yscale("log");axes[1].set_ylim(1e-6,1e3);axes[1].set_xlim(0,2.);axes[1].set_title("单变量干预：环带是否随机制消失")
    for ax in axes:
        ax.set_xlabel("距注视点的角半径 / °");ax.set_ylabel("每平方度能量 / 入射束能量");ax.grid(alpha=.12)
        ax.legend(facecolor="#1d2a33",edgecolor="#465865",labelcolor="#d6e1e8",fontsize=8)
    fig.tight_layout();fig.savefig(output/"radial-profiles.png",dpi=170);fig.savefig(output/"radial-profiles.svg");plt.close(fig)


def build_eye_demo(output,count=262144,n=768,fov=8.):
    from PIL import Image,ImageDraw,ImageFont
    output=Path(output);output.mkdir(parents=True,exist_ok=True)
    assets=output/"assets";assets.mkdir(exist_ok=True)
    base=calibrate_eye()
    background=_background(n,fov)
    emitter=_finite_emitter(n,fov,.05)
    records={};items=[]
    for key,title,eye,description in variants(base):
        rays=trace_eye(eye,count=count)
        raw=retinal_histogram(rays,n,fov)
        diffraction=_airy(eye,n,fov)
        point_maps=_smooth_maps(raw,diffraction)
        scene_maps=np.asarray([_convolve_centered(background,kernel) if kernel.sum()>0 else np.zeros_like(background) for kernel in point_maps])
        ledgers=[]
        for lamp in LAMPS:
            lamp_rays=trace_eye(eye,field_deg=lamp["field_deg"],count=count)
            lamp_hist=retinal_histogram(lamp_rays,n,fov)
            lamp_maps=_smooth_maps(lamp_hist,diffraction)
            lamp_maps=_smooth_maps(lamp_maps,emitter)
            scene_maps+=lamp["flux"]*lamp_maps
            ledgers.append({"source":lamp,"ledger":lamp_rays["ledger"],
                            "detector_captured_before_blur":float(lamp_hist.sum()),"detector_captured_after_blur":float(lamp_maps.sum())})
        rr,curves=_radial_by_path(rays)
        ledger=rays["ledger"]
        ledger["detector_captured_before_blur"]=float(raw.sum())
        ledger["detector_captured_after_blur"]=float(point_maps.sum())
        ledger["outside_detector_before_blur"]=ledger["retina_arrived"]-float(raw.sum())
        ring=_ring_summary(rays)
        records[key]={"title":title,"description":description,"eye":eye.to_dict(),"ledger":ledger,
                      "sources":ledgers,"ring_summary":ring,"radial_deg":rr.tolist(),"radial_power_density":curves.tolist()}
        np.savez_compressed(output/f"{key}-linear.npz",point_components=point_maps.astype(np.float32),scene_components=scene_maps.astype(np.float32),
                            geometric_point_components=raw.astype(np.float32),pixel_deg=np.float64(fov/n),path_names=np.asarray(PATHS))
        # Display path groups reflect computed fields, never added ring assets.
        groups={"all":np.arange(len(PATHS)),"main":np.array([0,5]),"edge":np.array([2,4]),"port":np.array([1,3])}
        for group,indices in groups.items():
            for view,maps in (("point",point_maps),("scene",scene_maps)):
                linear=maps[indices].sum(axis=0)
                for exposure_id,exposure in enumerate(EXPOSURES):
                    Image.fromarray(_display(linear,exposure)).save(assets/f"{key}-{view}-{group}-{exposure_id}.png")
        items.append({"id":key,"title":title,"description":description,"pupil_mm":eye.pupil_mm,"optic_mm":eye.optic_mm,
                      "port_mm":eye.port_mm,"decenter_mm":eye.icl_decenter_x_mm,"implant":eye.implant,
                      "retinal_power":ledger["retina_arrived"],"admitted_power":ledger["admitted_after_iris"],
                      "ring":ring,"path_power":ledger["path_power"]})
        print(f"Computed {key}: {count} rays × 5 fields; power balance {ledger['energy_balance_error']:.2g}",flush=True)
    plot_paths(base,output);plot_profiles(records,output)
    manifest={"version":"0.3.0-synthetic-thick-eye", "samples_per_field":count,"pixel_n":n,"fov_deg":fov,
              "base_eye":base.to_dict(),"retina_calibration":"paraxial material-zone focus; frozen across comparisons",
              "units":"geometry m internally; power relative to fixed incident entrance beam; viewing angle degree",
              "path_names":PATHS,"lamps":LAMPS,"exposures":EXPOSURES,"records":records,
              "approximations":["Synthetic biconcave implant, finite optical volume surrounded by aqueous; no haptic model or proprietary prescription.",
                "Ordinary Fresnel-reflected power is recorded but reflected ghost branches are not rendered; internal TIR is followed.",
                "Monochromatic 550 nm geometric transport, constant refractive indices; Airy intensity convolution is an approximation, not coherent pupil propagation.",
                "Flat retinal detector and calibrated angular tangent mapping; output limited to ±4 degrees.",
                "Finite lamp diameter approximated locally by intensity convolution; faint chart background uses the on-axis local PSF.",
                "No measured scatter, neural adaptation, calibrated perceived brightness or patient-vision validation."],
              "runtime":{"python":platform.python_version(),"numpy":np.__version__}}
    (output/"model.json").write_text(json.dumps(manifest,ensure_ascii=False,indent=2,allow_nan=False)+"\n")
    write_page(output,items,manifest)
    width=1200;tile=400
    composite=Image.new("RGB",(width,tile+75),(14,22,29));draw=ImageDraw.Draw(composite)
    font_path=next((str(p) for p in [Path("/System/Library/Fonts/PingFang.ttc"),Path("/System/Library/Fonts/STHeiti Light.ttc")] if p.exists()),"")
    font=ImageFont.truetype(font_path,23) if font_path else ImageFont.load_default()
    for i,key in enumerate(("preop","night","small")):
        image=Image.open(assets/f"{key}-scene-all-2.png").convert("RGB").resize((tile,tile))
        composite.paste(image,(i*tile,65));draw.text((i*tile+18,15),records[key]["title"],fill="#e6edf3",font=font)
    composite.save(output/"comparison.png")
    return output/"index.html"


def write_page(output,items,manifest):
    data=json.dumps({"items":items,"exposures":EXPOSURES},ensure_ascii=False)
    page='''<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ICL 光学实验室 · 夜间光圈</title><style>
    *{box-sizing:border-box}body{margin:0;background:#0e161d;color:#e7f0f5;font:16px/1.65 system-ui,sans-serif}main{max-width:1320px;margin:auto;padding:28px 28px 48px}small{color:#81dfcc;letter-spacing:.08em}h1{font-size:34px;line-height:1.3;margin:12px 0}h2{font-size:24px;margin-top:30px}p{color:#b9c9d5}.intro{max-width:1000px;font-size:15px}.experiment{display:grid;grid-template-columns:minmax(0,1fr) 310px;gap:24px;margin-top:22px}.viewer{background:#000;border:1px solid #324550;position:relative;min-height:400px}.viewer img{width:100%;display:block}.badge{position:absolute;top:14px;left:16px;padding:5px 10px;background:#10232bd9;border:1px solid #376270;border-radius:30px;font-size:13px;color:#a1e4d7}.scale{position:absolute;bottom:14px;left:16px;background:#0b1c24d9;padding:4px 10px;font-size:12px;color:#adc5d2}.panel{background:#18242d;border:1px solid #314550;padding:20px;border-radius:10px}.panel p{font-size:14px}label{display:block;color:#c1d1dc;font-size:14px;margin-top:17px}select{width:100%;padding:11px 8px;margin-top:5px;background:#0e1a23;color:#e4f0f7;border:1px solid #44616f;border-radius:5px;font-size:15px}input[type=range]{width:100%;accent-color:#7cdbc6}button{padding:8px 11px;border:1px solid #405d6b;background:#172b35;color:#bbd5e2;cursor:pointer;border-radius:5px}button.active{background:#7adcc5;color:#102820;border-color:#7adcc5}.tabs{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}.numbers{font-size:14px;border-top:1px solid #344b58;margin-top:18px;padding-top:10px}.numbers strong{color:#ffc36e}.note{padding:13px 16px;background:#18242d;border-left:3px solid #d4ae71;font-size:14px}.explain{display:grid;grid-template-columns:1fr 1fr 1fr;gap:18px;margin:20px 0}.explain div{background:#18242d;padding:18px;border:1px solid #314550;border-radius:8px}.explain h3{font-size:18px;margin:0 0 10px}.explain p{font-size:14px;margin:0}.figure{width:100%;border:1px solid #314550}.caption{font-size:13px;color:#9db6c5}.links a{color:#83ddc9}details{background:#18242d;padding:15px 20px;border:1px solid #314550;margin-top:20px;font-size:14px}summary{cursor:pointer;color:#d2e1ea}#description{min-height:95px}.legend{font-size:14px;display:flex;gap:18px;flex-wrap:wrap;margin:15px 0}.legend span::before{content:'●';color:var(--c);padding-right:6px}@media(max-width:850px){.experiment{grid-template-columns:1fr}.explain{grid-template-columns:1fr}.viewer{min-height:200px}main{padding:20px 15px}h1{font-size:28px}#description{min-height:0}}
    </style><main><small>ICL OPTICS LAB / 合成眼 · 机制实验</small><h1>夜间的点光源，怎样变成光圈</h1><p class="intro">光线逐面经过角膜、虹膜、有限厚度 ICL、天然晶状体，再落到视网膜。选择对照条件或单独查看一组光路，检查光圈随哪个机制出现、改变或消失。</p><div class="experiment"><section><div class="tabs"><button data-view="scene" class="active">夜间场景</button><button data-view="point">单点光源</button><button id="compare">大/小瞳孔对照</button></div><div class="viewer"><img id="result" alt="根据光线传播计算的视网膜场景"><span class="badge" id="badge"></span><span class="scale">固定角坐标 · 总视场 8° × 8° · 显示 +y 向上</span></div><p class="caption">550 nm 单色模拟，灰度显示。每盏亮灯按其实际视场角单独追踪，再叠加强度；曝光只影响显示，不改变光学几何。</p></section><aside class="panel"><label for="scenario">实验条件</label><select id="scenario"></select><p id="description"></p><label for="path">单独查看光路</label><select id="path"><option value="all">全部光路</option><option value="main">晶体材料主光路</option><option value="edge">光学区外侧与侧壁</option><option value="port">透明孔与孔壁</option></select><label for="exposure">统一显示曝光 <span id="expText"></span></label><input id="exposure" type="range" min="0" max="4" step="1" value="2"><div class="numbers" id="numbers"></div><p class="caption">以上为一组公开合成曲面下的结果。不是某款真实 ICL 或个体术后视觉预测。</p></aside></div>
    <h2>这个模型中的大环从哪里来</h2><div class="explain"><div><h3 style="color:#75ddca">① 材料区：聚成灯芯</h3><p>负光焦度 ICL 改变原有近视眼的会聚光路。经过矫正区的多数光线在视网膜附近聚焦，形成清晰主像。</p></div><div><h3 style="color:#ffc36e">② 外侧区：形成环带</h3><p>本合成设计在有限光学区外设为房水。大瞳孔让部分光线绕过晶体矫正；它们在视网膜上落到外圈。这个环带的位置和宽度由光路决定。</p></div><div><h3 style="color:#ed8cce">③ 中央孔：另一组通路</h3><p>孔是透明液体通道，不能当成黑盘。其通路及孔壁可能形成不同分布，但本模型中的大环在填孔后仍存在；不能把所有术后光圈归因于中央孔。</p></div></div>
    <div class="legend"><span style="--c:#75ddca">材料主光路</span><span style="--c:#ffc36e">外侧未矫正光路</span><span style="--c:#ed8cce">透明孔通路</span></div><img class="figure" src="ray-paths.png" alt="真实计算路径的眼球轴向截面"><p class="caption">线段来自实际表面交点；横纵轴均标注 mm，局部放大不与整眼图共用比例。天然晶状体被保留。</p><h2>用干预确认原因</h2><img class="figure" src="radial-profiles.png" alt="分光路径向光强与对照曲线"><p class="caption">轴上点光源、原始几何能量的径向密度。缩小瞳孔或扩大光学区，外侧环带消失；填孔不移除这组外侧通路。纵轴对数刻度，用于显示弱光场。</p>
    <details><summary>模型范围、数值方法与原始数据</summary><p>这是完整公开的合成厚眼几何：两面角膜、虹膜孔径、两面 ICL 和内外侧壁、两面天然晶状体、平面视网膜。每个交点使用向量 Snell 折射与无偏振 Fresnel 功率；ICL 内全反射继续追踪。非全反射的普通反射分支仅记为损失，尚未渲染反射鬼像。</p><p>图像为 550 nm 几何光强与圆瞳 Airy 强度核的近似卷积，不是完整相干波前传播；不能用它判断真实孔衍射干涉。有限灯面以局部强度积分近似，弱背景使用轴上局部成像。孔壁等弱通路样本较少，本轮不对其细微形态作定量结论。光学区外的房水假设、合成非球面与主像焦点都不是厂家或患者实测参数。</p><p>当前不含实测散射、泪膜变化、神经适应或主观亮度标定。环带半径是此模型的角坐标，不等于个体主观感觉。可通过原始参数和能量账本复核每张图。</p><p class="links"><a href="model.json">完整参数与能量账本</a> · <a href="ray-paths.svg">光路矢量图</a> · <a href="radial-profiles.svg">光强曲线矢量图</a> · <a id="rawLink" href="night-linear.npz">当前条件线性光场</a></p></details><p class="note">可核验的结论是：这组合成设计下，外侧未矫正光线能够形成环带，且可通过改变瞳孔/光学区消融。真实 ICL 的孔、周边结构、反射、像差和感知贡献仍需型号资料、独立光学模型及实测验证。</p></main><script>
    const DATA=__DATA__;let view='scene';const sc=document.querySelector('#scenario'),path=document.querySelector('#path'),ex=document.querySelector('#exposure');
    DATA.items.forEach(item=>{const op=document.createElement('option');op.value=item.id;op.textContent=item.title;sc.append(op)});sc.value='night';
    function update(){const a=DATA.items.find(i=>i.id===sc.value),e=Number(ex.value);document.querySelector('#result').src=`assets/${a.id}-${view}-${path.value}-${e}.png`;document.querySelector('#badge').textContent=a.title+' · '+path.options[path.selectedIndex].text;document.querySelector('#description').textContent=a.description;document.querySelector('#expText').textContent='×'+DATA.exposures[e];document.querySelector('#rawLink').href=a.id+'-linear.npz';const r=a.ring;document.querySelector('#numbers').innerHTML=`瞳孔 <b>${a.pupil_mm.toFixed(1)} mm</b><br>合成光学区 <b>${a.implant?a.optic_mm.toFixed(1)+' mm':'未植入'}</b><br>孔径 <b>${a.implant?a.port_mm.toFixed(2)+' mm':'—'}</b><br>外侧路径环带：<strong>${r?r.radius_quantiles_deg[1].toFixed(2)+'° 中位角半径':'无此路径'}</strong><br>${r?'外侧路径占到达视网膜能量 '+(r.power_fraction_retina*100).toFixed(1)+'%':''}`;document.querySelectorAll('[data-view]').forEach(b=>b.classList.toggle('active',b.dataset.view===view));}
    sc.addEventListener('change',update);path.addEventListener('change',update);ex.addEventListener('input',update);document.querySelectorAll('[data-view]').forEach(b=>b.addEventListener('click',()=>{view=b.dataset.view;update()}));document.querySelector('#compare').addEventListener('click',()=>{sc.value=sc.value==='night'?'small':'night';update()});update();
    </script></html>'''
    (output/"index.html").write_text(page.replace("__DATA__",data))
