Skip to main content

blockwright-measure

Этап 2 Blockwright — обмер плана: по векторному плану (vectorplan.read), по карте layout.png, по 3D-модели или захвату (model.read), либо по объявленной таблице (declared.layout). Используй, когда нужно получить раму здания (угол, начало, протяжённость), разложить план на части, найти участок внутри дорожного кольца, или когда что-то не так с Frame.fit_mask, plan.decompose, model.read, declared.layout, flatmap.parcel, flatmap.guides.

Source facts

Repository
MelonHell/VibeBuilding
Last source activity
August 17, 2026 at 16:54
Detected SKILL.md language
Russian
Stars
1
Forks
1

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
blockwright-measure
description
Этап 2 Blockwright — обмер плана: по векторному плану (vectorplan.read), по карте layout.png, по 3D-модели или захвату (model.read), либо по объявленной таблице (declared.layout). Используй, когда нужно получить раму здания (угол, начало, протяжённость), разложить план на части, найти участок внутри дорожного кольца, или когда что-то не так с Frame.fit_mask, plan.decompose, model.read, declared.layout, flatmap.parcel, flatmap.guides.
# Этап 2 — обмер плана На вопрос плана отвечает сильнейший из положенных входов, и от этого зависит, кто **рисует** план — какую из четырёх читалок вызвать. Нарисованные части остаются; эталон добавляет те, которых рисунок не держал. После сборки — те же именованные части в тех же `(u, v)`, тот же `build.py`, тот же гейт. | кто ответил | чем читается | |---|---| | векторный план | `vectorplan.read` — полигон уже контур | | карта `layout.png` | `plan.decompose` — по нарисованным линиям | | 3D-модель или захват | `model.read` — по ступеням крыши | | описание, эскиз, чертёж | `declared.layout` — таблица с указанием источника | Кто именно отвечает при ничьей — не «кого объявили позже», а явный список `sources.PREFER`, по строке на вопрос: на план вектор бьёт модель, модель бьёт карту, карта бьёт чертёж; на высоты модель бьёт чертёж, чертёж бьёт захват. `probes/derive.py` выбирает, кто рисует, через `sources.survey`, собирает план в `Survey.assemble` и печатает и ветку, и таблицу покрытия. Скрипт сборки берёт готовое через `derive.plan_of()` и не знает, откуда оно взялось, — иначе два файла однажды прочтут план по-разному. ## Ветка вектора `vectorplan.read` растеризует полигоны GeoJSON или пути SVG на метровую сетку по правилу «центр ячейки внутри» — то же правило, что у `Frame.region`, поэтому вектор и фигура, начерченная в (u, v), ложатся в одни клетки. Долгота и широта распознаются и проецируются локально; проецированный экспорт уже в метрах; у SVG единиц нет, и `VECTOR_SCALE` заявляется как `MODEL_SCALE` у OBJ. Кривые не аппроксимируются — путь с безье экспортируют с флаттеном, иначе углы здания придумает читалка. Части приезжают именованными: одна фигура на часть, `name` из properties — имя части. Единственная ветка, которой не нужен `STRIPS`. ## Ветка карты **Сначала палитра.** Какие оттенки серого на этой карте означают здание, линию и дорогу, заявляет здание в `MAP_PALETTE`; по умолчанию стоит палитра одной конкретной карты. Кроп из другой игры, экспорт OSM, скан чёрным по белому — своя. ```sh python -m tools.map_probe buildings/<имя>/input/layout.png ``` печатает гистограмму серых и предлагает пороги. Если палитра не подходит, `footprint` останавливается и показывает ту же гистограмму — он не возвращает пустую маску и не находит «что-нибудь». Карта измеряется **один раз** — в раму и набор частей — и после этого откладывается. Геометрия чертится потом, чистыми фигурами по измеренным числам. Причина: карта — рисунок, а не съёмка. Её рёбра гуляют на метр-другой, внутренние линии проведены от руки. Трассировка по клеткам наследует каждую дрожь, и после трассировки уже никакой шаг не отличит намеренную ступеньку в плане от нетвёрдой руки. ## Рама `Frame.fit_mask` вписывает **минимальный по площади** ориентированный прямоугольник в пятно карты и возвращает раму: начало, угол и локальные координаты `(u, v)` — вдоль здания и поперёк. **Минимальный прямоугольник, а не PCA.** PCA взвешивает по массе, поэтому длинное крыло с тяжёлым отростком на конце утаскивает ось. На реальном примере PCA дал 57.2° против истинных 52.48° — пять градусов, а это на здании длиной 165 м четырнадцать метров ошибки на дальнем конце. Минимальный прямоугольник получается вращающимися штангенциркулями по выпуклой оболочке и точен. **Дальше рама садится на решётку.** `Survey.on_a_lattice` поворачивает её на ближайший наклон `p:q`, у которого мотив лестницы не длиннее трёх пробегов, — на одной точке для всех четырёх веток, потому что `build.py` и `gate.py` тоже зовут `plan_of`, и рама, повернувшаяся в обмере и не повернувшаяся в сборке, — это два здания. Части перемеряются в новой раме; **маски, которые нарисовала карта, не двигаются** — поворачивается чертёжная доска, а не чертёж. Цель — кратчайший период, ограничение — цена в метрах на торцах. Здание объявляет `LATTICE` («auto» или пару), `LATTICE_BUDGET`, `LATTICE_PITCH`, а `UNLATTICED` — причину, по которой решётки нет вовсе. Цена печатается на каждом прогоне и лежит в `derived["frame"]["slope"]`. Рама эталона поворачивается на **ту же дельту** (`survey.with_the_plan`): обе стороны регистрации обязаны мериться вдоль одних направлений, иначе разница вылезает как расхождение масштабов по осям и прогон останавливается сообщением про подгонку, которая ни при чём. ## Части `flatmap.footprint` сливает нарисованные линии с массой и возвращает одну кляксу. Это правильный вход для вопроса «где стоит здание и куда смотрит» и неправильный для всего дальнейшего: у кляксы нет комнат, поэтому постройка выйдет одним выдавленным контуром, а части, которые картограф нарисовал, потеряются раньше, чем будет положен первый блок. `plan.decompose` вместо этого **вычитает слой линий из массы**. Остаются замкнутые области, каждая со своей маской, своей протяжённостью в `(u, v)` и своей классификацией. **Классификация — вписанной окружностью, а не изопериметрическим отношением `4πA/P²`.** Обход контура засчитывает один шаг за диагональ и потому недомеряет периметр фигуры, нарисованной под 52°, из-за чего отношение объявляет прямоугольник довольно круглым. Подгонка окружности не зависит от масштаба и сама сообщает свою невязку — вот это число и стоит доверия. Именно это позволяет скрипту постройки говорить «круглая» и «длинная» вместо координат, которые человек списал с картинки. ## Ветка модели и захвата `model.read` берёт всё, что стоит выше двух метров над землёй, роняет в сетку метр на метр, берёт высшую точку в каждой ячейке и заливает внутренние дыры — модель это поверхность, внутри контура у неё вершин нет. Двор, открытый небу, тоже зальётся, и так и надо: план здания с двором включает двор, а что внутри него происходит — решение сборки, а не потеря на обмере. **Части берутся со ступеней крыши.** Высоты кластеризуются по разрывам, не по корзинам: крылья на 11.8 и 12.1 — одна площадка при любой сетке корзин, а на 11.9 и 15.9 — две, хотя четырёхметровая корзина сложила бы их вместе. Каждая площадка бьётся на связные области, мелкие остатки раздаются ближайшей части, чтобы в плане не осталось дыр. **Берутся все куски, а не самый большой.** Здание с отдельным крылом через двор — это два куска одного здания. Опора — на то, что файл клипнут по этому зданию; сосед, оставшийся в клипе, приедет ещё одним куском, и его поймает проверка регистрации в гейте. **Масштаб заявляется, а не угадывается.** OBJ не несёт единиц, и `model.read` останавливается на абсурдном габарите вместо того, чтобы домножить на подходящее число. Один угаданный коэффициент проходит через каждый размер и ничем потом не ловится: постройка будет идеально согласована сама с собой, только не того размера. Ось «вверх» — там же, `MODEL_UP`: половина экспортёров пишет Z-up, и в файле об этом ничего не сказано. Модель авторитетна для формы, захват — нет. В геометрии эта разница не видна, её помнит `sources.py`. Тянуть кривую из захвата и называть её ротондой — ровно та ошибка, ради предотвращения которой построен весь пайплайн. ## Ветка объявлений `declared.layout` растеризует таблицу `DECLARED_PLAN` — прямоугольники и круги, которые человек напечатал, прочитав бриф или лист. У каждой фигуры обязателен `source`: без него функция останавливается, а не предупреждает. Объявленное число без источника нельзя ни перечитать, ни оспорить, и оно неотличимо от того, которое кто-то неверно вспомнил. Угол по умолчанию нулевой. Объявленному плану не в чем стоять: уличной сетки нет, а рисование по осям убирает растеризационную лестницу из постройки, у которой размеры и так самое мягкое место. Угол задаётся, только если его что-то фиксирует. ## Сборка с эталона Нарисованный источник оставляет каждую часть, которую нарисовал. Эталон добавляет те, которых там нет. Сопоставление — IoU на пороге `MATCH_FLOOR`. Умолчание 0.30 нарочно свободное: контур карты и силуэт захвата одного здания обычно сидят на 0.5–0.7, и всё, что делит меньше трети площади со всем нарисованным, — другое здание. Отпечаток эталона пересэмплируется **назад** на сетку плана через ту же регистрацию, которой потом читают скайлайн: для каждой клетки плана — что эталон держит там. Вперёд «кляксой» на фикстуре дырявил седьмую часть пятна узором, который никто не прочтёт иначе как шум. `PART_LEAST_AREA` считает клетки этой сетки, после пересэмплирования. Одна клетка — один метр. Сборка отказывается, а не угадывает. Часть, от которой на холсте меньше половины клеток, не принимают: постройка перерисовала бы обрезок как целое здание. Если профиль ширины не отличается от своего переворота и рамы стоят дальше двух градусов, прогон останавливается: иначе добавленная часть сядет не на тот конец, а все строки ниже согласятся. Мелочь участка отсекается `PART_LEAST_AREA` / `PART_LEAST_HEIGHT`, и число отброшенного печатается. Первый прогон называет добавленное `capture-1`; `CAPTURE_PARTS` заполняют после, как `STRIPS`. ## Участок `flatmap.parcel` возвращает землю внутри дорожного кольца. Работает только с картой: у модели и у описания нарисованной границы нет. Это и есть настоящая единица работы: здание — один объект, стоящий на ней, а не вся задача. **Порога серого, отделяющего дорогу от здания, не существует.** Нарисованная дорога тёмно-серая с широкой сглаженной каймой, идущей от дорожных 82 до земляных 217 через все промежуточные значения — включая 176, то есть собственную заливку здания. При любом пороге кайма сливает дороги и здания в одну кляксу, а заливка, посеянная внутри, уходит за пределы картинки. Однозначно только **ядро дороги**: ахроматичное и темнее всего остального на карте. Оно тоньше, чем дорога выглядит, и это ничего не стоит — барьеру нужно быть замкнутым, а не широким. **Заливка идёт наружу от здания**, а не внутрь от границы, поэтому ответ — «земля, на которой стоит это здание», а не «какая область оказалась больше». Два действия, которые легко пропустить: - **Сообщать о побеге.** Если заливка дошла до края кропа, кроп мал и участок молча обрезан. `parcel` бросает исключение. Вернуть половину карты было бы хуже, чем остановиться. - **Не вписывать в него раму.** Участок ограничен кривыми дорогами. Ориентированный прямоугольник его не описывает, и это не нужно: это маска в тех же мировых координатах, что и всё остальное. Дальше цветовой оттенок делит участок на воду, траву и голую землю. Это разбиение **всего** участка, включая пятно здания, — то, что под зданием, это земля. Вызывающему, которому нужна только открытая земля, следует вычесть план самому, а не поручать это угадывать. ## Направляющие `flatmap.guides` возвращает нарисованные внутренние линии. `decompose` их уже использовал — части это то, что остаётся после их вычитания, — поэтому в `derived.json` они записываются как подтверждение, а не как новость, и ради одного случая: картограф провёл деление, которое разложение потом не смогло замкнуть в часть. ## Что дальше С рамой и частями на руках план меряется в `(u, v)`: где пятно уступает, какой ширины каждый отрезок, где идут нарисованные направляющие. Это горсть чисел — и строится по ним, а не по растру.
View on GitHub