// g3d.js — мини-3D движок для стереометрии. SVG-рендеринг, изометрическая проекция, drag-to-rotate.
(function(){
'use strict';

// === Векторная математика ===
function vAdd(a, b) { return {x:a.x+b.x, y:a.y+b.y, z:a.z+b.z}; }
function vSub(a, b) { return {x:a.x-b.x, y:a.y-b.y, z:a.z-b.z}; }
function vScale(a, k) { return {x:a.x*k, y:a.y*k, z:a.z*k}; }
function vDot(a, b) { return a.x*b.x + a.y*b.y + a.z*b.z; }
function vCross(a, b) {
  return {
    x: a.y*b.z - a.z*b.y,
    y: a.z*b.x - a.x*b.z,
    z: a.x*b.y - a.y*b.x
  };
}
function vLen(a) { return Math.sqrt(a.x*a.x + a.y*a.y + a.z*a.z); }
function vNorm(a) { var l = vLen(a) || 1; return vScale(a, 1/l); }

// === Матрицы 3x3 ===
function matRotX(t) {
  var c = Math.cos(t), s = Math.sin(t);
  return [[1,0,0],[0,c,-s],[0,s,c]];
}
function matRotY(t) {
  var c = Math.cos(t), s = Math.sin(t);
  return [[c,0,s],[0,1,0],[-s,0,c]];
}
function matMul(A, B) {
  var R = [[0,0,0],[0,0,0],[0,0,0]];
  for (var i=0;i<3;i++) for (var j=0;j<3;j++)
    R[i][j] = A[i][0]*B[0][j] + A[i][1]*B[1][j] + A[i][2]*B[2][j];
  return R;
}
function vApply(M, v) {
  return {
    x: M[0][0]*v.x + M[0][1]*v.y + M[0][2]*v.z,
    y: M[1][0]*v.x + M[1][1]*v.y + M[1][2]*v.z,
    z: M[2][0]*v.x + M[2][1]*v.y + M[2][2]*v.z
  };
}

// === Проекция (перспективная с камерой по оси Z) ===
function projectPersp(v, camDist, cx, cy, scale) {
  var z = camDist - v.z;
  if (z < 0.1) return null;
  var k = scale * camDist / z;
  return { x: cx + v.x * k, y: cy - v.y * k, depth: z };
}

// === Изометрическая проекция (без перспективы) ===
function projectIso(v, cx, cy, scale) {
  return {
    x: cx + scale * (v.x - v.z) * 0.866,
    y: cy - scale * (v.y - (v.x + v.z) * 0.5),
    depth: -v.z + v.x*0.5 + v.y*0.5
  };
}

// === Сцена ===
function createScene(opts) {
  opts = opts || {};
  return {
    W: opts.W || 480,
    H: opts.H || 360,
    cx: opts.cx !== undefined ? opts.cx : (opts.W || 480) / 2,
    cy: opts.cy !== undefined ? opts.cy : (opts.H || 360) / 2,
    scale: opts.scale || 50,
    camDist: opts.camDist || 8,
    rotX: opts.rotX !== undefined ? opts.rotX : -0.35,
    rotY: opts.rotY !== undefined ? opts.rotY : 0.7,
    proj: opts.proj || 'persp',
    bg: opts.bg || 'transparent'
  };
}

// === Вершины фигур ===

function prismPolygonBase(n, R, theta0) {
  theta0 = theta0 || -Math.PI/2;
  var verts = [];
  for (var i = 0; i < n; i++) {
    var a = theta0 + 2*Math.PI*i/n;
    verts.push({ x: R*Math.cos(a), z: R*Math.sin(a) });
  }
  return verts;
}

function prismMesh(n, R, h) {
  var baseXZ = prismPolygonBase(n, R);
  var verts = [];
  for (var i = 0; i < baseXZ.length; i++) verts.push({ x: baseXZ[i].x, y: -h/2, z: baseXZ[i].z });
  for (var j = 0; j < baseXZ.length; j++) verts.push({ x: baseXZ[j].x, y:  h/2, z: baseXZ[j].z });
  var faces = [];
  var bottom = [];
  for (var k = n - 1; k >= 0; k--) bottom.push(k);
  faces.push({ indices: bottom, kind: 'base' });
  var top = [];
  for (var t = 0; t < n; t++) top.push(n + t);
  faces.push({ indices: top, kind: 'base' });
  for (var s = 0; s < n; s++) {
    var ss = (s + 1) % n;
    faces.push({ indices: [s, ss, n + ss, n + s], kind: 'lateral' });
  }
  return { verts: verts, faces: faces };
}

function pyramidMesh(n, R, h) {
  var baseXZ = prismPolygonBase(n, R);
  var verts = baseXZ.map(function(p){ return { x: p.x, y: -h/2, z: p.z }; });
  verts.push({ x: 0, y: h/2, z: 0 });
  var faces = [];
  var apex = n;
  var bottom = [];
  for (var i = n - 1; i >= 0; i--) bottom.push(i);
  faces.push({ indices: bottom, kind: 'base' });
  for (var j = 0; j < n; j++) {
    var jj = (j + 1) % n;
    faces.push({ indices: [j, jj, apex], kind: 'lateral' });
  }
  return { verts: verts, faces: faces };
}

function cylinderMesh(R, h, segments) {
  segments = segments || 32;
  var bottom = [], top = [];
  for (var i = 0; i < segments; i++) {
    var a = 2*Math.PI*i/segments;
    bottom.push({ x: R*Math.cos(a), y: -h/2, z: R*Math.sin(a) });
    top.push({    x: R*Math.cos(a), y:  h/2, z: R*Math.sin(a) });
  }
  var verts = bottom.concat(top);
  var faces = [];
  var bIdx = []; for (var k = segments - 1; k >= 0; k--) bIdx.push(k);
  faces.push({ indices: bIdx, kind: 'base', isRound: true });
  var tIdx = []; for (var t = 0; t < segments; t++) tIdx.push(segments + t);
  faces.push({ indices: tIdx, kind: 'base', isRound: true });
  for (var s = 0; s < segments; s++) {
    var ss = (s + 1) % segments;
    faces.push({ indices: [s, ss, segments + ss, segments + s], kind: 'lateral' });
  }
  return { verts: verts, faces: faces, isRound: true };
}

function coneMesh(R, h, segments) {
  segments = segments || 32;
  var base = [];
  for (var i = 0; i < segments; i++) {
    var a = 2*Math.PI*i/segments;
    base.push({ x: R*Math.cos(a), y: -h/2, z: R*Math.sin(a) });
  }
  var verts = base.concat([{ x: 0, y: h/2, z: 0 }]);
  var apex = segments;
  var faces = [];
  var bIdx = []; for (var k = segments - 1; k >= 0; k--) bIdx.push(k);
  faces.push({ indices: bIdx, kind: 'base', isRound: true });
  for (var s = 0; s < segments; s++) {
    var ss = (s + 1) % segments;
    faces.push({ indices: [s, ss, apex], kind: 'lateral' });
  }
  return { verts: verts, faces: faces, isRound: true };
}

function sphereWireframe(R, lat, lon) {
  lat = lat || 6;
  lon = lon || 12;
  var lines = [];
  for (var i = 1; i < lat; i++) {
    var phi = -Math.PI/2 + Math.PI * i/lat;
    var r = R * Math.cos(phi);
    var y = R * Math.sin(phi);
    var pts = [];
    for (var j = 0; j <= 48; j++) {
      var t = 2*Math.PI*j/48;
      pts.push({ x: r*Math.cos(t), y: y, z: r*Math.sin(t) });
    }
    lines.push({ pts: pts, kind: 'parallel' });
  }
  for (var k = 0; k < lon; k++) {
    var theta = 2*Math.PI*k/lon;
    var pts2 = [];
    for (var ii = 0; ii <= 48; ii++) {
      var phi2 = -Math.PI/2 + Math.PI * ii/48;
      var r2 = R * Math.cos(phi2);
      pts2.push({ x: r2*Math.cos(theta), y: R*Math.sin(phi2), z: r2*Math.sin(theta) });
    }
    lines.push({ pts: pts2, kind: 'meridian' });
  }
  return { lines: lines, R: R };
}

// === Рендеринг ===

function renderMesh(mesh, M, scene, opts) {
  opts = opts || {};
  var fillBase = opts.fillBase || 'rgba(252,231,243,.55)';
  var fillSide = opts.fillSide || 'rgba(219,234,254,.55)';
  var strokeVisible = opts.strokeVisible || '#0f172a';
  var strokeHidden  = opts.strokeHidden  || '#94a3b8';

  var rotated = mesh.verts.map(function(v){ return vApply(M, v); });
  var projector;
  if (scene.proj === 'iso') {
    projector = function(v){ return projectIso(v, scene.cx, scene.cy, scene.scale); };
  } else {
    projector = function(v){ return projectPersp(v, scene.camDist, scene.cx, scene.cy, scene.scale); };
  }
  var projected = rotated.map(projector);

  var facesWithDepth = mesh.faces.map(function(face){
    var idx = face.indices;
    var normal = null;
    if (idx.length >= 3) {
      var v0 = rotated[idx[0]], v1 = rotated[idx[1]], v2 = rotated[idx[2]];
      var e1 = vSub(v1, v0), e2 = vSub(v2, v0);
      normal = vCross(e1, e2);
    }
    var avgZ = 0;
    for (var i = 0; i < idx.length; i++) avgZ += rotated[idx[i]].z;
    avgZ /= idx.length;
    var visible = normal ? (normal.z > -1e-6) : true;
    return { face: face, visible: visible, avgZ: avgZ, projected: idx.map(function(j){ return projected[j]; }) };
  });
  facesWithDepth.sort(function(a, b){ return a.avgZ - b.avgZ; });

  var svg = '';
  for (var f = 0; f < facesWithDepth.length; f++) {
    var fd = facesWithDepth[f];
    var anyMissing = false;
    for (var p = 0; p < fd.projected.length; p++) if (!fd.projected[p]) { anyMissing = true; break; }
    if (anyMissing) continue;
    var points = fd.projected.map(function(pp){ return pp.x.toFixed(1) + ',' + pp.y.toFixed(1); }).join(' ');
    var fill = fd.face.kind === 'base' ? fillBase : fillSide;
    if (fd.visible) {
      svg += '<polygon points="' + points + '" fill="' + fill + '" stroke="' + strokeVisible + '" stroke-width="1.8" stroke-linejoin="round"/>';
    } else {
      var idx2 = fd.face.indices;
      for (var ii = 0; ii < idx2.length; ii++) {
        var a = projected[idx2[ii]], b = projected[idx2[(ii+1) % idx2.length]];
        if (a && b) {
          svg += '<line x1="' + a.x.toFixed(1) + '" y1="' + a.y.toFixed(1) + '" x2="' + b.x.toFixed(1) + '" y2="' + b.y.toFixed(1) + '" stroke="' + strokeHidden + '" stroke-width="1" stroke-dasharray="4 3" opacity=".6"/>';
        }
      }
    }
  }
  return svg;
}

// === Рендеринг wireframe сферы ===
function renderSphereWireframe(sphere, M, scene, opts) {
  opts = opts || {};
  var strokeParallel = opts.strokeParallel || '#0891b2';
  var strokeMeridian = opts.strokeMeridian || '#0e7490';
  var projector;
  if (scene.proj === 'iso') {
    projector = function(v){ return projectIso(v, scene.cx, scene.cy, scene.scale); };
  } else {
    projector = function(v){ return projectPersp(v, scene.camDist, scene.cx, scene.cy, scene.scale); };
  }
  var svg = '';
  // outline circle: проецируем экватор после поворота (приближённо радиус как scale*R)
  var R = sphere.R;
  var origin = projector({x:0,y:0,z:0});
  if (origin) {
    svg += '<circle cx="' + origin.x.toFixed(1) + '" cy="' + origin.y.toFixed(1) + '" r="' + (scene.scale * R).toFixed(1) + '" fill="rgba(207,250,254,.25)" stroke="#0e7490" stroke-width="2"/>';
  }
  for (var i = 0; i < sphere.lines.length; i++) {
    var line = sphere.lines[i];
    var col = line.kind === 'parallel' ? strokeParallel : strokeMeridian;
    var d = '';
    var prev = false;
    for (var j = 0; j < line.pts.length; j++) {
      var rv = vApply(M, line.pts[j]);
      var pp = projector(rv);
      if (!pp) { prev = false; continue; }
      // back-face: если z вершины > 0 (после поворота), рисуем сплошной, иначе пунктир
      var solid = rv.z > -1e-6;
      // упрощённо: рисуем непрерывную полилинию, цвет в зависимости от middle z
      d += (prev ? ' L' : ' M') + pp.x.toFixed(1) + ',' + pp.y.toFixed(1);
      prev = true;
    }
    svg += '<path d="' + d + '" stroke="' + col + '" stroke-width="1" fill="none" opacity=".55"/>';
  }
  return svg;
}

// === Drag-to-rotate ===
function attachOrbit(svgEl, scene, onChange) {
  var dragging = false, lastX = 0, lastY = 0;
  svgEl.style.touchAction = 'none';
  svgEl.style.cursor = 'grab';
  function onDown(e) {
    dragging = true;
    svgEl.style.cursor = 'grabbing';
    var p = e.touches ? e.touches[0] : e;
    lastX = p.clientX; lastY = p.clientY;
    e.preventDefault();
  }
  function onMove(e) {
    if (!dragging) return;
    var p = e.touches ? e.touches[0] : e;
    var dx = p.clientX - lastX, dy = p.clientY - lastY;
    scene.rotY += dx * 0.012;
    scene.rotX += dy * 0.012;
    scene.rotX = Math.max(-1.4, Math.min(1.4, scene.rotX));
    lastX = p.clientX; lastY = p.clientY;
    if (onChange) onChange();
    e.preventDefault();
  }
  function onUp() { dragging = false; svgEl.style.cursor = 'grab'; }
  svgEl.addEventListener('mousedown', onDown, { passive: false });
  svgEl.addEventListener('touchstart', onDown, { passive: false });
  window.addEventListener('mousemove', onMove, { passive: false });
  window.addEventListener('touchmove', onMove, { passive: false });
  window.addEventListener('mouseup', onUp);
  window.addEventListener('touchend', onUp);
}

function buildRotMatrix(scene) {
  return matMul(matRotX(scene.rotX), matRotY(scene.rotY));
}

function presetView(scene, name, onChange) {
  if (name === 'front')     { scene.rotX = 0;                       scene.rotY = 0; }
  else if (name === 'top')  { scene.rotX = -Math.PI/2 + 0.01;       scene.rotY = 0; }
  else if (name === 'side') { scene.rotX = 0;                       scene.rotY = Math.PI/2; }
  else if (name === 'iso')  { scene.rotX = -0.35;                   scene.rotY = 0.7; }
  if (onChange) onChange();
}

// === Экспорт ===
window.G3D = {
  vAdd: vAdd, vSub: vSub, vScale: vScale, vDot: vDot, vCross: vCross, vLen: vLen, vNorm: vNorm,
  matRotX: matRotX, matRotY: matRotY, matMul: matMul, vApply: vApply,
  projectPersp: projectPersp, projectIso: projectIso,
  createScene: createScene, buildRotMatrix: buildRotMatrix, attachOrbit: attachOrbit, presetView: presetView,
  prismMesh: prismMesh, pyramidMesh: pyramidMesh, cylinderMesh: cylinderMesh, coneMesh: coneMesh,
  sphereWireframe: sphereWireframe,
  renderMesh: renderMesh, renderSphereWireframe: renderSphereWireframe
};
})();
