Files
mcm-26-b/visualization/question1.html
T
2026-09-10 20:46:45 +08:00

186 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div id="q1-localization">
<style>
#q1-localization .q1-plot { width:100%; display:block; overflow:hidden; }
#q1-localization .q1-legend { display:flex; flex-wrap:wrap; gap:12px; margin-block:12px; }
#q1-localization .q1-legend span { display:inline-flex; align-items:center; gap:6px; }
#q1-localization .q1-swatch { display:inline-block; width:18px; height:3px; }
#q1-localization .q1-stats { display:flex; flex-wrap:wrap; gap:12px 24px; margin-block:12px; }
#q1-localization .q1-status { margin-block:8px; }
#q1-localization .q1-plot text { fill:var(--foreground); font-size:12px; font-weight:400; }
#q1-localization details { margin-block:12px; }
#q1-localization summary { cursor:pointer; }
</style>
<h2>带误差的交会定位</h2>
<div class="viz-controls">
<label class="form-label">观测案例
<select class="form-select" data-q1="case" aria-label="观测案例"></select>
</label>
<label class="form-label">视图
<select class="form-select" data-q1="view" aria-label="视图">
<option value="region">定位区域放大</option>
<option value="global">检测点全景</option>
</select>
</label>
<label class="form-label">已加入测点 <span data-q1="step-label" class="tabular-nums"></span>
<input class="form-range" data-q1="step" type="range" min="1" max="3" value="3" step="1" aria-label="已加入测点数量">
</label>
</div>
<div class="q1-stats tabular-nums" aria-live="polite" data-q1="stats"></div>
<svg class="q1-plot" data-q1="plot" role="img" aria-label="检测角域、定位多边形、直径线段和两个包围圆"></svg>
<div class="q1-legend text-small">
<span><i class="q1-swatch" style="background:var(--viz-series-1)"></i>定位区域</span>
<span><i class="q1-swatch" style="background:var(--viz-series-2)"></i>直径线段及直径圆</span>
<span><i class="q1-swatch" style="background:var(--viz-series-3)"></i>最小包围圆</span>
<span>□ 检测点 + 合成真值 ◎ 覆盖反例顶点</span>
</div>
<div class="q1-status text-small" data-q1="status" aria-live="polite"></div>
<details>
<summary>检测点与示向度</summary>
<div class="table-responsive">
<table class="table table-sm tabular-nums">
<thead><tr><th>测点</th><th class="text-end">x / m</th><th class="text-end">y / m</th><th class="text-end">示向度 / °</th><th class="text-end">误差半角 / °</th></tr></thead>
<tbody data-q1="rows"></tbody>
</table>
</div>
</details>
<script>
(() => {
const root=document.getElementById('q1-localization');
const cases=[{"id": "normal", "name": "三点交会", "source": "synthetic", "target": [80, 90], "measurements": [{"x": -600, "y": -250, "bearing_deg": 27.21505117707799, "half_angle_deg": 1.0}, {"x": 650, "y": -100, "bearing_deg": 161.015051177078, "half_angle_deg": 1.0}, {"x": 100, "y": 700, "bearing_deg": 268.4721225527146, "half_angle_deg": 1.0}], "frames": [{"status": "unbounded", "vertices": [[-600.0, -250.0]], "tolerance_m": 1e-09, "diameter_infinite": true, "diameter_m": null, "recession_direction": [0.8971423583981722, 0.44174154068614097], "message": "存在非零衰退方向,区域无界,直径为无穷大"}, {"status": "polygon", "vertices": [[45.3908396669742, 96.27384560337686], [80.26352724282751, 84.95314961314784], [106.24825816469405, 97.74770218809789], [71.61376404345356, 110.34332460542697]], "tolerance_m": 6.294839156007086e-07, "diameter_infinite": false, "diameter_m": 60.87526295170263, "diameter_pair": [[45.3908396669742, 96.27384560337686], [106.24825816469405, 97.74770218809789]], "area_m2": 778.9645078415749, "diameter_circle": {"center": [75.81954891583413, 97.01077389573737], "radius_m": 30.437631475851315, "max_vertex_distance_m": 30.43763147585132, "covers": true, "coverage_tolerance_m": 2.5179356624028345e-06, "max_excess_m": 4.367922104569409e-15, "outside_vertex_indices": [], "witness_vertex": null}, "minimum_enclosing_circle": {"center": [75.81954891583412, 97.01077389573737], "radius_m": 30.437631475851326}, "guaranteed_clearable_at_mec_center": false, "clearance_certificate": {"model": "angular_intersection_only", "clear_radius_m": 20.0, "numerical_margin_m": 2.5179356624028345e-06, "radius_with_margin_m": 30.437633993786985, "remaining_margin_m": -10.437633993786985, "center": [75.81954891583412, 97.01077389573737]}}, {"status": "polygon", "vertices": [[72.95144059951443, 87.32686714676511], [80.26352724282755, 84.95314961314779], [94.39740997503758, 91.91249455495989], [94.49056873706193, 102.02365367196415], [73.93038642999645, 109.5008321248877]], "tolerance_m": 7.462871059079371e-07, "diameter_infinite": false, "diameter_m": 26.986082918182134, "diameter_pair": [[94.39740997503758, 91.91249455495989], [73.93038642999645, 109.5008321248877]], "area_m2": 382.03705547378974, "diameter_circle": {"center": [84.163898202517, 100.7066633399238], "radius_m": 13.493041459091067, "max_vertex_distance_m": 17.45675089097608, "covers": false, "coverage_tolerance_m": 2.9851484236317485e-06, "max_excess_m": 3.96370943188501, "outside_vertex_indices": [0, 1], "witness_vertex": [72.95144059951443, 87.32686714676511]}, "minimum_enclosing_circle": {"center": [81.87363979249537, 98.04155797588491], "radius_m": 13.943107186092131}, "guaranteed_clearable_at_mec_center": true, "clearance_certificate": {"model": "angular_intersection_only", "clear_radius_m": 20.0, "numerical_margin_m": 2.9851484236317485e-06, "radius_with_margin_m": 13.943110171240557, "remaining_margin_m": 6.056889828759443, "center": [81.87363979249537, 98.04155797588491]}}]}, {"id": "triangle", "name": "直径圆反例", "source": "synthetic", "target": [20, 11.547005383792515], "measurements": [{"x": -1000, "y": 0, "bearing_deg": 1, "half_angle_deg": 1.0}, {"x": 540, "y": -866.0254037844386, "bearing_deg": 121, "half_angle_deg": 1.0}, {"x": 520, "y": 900.6664199358162, "bearing_deg": 241, "half_angle_deg": 1.0}], "frames": [{"status": "unbounded", "vertices": [[-1000.0, 0.0]], "tolerance_m": 1e-09, "diameter_infinite": true, "diameter_m": null, "recession_direction": [1.0, 0.0], "message": "存在非零衰退方向,区域无界,直径为无穷大"}, {"status": "polygon", "vertices": [[-22.483050583603557, 34.1356440648463], [-1.1527327997955013, 0.0], [40.000000000000284, 0.0], [19.44641513032613, 35.59985327195466]], "tolerance_m": 8.83402513014311e-07, "diameter_infinite": false, "diameter_m": 71.19953515266143, "diameter_pair": [[-22.483050583603557, 34.1356440648463], [40.000000000000284, 0.0]], "area_m2": 1463.776307271084, "diameter_circle": {"center": [8.75847470819835, 17.06782203242318], "radius_m": 35.59976757633071, "max_vertex_distance_m": 35.59976757633074, "covers": true, "coverage_tolerance_m": 3.533610052057244e-06, "max_excess_m": 2.4519345249005315e-14, "outside_vertex_indices": [], "witness_vertex": null}, "minimum_enclosing_circle": {"center": [8.75847470819835, 17.06782203242318], "radius_m": 35.59976757633074}, "guaranteed_clearable_at_mec_center": false, "clearance_certificate": {"model": "angular_intersection_only", "clear_radius_m": 20.0, "numerical_margin_m": 3.533610052057244e-06, "radius_with_margin_m": 35.59977110994079, "remaining_margin_m": -15.59977110994079, "center": [8.75847470819835, 17.06782203242318]}}, {"status": "polygon", "vertices": [[-5.755396159656812e-13, 0.0], [40.000000000000234, 0.0], [19.999999999999847, 34.64101615137823]], "tolerance_m": 1.0200653573832087e-06, "diameter_infinite": false, "diameter_m": 40.00000000000081, "diameter_pair": [[-5.755396159656812e-13, 0.0], [40.000000000000234, 0.0]], "area_m2": 692.8203230275786, "diameter_circle": {"center": [19.99999999999983, 0.0], "radius_m": 20.000000000000405, "max_vertex_distance_m": 34.641016151378224, "covers": false, "coverage_tolerance_m": 4.080261429532835e-06, "max_excess_m": 14.641016151377823, "outside_vertex_indices": [2], "witness_vertex": [19.999999999999847, 34.64101615137823]}, "minimum_enclosing_circle": {"center": [19.99999999999983, 11.547005383792735], "radius_m": 23.094010767585495}, "guaranteed_clearable_at_mec_center": false, "clearance_certificate": {"model": "angular_intersection_only", "clear_radius_m": 20.0, "numerical_margin_m": 4.080261429532835e-06, "radius_with_margin_m": 23.094014847846925, "remaining_margin_m": -3.0940148478469247, "center": [19.99999999999983, 11.547005383792735]}}]}, {"id": "poor", "name": "近共线检测点", "source": "synthetic", "target": [900, 50], "measurements": [{"x": 0, "y": 0, "bearing_deg": 3.1798301198642345, "half_angle_deg": 1.0}, {"x": 100, "y": 0, "bearing_deg": 3.576334374997351, "half_angle_deg": 1.0}, {"x": 200, "y": 0, "bearing_deg": 4.085616779974877, "half_angle_deg": 1.0}], "frames": [{"status": "unbounded", "vertices": [[0.0, 0.0]], "tolerance_m": 1e-09, "diameter_infinite": true, "diameter_m": null, "recession_direction": [0.9992763681850043, 0.03803603537420482], "message": "存在非零衰退方向,区域无界,直径为无穷大"}, {"status": "unbounded", "vertices": [[190.67332308426802, 7.257709171010532], [649.0783141257873, 24.706243940862326], [1149.8860918066787, 84.03538833511422]], "tolerance_m": 5.0000000000000004e-08, "diameter_infinite": true, "diameter_m": null, "recession_direction": [0.9973401975299315, 0.07288710716551386], "message": "存在非零衰退方向,区域无界,直径为无穷大"}, {"status": "unbounded", "vertices": [[349.47112173238565, 13.302121787008087], [649.0783141257873, 24.706243940862326], [704.9784261478786, 27.221516841665565], [1149.886091806679, 84.03538833511422], [1094.1078143807658, 79.57076189541714]], "tolerance_m": 1.0000000000000001e-07, "diameter_infinite": true, "diameter_m": null, "recession_direction": [0.9973401975299315, 0.07288710716551386], "message": "存在非零衰退方向,区域无界,直径为无穷大"}]}, {"id": "empty", "name": "不相容观测", "source": "synthetic", "target": null, "measurements": [{"x": -100, "y": 0, "bearing_deg": 180, "half_angle_deg": 1.0}, {"x": 100, "y": 0, "bearing_deg": 0, "half_angle_deg": 1.0}], "frames": [{"status": "unbounded", "vertices": [[-100.0, 0.0]], "tolerance_m": 1e-09, "diameter_infinite": true, "diameter_m": null, "recession_direction": [-0.9998476951563913, 0.01745240643728344], "message": "存在非零衰退方向,区域无界,直径为无穷大"}, {"status": "empty", "vertices": [], "diameter_m": null, "diameter_infinite": false, "tolerance_m": 1.0000000000000001e-07, "message": "测向角域没有公共点;检查频道、数据及误差界"}]}, {"id": "segment", "name": "零误差退化线段", "source": "synthetic", "target": [40, 0], "measurements": [{"x": 0, "y": 0, "bearing_deg": 0, "half_angle_deg": 0}, {"x": 100, "y": 0, "bearing_deg": 180, "half_angle_deg": 0}], "frames": [{"status": "unbounded", "vertices": [[0.0, 0.0]], "tolerance_m": 1e-09, "diameter_infinite": true, "diameter_m": null, "recession_direction": [1.0, 0.0], "message": "存在非零衰退方向,区域无界,直径为无穷大"}, {"status": "segment", "vertices": [[0.0, 0.0], [100.0, 0.0]], "tolerance_m": 5.0000000000000004e-08, "diameter_infinite": false, "diameter_m": 100.0, "diameter_pair": [[0.0, 0.0], [100.0, 0.0]], "area_m2": 0.0, "diameter_circle": {"center": [50.0, 0.0], "radius_m": 50.0, "max_vertex_distance_m": 50.0, "covers": true, "coverage_tolerance_m": 2.0000000000000002e-07, "max_excess_m": 0.0, "outside_vertex_indices": [], "witness_vertex": null}, "minimum_enclosing_circle": {"center": [50.0, 0.0], "radius_m": 50.0}, "guaranteed_clearable_at_mec_center": false, "clearance_certificate": {"model": "angular_intersection_only", "clear_radius_m": 20.0, "numerical_margin_m": 2.0000000000000002e-07, "radius_with_margin_m": 50.0000002, "remaining_margin_m": -30.000000200000002, "center": [50.0, 0.0]}}]}, {"id": "practice-U5ZW-P9KN-TCAG-R7ST", "name": "官方演练 U5ZW-P9KN-TCAG-R7ST", "source": "official_practice", "target": null, "measurements": [{"x": 0, "y": 0, "bearing_deg": 131.52, "half_angle_deg": 1.0}, {"x": -613.7618853216438, "y": 391.53077545248124, "bearing_deg": 34.92, "half_angle_deg": 1.0}, {"x": -314.2721344680094, "y": 656.6833525352392, "bearing_deg": 227.19, "half_angle_deg": 1.0}], "frames": [{"status": "unbounded", "vertices": [[0.0, 0.0]], "tolerance_m": 1e-09, "diameter_infinite": true, "diameter_m": null, "recession_direction": [-0.6497134405131868, 0.7601792191427741], "message": "存在非零衰退方向,区域无界,直径为无穷大"}, {"status": "polygon", "vertices": [[-460.69830582516533, 502.4117755118043], [-456.18958970820563, 497.4948221803977], [-436.511353451526, 510.7280211898303], [-441.37035594940073, 516.4131625064848]], "tolerance_m": 3.6400549446402593e-07, "diameter_infinite": false, "diameter_m": 25.57672002626576, "diameter_pair": [[-460.69830582516533, 502.4117755118043], [-436.511353451526, 510.7280211898303]], "area_m2": 167.16830387852585, "diameter_circle": {"center": [-448.6048296383457, 506.5698983508173], "radius_m": 12.78836001313288, "max_vertex_distance_m": 12.788360013132888, "covers": true, "coverage_tolerance_m": 1.4560219778561037e-06, "max_excess_m": 7.577386519575496e-15, "outside_vertex_indices": [], "witness_vertex": null}, "minimum_enclosing_circle": {"center": [-448.6048296383457, 506.5698983508173], "radius_m": 12.788360013132888}, "guaranteed_clearable_at_mec_center": true, "clearance_certificate": {"model": "angular_intersection_only", "clear_radius_m": 20.0, "numerical_margin_m": 1.4560219778561037e-06, "radius_with_margin_m": 12.788361469154864, "remaining_margin_m": 7.211638530845136, "center": [-448.6048296383457, 506.5698983508173]}}, {"status": "polygon", "vertices": [[-460.69830582516533, 502.41177551180425], [-456.4242356873799, 497.75071394627366], [-440.56989343525083, 515.4766035082919], [-441.3703559494006, 516.4131625064849]], "tolerance_m": 4.666666666666667e-07, "diameter_infinite": false, "diameter_m": 23.996723024535473, "diameter_pair": [[-460.69830582516533, 502.41177551180425], [-440.56989343525083, 515.4766035082919]], "area_m2": 89.48455568449387, "diameter_circle": {"center": [-450.6340996302081, 508.9441895100481], "radius_m": 11.998361512267737, "max_vertex_distance_m": 12.60236369723464, "covers": false, "coverage_tolerance_m": 1.8666666666666669e-06, "max_excess_m": 0.604002184966904, "outside_vertex_indices": [1], "witness_vertex": [-456.4242356873799, 497.75071394627366]}, "minimum_enclosing_circle": {"center": [-449.97695530423994, 507.9528348831342], "radius_m": 12.068583013309823}, "guaranteed_clearable_at_mec_center": true, "clearance_certificate": {"model": "angular_intersection_only", "clear_radius_m": 20.0, "numerical_margin_m": 1.8666666666666669e-06, "radius_with_margin_m": 12.068584879976491, "remaining_margin_m": 7.931415120023509, "center": [-449.97695530423994, 507.9528348831342]}}]}];
const get=name=>root.querySelector('[data-q1="'+name+'"]');
const picker=get('case'), step=get('step'), view=get('view'), svg=get('plot');
const NS='http://www.w3.org/2000/svg';
const fmt=x=>Number.isFinite(x)?x.toLocaleString('zh-CN',{maximumFractionDigits:4}):'—';
const names={polygon:'有界多边形',segment:'线段',point:'单点',empty:'空集',unbounded:'无界区域'};
function node(tag,attrs={},parent=svg,text=null) {
const el=document.createElementNS(NS,tag);
Object.entries(attrs).forEach(([k,v])=>el.setAttribute(k,String(v)));
if(text!==null)el.textContent=text;
parent.appendChild(el);return el;
}
cases.forEach((c,i)=>picker.add(new Option(c.name,i)));
picker.value=String(Math.max(0,cases.findIndex(c=>c.source==='official_practice')));
function load() {
const c=cases[+picker.value];
step.max=String(c.measurements.length);step.value=step.max;draw();
}
function draw() {
const c=cases[+picker.value], n=+step.value, ms=c.measurements.slice(0,n), r=c.frames[n-1];
get('step-label').textContent=n+' / '+c.measurements.length;
get('stats').replaceChildren();
const values=[['区域',names[r.status]],['直径 D',r.diameter_infinite?'∞':fmt(r.diameter_m)+' m'],
['最小包围半径',r.minimum_enclosing_circle?fmt(r.minimum_enclosing_circle.radius_m)+' m':'—']];
values.forEach(([label,value])=>{const span=document.createElement('span');span.textContent=label+':'+value;get('stats').appendChild(span);});
const source=c.source==='official_practice'?'官方演练观测;真实源位置未公开':'本地合成案例';
get('status').textContent=source+' · '+(r.status==='unbounded'?'阴影只显示视窗内部分,真实区域无界':
r.status==='empty'?'这些观测在给定误差界下不相容':
'直径圆'+(r.diameter_circle.covers?'可以覆盖':'不能覆盖,最大超出 '+fmt(r.diameter_circle.max_excess_m)+' m')+
' · 清除半径余量 '+fmt(r.clearance_certificate.remaining_margin_m)+' m');
get('rows').replaceChildren();
ms.forEach((m,i)=>{
const tr=document.createElement('tr');
[String(i+1),fmt(m.x),fmt(m.y),fmt(m.bearing_deg),fmt(m.half_angle_deg)].forEach((v,j)=>{
const td=document.createElement('td');td.textContent=v;if(j)td.className='text-end';tr.appendChild(td);
});get('rows').appendChild(tr);
});
svg.replaceChildren();
const width=Math.max(280,root.getBoundingClientRect().width);
const height=Math.max(310,Math.min(440,width*.65));
svg.setAttribute('viewBox',`0 0 ${width} ${height}`);
svg.style.height=height+'px';
node('title',{},svg,c.name+':'+names[r.status]);
node('desc',{},svg,get('status').textContent);
const l=64,t=20,right=20,bottom=46,w=width-l-right,h=height-t-bottom;
const finite=['polygon','segment','point'].includes(r.status);
let pts=(view.value==='region'&&finite)?r.vertices.map(p=>[...p]):ms.map(m=>[m.x,m.y]);
if(!pts.length)pts=[[0,0]];
if(view.value==='global'&&finite)pts.push(...r.vertices);
if(c.target)pts.push(c.target);
if(finite&&view.value==='region') {
[r.diameter_circle,r.minimum_enclosing_circle].forEach(circle=>{
const [x,y]=circle.center,d=circle.radius_m;
pts.push([x-d,y-d],[x+d,y+d]);
});
}
const xs=pts.map(p=>p[0]),ys=pts.map(p=>p[1]);
const cx=(Math.min(...xs)+Math.max(...xs))/2, cy=(Math.min(...ys)+Math.max(...ys))/2;
let dx=Math.max(...xs)-Math.min(...xs),dy=Math.max(...ys)-Math.min(...ys);
const minSpan=finite&&view.value==='region'?Math.max(2,(r.diameter_m||0)*.08):200;
const scale=Math.min(w/Math.max(dx*1.3,minSpan),h/Math.max(dy*1.3,minSpan));
const xmin=cx-w/(2*scale),xmax=cx+w/(2*scale),ymin=cy-h/(2*scale),ymax=cy+h/(2*scale);
const X=x=>l+(x-xmin)*scale,Y=y=>t+h-(y-ymin)*scale;
const defs=node('defs');const clip=node('clipPath',{id:'q1-clip'},defs);
node('rect',{x:l,y:t,width:w,height:h},clip);
for(let i=0;i<4;i++) {
const x=xmin+(xmax-xmin)*i/3, y=ymin+(ymax-ymin)*i/3;
node('line',{x1:X(x),x2:X(x),y1:t,y2:t+h,stroke:'var(--border)'});
node('line',{x1:l,x2:l+w,y1:Y(y),y2:Y(y),stroke:'var(--border)'});
const tick=z=>Math.abs(z)>=1e6?z.toExponential(1):Math.abs(xmax-xmin)<20?z.toFixed(1):Math.round(z).toString();
node('text',{x:X(x),y:t+h+19,'text-anchor':i===0?'start':i===3?'end':'middle'},svg,tick(x));
node('text',{x:l-8,y:Y(y)+4,'text-anchor':'end'},svg,tick(y));
}
node('text',{x:l+w/2,y:height-7,'text-anchor':'middle',class:'axis-title','data-axis':'x'},svg,'x / m');
node('text',{x:14,y:t+h/2,'text-anchor':'middle',transform:`rotate(-90 14 ${t+h/2})`,class:'axis-title','data-axis':'y'},svg,'y / m');
const layer=node('g',{'clip-path':'url(#q1-clip)'});
let displayPoly;
if(finite)displayPoly=r.vertices;
else if(r.status==='empty')displayPoly=[];
else {
// Display clipping ONLY: Python's result remains explicitly unbounded.
displayPoly=[[xmin,ymin],[xmax,ymin],[xmax,ymax],[xmin,ymax]];
ms.forEach(m=>{
const theta=m.bearing_deg*Math.PI/180,d=m.half_angle_deg*Math.PI/180;
const normals=[[Math.sin(theta-d),-Math.cos(theta-d)],[-Math.sin(theta+d),Math.cos(theta+d)],[-Math.cos(theta),-Math.sin(theta)]];
normals.forEach(([a,b])=>{
const bound=a*m.x+b*m.y,out=[];
for(let i=0;i<displayPoly.length;i++) {
const p=displayPoly[i],q=displayPoly[(i+1)%displayPoly.length];
const f=a*p[0]+b*p[1]-bound,g=a*q[0]+b*q[1]-bound;
if(f<=1e-8)out.push(p);
if((f<0&&g>0)||(f>0&&g<0))out.push([p[0]+(q[0]-p[0])*f/(f-g),p[1]+(q[1]-p[1])*f/(f-g)]);
}displayPoly=out;
});
});
}
if(displayPoly.length>=3)node('polygon',{points:displayPoly.map(p=>X(p[0])+','+Y(p[1])).join(' '),fill:'var(--viz-series-1)','fill-opacity':.18,stroke:'var(--viz-series-1)','stroke-width':1.5},layer);
const reach=Math.hypot(xmax-xmin,ymax-ymin)+Math.max(...ms.map(m=>Math.hypot(m.x-cx,m.y-cy)))*2;
ms.forEach((m,i)=>{
[-m.half_angle_deg,0,m.half_angle_deg].forEach(offset=>{
const a=(m.bearing_deg+offset)*Math.PI/180;
node('line',{x1:X(m.x),y1:Y(m.y),x2:X(m.x+reach*Math.cos(a)),y2:Y(m.y+reach*Math.sin(a)),
stroke:'var(--foreground)','stroke-opacity':offset===0?.35:.22,'stroke-width':1,'stroke-dasharray':offset===0?'none':'4 4'},layer);
});
if(m.x>=xmin&&m.x<=xmax&&m.y>=ymin&&m.y<=ymax) {
node('rect',{x:X(m.x)-4,y:Y(m.y)-4,width:8,height:8,fill:'var(--foreground)'},layer);
node('text',{x:Math.min(l+w-22,X(m.x)+9),y:Math.max(t+14,Y(m.y)-9)},layer,'S'+(i+1));
}
});
if(finite) {
[r.diameter_circle,r.minimum_enclosing_circle].forEach((circle,i)=>{
node('circle',{cx:X(circle.center[0]),cy:Y(circle.center[1]),r:circle.radius_m*scale,
fill:'none',stroke:`var(--viz-series-${i+2})`,'stroke-width':2,'stroke-dasharray':i===0?'6 4':'none'},layer);
});
const [a,b]=r.diameter_pair;
node('line',{x1:X(a[0]),y1:Y(a[1]),x2:X(b[0]),y2:Y(b[1]),stroke:'var(--viz-series-2)','stroke-width':2.5},layer);
r.vertices.forEach((p,i)=>{
const mark=node('circle',{cx:X(p[0]),cy:Y(p[1]),r:4,fill:'var(--viz-series-1)'},layer);
node('title',{},mark,`V${i+1} (${fmt(p[0])}, ${fmt(p[1])})`);
});
const witness=r.diameter_circle.witness_vertex;
if(witness)node('circle',{cx:X(witness[0]),cy:Y(witness[1]),r:8,
fill:'none',stroke:'var(--viz-series-2)','stroke-width':2},layer);
}
if(c.target&&c.target[0]>=xmin&&c.target[0]<=xmax&&c.target[1]>=ymin&&c.target[1]<=ymax) {
const x=X(c.target[0]),y=Y(c.target[1]);
node('path',{d:`M${x-5},${y}h10 M${x},${y-5}v10`,stroke:'var(--foreground)','stroke-width':2},layer);
}
node('rect',{x:l,y:t,width:w,height:h,fill:'none',stroke:'var(--border)','data-chart-frame':''});
}
picker.addEventListener('change',load);step.addEventListener('input',draw);view.addEventListener('change',draw);
new ResizeObserver(draw).observe(root);
load();
})();
</script>
</div>