形 — derive(model)
原本に形は無い。平面図も面積も動線も、書かれるのではなく導かれる。その導出を行う関数は一つしかない。
import { derive } from "@kensnzk/koyu";
const form = derive(model); // 切断高さは既定の 1200mm
const cut = derive(model, { cut: 900 });
derive(model, {cut?}) → Form が形の唯一の入口である。面積を測るのも、壁の位置を知るのも、平面の要素を得るのも、立体を組むのも、すべてこの一つの戻り値から出る。
四つの約束
1. 形は正準形の関数である
toCanonical(a) === toCanonical(b) ⟹ derive(a) ≡ derive(b)
正準 JSON は「同じ建物とは何か」の定義である。したがって正準形が捨てる情報は、形を変えてはならない — 線の端点の書き順も、境界の宣言順も、行の並びも、形に効かない。
この含意は文としての約束ではなく、機械が縛れる述語である。同梱例に対して、線の端点を入れ替えた版・境界を並べ替えた版・a/b の向きを逆に書いた版を作り、正準形が等しいことを前提として確かめてから形の一致を主張する。前提が崩れれば「この組は何も証明していない」として落ちる。
ここから二つの規則が出る。描かれた線は向きを持たない(凸片)。切り分けは正準の境界順に効く(同)。
2. 形を読む入口は一つである
空間の形は pieces(導出された凸片)であって rects(書かれた割付)ではない。割付はセルの綴りであり、形ではない。面積・共有辺・外周・被覆・柱の立地・投影 — 形を読むすべての導出がこの一つの入口を通る。
Form が返す FormSpace.outline は常に導出された凸片である。割付は Form に現れない。
3. 既定値を捏造しない
必要な値が書かれていなければ、既定を勝手に置くのではなくその要素を作らない。天井高が決まらなければ天井も屋根も生成されず、slab: が無ければ床が一枚も生成されない。「形が痩せている」ことは黙って起きてはならないので、SUF の診断が error として言う。
例外は導出定数である。壁厚 100mm も、まぐさ高 2000mm も、蹴上げ上限 180mm も、仕様が定めた規則であって捏造ではない。
4. 凸片は反時計回りである
Form が返す輪郭はすべて反時計回り(符号つき面積が正)であり、辺の方位 N/E/S/W の読みはこの向きを前提とする。
Form は見た目を持たない
Form が持つもの | Form が持たないもの |
|---|---|
| 座標 — 領域の凸片・境界線分・開口の中心・柱の断面・段板の矩形 | 色・書体・文字寸・線幅・線種 |
| 厚み — 壁・手すり・面 | 注記の言葉 — UP も 12段 蹴上175/踏面300 も |
| z 範囲 — 壁・開口・柱・面・立体 | 作図の記号 — 吹抜けの対角線・切断線の二本の斜線・通り芯の丸・矢印の頭 |
| 向き — 辺の方位・上る向き・扉の吊元と開く側・矢印が上るか | 縮尺・紙面の余白・viewBox |
| 対象の同一性 — どの空間の、どの境界の、どの開口の形か | 描く順序・重ね順・陰影 |
| 平面の分類 — cut / below / above / swing / anchor | 何を描き何を省くかの判断 |
svgPlan / svgAxo と外部のビュアーは、この Form を描くだけである。複数あってよいのは見た目であって、形ではない。「同じ構成から複数の形が出る」ことは欠陥である。
見た目を持たないことは機械が縛る — 同梱例の Form を JSON にして、色の綴りも日本語も UP/DN も一つも現れないことを検査する。
Form の中身
interface Form {
input: FormInput; // 導出に渡した引数 (切断高さ)
levels: FormLevel[]; // レベルと階高
spaces: FormSpace[]; // 導出された凸片・面積・気積・屋内/半屋外/被覆
boundaries: FormBoundary[];// 芯線分と、物があるならその材 (開口で割られた区間)
openings: FormOpening[]; // 中心・幅・z 範囲・建具厚・扉の振れ
segs: FormSeg[]; // 数えない分節の位置
slabs: Slab[]; // 床・天井・屋根
columns: FormColumn[]; // 通り芯の交点に立つ柱
runs: FormRun[]; // 縦動線の段割りと立体
site: FormSite[]; // 与件の敷地形状と面積
plans: FormPlan[]; // レベルごとの、分類つき2Dエンティティ集合
}
levels の各要素は pitch(階高 — 壁と柱がどこまで立つか)を持つ。上のレベルがあればその差、無ければその階の最大天井高 + 屋根版の厚さである。levelPitch(model, level) が単独でも答える。
levels: [ { name: "L1", z: 0, h: 2400, slab: 150, pitch: 2600 } ]
(examples/two-rooms.muro。上のレベルが無いので 2400 + 200。)
対象の同一性
Form の各要素は ref を持つ。境界は <a>|<b>@<i>、開口は <境界の ref>/<番号>、seg は <境界の ref>~<番号>、柱は <レベル>/<X通り>/<Y通り>、縦動線は宣言した空間のパスである。
"/L1/a|/L1/b@0" 境界
"/L1/b|/out@2/0" その境界の 0 番目の開口
"L1/X2/Y3" 柱
添字 @i は正準の境界順の添字であって、宣言順ではない。宣言順は正準 JSON が捨てる情報なので、宣言順で振ると同じ正準形から違う綴りが出る。並びを得る関数 canonicalBoundaryOrder(model) が公開されているので、消費者は model.boundaries[i] を当ててはならない。
実体の構成子
Form が持つのは芯線と厚みと z である。そこから実体を起こす規則も導出の一部なので、実装は一つしかない。消費者がそれぞれ書き直せば、部品を共有していても組み立ての規則は共有されず、同じ Form から違う形が出る余地がまた開く。
| 構成子 | 何を起こすか |
|---|---|
thicken(x1, y1, x2, y2, t) | 芯線を厚みのある四辺形へ。斜めの線分でも同じ一つの式 |
bandLine(seg, cx, cy, w) | 帯(開口・seg)が線分上で占める区間 |
band(seg, cx, cy, w, t) | その区間を厚みのある四辺形へ — bandLine を thicken したもの |
columnRect(c) | 柱の断面 |
runPrism(s) | 縦動線の立体を角柱(底面の輪郭と頂点ごとの上端/下端 z)へ |
四辺形の頂点は 始点+n → 終点+n → 終点−n → 始点−n の順であるので、向かい合う二辺の中点を結べば芯線に戻る。平面のエンティティは区間の足あと(四辺形)と芯線の両方を持つ — 厚みを持つものとして描くか一本の線として描くか(遮蔽しない手すり・柵)は見た目の判断なので、消費者が足あとから芯線を復元しなくて済むようにしてある。