/** * Редактор габаритов (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(); } })();