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


План стал почти рабочим, но я бы пока не нажимал approve. После предыдущих замечаний Claude исправил основные ошибки: отказался от сфер, убрал глобальный TIE_EPS=0.15, разделил работу на ворота и понял проблему длинных граней поясов.

Остались четыре важных момента.

1. Gate C не должен выполняться «всё равно»

Сейчас написано:

если после Gate B чисто — Gate C делать всё равно превентивно

Это противоречит самой идее ворот. Если снятие крышек полностью устранит дефект, сегментация поясов станет ненужным изменением и может породить новые швы.

Нужно заменить на:

Gate C выполняется только при одном из двух условий:

1. После Gate B на контрольных увеличениях остаётся визуально доказанная ошибка сортировки боковых граней пояса относительно раскосов.
2. Gate A показал, что длинная грань пояса с удалённым центроидом действительно является причиной неправильного порядка в конкретном узле.

Если после Gate B все контрольные узлы и оба торца визуально чистые, Gate C не выполнять. Зафиксировать, что минимальной правки оказалось достаточно.

Запрещено внедрять превентивные изменения только потому, что они теоретически могут улучшить сортировку.

Иначе Claude опять может усложнить уже исправленный результат.

2. Утопление торца нельзя привязывать к «четырём раскосам»

Формула геометрически осмысленная, но реализация вида «если это первый или последний раскос — сдвинуть» снова создаёт специальную логику для крайних элементов.

Нужна общая функция ограничения геометрии границами пролёта:

Не хардкодить k=0, k=panelCount-1 или список из четырёх раскосов.

Создать общий helper, например:

fitRodEndpointToBeamBounds({
  logicalPoint,
  rodDirection,
  rodRadius,
  uMin: 0,
  uMax: length,
  ringBasis
})

Функция применяется к любому концу любого стержня, логический узел которого находится на истинной границе U=0 или U=length.

Она вычисляет renderPoint из фактической геометрии кольца, а не из номера панели.

Ещё лучше считать сдвиг по реальным восьми вершинам кольца, а не по окружности:

Сначала построить фактические unit-векторы basis1/basis2, используемые bar3d.

Для каждого из sides=8 направлений кольца вычислить его U-offset.
Найти реальный minRingOffsetU/maxRingOffsetU.

Render-сдвиг выбирать минимальный, при котором фактические вершины восьмигранного кольца удовлетворяют U∈[0,length].

Не использовать коэффициент 1.05 как замену проверки. Допускается только небольшой числовой eps после точного расчёта по вершинам.

Текущая формула по описанной окружности безопасна, но она названа «аналитически точной», хотя содержит эвристический множитель 1.05.

3. Нельзя ослаблять старый ассерт совпадения узлов

После утопления физическая точка bar3d уже не совпадает с leftBottom(0). Поэтому нужно явно разделить:

логическую топологию;
отрисовываемую геометрию.

Добавить:

Для каждого раскоса хранить отдельно:

logicalStart / logicalEnd
renderStart / renderEnd

Ассерты топологии продолжают проверять:
logicalStart/logicalEnd == leftBottom/rightBottom/top с допуском eps.

Ассерты геометрии проверяют:
- renderStart/renderEnd получены только из logicalStart/logicalEnd общим boundary-helper;
- полная геометрия находится в U∈[0,length];
- для внутренних узлов renderPoint == logicalPoint;
- для граничного узла renderPoint лежит внутри объёма соответствующего пояса либо геометрия клипована плоскостью границы.

Иначе Claude может просто изменить старый ассерт так, чтобы он перестал ловить ошибку.

Также нужно проверить не только отсутствие выхода за пролёт, но и отсутствие щели:

Если используется утопленный renderPoint, открытое торцевое кольцо раскоса должно быть полностью скрыто объёмом нижнего пояса в текущей камере и не создавать видимого отверстия.

Если кольцо не скрывается полностью, утопление отклоняется и применяется геометрическое клиппирование.
4. Gate D всё ещё опасен из-за компаратора с epsilon

Даже 1e-6 внутри Array.sort() может дать нетранзитивный порядок:

A близка к B;
B близка к C;
A уже не близка к C.

В результате сортировка иногда становится нестабильной.

Замени Gate D на:

Не внедрять условный epsilon-компаратор, способный нарушить транзитивность Array.sort.

Если Gate D понадобится:

1. Сначала выполнить обычную глобальную стабильную сортировку far→near по depthKey.
2. После неё выполнить отдельный локальный reorder только для заранее сформированных групп:
   beamId + jointId.
3. Локальный reorder не может перемещать грань за пределы диапазона DOM-элементов этого конкретного узла.
4. Приоритет применяется детерминированно:
   diagonal side faces → chord side faces → настоящие свободные caps.
5. Грани разных jointId никогда не сравниваются по типовому приоритету.

Либо использовать полностью детерминированный составной ключ, не зависящий от попарного epsilon-сравнения.

Но, скорее всего, после удаления крышек и корректной сегментации до Gate D вообще не дойдёт.

Полезная численная проверка Gate C

При sides=8 можно заранее проверить ожидаемое изменение числа треугольников.

Если текущая реализация действительно создаёт:

16 боковых треугольников на один bar3d;
6 треугольников на каждую крышку;

то удаление крышек у 20 раскосов уберёт:

20 × 12 = 240 треугольников на балку

Сегментация трёх поясов добавит примерно 208 боковых треугольников относительно прежних цельных поясов. Итог должен быть примерно:

−32 треугольника на балку

Это не обязательное абсолютное число, если фактическая реализация отличается, но хорошая проверка: после B+C число граней не должно неожиданно вырасти.

Готовый комментарий для Claude
План близок к утверждению, но нужны четыре корректировки.

1. Gate C не выполнять превентивно. Если Gate B полностью устранил дефект на общем виде, обоих торцах и внутренних узлах, остановиться после B. Gate C разрешён только при визуально или инструментально доказанной остаточной ошибке длинных граней пояса.

2. Не хардкодить «4 краевых раскоса». Реализовать общий fitRodEndpointToBeamBounds для любого конца стержня, логический узел которого находится на U=0 или U=length. Сдвиг renderPoint вычислять по ФАКТИЧЕСКИМ восьми вершинам кольца, построенным тем же stableBasis, а не по номеру панели и не только по описанной окружности. Коэффициент 1.05 не называть точным решением; использовать минимальный расчётный сдвиг + числовой eps.

3. Разделить logicalStart/logicalEnd и renderStart/renderEnd. Старый ассерт совпадения раскосов с leftBottom/rightBottom/top сохраняется для логических точек и не ослабляется. Отдельно проверять границы полной render-геометрии. Если утопленное открытое кольцо не полностью скрыто поясом и создаёт щель, утопление отклоняется и применяется настоящее клиппирование плоскостью U=0/U=length.

4. В Gate D не использовать попарный epsilon-компаратор Array.sort, даже с 1e-6: он может быть нетранзитивным. Сначала обычная стабильная depth-сортировка, затем отдельный локальный reorder внутри заранее сформированной группы beamId+jointId, либо детерминированный составной ключ. Грани разных узлов локальным приоритетом не переставлять.

Дополнительно: при сегментации поясов оттенок боковой грани должен определяться её нормалью и фиксированным светом, а не номером сегмента или его центроидом. На стыках не должно появиться чередования оттенков или видимых полос.

После этих изменений план можно реализовывать по Gates A→B, затем C только при необходимости, D только при доказанной остаточной проблеме.

После этих правок план уже можно утверждать. Главная оставшаяся опасность сейчас — Claude заранее решил сделать Gate C даже при успешном Gate B и может снова внести лишнюю сложность вместо минимального исправления.