Initial import: WebAisMap
Closes TG-4 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user