Initial import: WebAisMap

Closes TG-4

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-04 07:56:45 +03:00
commit 03075f1ef1
1460 changed files with 16334 additions and 0 deletions
+473
View File
@@ -0,0 +1,473 @@
/**
* Редактор габаритов (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();
}
})();