Files
Learn_System/frontend/js/labs/_measure.js
T
Maxim Dolgolyov 59ea5e7d65 fix(lab-measure): оверлей измерений на весь экран (SVG не растягивался по inset)
#lm-svg — это <svg> (заменяемый элемент с intrinsic 300x150); inset:0 без явных
размеров его не растягивал, поэтому линейка/угол рисовались за пределами видимой
области и казались нерабочими (панель-div при этом видна). Добавлены width:100vw;
height:100vh — оверлей теперь покрывает вьюпорт, инструменты видны и перетаскиваются.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:26:31 +03:00

126 lines
7.7 KiB
JavaScript
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.
'use strict';
/* LabMeasure — измерительные инструменты поверх любой симуляции (Фаза 2).
* Линейка (длина в px + ≈ метрах при PX_PER_M) и угломер (угол при вершине).
* SVG-оверлей на весь экран с pointer-events:none, чтобы симуляция оставалась
* интерактивной; перехватывают события только перетаскиваемые ручки.
* Самодостаточно: свой DOM/CSS, не трогает симуляции. Точка входа — LabMeasure.toggle(). */
(function (global) {
var NS = 'http://www.w3.org/2000/svg';
var PXM = (global.LabPalette && LabPalette.PX_PER_M) || 100;
var svg = null, bar = null, mode = null, drag = null;
var ruler = null, angle = null;
function el(tag, attrs) { var e = document.createElementNS(NS, tag); for (var k in attrs) e.setAttribute(k, attrs[k]); return e; }
function center() { return { x: global.innerWidth / 2, y: global.innerHeight / 2 }; }
function ensureStyle() {
if (document.getElementById('lm-style')) return;
var s = document.createElement('style'); s.id = 'lm-style';
s.textContent = [
'#lm-svg{position:fixed;inset:0;width:100vw;height:100vh;z-index:60;pointer-events:none;display:none;}',
'#lm-svg.on{display:block;}',
'#lm-svg .lm-hit{pointer-events:auto;cursor:grab;}',
'#lm-svg .lm-hit:active{cursor:grabbing;}',
'#lm-bar{position:fixed;top:64px;left:50%;transform:translateX(-50%);z-index:61;display:none;gap:6px;padding:6px;border-radius:12px;',
'background:var(--surface,rgba(255,255,255,.9));backdrop-filter:var(--blur,blur(20px));border:1px solid var(--border,rgba(15,23,42,.1));box-shadow:0 8px 28px rgba(15,23,42,.18);}',
'#lm-bar.on{display:flex;}',
'.lm-tb{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border:none;border-radius:8px;background:transparent;',
'font:700 .8rem Manrope,sans-serif;color:var(--text-2,#475569);cursor:pointer;}',
'.lm-tb:hover{background:rgba(155,93,229,.08);color:var(--violet,#9B5DE5);}',
'.lm-tb.on{background:var(--violet,#9B5DE5);color:#fff;}',
'.lm-tb .ic{width:15px;height:15px;}',
].join('');
document.head.appendChild(s);
}
function ensure() {
if (svg) return;
ensureStyle();
svg = el('svg', { id: 'lm-svg' });
document.body.appendChild(svg);
bar = document.createElement('div'); bar.id = 'lm-bar';
bar.innerHTML =
'<button class="lm-tb" data-t="ruler"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.3 8.7 8.7 21.3a1 1 0 0 1-1.4 0l-4.6-4.6a1 1 0 0 1 0-1.4L15.3 2.7a1 1 0 0 1 1.4 0l4.6 4.6a1 1 0 0 1 0 1.4Z"/><path d="m7.5 10.5 2 2M10.5 7.5l2 2M13.5 4.5l2 2M4.5 13.5l2 2"/></svg>Линейка</button>' +
'<button class="lm-tb" data-t="angle"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"/><path d="M3 21 21 3"/><path d="M3 21a18 18 0 0 0 4-7"/></svg>Угол</button>' +
'<button class="lm-tb" data-t="off"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>Скрыть</button>';
document.body.appendChild(bar);
bar.addEventListener('click', function (e) { var b = e.target.closest('.lm-tb'); if (b) setTool(b.dataset.t); });
// drag delegation
svg.addEventListener('pointerdown', function (e) {
var h = e.target.getAttribute && e.target.getAttribute('data-h');
if (!h) return;
drag = h; e.target.setPointerCapture && e.target.setPointerCapture(e.pointerId); e.preventDefault();
});
svg.addEventListener('pointermove', function (e) {
if (!drag) return;
var p = (mode === 'ruler') ? ruler : angle;
p[drag + 'x'] = e.clientX; p[drag + 'y'] = e.clientY;
render();
});
function end() { drag = null; }
svg.addEventListener('pointerup', end);
svg.addEventListener('pointercancel', end);
global.addEventListener('resize', function () { if (mode) render(); });
}
function setTool(t) {
ensure();
if (t === 'off') { mode = null; svg.classList.remove('on'); bar.classList.remove('on'); paintBar(); render(); return; }
mode = t;
var c = center();
if (t === 'ruler' && !ruler) ruler = { ax: c.x - 110, ay: c.y, bx: c.x + 110, by: c.y };
if (t === 'angle' && !angle) angle = { vx: c.x, vy: c.y + 70, ax: c.x - 120, ay: c.y - 40, bx: c.x + 120, by: c.y - 40 };
svg.classList.add('on');
paintBar(); render();
}
function paintBar() { if (!bar) return; bar.querySelectorAll('.lm-tb').forEach(function (b) { b.classList.toggle('on', b.dataset.t === mode); }); }
function lineLabel(x, y, text) {
var g = el('g', {});
var pad = 5, w = text.length * 7.2 + pad * 2, h = 20;
g.appendChild(el('rect', { x: x - w / 2, y: y - h / 2, width: w, height: h, rx: 6, fill: 'rgba(13,13,26,.85)' }));
var t = el('text', { x: x, y: y + 4, 'text-anchor': 'middle', fill: '#fff', 'font-size': 12, 'font-family': 'Manrope,sans-serif', 'font-weight': 700 });
t.textContent = text; g.appendChild(t);
return g;
}
function handle(hx, hy, name) { return el('circle', { cx: hx, cy: hy, r: 9, class: 'lm-hit', fill: '#9B5DE5', stroke: '#fff', 'stroke-width': 2, 'data-h': name }); }
function render() {
if (!svg) return;
while (svg.firstChild) svg.removeChild(svg.firstChild);
if (!mode) return;
if (mode === 'ruler') {
var r = ruler, dx = r.bx - r.ax, dy = r.by - r.ay;
var dpx = Math.hypot(dx, dy), deg = Math.abs(Math.atan2(dy, dx) * 180 / Math.PI);
if (deg > 90) deg = 180 - deg;
svg.appendChild(el('line', { x1: r.ax, y1: r.ay, x2: r.bx, y2: r.by, stroke: '#9B5DE5', 'stroke-width': 2.5 }));
svg.appendChild(handle(r.ax, r.ay, 'a'));
svg.appendChild(handle(r.bx, r.by, 'b'));
var lab = Math.round(dpx) + ' px · ' + (dpx / PXM).toFixed(2) + ' м · ' + deg.toFixed(1) + '°';
svg.appendChild(lineLabel((r.ax + r.bx) / 2, (r.ay + r.by) / 2 - 18, lab));
} else if (mode === 'angle') {
var a = angle;
var a1 = Math.atan2(a.ay - a.vy, a.ax - a.vx), a2 = Math.atan2(a.by - a.vy, a.bx - a.vx);
var deg2 = Math.abs((a1 - a2) * 180 / Math.PI); if (deg2 > 180) deg2 = 360 - deg2;
svg.appendChild(el('line', { x1: a.vx, y1: a.vy, x2: a.ax, y2: a.ay, stroke: '#06D6E0', 'stroke-width': 2.5 }));
svg.appendChild(el('line', { x1: a.vx, y1: a.vy, x2: a.bx, y2: a.by, stroke: '#06D6E0', 'stroke-width': 2.5 }));
// дуга угла
var rr = 36, s = el('path', { d: 'M ' + (a.vx + rr * Math.cos(a1)) + ' ' + (a.vy + rr * Math.sin(a1)) +
' A ' + rr + ' ' + rr + ' 0 ' + (deg2 > 180 ? 1 : 0) + ' ' + ((a2 - a1 + 2 * Math.PI) % (2 * Math.PI) < Math.PI ? 1 : 0) + ' ' +
(a.vx + rr * Math.cos(a2)) + ' ' + (a.vy + rr * Math.sin(a2)), fill: 'none', stroke: '#06D6E0', 'stroke-width': 2, opacity: .6 });
svg.appendChild(s);
svg.appendChild(handle(a.ax, a.ay, 'a'));
svg.appendChild(handle(a.bx, a.by, 'b'));
svg.appendChild(handle(a.vx, a.vy, 'v'));
svg.appendChild(lineLabel(a.vx, a.vy + 26, deg2.toFixed(1) + '°'));
}
}
global.LabMeasure = {
toggle: function () { ensure(); if (bar.classList.contains('on')) { this.hide(); } else { bar.classList.add('on'); if (!mode) setTool('ruler'); } },
hide: function () { if (bar) bar.classList.remove('on'); if (svg) svg.classList.remove('on'); mode = null; paintBar(); render(); },
setTool: setTool,
};
})(window);