Files
WebAisMap/static/js/ship_dims_editor.js
T
Grigo 03075f1ef1 Initial import: WebAisMap
Closes TG-4

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 07:56:45 +03:00

474 lines
16 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.
/**
* Редактор габаритов (ITU Fig. 38).
* Корма — L, правый борт — W (доли A/L и C/W фиксируются на время жеста); точка GPS — перетаскивание.
* Для роста L/W координаты могут выходить за контур корпуса (опорный масштаб wPxRef/hPxRef с pointerdown).
*/
(function () {
'use strict';
var NS = 'http://www.w3.org/2000/svg';
var MIN_LEN = 20;
var MIN_BEAM = 6;
var MAX_AB = 511;
var MAX_CD = 63;
var MAX_L = MAX_AB + MAX_AB;
var MAX_W = MAX_CD + MAX_CD;
/** Макс. размер корпуса в пикселях (пропорции сохраняются) */
var MAX_DRAW_W = 175;
var MAX_DRAW_H = 210;
/** Минимум по меньшей стороне корпуса в px — иначе ручки и клампы «схлопываются» */
var MIN_HULL_PX = 48;
var PAD_L = 12;
var PAD_T = 48;
var PAD_R = 78;
var PAD_B = 42;
var svg, inner, hull, marker, handles;
var gridH, gridV, lblBow, lblStern, dimBeam, dimLength;
var drag = null;
/** Текущая геометрия (обновляется в refresh) */
var layout = {
wPx: 100,
hPx: 160,
dispL: MIN_LEN,
dispW: MIN_BEAM,
};
function clamp(n, lo, hi) {
return Math.max(lo, Math.min(hi, n));
}
function el(name, attrs) {
var e = document.createElementNS(NS, name);
if (attrs) {
Object.keys(attrs).forEach(function (k) {
e.setAttribute(k, attrs[k]);
});
}
return e;
}
function readDims() {
var A = parseInt(document.getElementById('tp-to-bow').value, 10) || 0;
var B = parseInt(document.getElementById('tp-to-stern').value, 10) || 0;
var C = parseInt(document.getElementById('tp-to-port').value, 10) || 0;
var D = parseInt(document.getElementById('tp-to-starboard').value, 10) || 0;
A = clamp(A, 0, MAX_AB);
B = clamp(B, 0, MAX_AB);
C = clamp(C, 0, MAX_CD);
D = clamp(D, 0, MAX_CD);
return { A: A, B: B, C: C, D: D, L: A + B, W: C + D };
}
function writeDims(A, B, C, D) {
A = clamp(Math.round(A), 0, MAX_AB);
B = clamp(Math.round(B), 0, MAX_AB);
C = clamp(Math.round(C), 0, MAX_CD);
D = clamp(Math.round(D), 0, MAX_CD);
document.getElementById('tp-to-bow').value = A;
document.getElementById('tp-to-stern').value = B;
document.getElementById('tp-to-port').value = C;
document.getElementById('tp-to-starboard').value = D;
}
function displayLW(d) {
var L = d.L > 0 ? d.L : MIN_LEN;
var W = d.W > 0 ? d.W : MIN_BEAM;
return { L: L, W: W, template: d.L === 0 && d.W === 0 };
}
function hullPath(w, h) {
var bow = Math.min(20, h * 0.11);
var tipX = w / 2;
return (
'M ' + tipX + ',0 L ' + w + ',' + bow + ' L ' + w + ',' + h + ' L 0,' + h + ' L 0,' + bow + ' Z'
);
}
function computeLayout(d, disp) {
var scale = Math.min(MAX_DRAW_W / disp.W, MAX_DRAW_H / disp.L);
if (!isFinite(scale) || scale <= 0) scale = 1;
var wPx0 = disp.W * scale;
var hPx0 = disp.L * scale;
var bump = 1;
if (wPx0 < MIN_HULL_PX) bump = Math.max(bump, MIN_HULL_PX / Math.max(wPx0, 1e-6));
if (hPx0 < MIN_HULL_PX) bump = Math.max(bump, MIN_HULL_PX / Math.max(hPx0, 1e-6));
scale *= bump;
var wPx = disp.W * scale;
var hPx = disp.L * scale;
layout.wPx = wPx;
layout.hPx = hPx;
layout.dispL = disp.L;
layout.dispW = disp.W;
layout.scale = scale;
return { sx: scale, sy: scale, wPx: wPx, hPx: hPx };
}
function scales(d, disp, geo) {
var wPx = geo.wPx;
var hPx = geo.hPx;
var padX = Math.max(1, Math.min(8, wPx * 0.1));
var padY = Math.max(1, Math.min(8, hPx * 0.1));
var aVis = d.L > 0 ? d.A : disp.L / 2;
var cVis = d.W > 0 ? d.C : disp.W / 2;
var mx = clamp(cVis * geo.sx, padX, wPx - padX);
var my = clamp(aVis * geo.sy, padY, hPx - padY);
return { mx: mx, my: my };
}
function ensureNonZeroDims() {
var d = readDims();
if (d.L === 0 && d.W === 0) {
writeDims(MIN_LEN / 2, MIN_LEN / 2, MIN_BEAM / 2, MIN_BEAM / 2);
}
}
function clearG(g) {
while (g.firstChild) g.removeChild(g.firstChild);
}
function rebuildBeamDimension(g, wPx, hPx, Wm) {
clearG(g);
var bow = Math.min(20, hPx * 0.11);
var off = 24;
var y = -off;
var yPast = y - 1.5;
g.appendChild(
el('line', { class: 'ship-dim-ext', x1: 0, y1: bow, x2: 0, y2: yPast })
);
g.appendChild(
el('line', { class: 'ship-dim-ext', x1: wPx, y1: bow, x2: wPx, y2: yPast })
);
g.appendChild(
el('line', {
class: 'ship-dim-main',
x1: 0,
y1: y,
x2: wPx,
y2: y,
'marker-start': 'url(#ship-dim-arrow)',
'marker-end': 'url(#ship-dim-arrow)',
})
);
var t = el('text', {
class: 'ship-dim-txt',
x: wPx / 2,
y: y - 8,
'text-anchor': 'middle',
});
t.textContent = 'W = ' + Wm + ' м';
g.appendChild(t);
}
function rebuildLengthDimension(g, wPx, hPx, Lm) {
clearG(g);
var bow = Math.min(20, hPx * 0.11);
var off = 22;
var x = wPx + off;
var xPast = x + 1.5;
g.appendChild(
el('line', { class: 'ship-dim-ext', x1: wPx, y1: bow, x2: x, y2: bow })
);
g.appendChild(
el('line', { class: 'ship-dim-ext', x1: x, y1: bow, x2: x, y2: 0 })
);
g.appendChild(
el('line', { class: 'ship-dim-ext', x1: wPx, y1: hPx, x2: xPast, y2: hPx })
);
g.appendChild(
el('line', {
class: 'ship-dim-main',
x1: x,
y1: 0,
x2: x,
y2: hPx,
'marker-start': 'url(#ship-dim-arrow)',
'marker-end': 'url(#ship-dim-arrow)',
})
);
var t = el('text', {
class: 'ship-dim-txt',
x: x + 14,
y: hPx / 2,
'text-anchor': 'middle',
transform: 'rotate(-90 ' + (x + 14) + ' ' + hPx / 2 + ')',
});
t.textContent = 'L = ' + Lm + ' м';
g.appendChild(t);
}
function positionHandles(wPx, hPx) {
if (!handles) return;
var stern = handles.querySelector('[data-ship-edge="stern"]');
var sb = handles.querySelector('[data-ship-edge="starboard"]');
if (stern) stern.setAttribute('transform', 'translate(' + wPx / 2 + ',' + hPx + ')');
if (sb) sb.setAttribute('transform', 'translate(' + wPx + ',' + hPx / 2 + ')');
}
function refreshFromInputs() {
if (!svg || !inner || !hull || !marker) return;
var d = readDims();
var disp = displayLW(d);
var geo = computeLayout(d, disp);
var sc = scales(d, disp, geo);
var wPx = geo.wPx;
var hPx = geo.hPx;
inner.setAttribute('transform', 'translate(' + PAD_L + ',' + PAD_T + ')');
var vbW = PAD_L + wPx + PAD_R;
var vbH = PAD_T + hPx + PAD_B;
svg.setAttribute('viewBox', '0 0 ' + vbW + ' ' + vbH);
rebuildBeamDimension(dimBeam, wPx, hPx, disp.W);
rebuildLengthDimension(dimLength, wPx, hPx, disp.L);
hull.setAttribute('d', hullPath(wPx, hPx));
if (gridH && gridV) {
gridH.setAttribute('x1', 0);
gridH.setAttribute('y1', sc.my);
gridH.setAttribute('x2', wPx);
gridH.setAttribute('y2', sc.my);
gridV.setAttribute('x1', sc.mx);
gridV.setAttribute('y1', 0);
gridV.setAttribute('x2', sc.mx);
gridV.setAttribute('y2', hPx);
}
if (lblBow) {
lblBow.setAttribute('x', wPx / 2);
lblBow.setAttribute('y', -38);
}
if (lblStern) {
lblStern.setAttribute('x', wPx / 2);
lblStern.setAttribute('y', hPx + 22);
}
marker.setAttribute('transform', 'translate(' + sc.mx + ',' + sc.my + ')');
marker.setAttribute('class', 'ship-gps-group' + (disp.template ? ' ship-gps-group--template' : ''));
positionHandles(wPx, hPx);
}
function svgPointFromClient(clientX, clientY) {
var pt = svg.createSVGPoint();
pt.x = clientX;
pt.y = clientY;
var ctm = inner.getScreenCTM();
if (!ctm) return null;
var p = pt.matrixTransform(ctm.inverse());
return { x: p.x, y: p.y };
}
function splitLength(u, Ls) {
u = clamp(u, 0, 1);
var A = Math.round(u * Ls);
A = clamp(A, 0, MAX_AB);
var B = Ls - A;
if (B > MAX_AB) {
B = MAX_AB;
A = clamp(Ls - B, 0, MAX_AB);
}
if (B < 0) {
B = 0;
A = clamp(Ls, 0, MAX_AB);
}
return { A: A, B: B };
}
function splitBeam(v, Ws) {
v = clamp(v, 0, 1);
var C = Math.round(v * Ws);
C = clamp(C, 0, MAX_CD);
var D = Ws - C;
if (D > MAX_CD) {
D = MAX_CD;
C = clamp(Ws - D, 0, MAX_CD);
}
if (D < 0) {
D = 0;
C = clamp(Ws, 0, MAX_CD);
}
return { C: C, D: D };
}
/** Новая длина L; сохраняем долю носа A/L ≈ rA. */
function abFromLength(Ln, rA) {
Ln = clamp(Math.round(Ln), 1, MAX_L);
var r = clamp(isFinite(rA) ? rA : 0.5, 0, 1);
var A = Math.round(r * Ln);
A = clamp(A, 0, MAX_AB);
var B = Ln - A;
if (B > MAX_AB) {
B = MAX_AB;
A = clamp(Ln - B, 0, MAX_AB);
}
if (B < 0) {
B = 0;
A = clamp(Ln, 0, MAX_AB);
}
return { A: A, B: B };
}
/** Новая ширина W; сохраняем долю порта C/W ≈ rC. */
function cdFromWidth(Wn, rC) {
Wn = clamp(Math.round(Wn), 1, MAX_W);
var r = clamp(isFinite(rC) ? rC : 0.5, 0, 1);
var C = Math.round(r * Wn);
C = clamp(C, 0, MAX_CD);
var D = Wn - C;
if (D > MAX_CD) {
D = MAX_CD;
C = clamp(Wn - D, 0, MAX_CD);
}
if (D < 0) {
D = 0;
C = clamp(Wn, 0, MAX_CD);
}
return { C: C, D: D };
}
function startDrag(ev, kind, captureEl) {
ev.preventDefault();
captureEl.setPointerCapture(ev.pointerId);
document.body.classList.add('ship-editor-dragging');
document.addEventListener('pointermove', onDocumentPointerMove);
document.addEventListener('pointerup', onDocumentPointerEnd);
document.addEventListener('pointercancel', onDocumentPointerEnd);
return { kind: kind, pid: ev.pointerId, el: captureEl };
}
function stopDragListeners() {
document.removeEventListener('pointermove', onDocumentPointerMove);
document.removeEventListener('pointerup', onDocumentPointerEnd);
document.removeEventListener('pointercancel', onDocumentPointerEnd);
}
function onInnerPointerDown(ev) {
if (!inner) return;
var edgeG = ev.target.closest ? ev.target.closest('[data-ship-edge]') : null;
if (edgeG) {
ensureNonZeroDims();
var d0 = readDims();
var disp0 = displayLW(d0);
var geo0 = computeLayout(d0, disp0);
var kind = edgeG.getAttribute('data-ship-edge');
drag = startDrag(ev, kind, edgeG);
drag.L0 = d0.L;
drag.W0 = d0.W;
drag.rA = d0.L > 0 ? d0.A / d0.L : 0.5;
drag.rC = d0.W > 0 ? d0.C / d0.W : 0.5;
drag.wPxRef = geo0.wPx;
drag.hPxRef = geo0.hPx;
return;
}
if (marker && marker.contains(ev.target)) {
ensureNonZeroDims();
var dg = readDims();
var Ls = dg.L > 0 ? Math.min(dg.L, MAX_L) : MIN_LEN;
var Ws = dg.W > 0 ? Math.min(dg.W, MAX_W) : MIN_BEAM;
Ls = clamp(Ls, 1, MAX_L);
Ws = clamp(Ws, 1, MAX_W);
drag = startDrag(ev, 'gps', marker);
drag.Ls = Ls;
drag.Ws = Ws;
}
}
function onDocumentPointerMove(ev) {
if (!drag || ev.pointerId !== drag.pid) return;
var p = svgPointFromClient(ev.clientX, ev.clientY);
if (!p) return;
var d = readDims();
var disp = displayLW(d);
var geo = computeLayout(d, disp);
var wPx = geo.wPx;
var hPx = geo.hPx;
if (drag.kind === 'gps') {
var u = p.y / hPx;
var v = p.x / wPx;
var ab = splitLength(u, drag.Ls);
var cd = splitBeam(v, drag.Ws);
writeDims(ab.A, ab.B, cd.C, cd.D);
refreshFromInputs();
return;
}
var wRef = drag.wPxRef;
var hRef = drag.hPxRef;
if (drag.kind === 'stern' && wRef > 0 && hRef > 0) {
var yMin = (hRef * 1) / Math.max(drag.L0, 1);
var yMax = (hRef * MAX_L) / Math.max(drag.L0, 1);
var yS = clamp(p.y, yMin, yMax);
var L1 = Math.round((drag.L0 * yS) / hRef);
L1 = clamp(L1, 1, MAX_L);
var abS = abFromLength(L1, drag.rA);
writeDims(abS.A, abS.B, d.C, d.D);
} else if (drag.kind === 'starboard' && wRef > 0 && hRef > 0) {
var xMin = (wRef * 1) / Math.max(drag.W0, 1);
var xMax = (wRef * MAX_W) / Math.max(drag.W0, 1);
var xSb = clamp(p.x, xMin, xMax);
var W1s = Math.round((drag.W0 * xSb) / wRef);
W1s = clamp(W1s, 1, MAX_W);
var cd2 = cdFromWidth(W1s, drag.rC);
writeDims(d.A, d.B, cd2.C, cd2.D);
}
refreshFromInputs();
}
function onDocumentPointerEnd(ev) {
if (!drag || ev.pointerId !== drag.pid) return;
stopDragListeners();
try {
drag.el.releasePointerCapture(ev.pointerId);
} catch (e) { /* ignore */ }
drag = null;
document.body.classList.remove('ship-editor-dragging');
}
function bindInputs() {
['tp-to-bow', 'tp-to-stern', 'tp-to-port', 'tp-to-starboard'].forEach(function (id) {
var eln = document.getElementById(id);
if (eln) {
eln.addEventListener('input', refreshFromInputs);
eln.addEventListener('change', refreshFromInputs);
}
});
}
function boot() {
svg = document.getElementById('ship-editor-svg');
inner = document.getElementById('ship-editor-inner');
hull = document.getElementById('ship-hull');
marker = document.getElementById('ship-gps-marker');
handles = document.getElementById('ship-edge-handles');
gridH = document.getElementById('ship-grid-h');
gridV = document.getElementById('ship-grid-v');
lblBow = document.getElementById('ship-lbl-bow');
lblStern = document.getElementById('ship-lbl-stern');
dimBeam = document.getElementById('ship-dim-beam');
dimLength = document.getElementById('ship-dim-length');
if (!svg || !inner || !hull || !marker) return;
inner.addEventListener('pointerdown', onInnerPointerDown);
bindInputs();
refreshFromInputs();
}
window.shipDimsEditorRefresh = refreshFromInputs;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();