fix(textbooks): подписи на числовой прямой §2 больше не перекрываются

Было: 3 уровня (i%3) × 12px — близко стоящие √2 √3 √5 π √15 наложились друг на друга.

Стало:
- Точки сортируются по координате
- Для каждой подписи ищется минимальный уровень БЕЗ перекрытия с уже размещёнными (с учётом ширины метки ~44px и шкалы в пикселях)
- До 9 уровней по 20px вверх от оси
- От подписи к точке идёт тонкая линия-выноска (0.45 opacity)
- Box-shadow на метках для разделения если плотно

Также: ось перемещена с y=60 на y=100 — больше места сверху для уровней. Контейнер 120 → 140px высоты.
This commit is contained in:
Maxim Dolgolyov
2026-05-27 13:10:20 +03:00
parent 718772a2aa
commit 6864db5b94
+39 -10
View File
@@ -2238,7 +2238,7 @@ function buildP2(){
`)}
${widget('Числовая прямая с корнями', 'VISUAL', 'Нажимайте кнопки, чтобы поставить точки √n и π на прямую. Точные десятичные значения — в подсказках.', `
<div id="nl-line" style="position:relative;height:120px;background:var(--card);border:1px solid var(--border);border-radius:9px;margin-bottom:14px"></div>
<div id="nl-line" style="position:relative;height:140px;background:var(--card);border:1px solid var(--border);border-radius:9px;margin-bottom:14px"></div>
<div class="row-c">
<button class="btn" onclick="nlAdd(Math.sqrt(2),'√2')">√2</button>
<button class="btn" onclick="nlAdd(Math.sqrt(3),'√3')">√3</button>
@@ -2442,25 +2442,54 @@ function initNumLine(){
function nlRender(){
const line = document.getElementById('nl-line');
line.innerHTML = '';
const w = line.clientWidth;
// axis 0..15
const axis = el('div', {style:'position:absolute;top:60px;left:3%;right:3%;height:2px;background:var(--text)'});
// axis 0..15 — нижняя часть, чтобы сверху было место под подписи в несколько уровней
const AXIS_Y = 100;
const axis = el('div', {style:`position:absolute;top:${AXIS_Y}px;left:3%;right:3%;height:2px;background:var(--text)`});
line.appendChild(axis);
// ticks 0..15
const lo = 0, hi = 15;
for(let i = lo; i <= hi; i++){
const x = 3 + (i - lo) / (hi - lo) * 94;
const t = el('div', {style:`position:absolute;top:54px;left:${x}%;width:2px;height:14px;background:var(--text);transform:translateX(-50%)`});
const t = el('div', {style:`position:absolute;top:${AXIS_Y-6}px;left:${x}%;width:2px;height:14px;background:var(--text);transform:translateX(-50%)`});
line.appendChild(t);
const lab = el('div', {style:`position:absolute;top:72px;left:${x}%;transform:translateX(-50%);font-size:.74rem;font-family:'JetBrains Mono',monospace;color:var(--muted)`}, ''+i);
const lab = el('div', {style:`position:absolute;top:${AXIS_Y+12}px;left:${x}%;transform:translateX(-50%);font-size:.74rem;font-family:'JetBrains Mono',monospace;color:var(--muted)`}, ''+i);
line.appendChild(lab);
}
NL_POINTS.forEach((p,i)=>{
const x = 3 + (p.v - lo) / (hi - lo) * 94;
const pt = el('div', {style:`position:absolute;top:54px;left:${x}%;width:14px;height:14px;background:var(--pri);border-radius:50%;transform:translateX(-50%);border:2.5px solid var(--card);box-shadow:0 0 0 2px var(--pri);cursor:pointer;z-index:2`});
// Сортируем по x для расчёта уровней без перекрытия
const w = line.clientWidth || 600;
const labelHalfPxApprox = 22; // полу-ширина подписи в пикселях
const placed = []; // {xPct, level}
const sorted = NL_POINTS.map((p,i)=>({...p, _i:i})).sort((a,b)=>a.v-b.v);
sorted.forEach(p=>{
const xPct = 3 + (p.v - lo) / (hi - lo) * 94;
// Найти минимальный уровень без перекрытия с placed
let level = 0;
while(true){
const conflict = placed.some(q=>{
if(q.level !== level) return false;
const dxPx = Math.abs(q.xPct - xPct) / 100 * w;
return dxPx < (labelHalfPxApprox * 2 + 4);
});
if(!conflict) break;
level++;
if(level > 8) break;
}
placed.push({xPct, level});
p._level = level;
});
sorted.forEach(p=>{
const xPct = 3 + (p.v - lo) / (hi - lo) * 94;
const pt = el('div', {style:`position:absolute;top:${AXIS_Y-6}px;left:${xPct}%;width:14px;height:14px;background:var(--pri);border-radius:50%;transform:translateX(-50%);border:2.5px solid var(--card);box-shadow:0 0 0 2px var(--pri);cursor:pointer;z-index:2`});
pt.title = p.lab + ' ≈ ' + p.v.toFixed(4);
line.appendChild(pt);
const lab = el('div', {style:`position:absolute;top:${20 + (i%3)*12}px;left:${x}%;transform:translateX(-50%);font-size:.78rem;font-weight:700;color:var(--pri);background:var(--card);padding:2px 6px;border-radius:5px;border:1px solid var(--pri);font-family:'JetBrains Mono',monospace`}, p.lab);
// Подпись сверху, нескольких уровней; линия-выноска вниз к точке
const labY = 4 + p._level * 20;
const stemTop = labY + 18;
const stemHeight = AXIS_Y - stemTop - 2;
if(stemHeight > 0){
line.appendChild(el('div', {style:`position:absolute;top:${stemTop}px;left:${xPct}%;width:1px;height:${stemHeight}px;background:var(--pri);opacity:.45;transform:translateX(-50%);z-index:1`}));
}
const lab = el('div', {style:`position:absolute;top:${labY}px;left:${xPct}%;transform:translateX(-50%);font-size:.78rem;font-weight:700;color:var(--pri);background:var(--card);padding:2px 7px;border-radius:5px;border:1px solid var(--pri);font-family:'JetBrains Mono',monospace;white-space:nowrap;z-index:3;box-shadow:0 1px 4px rgba(0,0,0,.08)`}, p.lab);
line.appendChild(lab);
});
}