diff --git a/src/main/java/ru/mcs/tetragenesis/Universe.java b/src/main/java/ru/mcs/tetragenesis/Universe.java index e850e4c..adac6ea 100644 --- a/src/main/java/ru/mcs/tetragenesis/Universe.java +++ b/src/main/java/ru/mcs/tetragenesis/Universe.java @@ -208,6 +208,7 @@ } applyWallForces(); + applyNodeTetraRepulsion(); for (Node n : nodes) { n.vx *= damping; @@ -479,6 +480,11 @@ continue; } + if (isInsideAnyTetra(newX, newY, newZ)) { + growingApexes.remove(apex.faceKey); + continue; + } + int newIdx = nodes.size(); Node newNode = new Node(newX, newY, newZ); nodes.add(newNode); @@ -692,4 +698,136 @@ public void setK(double k) { this.k = k; } public void setDamping(double d) { this.damping = d; } public void setFluctuation(double f) { this.fluctAmp = f; } + + /** + * Отталкивает узлы, которые оказались внутри чужих тетраэдров. + * Использует барицентрические координаты для точного определения + * положения узла относительно объёма тетраэдра. + */ + private void applyNodeTetraRepulsion() { + double repulsionK = 3.0; // жёсткость выталкивания + double maxRepForce = 5.0; // ограничение силы + + for (Tetra t : tetras) { + Node n0 = nodes.get(t.a); + Node n1 = nodes.get(t.b); + Node n2 = nodes.get(t.c); + Node n3 = nodes.get(t.d); + + for (int vi = 0; vi < nodes.size(); vi++) { + // Узел, принадлежащий тетраэдру — пропускаем + if (vi == t.a || vi == t.b || vi == t.c || vi == t.d) continue; + + Node p = nodes.get(vi); + + // Барицентрические координаты точки p в тетраэдре (n0,n1,n2,n3) + double[] bary = barycentric(p, n0, n1, n2, n3); + + // Если все координаты > 0 — точка внутри тетраэдра + double minBary = Math.min(Math.min(bary[0], bary[1]), + Math.min(bary[2], bary[3])); + if (minBary <= 0) continue; // снаружи — всё хорошо + + // Глубина проникновения = наименьшая барицентрическая координата + // (насколько близко к ближайшей грани изнутри) + double penetration = minBary; + + // Находим ближайшую грань и её нормаль наружу + double[] pushDir = closestFaceNormal(p, n0, n1, n2, n3); + + double forceMag = Math.min(repulsionK * penetration * 100.0, maxRepForce); + + p.vx += pushDir[0] * forceMag * dt; + p.vy += pushDir[1] * forceMag * dt; + p.vz += pushDir[2] * forceMag * dt; + + // Реакция на вершины тетраэдра (3-й закон Ньютона, делим на 4) + double share = forceMag * dt / 4.0; + n0.vx -= pushDir[0] * share; n0.vy -= pushDir[1] * share; n0.vz -= pushDir[2] * share; + n1.vx -= pushDir[0] * share; n1.vy -= pushDir[1] * share; n1.vz -= pushDir[2] * share; + n2.vx -= pushDir[0] * share; n2.vy -= pushDir[1] * share; n2.vz -= pushDir[2] * share; + n3.vx -= pushDir[0] * share; n3.vy -= pushDir[1] * share; n3.vz -= pushDir[2] * share; + } + } + } + + /** + * Барицентрические координаты точки p в тетраэдре (a,b,c,d). + * Все 4 значения > 0 ⟺ точка строго внутри тетраэдра. + */ + private double[] barycentric(Node p, Node a, Node b, Node c, Node d) { + double ax = b.x-a.x, ay = b.y-a.y, az = b.z-a.z; + double bx = c.x-a.x, by = c.y-a.y, bz = c.z-a.z; + double cx = d.x-a.x, cy = d.y-a.y, cz = d.z-a.z; + double px = p.x-a.x, py = p.y-a.y, pz = p.z-a.z; + + // Матрица [a b c] — столбцы, решаем систему методом Крамера + double det = ax*(by*cz - bz*cy) + - ay*(bx*cz - bz*cx) + + az*(bx*cy - by*cx); + if (Math.abs(det) < 1e-10) return new double[]{-1,-1,-1,-1}; + + double l1 = (px*(by*cz - bz*cy) - ay*(px*(cz) - pz*cx + bx*(pz-cz)) // упрощённо через Крамера + + az*(px*cy - py*cx)) / det; + // Упрощение: считаем через объёмы субтетраэдров + double vol = signedVolume(a, b, c, d); + if (Math.abs(vol) < 1e-10) return new double[]{-1,-1,-1,-1}; + + double v1 = signedVolume(p, b, c, d) / vol; + double v2 = signedVolume(a, p, c, d) / vol; + double v3 = signedVolume(a, b, p, d) / vol; + double v4 = 1.0 - v1 - v2 - v3; + return new double[]{v1, v2, v3, v4}; + } + + private double signedVolume(Node a, Node b, Node c, Node d) { + double ax = b.x-a.x, ay = b.y-a.y, az = b.z-a.z; + double bx = c.x-a.x, by = c.y-a.y, bz = c.z-a.z; + double cx = d.x-a.x, cy = d.y-a.y, cz = d.z-a.z; + return (ax*(by*cz - bz*cy) - ay*(bx*cz - bz*cx) + az*(bx*cy - by*cx)) / 6.0; + } + + /** + * Нормаль ближайшей грани тетраэдра, направленная наружу от p. + */ + private double[] closestFaceNormal(Node p, Node n0, Node n1, Node n2, Node n3) { + // 4 грани тетраэдра; берём ту, к которой p ближе всего изнутри + double[][] faces = { + outwardUnitNormal(n0, n1, n2, n3), + outwardUnitNormal(n0, n1, n3, n2), + outwardUnitNormal(n0, n2, n3, n1), + outwardUnitNormal(n1, n2, n3, n0) + }; + Node[] bases = { n0, n0, n0, n1 }; + + double minDist = Double.MAX_VALUE; + double[] best = faces[0]; + + for (int i = 0; i < 4; i++) { + double[] n = faces[i]; + Node base = bases[i]; + double dist = (p.x - base.x) * n[0] + + (p.y - base.y) * n[1] + + (p.z - base.z) * n[2]; + // dist < 0 означает: p по другую сторону нормали (изнутри) + // расстояние до грани = |dist| + if (Math.abs(dist) < minDist) { + minDist = Math.abs(dist); + best = n; // нормаль уже смотрит наружу → нужное направление выталкивания + } + } + return best; + } + + private boolean isInsideAnyTetra(double x, double y, double z) { + Node probe = new Node(x, y, z); + for (Tetra t : tetras) { + double[] bary = barycentric(probe, + nodes.get(t.a), nodes.get(t.b), + nodes.get(t.c), nodes.get(t.d)); + if (bary[0] > 0 && bary[1] > 0 && bary[2] > 0 && bary[3] > 0) + return true; + } + return false; + } } \ No newline at end of file