Загрузка данных
Да, продолжаем. Этот план уже намного ближе к рабочему, но Claude опять оставил несколько технических мин. В текущем виде я бы его ещё не утверждал.
1. В формуле утопления перепутано направление
Сейчас написано:
return logicalPoint + recess * rodDirection
Это правильно только для одного конца стержня.
Если rodDirection направлен от pA к pB:
для pA на U=0 сдвиг вдоль +rodDirection идёт внутрь;
для pB на U=length тот же +rodDirection идёт наружу, ещё дальше за пролёт.
Helper должен знать второй конец стержня и двигать точку к противоположному концу, а не всегда по rodDirection.
Правильная сигнатура:
fitRodEndpointToBeamBounds({
logicalPoint,
otherLogicalPoint,
rodFrame,
rodRadius,
sides,
uMin,
uMax
})
Внутреннее направление:
inwardDirection = normalize(otherLogicalPoint - logicalPoint)
И только потом:
renderPoint =
logicalPoint + inwardDirection * recess
Это сейчас главный реальный баг плана.
2. stableBasis() нельзя независимо вычислять два раза
В плане helper сам вызывает:
stableBasis(rodDirection)
А затем bar3d() снова отдельно вызывает stableBasis(). Это опасно:
направление может оказаться передано с противоположным знаком;
базис может получить другую фазу или ориентацию;
helper проверит одни восемь точек кольца, а bar3d построит немного другие.
Нужно вычислять геометрический каркас стержня один раз:
const rodFrame = createRodFrame(logicalStart, logicalEnd, sides)
Он должен содержать:
{
direction,
basis1,
basis2,
ringOffsets
}
После этого тот же объект rodFrame используется:
для расчёта допустимого сдвига;
для построения ringA;
для построения ringB;
для ассертов.
Никакого повторного независимого построения базиса.
3. Проверка щели математически неверна
Сейчас предлагается:
distance(renderPoint, logicalPoint) <= chordRadius
Но это проверяет только центр открытого кольца. Она ничего не доказывает про восемь вершин самого кольца.
Центр может находиться внутри радиуса пояса, а половина открытого кольца всё равно может торчать наружу и быть видимой.
Нужно убрать утверждение, что это гарантирует отсутствие щели.
Проверка должна быть такой:
Для каждого утопленного конца сохранить фактические вершины открытого кольца.
Проверить:
1. все вершины находятся внутри U∈[0,length];
2. открытое кольцо не создаёт видимого отверстия на контрольном рендере;
3. боковые грани раскоса реально пересекаются с объёмом принимающего пояса, а не заканчиваются перед ним.
Ещё лучше инструментально проверить для каждой вершины кольца расстояние до оси принимающего пояса:
distancePointToChordAxis(vertex) <= chordRadius + tolerance
Но даже эта проверка может оказаться слишком строгой для двух цилиндров, пересекающихся под углом. Поэтому она не заменяет рендер.
Если после утопления виден провал, запасной путь должен быть не только:
клиппирование по U=0/U=length
Это исправит выход за пролёт, но не исправит соединение с цилиндром пояса.
Нужно написать:
Если открытое кольцо остаётся видимым, применить геометрическое пересечение/подрезку раскоса по объёму принимающего пояса либо продлить раскос внутрь пояса и обеспечить корректный порядок отрисовки. Клиппирование только плоскостью границы пролёта не считается исправлением узлового зазора.
4. renderPoint == logicalPoint строго снова нельзя использовать
В плане снова появляется строгое равенство:
для внутренних узлов renderPoint == logicalPoint строго
Если helper возвращает новый объект с теми же координатами, объектное сравнение через === провалится.
Проверять нужно координаты:
distance(renderPoint, logicalPoint) <= 1e-12
Либо helper действительно возвращает тот же объект, но тогда это проверка реализации JavaScript, а не геометрии.
5. Gate D с localNudge всё ещё не является локальным
Вот это:
sortKey = depthKey + localNudge(jointId, kind)
формально транзитивно, но всё равно меняет глобальную глубину грани. Оно не гарантирует, что перестановка затронет только один узел.
Кроме того, непонятно, какой jointId назначать боковому треугольнику стержня:
у него есть jointStartId;
есть jointEndId;
сама грань проходит между двумя узлами.
То есть один треугольник нельзя честно отнести только к одному стыку.
А «минимальный разрыв depthKey между разными узлами» может быть:
нулевым;
почти нулевым;
разным при анимации;
разным для мобильного размера сцены.
Gate D лучше переписать так:
Gate D не проектировать через добавление localNudge к глобальному depthKey.
Если после Gate C останется доказанный дефект порядка:
1. Сначала сохранить обычную глобальную стабильную сортировку по depthKey.
2. Найти конкретные грани, создающие дефект в конкретной экранной области узла.
3. Выполнить локальную перестановку только этих идентификаторов граней после глобальной сортировки.
4. Перестановка не должна перемещать грани через элементы другого узла, балки или объекта сцены.
5. Если невозможно сформировать строго локальную группу без затрагивания других граней, Gate D отклонить и исправлять тесселяцию/разбиение меша.
Скорее всего, Gate D вообще не понадобится.
Готовый комментарий для Claude
Отправь ему этот блок:
План почти принят, но остаются 5 обязательных исправлений.
1. В fitRodEndpointToBeamBounds нельзя всегда возвращать:
logicalPoint + recess * rodDirection
Для конечной точки pB на U=length это может двигать её НАРУЖУ. Helper должен принимать otherLogicalPoint и вычислять:
inwardDirection =
normalize(otherLogicalPoint - logicalPoint)
renderPoint =
logicalPoint + inwardDirection * recess
Направление сдвига всегда идёт от граничного узла к противоположному концу этого же стержня.
2. Не вычислять stableBasis независимо в helper и bar3d.
Создать один rodFrame:
createRodFrame(logicalStart, logicalEnd, sides)
Он содержит:
direction,
basis1,
basis2,
ringOffsets.
Тот же rodFrame передаётся и в fitRodEndpointToBeamBounds, и в bar3d. Ассерты проверяют именно кольца, реально построенные из этого rodFrame.
3. distance(renderPoint, logicalPoint) <= chordRadius НЕ доказывает отсутствие щели: это проверка центра, а не открытого восьмиугольного кольца.
Проверять фактические вершины кольца и реальное визуальное сопряжение с принимающим поясом. Если открытое кольцо остаётся видно, клиппирование только по U=0/U=length недостаточно — требуется подрезка/пересечение относительно объёма пояса либо корректное погружение раскоса внутрь пояса с правильным порядком отрисовки.
4. Для внутренних узлов не использовать объектное или строгое равенство renderPoint == logicalPoint. Проверять:
distance(renderPoint, logicalPoint) <= eps
например eps=1e-12.
5. Gate D через sortKey = depthKey + localNudge не утверждён. Это всё равно меняет глобальную глубину и не является строго локальным; кроме того, боковая грань имеет два узла — jointStartId и jointEndId — и не может однозначно получить один jointId.
Если Gate D вообще понадобится:
- сначала обычная стабильная глобальная depth-сортировка;
- затем точечная перестановка только заранее выявленных граней конкретного узла;
- запрещено переносить грань через геометрию другого узла/балки/объекта.
Если локальную группу нельзя строго изолировать, исправлять тесселяцию, а не depthKey.
После этих исправлений разрешено выполнять Gate A, затем Gate B. Gate C — только при доказанной остаточной ошибке. Gate D — только как последний резерв после отдельного согласования.
После этого план уже можно утверждать. Самое опасное сейчас — знак сдвига для правого торца: в записанном алгоритме Claude может исправить левый конец и ещё сильнее испортить правый.