Загрузка данных


План стал заметно лучше, но в текущем виде ещё не утверждай. Он правильно отказался от шаров и нашёл реальную потенциальную причину — торцевые крышки раскосов. Однако сейчас Claude слишком рано решил, что именно крышки объясняют весь дефект.

По скриншоту проблема выглядит крупнее обычного шва от крышки: заметны также наложения боковых граней и неаккуратное поведение геометрии у крайних узлов.

Что в плане правильно

Правильно:

удалить крышки с обоих концов раскосов;
оставить крышки только на настоящих свободных концах поясов;
не менять координаты узлов и шаг панелей;
не добавлять сферы, муфты и декоративные накладки;
сначала исследовать увеличенные узлы;
не увеличивать количество геометрии.

Но нужно исправить пять моментов.

1. Отключение крышки не убирает торчащее кольцо стержня

У bar3d торцевая крышка — только набор треугольников, закрывающих отверстие. После capStart=false останется торцевое кольцо, к которому приходят боковые грани призмы.

Поскольку раскос наклонён, плоскость этого кольца имеет разброс по U. В крайнем узле с центром u=0 часть вершин кольца может иметь U<0. На другом конце — U>length.

То есть можно удалить крышку, но синий «шип» за торцом останется.

В плане нельзя оставлять:

если перелёт субвизуален — обосновать

По критериям проекта любой выход за пролёт недопустим. Нужно либо исправить, либо не сдавать.

Добавь:

Отключение capStart/capEnd удаляет только треугольники крышки, но НЕ удаляет и НЕ перемещает торцевое кольцо боковых граней bar3d.

Поэтому после удаления крышек обязательно проверить ВСЕ вершины торцевых колец первого и последнего раскоса.

Если хотя бы одна вершина имеет U < -eps или U > length + eps, это считается реальным нарушением, независимо от того, насколько оно заметно на общем кадре.

Разрешённые способы исправления:
1. геометрически клиповать треугольники раскоса плоскостями U=0 и U=length;
2. либо использовать отдельные render-endpoints, утопленные внутрь объёма пояса настолько, чтобы полное торцевое кольцо оставалось внутри пролёта и было закрыто поясом.

Логические узлы leftBottom/rightBottom/top при этом не меняются. Запрещены ручные поправки для конкретной балки или конкретного торца.

В финале:
minU полной геометрии >= -eps;
maxU полной геометрии <= length + eps.
Объяснение «перелёт почти не виден» не принимается.
2. TIE_EPS=0.15 применять нельзя

Это очень большое значение:

rodRadius = 0.03;
0.15 — пять радиусов;
это около 16% длины панели 0.92.

Такой компаратор начнёт переставлять не только грани одного соединения, но и соседние поверхности. Фраза «на порядок больше rodRadius» также математически неверна: 0.15 больше 0.03 в пять раз, а не в десять.

Глобальный tie-break по одному лишь расстоянию глубины может снова создать хаотичные наложения.

Замени весь пункт 3 на:

Tie-breaker НЕ внедрять одновременно с удалением крышек.

Этап A:
- удалить крышки раскосов;
- отрендерить контрольные узлы;
- проверить результат.

Только если после этапа A останется доказанная ошибка порядка граней, переходить к этапу B.

Этап B:
tie-breaker разрешён только для граней:
- одной балки;
- одного и того же соединения jointId;
- фактически пересекающихся в проекции в области этого узла.

Запрещено использовать глобальный TIE_EPS=0.15.

Каждый узел получает стабильный jointId, например:
beam-2-left-lower-3
beam-2-right-lower-3
beam-2-top-2

Приоритет типа грани применяется только внутри одинакового jointId. Грани разных узлов, разных балок и удалённых участков никогда не переставляются этим правилом.

Если числовой допуск всё же необходим, начать с допуска порядка 1e-6 в единицах depthKey и повышать его только на основании измеренной разницы конкретных граней одного узла.
3. Один длинный пояс плохо совместим с локальной сортировкой

Сейчас каждый пояс — один bar3d длиной 4.6. Значит, его боковые грани представляют собой очень длинные треугольники, а depthKey вычисляется по их центроиду примерно в середине балки.

В районе торца такой длинный треугольник может пересекаться с коротким раскосом, но сортироваться по глубине своего далёкого центроида. Локальный jointId здесь не поможет: одна грань пояса одновременно проходит через много узлов.

Это очень вероятная причина визуальной грязи.

Нужно добавить:

Продольный пояс остаётся ОДНИМ непрерывным конструктивным элементом, но его render mesh необходимо разбить по узлам на несколько последовательно примыкающих участков.

Нижние пояса разделить в точках:
u = 0, pitch, 2*pitch, ..., length.

Верхний пояс разделить в точках:
u = 0, um_0, um_1, ..., um_last, length.

Правила:
- внутренние участки не имеют торцевых крышек;
- крышка существует только на первом внешнем конце первого участка и последнем внешнем конце последнего участка;
- соседние участки используют абсолютно одинаковую ориентацию восьмигранного сечения, чтобы на стыке не было поворота фасок;
- визуально пояс остаётся непрерывным;
- логическая модель одного пояса не меняется.

Цель разделения — получить короткие локальные боковые грани, глубина которых корректно сортируется около соответствующего узла, вместо одной длинной грани с центроидом в середине балки.

Это не возвращает «куски пояса» конструктивно. Это только правильная тесселяция для painter’s algorithm.

4. Проверка количества крышек сформулирована неоднозначно

В плане написано:

крышек поясов — ровно 6

Но если восьмиугольная крышка разбивается на восемь треугольников, в массиве будет не 6 граней, а:

3 пояса × 2 конца × 8 треугольников = 48 треугольников крышек

Нужно считать не полигоны, а группы крышек или уникальные capId.

Замени проверку:

Каждая крышка получает уникальный capId.

Для одной балки должно быть:
- 0 уникальных capId у раскосов;
- ровно 6 уникальных capId у продольных поясов:
  3 пояса × 2 свободных конца.

Количество треугольников крышек зависит от sides:
при sides=8 ожидается 6 × 8 = 48 cap-треугольников на балку, если каждая крышка строится веером из 8 треугольников.

Не путать число физических крышек с числом треугольных граней.
5. Сигнатура с двумя позиционными boolean опасна

Вот это:

bar3d(..., kind, capStart, capEnd)

легко вызвать как false, true вместо true, false. Лучше объект параметров:

bar3d(faces, pA, pB, radius, sides, beamId, kind, {
  capStart = true,
  capEnd = true,
  jointStartId = null,
  jointEndId = null
})

Для раскоса:

bar3d(..., {
  capStart: false,
  capEnd: false,
  jointStartId,
  jointEndId
})

Для сегментов пояса внутренние крышки задаются явно.

Готовый комментарий Claude

Отправь ему это:

План почти принят, но требует корректировки перед реализацией.

1. Удаление capStart/capEnd удаляет только крышку, но не торцевое кольцо боковых граней. У первого/последнего наклонного раскоса кольцо может выходить за U=0..length. «Субвизуальный перелёт» не допускается. Полная геометрия после исправления обязана удовлетворять minU >= -eps и maxU <= length+eps. При нарушении клиповать меш плоскостями пролёта либо использовать единообразные render-endpoints, утопленные внутрь объёма пояса. Логические узлы не менять.

2. Не вводить TIE_EPS=0.15. Это слишком большое значение: 5×rodRadius и около 16% pitch. Оно может переставить грани соседних узлов. Сначала реализовать только управление крышками и проверить рендер. Tie-breaker добавлять лишь при доказанной остаточной ошибке.

3. Если tie-breaker понадобится, он действует только для одинаковых beamId + jointId, а не для всех граней с близким depthKey. Начальный числовой допуск порядка 1e-6, а не 0.15.

4. Один bar3d на всю длину пояса создаёт длинные треугольники, сортируемые по центроиду в середине балки. Для корректной локальной сортировки разбить только render mesh поясов по точкам соединений:
- нижние пояса — по u_k;
- верхний пояс — по um_k.
Внутренних крышек нет, внешние крышки только на двух истинных концах. Ориентация восьмигранного сечения всех соседних участков идентична. Логически это по-прежнему один непрерывный пояс.

5. Считать крышки по уникальному capId:
- диагонали: 0 физических крышек;
- пояса: 6 физических крышек на балку.
При sides=8 это может быть 48 cap-треугольников, а не 6 граней.

6. Вместо позиционных capStart/capEnd использовать options object, чтобы не перепутать boolean:
bar3d(..., {
  capStart,
  capEnd,
  jointStartId,
  jointEndId
})

7. Реализацию проводить по воротам:
A. Диагностика.
B. Удаление крышек раскосов и проверочный рендер.
C. Разбиение render mesh длинных поясов и повторный рендер.
D. Только если всё ещё доказана ошибка сортировки — локальный jointId tie-breaker.

Не внедрять все три изменения одновременно: иначе невозможно определить, какое из них исправило или ухудшило результат.

После этих изменений план можно утверждать. В нынешнем варианте его главный риск — огромный глобальный TIE_EPS и надежда, что удаление дисков автоматически уберёт всю грязь узла. Это не гарантировано.