Skip to main content

blockwright-draw

Этапы 4 и 5 Blockwright — чертёж здания в build.py. Используй при написании или правке рецепта постройки: маски, Frame.region, эрозия и дилатация, Canvas, Facade, оконные проёмы, лестничное правило 1.41, словарь блоков, плиты и панели.

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-draw
description
Этапы 4 и 5 Blockwright — чертёж здания в build.py. Используй при написании или правке рецепта постройки: маски, Frame.region, эрозия и дилатация, Canvas, Facade, оконные проёмы, лестничное правило 1.41, словарь блоков, плиты и панели.
# Чертёж — этапы 4 и 5 Каждый объём пишется неравенствами в `(u, v)` и растеризуется `Frame.region`, который спрашивает предикат про центр каждой мировой клетки. Фигура, записанная как `0 <= u < 105 and 4 <= v < 13`, выходит маской под углом здания, и ступенька падает туда, куда её кладёт геометрия. **Угол обрабатывается ровно один раз — в растеризаторе.** Ни один примитив ничего не поворачивает. Это касается и деталей: лестница или панель кладутся на ту же машинерию масок, что и стена, и угол не выводится заново. Секция, которая считает свои синус и косинус, — это секция, которая разойдётся с остальными на полградуса после того, как карту перерисуют. ## Всё — это двумерная маска, поднятая через диапазон высот | Приём | Операция | |---|---| | кольцо стены | пятно минус его эрозия | | балкон за плитой | `erode(вынос)` | | верхний объём с отступом | `erode(расстояние)` | | выступающий карниз | `dilate(вынос).outline(...)` | | двор | план минус крылья | | терраса на участке | `dilate(d1) - dilate(d0)` | ## Две детали, каждая ценой в пересборку **Смещения обязаны быть евклидовыми.** Отступ в 3 должен означать три метра в любом направлении. Чебышёвское или chamfer-смещение промахивается на 40 % вдоль фасада под 52°. **Однослойное кольцо не герметично под углом.** Чтобы удержать воду вдоль стены под углом θ, нужно `|cos θ| + |sin θ| ≈ 1.41` блока. Во внутреннем углу каждой ступеньки ближайшая свободная клетка — диагональная, поэтому метрическая полоса в один блок этот угол пропускает — около пятой части стены. `Mask.outline` объединяет полосу с `rim()`, множеством клеток, у которых свободен любой из восьми соседей, и тем закрывает их по построению. ## Лестничное правило Те же 1.41 задают нижнюю границу толщины элемента. Ферма шириной в блок, проведённая вдоль оси здания, растеризуется в пунктирную лестницу. **1.5 м — самый узкий непрерывный элемент** при таком угле. Число измеряется, а не вписывается: `derived["frame"]["staircase"]`. Перед тем как чертить что-то узкое, сверься с ним и **останови сборку**, а не рисуй: ```python if width < site.staircase: raise SystemExit( f"элемент шириной {width:.1f} м уже, чем ступенька рамы " f"{site.staircase:.2f} м; он растеризуется в клетки, соединённые " "только углами") ``` ## Решётка: чем стена отличается от вектора Рама сидит на наклоне `p:q`, у которого мотив — не длиннее трёх пробегов (`derived["frame"]["slope"]`). Из этого следуют две вещи, и обе надо держать в голове, когда чертишь. **Лестница периодична.** Пробеги идут `2,1,1 | 2,1,1 | 2,1,1`, а не как попало. Это и есть разница между «нарисованной линией» и «вокселизованным вектором», и она бесплатна: цена снапа — доли градуса, измеренные и напечатанные. **Повтор возможен.** `site.period` — метры от клетки мотива до той же клетки следующего; `site.periods(метры)` — во сколько периодов уложился измеренный питч. Шаг секции обязан быть целым числом периодов, иначе копия не копия. Измеренные 20.6 м становятся 20.0 м = 4 периода по 5.0 — это решение, и печатать его надо рядом с тем, что измерено. Круг рисуется по мировой решётке, а не по раме (`Mask.circle`): у круга нет угла, а есть симметрия, и рама её ломает. Грани, идущие не по осям рамы — скошенный торец, разведённое крыло, — садятся каждая на свою дробь в `Site.faceted`. ## Декларируй то, что положил `sched.declare(name, mask, y0, y1)` берёт маску **того, куда легли блоки**, а не входной контур. Объявишь перекрытие на весь контур — его удержит кольцо стены, стоящее в тех же клетках, и в день, когда плиты перестанут класться, гейт этого не заметит. Плита — `foot.erode(THICK)`, стена — `foot.outline(THICK)`, проёмы — `build.openings(foot, face, period, width, THICK)`. И назови материал в манифесте: `Item(..., blocks=GLASS)`. Без этого объявление удовлетворяется чем угодно не-воздушным в своей маске — например, той самой стеной, в которую прорезан проём. ## Полблока припуска — только для плана с карты `site.footprint(name)` открывает часть на `site.pad` во все стороны, и это число зависит от того, чем план измерен. Карта рисует стену линией, а `plan.decompose` эту линию вычитает — значит, часть с карты кончается на полблока внутри настоящей грани, и припуск возвращает её на место. Часть, снятая с модели или с захвата, **уже** является внешней гранью, и повторный припуск строит здание на полметра больше с каждой стороны. В самой постройке этого не видно, зато слышно на гейте: абсолютный припуск раздувает узкую ось в большей доле, чем длинную, поэтому регистрация возвращается с разъехавшимися масштабами по u и v. Шаблон выбирает припуск сам, по `derived["plan"]["by"]`; трогать его руками нужно только если знаешь, почему. ## Пролёты — по длине дуги, а не по u Постоянный шаг по `u` покрывает разную длину стены везде, где фасад не прямой, поэтому оконные пролёты уходят из ритма на повороте. `Facade` обходит границу пятна и даёт каждой клетке стены её расстояние вдоль стены. Обведённый контур сглаживается перед измерением: цифровая лестница завышает собственную длину примерно на 5 %, а это восемь метров накопленной ошибки фазы на здании такой длины. ## Полое, а не сплошное Заполненный объём — это объёмный этюд: он проходит сечение, читается в каждом рендере глыбой камня и в него нельзя войти. `build.walls` даёт кольцо стены, герметичное по построению; `build.storeys` повторяет его с плитой на каждом уровне. Плиты перекрытий по умолчанию заводятся за стену. Если пустить их до края пятна, на фасаде появится полоса материала перекрытия на каждом уровне, и при метре на блок между полосами не остаётся сплошной стены — здание читается стопкой плит. ## Ровность: чем жертвовать и чем нет Пара градусов расхождения с референсом ничего не стоит. Пила в блок стоит всей кромки — это первое, что видит человек рядом с фотографией, и **ни одна другая строка гейта её не видит**: зубчатая кромка меряется так же на каждой станции, охватывает ту же площадь и даёт тот же силуэт, что прямая. Пять правил. Каждое — про то, что здание должно быть ровнее эталона, а не точнее его. **Прямая линия — одна линия.** Стена, отклоняющаяся от прямой в раме меньше чем на блок, выпрямляется. Карта рисована от руки, её кромка гуляет на клетку-две, и это дрожь руки, а не форма. Пять идиом, и последняя — та, которая нужна чаще всего: | | что делает | когда | |---|---|---| | `site.footprint(name)` | маска как нарисована | форма плана несёт измерение: скос, изгиб, диагональный ряд | | `site.box(name)` | прямоугольник по габариту | часть действительно прямоугольная, а кромка — шум | | `site.squared(name)` | прямоугольник по робастным квантилям | то же, но кромку рвут выбросы, и габарит по ним врёт | | `site.faceted(name)` | многоугольник из фитованных прямых | у части есть скошенная или разведённая грань: её направление — измерение, и брать его надо линией, а не парой крайних точек. Углы выходят пересечениями граней и прямые по построению; при меньше чем трёх гранях отказывает | | `STRAIGHT = 1.0` на `Site` | углы сохраняются, дрожь убирается | **почти всегда**. Контур читается в раме, упрощается Дугласом–Пекером, растеризуется заново | Скос в 8° переживает выпрямление при любом допуске: скос — это угол, а не дрожь. `checks.jaggedness` печатает и долю дрожи, и **из скольких прямых отрезков состоит контур** — прямоугольник, нарисованный ровно, приходит пятью. Пятьдесят три — это пятьдесят три, и никакой допуск не сделает из этого прямоугольник. **Горизонталь — одна отметка**, пока обмер не докажет ступень. Плоская крыша, нарезанная по станциям, превращает каждое качание фотограмметрии через границу блока в ступень; `checks.level_runs` считает, сколько разных верхних отметок у того, что обязано быть одной. **Углы — 0 или 90 к раме, или дуга.** Промежуточный угол *внутри* одной части почти всегда шум обмера. Угол здания живёт в раме и обрабатывается ровно один раз — в растеризаторе. **Симметричная часть строится из половины.** См. ниже. **Повторяющийся элемент ставится один раз и копируется.** Тоже ниже. Что гейт спрашивает: `<часть> is drawn straight` (бюджет `JAGGED`, по умолчанию 15% и его стоит опускать на прошедшую сборку), `<часть> is level` (`LEVEL`), `<a> mirrors <b>` (`TWINS`). ## То, что повторяется, — рисуется один раз Здание, у которого две одинаковые башни, восемь одинаковых вилл или семь одинаковых этажей, **не должно выводить каждую копию из чисел заново**. Числа шумят, и повторяющееся совпадает только пока совпадают числа. Это не теория. У одного отеля две башни-близнеца мерились по отдельности, захват моделирует одну хуже другой, и в постройку уехала разница: пять ступеней кровли против девяти, расхождение до 5 м там, где у настоящих крыш один уровень. Все проверки при этом зелёные — две башни одного пятна дают один силуэт и один профиль, а сечение судит каждую по своей половине эталона. Три примитива, каждый на своё: **`site.flipped(axis_u)`** — тот же участок, но всё рисуется зеркально по u. Напиши секцию один раз, вызови дважды. Копии совпадают по построению, а не по согласию двух наборов чисел. **`canvas.repeat(y0, y1, step, count, mask)`** — скопировать курсы вверх. Этаж чертится один раз и штампуется; цикл, перерисовывающий каждый этаж из тех же чисел, одинаков ровно до первого уровня, который округлится иначе. Шаг у штампа один, и это обязано остаться так. Когда `count × step` не сходится с измеренной высотой, у многоэтажки меняют `count`, а не режут отдельные этажи до двух блоков или одного: этажом выше или ниже — никто не заметит, этаж в один блок посреди фасада — первое, что видно на рендере, и ни одна строка гейта его не ловит. Подробности и граница, ниже которой правило не работает, — в этапе 3. **`site.stamp(canvas, motif, y0, y1, periods, count)`** — скопировать секцию вдоль здания. Это горизонтальный аналог `repeat`, и работает он потому, что рама сидит на решёточном наклоне: собственная ось здания там — целый вектор, поэтому сдвиг на `periods × period` переносит блоки клетка-в-клетку. Копируется и воздух внутри мотива — прорезанное в первой секции окно есть часть того, чего вторая копия является копией, — поэтому `[y0, y1)` обязан быть тем объёмом, который мотив реально занимает. Шаг считается, а не печатается: `site.periods(20.6)` переводит измеренный питч в целое число периодов, `site.pitch(20.6)` говорит, во что он при этом округлился, `site.step(n)` даёт вектор. Проёмы — `site.openings(...)`, то же самое для маски: одна фигура, отштампованная вдоль стены, вместо `Facade.bays`, который разносит проёмы по длине дуги и сажает каждый в свою субпиксельную фазу. `Facade.bays` остаётся правильным на кривой стене, где никакого переносящего вектора не существует вовсе. **`canvas.place(motif, y0, y1, corner, turn, mirror)`** — та же копия, но куда угодно и с поворотом. Для здания, которого несколько: двор одинаковых коттеджей, пара павильонов, сторожка в обоих концах подъезда. Повороты только кратные 90° и зеркала только по осям мира — это вся группа, сохраняющая решётку. Состояние блока поворачивается вместе с геометрией (`blocks.turned`), иначе у копии лестницы смотрят так же, как у оригинала, и это дефект, которого не читает ни одна строка: все блоки на месте, все нужного рода, все в нужных клетках. **`measure.twins({имя: профиль})`** — сказать, насколько две копии разошлись в обмере. Он **не решает**: усреднить чистое чтение с шумным — это третье чтение, не совпадающее ни с одним, и сечение завалит прогоны станций уже на обеих копиях. Расхождение означает, что одну копию эталон моделирует хуже, и это предложение в отчёт, а не число для усреднения. **Зеркалить под произвольным углом надо предикат, а не воксели.** Рама стоит под углом к сетке мира; отражение вокселей — это ресемплинг, потому что центры клеток не ложатся в центры клеток. Прямой сплэт даёт дырки (а дырка под блоком — это висящий блок, бюджет `FLOATING_BLOCKS = 0`), обратная выборка перерастеризует каждую кромку поверх уже запечённой лестницы, и ни то ни другое не переживает `finalize`: состояния `north/east/south/west` привязаны к мировым сторонам. Без потерь только три вещи: сдвиг на целый вектор, поворот кратный 90° и зеркало по оси мира. Всё остальное — предикатом. Гейт спрашивает про это тремя строками. `evenness` — таблицы `TWINS` и `LEVEL` про зеркальные пары. `<часть> repeats` — про повтор сдвигом: заявляется `Item(..., copies=N)` в манифесте и `sched.declare_repeat(...)` рядом со штампом, и гейт сверяет блоки копий побайтово. Пустые обе — `ungraded`, а не молчание, ровно как пустой `COUNTS`; закрываются `SINGULAR` и `UNREPEATED` с причиной. ## Здание имеет право быть двумя разными зданиями Самый дорогой класс дефекта на сегодня, и он невидим целиком. Здание построили с лоджиями по всем трём крыльям — у правого их нет. Надстройку на кровле построили одной глухой коробкой — там коробка и открытая рама с кондиционерами. Все строки гейта были зелёные, и не по снисходительности: | проверка | почему молчит | |---|---| | сечение | судит скайлайн, у фасада скайлайна нет | | силуэт, `COUNTS` | три крыла одного пятна дают один силуэт | | аудит манифеста | лоджии объявлены и стоят — в тех клетках, в том блоке | | `level` | склеенная коробка даёт на отметку **меньше**, а не больше | | `TWINS` | спрашивает обратное: «ты сказал, что одинаковы — правда?» | **`Facade(foot)` обводит весь контур, и `openings` режет по всей его длине одним шагом.** Ветвления по граням нет. Ограничить можно пересечением — `& site.slice_of(...)` или `& site.rect(...)`, — но об этом надо додуматься, а обмер не подсказывал. **Мерь ритм по пролётам, а не по четырём орто.** Четыре орто — это север-юг-восток-запад **захвата**; крылья идут вдоль u, и ни одно из четырёх чтений не соответствует одному крылу. ```python found = measure.rhythm_by_span(image, scale, spans, *BAY_RANGE) groups = measure.apart([r["value"] for r in found]) # индексы, а не значения ``` `apart` возвращает индексы, потому что сказать надо, *какое* крыло разошлось. **Меньшинство, которое не согласно, — это находка.** `texture bay: 11.00 m, 2 of 4 elevations agree` при чтениях 10.99 / 8.48 / 14.50 / 11.01 — это не слабое измерение одного ритма, а твёрдое измерение двух. Строка теперь называет несогласных поимённо; прочитать её как «два согласны, берём 11.0» — это и есть тот дефект. **Строка гейта `facades`.** Пустая таблица — `ungraded`, как `COUNTS` и `TWINS`: ```python FACADES = {"west wing": (1.7, 26.4), "east wing": (81.1, 110.5)} FACADES_DIFFER = (("east wing", "west wing"),) # либо UNIFORM = True # эталон правда рисует один фасад по кругу — но сходи посмотри ``` Разница считается по **плоскости** стены вместе с воздухом. Именно вместе: лоджия — это дыра, а доли одних оставшихся блоков у прорезанного крыла и у глухого почти совпадают, потому что вырезанное ушло и из числителя, и из знаменателя. **На крышу ставится не только коробка.** Ряд кондиционеров стоит на стальной этажерке: под ней видно, между аппаратами видно небо, силуэт — гребёнка, а не крышка. `build.rack(canvas, foot, posts, base, top, post, deck=..., units=...)`; стойки — `site.grid(foot, pitch)`, потому что сетка должна лежать в раме здания, а не в мировой. ## Словарь блоков При масштабе 1:1 полный куб — это метр, а очень много архитектуры мельче: балюстрада, карниз, импост, ступень. Некубические блоки — способ это построить, и правило, которое делает их безопасными, стоит проговорить: **Некубичность — свойство записи, а не геометрии.** Маски остаются булевыми. `Frame.region` остаётся единственным местом, где живёт угол. Морфология не трогается. Меняется то, какую строку блока получает клетка, а это свойство клетки, а не фигуры. Отсюда три следствия: 1. **Позиционная запись.** `Canvas.fill` берёт одну строку блока на всю маску. То, у чего состояние меняется от клетки к клетке — ступени лицом наружу, панели, соединяющиеся с соседями, — требует `Canvas.set` и функции, считающей строку на клетку. 2. **Направление, по которому меняться.** Ступени нужно знать, где наружу. Три источника этого в библиотеке уже есть, и все три ответ выбрасывают: `Mask.rim` находит свободного соседа и теряет, какого именно; `Facade` обводит контур и оставляет от него только длину дуги; `Mask.distance_field` держит нормаль в своём градиенте. 3. **Состояния соединения.** WorldEdit вставляет схематику без обновлений блоков, поэтому панель, забор или стена, записанные состоянием по умолчанию, приезжают столбиком, а не полотном. Это вообще не выражается поклеточно — зависит от соседей — и требует одного прохода по готовому холсту перед записью. `canvas.finalize()` в `finish` делает его первым и ровно один раз. Два мелких правила, которые уже стоили денег: - **Листве нужен `persistent=true`.** Без него Minecraft разрушает всякий лист дальше шести блоков от бревна, то есть большую часть кроны пальмы, и посадки тихо исчезают через пару минут после вставки. - **Половинные высоты живут в `type=top|bottom`.** Квантование измеренной плиты на 1.25 м к y = 1 выбрасывает полметра, которые состояние могло бы нести. Ось y остаётся целой: плита на 1.25 м — это нижняя плита в клетке 1, а не дробный y. ## Палитра Каждая строка должна быть в `blocks.COLORS` или производной от неё, иначе рендер выйдет пурпурным — и это проверяется, а не предполагается: `checks.palette` одна из строк гейта. **Сначала совпадает светлота, потом тон.** Блок на сотню уровней темнее читается другим материалом, даже когда цвет номинально верен, и фото-ревью это назовёт; блок правильной светлоты с потерянным оттенком читается тем же материалом при другом освещении, и этого никто не замечает. ## Горизонталь шумит, вертикаль чистая Единственный приём в этой библиотеке, **замеренный** на готовом городе, а не кем-то посоветованный. На чужой карте Лос-Сантоса три пятых всей текстуры плоских кровель — одна пара блоков в двадцати единицах RGB друг от друга, положенная честной монеткой 50/50: не шахматка, не полосы, не пятна. Фасады при этом не шумят вовсе — 16.6 % против 50 %. Та же пара статистик по нашим шести постройкам: фасад 0.10–0.22 (в такте), горизонталь **0.015–0.067** — в десять-тридцать раз площе. Каждая палуба, терраса и мощение у нас — один печатный прямоугольник одного блока. ```python DECK = "minecraft:light_gray_concrete" DECK_MIX = "minecraft:polished_andesite" # на 20.5 RGB в стороне build.dither(canvas, foot, top - 3, DECK, DECK_MIX, seed=3) ``` Четыре вещи, каждая нарочно: **Дизер пишет один курс.** Не диапазон. Поэтому им нельзя задизерить стену — то же самое, чем `slab` отказывается класть куб на полувысоту. Правило, которое держит форма сигнатуры, не протухает вместе с дисциплиной. **Пару называешь ты.** `blocks.why_pair(DECK)` печатает кандидатов с числами и на этом останавливается — как `nearest`/`why_not` для цвета. Цвет — всё, что таблица видит: `brown_concrete` и `jungle_log` в двенадцати единицах друг от друга, и ни одна крыша не сделана из обоих. Бери тот, который **того же материала**, и объяви обоих в манифесте: `blocks=(DECK, DECK_MIX)`. **Двадцать единиц — это параметр, а не допуск.** Пара в 6 единиц (`diorite`+`polished_diorite`) — другой приём, невидимый; пара в 185 (`quartz_block`+`stone`) — третий, крапина. Полоса 8–32 в `blocks.DITHER_NEAR/FAR` — края того приёма, который замерен. **Ячейки выбираются монеткой, а не модулем.** `mask.speckle(share, seed)`. `(x * 7 + z * 3) % 11 < 4` выглядит как рассев и является решёткой: повторяется по диагонали в 82 % случаев, то есть кладёт диагональные полосы. Так было написано в пяти местах двух зданий, и ни одна строка гейта этого не видит — полоса даёт тот же силуэт, ту же площадь и то же сечение, что монетка. Гейт печатает `texture` на каждом прогоне и **ничего по нему не решает**. Правильного значения нет: фотографии этого здания старше любого числа с чужой карты. Колонка `ref` — контекст. И обратной силы у этих чисел почти нет. Число про монетку вправе отклонить находку ревью **про монетку** («шум неровный» — 0.50 по шахматке). Находка «крыша не читается» — про читаемость, и над ней оно не начальник. ## Как устроен рецепт Одна функция на часть, каждая берёт `(canvas, site, sched)` и объявляет то, что поставила. Держать их раздельно — это то, что позволяет ответить на находку ревью переписыванием одной из них. `Site` собирается один раз и передаётся в каждую секцию, поэтому есть одно место, где измерение превращается в координату, и ни одна секция не может тихо разойтись с другой в том, где стоит стена. Уровни округляются до целых блоков там и больше нигде: уровень, дотащенный как 5.75 до заливки, превратится в двух секциях в два разных целых, и перекрытие уедет на полметра от стены, которая его несёт.
View on GitHub