メインコンテンツまでスキップ

図の生成

SVG の文字列を返す二つの関数である。ここに形の規則は一つも無い — 壁の厚みも、開口の位置も、扉の吊元も、階段がどこで切れるかも、derive(model) が返す Form に既に入っている。この二つが決めるのは色・線種・線幅・書体・記号・注記の言葉・縮尺・投影・紙面の余白だけである。

import { svgPlan, svgAxo } from "@kensnzk/koyu";
import type { PlanOptions, AxoOptions } from "@kensnzk/koyu";

領域としては @kensnzk/koyu/draw にも分けてある。同じ関数の別入口である。

import { svgPlan, svgAxo } from "@kensnzk/koyu/draw";

この面は凍らない

SVG の中身は約束の外にある。同じ入力から同じ形が出ることは約束されるが、同じバイトが出ることは約束されない。色・線種・書体・記号の見た目・要素の並びは断りなく変わる。

だからこの出力をゴールデンファイルにしてはならない。図を機械で比べたいなら toCanonicalderive の返り値を比べる — そちらは形そのものであり、決定性が約束されている。

svgPlan

function svgPlan(model: Model, opts?: PlanOptions): string

interface PlanOptions {
level?: string; // 既定: 最初に宣言されたレベル
scale?: number; // px per mm。既定 0.05
cut?: number; // 切断面の高さ mm (FL から)。既定 1200
}

平面図を返す。cut形を決める引数なので derive の入力へそのまま渡る — 縮尺と違って、これは見た目ではなく形の話である。

import { svgPlan } from "@kensnzk/koyu";
import { parseFile } from "@kensnzk/koyu/node";

const a = parseFile("examples/two-rooms.muro");
const svg = svgPlan(a, { level: "L1" });
console.log(svg.length + " chars");
console.log(svg.split("\n")[0]);
3843 chars
<svg xmlns="http://www.w3.org/2000/svg" width="528" height="393" viewBox="0 0 528 393" font-family="'Hiragino Sans','Noto Sans JP',sans-serif">

scalecut を動かすと紙面も変わる。

console.log(svgPlan(a, { level: "L1", scale: 0.1, cut: 800 }).split("\n")[0]);
<svg xmlns="http://www.w3.org/2000/svg" width="888" height="618" viewBox="0 0 888 618" font-family="'Hiragino Sans','Noto Sans JP',sans-serif">

紙面の外接範囲には書かれた割付も含める — 線で切られた形より外へ割付がはみ出しても、紙には載る。敷地形状は最下階の平面 (配置図兼用) にだけ敷地境界線として描かれる。これは紙面の構成の判断であって、形の規則ではない。

投げることがある

Error を投げる。SourceError ではないので、位置も行番号も持たない。呼び出し側で捕まえること。

メッセージいつ
No level is definedlevel を省き、模型にレベルが一つも無い
There is no space with a region on level <名>指定したレベルに領域を持つ空間が無い
try { svgPlan(a, { level: "L9" }); } catch (e) { console.log("throws:", (e as Error).message); }
throws: There is no space with a region on level L9

svgAxo

function svgAxo(model: Model, opts?: AxoOptions): string

interface AxoOptions {
dir?: "NE" | "NW" | "SE" | "SW"; // 見る向き。既定 SE
scale?: number; // px per mm。既定 0.02
levels?: string[]; // 描くレベル。既定すべて
ceilings?: boolean; // 天井も描く。既定 false
walls?: boolean; // 壁を描く。既定 true
}

軸測図 (アクソメ) を返す。平面図が「そのレベルで切った断面」であるのに対し、これは立体をそのまま投影した図である。WebGL も実行環境も要らない — 平面と同じく SVG のテキストが出るので、生成して見る、という同じ手で立体を確かめられる。

描かれるのは生成物だけである。床・屋根、境界から現れた壁、通りの交点から現れた柱、縦動線。どれもソースには無い。

import { svgAxo } from "@kensnzk/koyu";
import { parseFile } from "@kensnzk/koyu/node";

const h = parseFile("examples/house/main.muro");
const ax = svgAxo(h);
console.log(ax.length + " chars");
console.log(ax.split("\n")[0]);
33025 chars
<svg xmlns="http://www.w3.org/2000/svg" width="472.1363028335939" height="405.35" viewBox="0 0 472.1363028335939 405.35" font-family="'Hiragino Sans','Noto Sans JP',sans-serif">

dir は建物のどの隅から見下ろすかである。平面を90度ずつ回してから等角に落とすので、四つの向きで違う面が見える。

for (const dir of ["NE", "NW", "SE", "SW"] as const) console.log(dir, svgAxo(h, { dir }).length);
NE 33140
NW 33016
SE 33025
SW 32957

ceilings の既定が false なのは、描くと中が見えなくなるからである。walls: false にすると床・屋根・柱・縦動線だけが残り、構造と面の関係が見える。

console.log("levels:", svgAxo(h, { levels: ["L1"] }).length,
"ceilings:", svgAxo(h, { ceilings: true }).length,
"walls:false", svgAxo(h, { walls: false }).length);
levels: 19982 ceilings: 36021 walls:false 7425

投げることがある

描くものが一つも無ければ Error を投げる。

メッセージいつ
There is nothing to draw立体が一つも生成されない (レベルが無い、領域を持つ空間が無い、levels の指定が全部外れている)

自分で描くなら

Form を直接読んで自分の描画系へ写すのが正道である。同じ規則から出た形を、違う見た目で描く — それがこの分離の目的である。

import { derive } from "@kensnzk/koyu";

const form = derive(model, { cut: 1200 });
const plan = form.plans.find((p) => p.level === "L1")!;

for (const e of plan.entities) {
// e.class は cut / below / above / swing / anchor
// e.polygon は足あと、e.lines は芯線 — どちらで描くかは見た目の判断
}

分類と型の中身は 形の導出。芯線から実体を起こす構成子は 実体と生成物それを自分で書き直さないこと — 部品を共有していても組み立ての規則を共有しなければ、同じ Form から違う形が出る。

関連