Skip to main content

Questions about a model

Functions that read the same description in different ways. None of them passes judgement. They stop at numbers, shapes and routes; whether something is enough is validate's sentence.

import {
areaM2, columnsFor, daylightInputs, displayName, doorsBetween, effectiveUse,
heff, isCoveredAbove, isIndoor, isSemiOutdoor, levelsSorted, neighbors,
passable, siteReport, unionAreaM2, zoneAreaM2,
} from "@kensnzk/koyu";

Every piece of output on this page was produced against the model read from examples/house/main.muro.

import { parseFile } from "@kensnzk/koyu/node";
const m = parseFile("examples/house/main.muro");

The space graph

Nodes are spaces, edges are boundaries. "Are these two rooms connected?" is already a question about the graph — nothing needs converting.

doorsBetween

function doorsBetween(model: Model, from: string, to: string): Route | undefined

interface Route { doors: number; path: string[] }

Returns the route joining two spaces through the fewest doors. An open boundary and a stair cost nothing, a wall boundary with a door costs one, and everything else cannot be passed.

undefined comes back both when the target is unreachable and when the path does not exist. To tell those apart, check model.spaces.has(path) first.

console.log(doorsBetween(m, "/home/bed1", "/out/road"));
console.log(doorsBetween(m, "/home/bed1", "/home/nope"));
{
  doors: 3,
  path: [
    '/home/bed1',
    '/home/hall2',
    '/home/hall1',
    '/site/east',
    '/site/garden',
    '/out/road'
  ]
}
undefined

path is the list of nodes and doors is how many doors the route passes. The two counts do not match, because stairs and open boundaries cost nothing.

neighbors

function neighbors(model: Model, path: string): NeighborInfo[]

interface NeighborInfo {
space: Space;
boundary: Boundary;
passable: boolean;
doors: number; // how many doors sit on that boundary
}

Derived default boundaries come back too. What is next door is not answered by the declared boundaries alone.

import { displayName, neighbors } from "@kensnzk/koyu";

for (const n of neighbors(m, "/home/hall1")) {
console.log(`${n.space.path}\t${displayName(n.space)}\t${n.boundary.kind}\tpassable=${n.passable}\tdoors=${n.doors}`);
}
/home/ldk	LDK	wall	passable=true	doors=1
/site/east	東側通路	wall	passable=true	doors=1
/site/north	北側通路	wall	passable=false	doors=0
/home/hall2	2階ホール	stair	passable=true	doors=0

The last row is a stair. Vertical neighbours arrive in the same array — the graph is not a plan.

passable

function passable(b: Boundary): boolean

Whether one boundary can be passed.

kindPassable?
openalways
stairalways
wallonly with a door opening
shaftnever
voidnever

air:1 is about enclosure, not passage. A handrail wall with no door is impassable, air:1 or not.

console.log(passable(m.boundaries.find((b) => b.kind === "stair")!));
true

Areas

areaM2

function areaM2(s: Space): number | undefined

The wall-centre area of a space, in m². It is the sum over the derived convex pieces, so a space cut by a drawn line reports the area of that shape. A space with no region (an exterior) gives undefined.

The value is rounded to two decimals.

zoneAreaM2

function zoneAreaM2(model: Model, zonePath: string): number

The sum of the areas of the spaces gathered by path prefix. Voids and semi-outdoor spaces are not counted — this is the language of net saleable area.

unionAreaM2

function unionAreaM2(rects: Rect[]): number

The union area of a set of rectangles. Overlaps are counted once — computed exactly by coordinate compression. This is the horizontal projection used to derive building footprint.

import { areaM2, unionAreaM2, zoneAreaM2 } from "@kensnzk/koyu";

console.log(areaM2(m.spaces.get("/home/ldk")!), zoneAreaM2(m, "/home"),
unionAreaM2([...m.spaces.get("/home/ldk")!.rects, ...m.spaces.get("/home/hall1")!.rects]));
39.75 92.75 53

Indoor, semi-outdoor, covered

All three are derived, none is declared.

isIndoor

function isIndoor(model: Model, s: Space): boolean

Whether the space counts towards indoor floor area. True for a space that has a region, is neither exterior nor void, and is not semi-outdoor.

Every place that asks about gross floor area uses this one answer. The population is never decided locally.

isSemiOutdoor

function isSemiOutdoor(model: Model, s: Space): boolean

A space with a region that has an open or air:1 boundary to a type:exterior space is semi-outdoor. Balconies, terraces and gardens land here.

isCoveredAbove

function isCoveredAbove(model: Model, s: Space): boolean

Whether a space on any higher level overlaps it. Even having a roof is not declared. The semi-outdoor daylight factor (0.7 under a soffit, 1.0 where the sky is open) reads these two.

import { isCoveredAbove, isIndoor, isSemiOutdoor } from "@kensnzk/koyu";

for (const p of ["/home/ldk", "/site/garden", "/out/road"]) {
const s = m.spaces.get(p)!;
console.log(`${p}\tindoor=${isIndoor(m, s)}\tsemi=${isSemiOutdoor(m, s)}\tcovered=${isCoveredAbove(m, s)}`);
}
/home/ldk	indoor=true	semi=false	covered=true
/site/garden	indoor=false	semi=true	covered=false
/out/road	indoor=false	semi=false	covered=false

/home/ldk is covered=true because the first-floor spaces sit over it. The garden is open to the sky, so it is false.

Height and levels

heff

function heff(model: Model, s: Space): number | undefined

The effective ceiling height of a space, in mm. The space's own h: if it has one, otherwise the h of its level. With neither, undefined — and then no ceiling and no roof are generated (SUF01 says so as an error).

levelsSorted

function levelsSorted(model: Model): Level[]

Levels in ascending z. model.levels is a Record and promises no order, so go through this whenever you need storeys in sequence.

import { heff, levelsSorted } from "@kensnzk/koyu";
console.log(heff(m, m.spaces.get("/home/ldk")!), levelsSorted(m).map((l) => `${l.name}@${l.z}`));
2400 [ 'L1@0', 'L2@2900', 'R@5800' ]

Daylight inputs

daylightInputs

function daylightInputs(model: Model): DaylightInput[]

interface DaylightInput {
space: Space;
floor: number; // floor area, m²
window: number; // effective window area, m² (factor applied)
missingH: boolean; // was a window skipped because it had no h?
}

Only spaces carrying daylight:1 are considered; type is never consulted. Which rooms the daylight question applies to is the author's declaration.

The factor is a derivation of what lies beyond the window: 1.0 straight to the exterior, 0.7 through a semi-outdoor space with something above it, 1.0 through a semi-outdoor space open to the sky, and 0 otherwise (not counted). The factor is a derivation of form, not a judgement.

Only numbers come back — no ok, no need. Drawing the 1/7 line is architecture's call, made by daylight.ratio in validate.

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

for (const d of daylightInputs(m)) {
console.log(`${d.space.path} floor=${d.floor} window=${d.window.toFixed(2)} missingH=${d.missingH}`);
}
/home/ldk floor=39.75 window=7.54 missingH=false
/home/bed1 floor=26.5 window=5.72 missingH=false

With no eligible space you get an empty array, which is indistinguishable from "everything passed" — so look at length. When missingH is true the window area is incomplete, and that number must not be used for a judgement.

The site

siteReport

function siteReport(model: Model): SiteReport

interface SiteReport {
siteZone?: Zone;
polygon?: SitePolygon;
declaredArea?: number; // the zone's area: (surveyed), m²
derivedArea: number; // derived, m²
footprint: number; // building footprint (horizontal projection), m²
totalFloor: number; // gross floor area, m²
roads: RoadFrontage[];
}

interface RoadFrontage {
road: Space;
width: number; // carriageway width, mm (the road: attribute)
frontage: number; // length of frontage, mm (derived)
}

The site is the zone carrying site:1; a road is an exterior space carrying road:<width in mm>.

derivedArea is the area of the site polygon if there is one, otherwise the union of the horizontal projections of the spaces inside the site and the indoor spaces. footprint is the union of the horizontal projections of the derived shapes of the indoor spaces — counting the written allocation instead would include what a splayed corner cut away.

Frontage is the total length of the boundary segments between the site and the road. An external wall facing the road is not frontage.

Neither site coverage nor plot ratio comes back. Those are quotients of these numbers, and what goes in the denominator is a question for the regulation, not for the model.

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

const r = siteReport(m);
console.log({ zone: r.siteZone?.path, declared: r.declaredArea, derived: r.derivedArea,
footprint: r.footprint, totalFloor: r.totalFloor,
roads: r.roads.map((x) => ({ path: x.road.path, width: x.width, frontage: x.frontage })) });
{
  zone: '/site',
  declared: 126.24,
  derived: 126.24,
  footprint: 53,
  totalFloor: 92.75,
  roads: [ { path: '/out/road', width: 6000, frontage: 10280 } ]
}

Objecting to a mismatch between declaredArea and derivedArea is validation's job (site.area). Here the two numbers simply sit side by side.

Columns

columnsFor

function columnsFor(model: Model, level: string): Column[]

Derives the columns standing on that level. The position is written nowhere — a column stands at each grid intersection that falls inside a space with floor (exterior and void excluded).

No column stands where it would carry only sky. A space that is semi-outdoor and has no floor above it — a roof garden, a top-level terrace — is out of the population, because there is nothing for a column to hold up.

Two columns never share an intersection. When several declarations aim at the same one, the earlier declaration wins.

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

const c = parseFile("examples/complex/main.muro");
const cols = columnsFor(c, "L1");
console.log(cols.length, cols[0]);
46 {
  x: 0,
  y: 0,
  w: 900,
  d: 900,
  level: 'L1',
  grid: 'X1/Y1',
  decl: 0,
  attrs: {}
}

grid is the pair of grid lines it stands on, in the language of the drawing. decl indexes model.columns and says which declaration raised it — which is what makes "this declaration raises no column at all" a question you can ask.

Small things for display

effectiveUse

function effectiveUse(model: Model, s: Space): string | undefined

The effective use attribute. If the space has none, it is inherited from the deepest zone ancestor that has one.

displayName

function displayName(s: Space): string

The name: attribute, or the last segment of the path. A string always comes back.

import { displayName, effectiveUse } from "@kensnzk/koyu";
console.log(effectiveUse(m, m.spaces.get("/home/ldk")!), displayName(m.spaces.get("/home/ldk")!));
exclusive LDK

See also