A family house in IFC
The IFC page builds a wall with a window. This project builds a whole house with the same functions: a two-storey family house with some 700 elements, as an architect's tool would write it, under a sky whose sun follows a slider. Run it, move the slider through the day, then open it in the editor to read and change it.




main.ts
import { HouseAuthor } from "./house/build-house";
import { drawHouse } from "./scene/draw-house";
import { setUpEnvironment } from "./scene/environment";
const house = await new HouseAuthor(bitbybit.ifc, (message) => console.log(message)).build();
const drawn = await drawHouse(house);
setUpEnvironment(drawn);
bitbybit.babylon.scene.adjustActiveArcRotateCamera({ position: [27, 10, 21], lookAt: [6, 2.5, -3], maxZ: 1000 });
house/house-types.ts
import Base = Bit.Inputs.Base;
import IFC = Bit.Inputs.IFC;
export type Point = Base.Point2;
export type StoreyKey = "ground" | "first";
export type Layer = "ground" | "first" | "roof" | "garden";
export interface MaterialSpec {
name: string;
category: string;
color?: string;
transparency?: number;
}
export interface LayerSetSpec {
name: string;
layers: { material: string; thickness: number }[];
}
export interface WallSpec {
id: string;
name: string;
start: Point;
end: Point;
height?: number;
}
export interface WallRun {
storey: StoreyKey;
layerSet: string;
height: number;
baseOffset: number;
alignment: IFC.wallAlignmentEnum;
walls: WallSpec[];
joins: [string, string][];
external: boolean;
}
export interface WindowTypeSpec {
id: string;
name: string;
width: number;
height: number;
}
export interface DoorTypeSpec {
id: string;
name: string;
width: number;
height: number;
lining: string;
panel: string;
operation: IFC.doorOperationEnum;
handle: IFC.doorHandleEnum;
handleMaterial: string;
}
export interface FillingSpec {
id: string;
name: string;
wall: string;
type: string;
offset: number;
sill: number;
}
export interface PassageSpec {
id: string;
name: string;
wall: string;
offset: number;
width: number;
height: number;
}
export interface SlabSpec {
id: string;
name: string;
storey: StoreyKey;
outline: Point[];
holes?: Point[][];
layerSet: string;
topOffset: number;
kind: IFC.slabPredefinedTypeEnum;
layer: Layer;
}
export interface TreeSpec {
id: string;
species: string;
position: Point;
height: number;
crown: number;
trunk: number;
foliage: string;
bark: string;
}
export interface DetailSpec {
id: string;
name: string;
storey: StoreyKey;
start: Base.Point3;
end: Base.Point3;
material: string;
layer: Layer;
width?: number;
depth?: number;
radius?: number;
rotation?: number;
kind?: IFC.memberPredefinedTypeEnum;
beam?: boolean;
assembly?: string;
}
export interface AssemblySpec {
id: string;
name: string;
objectType: string;
}
export interface FacadeLine {
wall: string;
along: "x" | "y";
line: number;
outward: 1 | -1;
start: number;
direction: 1 | -1;
}
export interface FacadeSide extends FacadeLine {
from: number;
to: number;
}
export interface WallSide extends FacadeLine {
thickness: number;
baseOffset: number;
}
export interface FacadeOpening {
id: string;
from: number;
to: number;
bottom: number;
top: number;
}
export interface PropertySetSpec {
name: string;
elements: string[];
properties: IFC.PropertyDto[];
}
export type RailEnd = "flush" | "butt" | "lap";
export interface RailRun {
id: string;
along: "x" | "y";
at: number;
from: number;
to: number;
fromEnd: RailEnd;
toEnd: RailEnd;
}
export interface RailSpec {
id: string;
name: string;
base: number;
external: boolean;
runs: RailRun[];
}
export interface RoomSpec {
id: string;
number: string;
name: string;
storey: StoreyKey;
outline: Point[];
height: number;
}
export interface HouseModel {
model: IFC.IfcModelPointer;
layers: Map<string, Layer>;
rooms: RoomSpec[];
}
export type HouseIfc = typeof bitbybit.ifc;
house/house-plan.ts
import IFC = Bit.Inputs.IFC;
import type { DoorTypeSpec, FillingSpec, LayerSetSpec, MaterialSpec, PassageSpec, Point, PropertySetSpec, RailRun, RailSpec, RoomSpec, SlabSpec, TreeSpec, WallRun, WindowTypeSpec } from "./house-types";
const { wallAlignmentEnum, doorOperationEnum, doorHandleEnum, slabPredefinedTypeEnum } = IFC;
export const FIRST_FLOOR_ELEVATION = 3.2;
export const SLAB_DEPTH = 0.3;
export const FIRST_FLOOR_WALL_TOP = 3.0;
export const FACADE_DEPTH = 0.48;
export const TIMBER_WALL = 0.415;
export const CAVITY = 0.04;
export const BOARD = { width: 0.12, gap: 0.02, thickness: 0.025 };
export const ROOF_DEPTH = 0.56;
export const PARAPET = { thickness: 0.2, height: 0.91 };
export const PARAPET_TOP = FIRST_FLOOR_WALL_TOP + PARAPET.height;
export const COPING = { thickness: 0.03, drip: 0.03 };
export const DECK_TOP = 0.04;
const GLASS_DOOR_WIDTH = 0.9;
export const SOFFIT_DEPTH = 0.2;
export const CANTILEVER_DRIP = -SLAB_DEPTH - SOFFIT_DEPTH - 0.02;
export function rect(x0: number, y0: number, x1: number, y1: number): Point[] {
return [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
}
export const GROUND_INTERIOR = { x0: 0, y0: 0, x1: 14.2, y1: 8.4 };
export const FIRST_INTERIOR = { x0: 0, y0: -2.2, x1: 10.6, y1: 8.4 };
export const FIRST_OUTLINE = rect(FIRST_INTERIOR.x0, FIRST_INTERIOR.y0, FIRST_INTERIOR.x1, FIRST_INTERIOR.y1);
export const FIRST_FLOOR_EAST_FACE = FIRST_INTERIOR.x1 + TIMBER_WALL;
const VOID = { x0: 5.48, y0: 5.06, x1: 9.45, y1: 6.17 };
export const STAIR_VOID = rect(VOID.x0, VOID.y0, VOID.x1, VOID.y1);
export const PARAPET_INSET = TIMBER_WALL - PARAPET.thickness;
const UNDER_SLAB = FIRST_FLOOR_ELEVATION - SLAB_DEPTH;
const { x1: GX, y1: GY } = GROUND_INTERIOR;
export const MATERIALS: MaterialSpec[] = [
{ name: "Silicate render", category: "plaster", color: "#f1efea" },
{ name: "EPS insulation", category: "insulation" },
{ name: "Aerated concrete", category: "block" },
{ name: "Gypsum plaster", category: "plaster", color: "#ece9e3" },
{ name: "Facade membrane", category: "membrane", color: "#161514" },
{ name: "Wood fibre board", category: "insulation" },
{ name: "Timber frame with mineral wool", category: "wood" },
{ name: "Mineral wool", category: "insulation" },
{ name: "OSB", category: "wood" },
{ name: "Service void insulation", category: "insulation" },
{ name: "Gypsum board", category: "plaster", color: "#ece9e3" },
{ name: "Timber studs", category: "wood" },
{ name: "Charred larch", category: "wood", color: "#3b342e" },
{ name: "Polished concrete", category: "concrete", color: "#c9c5bc" },
{ name: "Reinforced concrete", category: "concrete", color: "#a9a59d" },
{ name: "XPS insulation", category: "insulation" },
{ name: "Oak", category: "wood", color: "#b2865a" },
{ name: "Screed", category: "concrete" },
{ name: "Impact insulation", category: "insulation" },
{ name: "Pale gravel", category: "stone", color: "#d3cfc6" },
{ name: "Roofing membrane", category: "membrane" },
{ name: "PIR insulation", category: "insulation" },
{ name: "Vapour control layer", category: "membrane" },
{ name: "Cross-laminated timber", category: "wood" },
{ name: "Thermo ash decking", category: "wood", color: "#8b6b52" },
{ name: "Limestone paving", category: "stone", color: "#dcd7cc" },
{ name: "Concrete pavers", category: "concrete", color: "#bdb8ae" },
{ name: "Fibre cement", category: "cement", color: "#e7e5e0" },
{ name: "Anthracite aluminium", category: "aluminium", color: "#2b2f33" },
{ name: "Clear glass", category: "glass", color: "#b8d0d8", transparency: 0.72 },
{ name: "White lacquer", category: "wood", color: "#f4f2ed" },
{ name: "Smoked oak", category: "wood", color: "#5c4433" },
{ name: "Brushed stainless steel", category: "steel", color: "#b4b8bc" },
{ name: "Blackened steel", category: "steel", color: "#2f3236" },
{ name: "Solar module", category: "glass", color: "#1c2530" },
{ name: "Sedum", category: "vegetation", color: "#8d9a5c" },
{ name: "Plywood", category: "wood" },
{ name: "Glulam spruce", category: "wood", color: "#c9a272" },
{ name: "Lawn", category: "soil", color: "#9fb184" },
{ name: "Birch foliage", category: "vegetation", color: "#9db277" },
{ name: "Maple foliage", category: "vegetation", color: "#6f8f58" },
{ name: "Pine needles", category: "vegetation", color: "#50684b" },
{ name: "Birch bark", category: "wood", color: "#ddd8cc" },
{ name: "Bark", category: "wood", color: "#5b4a3c" },
];
export const LAYER_SETS: LayerSetSpec[] = [
{ name: "Rendered masonry wall", layers: [{ material: "Silicate render", thickness: 0.015 }, { material: "EPS insulation", thickness: 0.2 }, { material: "Aerated concrete", thickness: 0.25 }, { material: "Gypsum plaster", thickness: 0.015 }] },
{
name: "Timber frame wall",
layers: [
{ material: "Facade membrane", thickness: 0.002 }, { material: "Wood fibre board", thickness: 0.08 }, { material: "Timber frame with mineral wool", thickness: 0.24 },
{ material: "OSB", thickness: 0.018 }, { material: "Service void insulation", thickness: 0.05 }, { material: "Gypsum board", thickness: 0.025 },
],
},
{ name: "Parapet", layers: [{ material: "Facade membrane", thickness: 0.002 }, { material: "Timber frame with mineral wool", thickness: 0.198 }] },
{ name: "Partition", layers: [{ material: "Gypsum board", thickness: 0.0125 }, { material: "Timber studs", thickness: 0.095 }, { material: "Gypsum board", thickness: 0.0125 }] },
{ name: "Glass partition", layers: [{ material: "Clear glass", thickness: 0.03 }] },
{ name: "Glass balustrade", layers: [{ material: "Clear glass", thickness: 0.02 }] },
{ name: "Insulated ground slab", layers: [{ material: "Polished concrete", thickness: 0.06 }, { material: "Reinforced concrete", thickness: 0.2 }, { material: "XPS insulation", thickness: 0.25 }] },
{ name: "Upper floor slab", layers: [{ material: "Oak", thickness: 0.02 }, { material: "Screed", thickness: 0.06 }, { material: "Impact insulation", thickness: 0.03 }, { material: "Reinforced concrete", thickness: 0.19 }] },
{ name: "Terrace roof slab", layers: [{ material: "Roofing membrane", thickness: 0.005 }, { material: "PIR insulation", thickness: 0.18 }, { material: "Reinforced concrete", thickness: 0.115 }] },
{ name: "Warm flat roof", layers: [{ material: "Pale gravel", thickness: 0.05 }, { material: "Roofing membrane", thickness: 0.005 }, { material: "PIR insulation", thickness: 0.3 }, { material: "Vapour control layer", thickness: 0.005 }, { material: "Cross-laminated timber", thickness: 0.2 }] },
{ name: "Insulated soffit", layers: [{ material: "Mineral wool", thickness: 0.18 }, { material: "Charred larch", thickness: 0.02 }] },
{ name: "Carport roof", layers: [{ material: "Sedum", thickness: 0.06 }, { material: "Roofing membrane", thickness: 0.005 }, { material: "Plywood", thickness: 0.025 }] },
{ name: "Decking", layers: [{ material: "Thermo ash decking", thickness: 0.04 }] },
{ name: "Garden paving", layers: [{ material: "Limestone paving", thickness: 0.05 }, { material: "Reinforced concrete", thickness: 0.1 }] },
{ name: "Driveway", layers: [{ material: "Concrete pavers", thickness: 0.08 }, { material: "Reinforced concrete", thickness: 0.1 }] },
];
export const WINDOW_TYPES: WindowTypeSpec[] = [
{ id: "w-glazing-3000", name: "Fixed glazing 3000 x 2700", width: 3.0, height: 2.7 },
{ id: "w-glazing-2400", name: "Fixed glazing 2400 x 2700", width: 2.4, height: 2.7 },
{ id: "w-ribbon-3000", name: "Ribbon window 3000 x 900", width: 3.0, height: 0.9 },
{ id: "w-1600", name: "Casement 1600 x 1500", width: 1.6, height: 1.5 },
{ id: "w-1500", name: "Casement 1500 x 1000", width: 1.5, height: 1.0 },
{ id: "w-1200", name: "Casement 1200 x 1000", width: 1.2, height: 1.0 },
{ id: "w-800", name: "Hopper 800 x 600", width: 0.8, height: 0.6 },
{ id: "w-slot-ground", name: "Slot window 600 x 2700", width: 0.6, height: 2.7 },
{ id: "w-glazing-2300", name: "Fixed glazing 2300 x 2600", width: 2.3, height: 2.6 },
{ id: "w-slot-first", name: "Slot window 600 x 2600", width: 0.6, height: 2.6 },
{ id: "w-ribbon-4000", name: "Ribbon window 4000 x 1200", width: 4.0, height: 1.2 },
{ id: "w-1600-high", name: "High window 1600 x 700", width: 1.6, height: 0.7 },
{ id: "w-1000", name: "Casement 1000 x 900", width: 1.0, height: 0.9 },
];
export const DOOR_TYPES: DoorTypeSpec[] = [
{ id: "d-entrance", name: "Smoked oak entrance door 1300 x 2700", width: 1.3, height: 2.7, lining: "Anthracite aluminium", panel: "Smoked oak", operation: doorOperationEnum.singleSwingRight, handle: doorHandleEnum.pullBar, handleMaterial: "Brushed stainless steel" },
{ id: "d-slider-left", name: "Sliding glass door 1800 x 2700", width: 1.8, height: 2.7, lining: "Anthracite aluminium", panel: "Clear glass", operation: doorOperationEnum.slidingToLeft, handle: doorHandleEnum.pullBar, handleMaterial: "Anthracite aluminium" },
{ id: "d-slider-right", name: "Sliding glass door 1800 x 2700", width: 1.8, height: 2.7, lining: "Anthracite aluminium", panel: "Clear glass", operation: doorOperationEnum.slidingToRight, handle: doorHandleEnum.pullBar, handleMaterial: "Anthracite aluminium" },
{ id: "d-slider-2000", name: "Sliding glass door 2000 x 2600", width: 2.0, height: 2.6, lining: "Anthracite aluminium", panel: "Clear glass", operation: doorOperationEnum.slidingToRight, handle: doorHandleEnum.pullBar, handleMaterial: "Anthracite aluminium" },
{ id: "d-interior", name: "Interior door 900 x 2200", width: 0.9, height: 2.2, lining: "White lacquer", panel: "White lacquer", operation: doorOperationEnum.singleSwingLeft, handle: doorHandleEnum.lever, handleMaterial: "Brushed stainless steel" },
{ id: "d-glass", name: "Glass door 900 x 2200", width: GLASS_DOOR_WIDTH, height: 2.2, lining: "Anthracite aluminium", panel: "Clear glass", operation: doorOperationEnum.singleSwingRight, handle: doorHandleEnum.lever, handleMaterial: "Brushed stainless steel" },
];
function closedRun(prefix: string, outline: Point[], names: string[], name?: string): WallRun["walls"] {
return outline.map((start, index) => {
const side = names[index] ?? String(index);
return {
id: `${prefix}-${side}`,
name: name ?? `${side.charAt(0).toUpperCase()}${side.slice(1)} wall`,
start,
end: outline[(index + 1) % outline.length] ?? start,
};
});
}
function loopJoins(walls: WallRun["walls"]): [string, string][] {
return walls.map((wall, index) => [wall.id, (walls[(index + 1) % walls.length] ?? wall).id]);
}
const SIDES = ["south", "east", "north", "west"];
const FIRST_FLOOR_WALLS = closedRun("ff", FIRST_OUTLINE, SIDES);
const PARAPET_WALLS = closedRun("parapet", rect(FIRST_INTERIOR.x0 - PARAPET_INSET, FIRST_INTERIOR.y0 - PARAPET_INSET, FIRST_INTERIOR.x1 + PARAPET_INSET, FIRST_INTERIOR.y1 + PARAPET_INSET), SIDES, "Parapet");
const TERRACE_EDGE = GX + FACADE_DEPTH;
export const GLASS_RAIL = { shoe: { width: 0.06, depth: 0.12 }, glass: 0.94, cap: { width: 0.05, depth: 0.04 } };
const TERRACE = { house: FIRST_INTERIOR.x1 + FACADE_DEPTH, south: -FACADE_DEPTH + 0.05, east: TERRACE_EDGE - 0.05, north: GY + FACADE_DEPTH - 0.05 };
const VOID_RAIL = { south: VOID.y0 - GLASS_RAIL.shoe.width / 2, east: VOID.x1 + GLASS_RAIL.shoe.width / 2 };
export const RAILS: RailSpec[] = [
{
id: "terrace", name: "Terrace balustrade", base: DECK_TOP, external: true,
runs: [
{ id: "south", along: "x", at: TERRACE.south, from: TERRACE.house, to: TERRACE.east, fromEnd: "flush", toEnd: "butt" },
{ id: "east", along: "y", at: TERRACE.east, from: TERRACE.south, to: TERRACE.north, fromEnd: "lap", toEnd: "lap" },
{ id: "north", along: "x", at: TERRACE.north, from: TERRACE.east, to: TERRACE.house, fromEnd: "butt", toEnd: "flush" },
],
},
{
id: "ff-void", name: "Stair balustrade", base: 0, external: false,
runs: [
{ id: "south", along: "x", at: VOID_RAIL.south, from: VOID.x0, to: VOID_RAIL.east, fromEnd: "flush", toEnd: "lap" },
{ id: "east", along: "y", at: VOID_RAIL.east, from: VOID_RAIL.south, to: VOID.y1, fromEnd: "butt", toEnd: "flush" },
],
},
];
const END_SHIFT = { flush: 0, butt: -1, lap: 1 };
export function railLine(run: RailRun, width: number): [Point, Point] {
const direction = Math.sign(run.to - run.from);
const from = run.from - direction * END_SHIFT[run.fromEnd] * width / 2;
const to = run.to + direction * END_SHIFT[run.toEnd] * width / 2;
return run.along === "x" ? [[from, run.at], [to, run.at]] : [[run.at, from], [run.at, to]];
}
export const STUDY_GLAZING = { wall: "ff-study-glass", line: 6.23, x0: 5.46, x1: 10.6, panes: 4, frame: { width: 0.05, depth: 0.02 } };
const STUDY_FRAME_FACE = STUDY_GLAZING.line + thicknessOf("Glass partition") / 2 + STUDY_GLAZING.frame.depth;
export const WALL_RUNS: WallRun[] = [
{
storey: "ground", layerSet: "Rendered masonry wall", height: UNDER_SLAB, baseOffset: 0, alignment: wallAlignmentEnum.right, external: true,
walls: [
{ id: "gf-south", name: "South wall", start: [0, 0], end: [FIRST_FLOOR_EAST_FACE, 0] },
{ id: "gf-south-terrace", name: "South wall", start: [FIRST_FLOOR_EAST_FACE, 0], end: [GX, 0], height: FIRST_FLOOR_ELEVATION },
{ id: "gf-east", name: "East wall", start: [GX, 0], end: [GX, GY], height: FIRST_FLOOR_ELEVATION },
{ id: "gf-north-terrace", name: "North wall", start: [GX, GY], end: [FIRST_FLOOR_EAST_FACE, GY], height: FIRST_FLOOR_ELEVATION },
{ id: "gf-north", name: "North wall", start: [FIRST_FLOOR_EAST_FACE, GY], end: [0, GY] },
{ id: "gf-west", name: "West wall", start: [0, GY], end: [0, 0] },
],
joins: [["gf-south-terrace", "gf-east"], ["gf-east", "gf-north-terrace"], ["gf-north", "gf-west"], ["gf-west", "gf-south"]],
},
{
storey: "first", layerSet: "Timber frame wall", height: SLAB_DEPTH + FIRST_FLOOR_WALL_TOP, baseOffset: -SLAB_DEPTH, alignment: wallAlignmentEnum.right, external: true,
walls: FIRST_FLOOR_WALLS,
joins: loopJoins(FIRST_FLOOR_WALLS),
},
{
storey: "first", layerSet: "Parapet", height: PARAPET.height, baseOffset: FIRST_FLOOR_WALL_TOP, alignment: wallAlignmentEnum.right, external: true,
walls: PARAPET_WALLS,
joins: loopJoins(PARAPET_WALLS),
},
{
storey: "ground", layerSet: "Partition", height: UNDER_SLAB, baseOffset: 0, alignment: wallAlignmentEnum.center, external: false, joins: [],
walls: [
{ id: "gf-spine", name: "Spine wall", start: [0, 5.0], end: [GX, 5.0] },
{ id: "gf-study-wc", name: "Study partition", start: [3.66, 5.06], end: [3.66, GY] },
{ id: "gf-wc-hall", name: "WC partition", start: [5.36, 5.06], end: [5.36, GY] },
{ id: "gf-hall-utility", name: "Utility partition", start: [10.6, 5.06], end: [10.6, GY] },
],
},
{
storey: "first", layerSet: "Partition", height: FIRST_FLOOR_WALL_TOP, baseOffset: 0, alignment: wallAlignmentEnum.center, external: false, joins: [],
walls: [
{ id: "ff-corridor", name: "Corridor wall", start: [0, 3.6], end: [10.6, 3.6] },
{ id: "ff-bed-1-2", name: "Bedroom partition", start: [3.46, -2.2], end: [3.46, 3.54] },
{ id: "ff-bed-2-3", name: "Bedroom partition", start: [6.96, -2.2], end: [6.96, 3.54] },
{ id: "ff-bath", name: "Bathroom wall", start: [0, 5.0], end: [3.46, 5.0] },
{ id: "ff-bath-wardrobe", name: "Wardrobe partition", start: [3.46, 5.06], end: [3.46, 8.4] },
{ id: "ff-wardrobe", name: "Wardrobe front", start: [3.52, 6.2], end: [5.46, 6.2] },
{ id: "ff-wardrobe-study", name: "Study partition", start: [5.46, STUDY_FRAME_FACE], end: [5.46, 8.4] },
],
},
{
storey: "first", layerSet: "Glass partition", height: FIRST_FLOOR_WALL_TOP, baseOffset: 0, alignment: wallAlignmentEnum.center, external: false, joins: [],
walls: [{ id: STUDY_GLAZING.wall, name: "Study glass wall", start: [STUDY_GLAZING.x0, STUDY_GLAZING.line], end: [STUDY_GLAZING.x1, STUDY_GLAZING.line] }],
},
...RAILS.map((rail): WallRun => ({
storey: "first", layerSet: "Glass balustrade", height: GLASS_RAIL.glass, baseOffset: rail.base + GLASS_RAIL.shoe.depth, alignment: wallAlignmentEnum.center, external: rail.external, joins: [],
walls: rail.runs.map((run) => {
const [start, end] = railLine(run, GLASS_RAIL.shoe.width);
return { id: `${rail.id}-rail-${run.id}`, name: rail.name, start, end };
}),
})),
];
export const WINDOWS: FillingSpec[] = [
{ id: "gf-living-south", name: "Living room glazing", wall: "gf-south", type: "w-glazing-3000", offset: 0.5, sill: 0 },
{ id: "gf-dining-south", name: "Dining glazing", wall: "gf-south", type: "w-glazing-2400", offset: 7.9, sill: 0 },
{ id: "gf-kitchen-ribbon", name: "Kitchen ribbon window", wall: "gf-south-terrace", type: "w-ribbon-3000", offset: 0.1, sill: 1.05 },
{ id: "gf-kitchen-east", name: "Kitchen glazing", wall: "gf-east", type: "w-glazing-2400", offset: 0.6, sill: 0 },
{ id: "gf-utility-east", name: "Utility window", wall: "gf-east", type: "w-1200", offset: 6.2, sill: 1.2 },
{ id: "gf-utility-north", name: "Utility window", wall: "gf-north-terrace", type: "w-1500", offset: 1.1, sill: 1.2 },
{ id: "gf-hall-slot", name: "Hall sidelight", wall: "gf-north", type: "w-slot-ground", offset: 2.8, sill: 0 },
{ id: "gf-wc", name: "WC window", wall: "gf-north", type: "w-800", offset: 6.1, sill: 1.6 },
{ id: "gf-study", name: "Study window", wall: "gf-north", type: "w-1600", offset: 8.4, sill: 0.9 },
{ id: "gf-living-west", name: "Living room glazing", wall: "gf-west", type: "w-glazing-3000", offset: 5.0, sill: 0 },
{ id: "ff-bedroom-1", name: "Bedroom glazing", wall: "ff-south", type: "w-glazing-2300", offset: 0.6, sill: SLAB_DEPTH },
{ id: "ff-bedroom-2", name: "Bedroom glazing", wall: "ff-south", type: "w-glazing-2300", offset: 4.15, sill: SLAB_DEPTH },
{ id: "ff-main-bedroom", name: "Main bedroom glazing", wall: "ff-south", type: "w-glazing-2300", offset: 7.6, sill: SLAB_DEPTH },
{ id: "ff-landing-slot", name: "Landing slot window", wall: "ff-east", type: "w-slot-first", offset: 7.5, sill: SLAB_DEPTH },
{ id: "ff-study", name: "Study ribbon window", wall: "ff-north", type: "w-ribbon-4000", offset: 0.7, sill: SLAB_DEPTH + 0.9 },
{ id: "ff-bathroom-north", name: "Bathroom window", wall: "ff-north", type: "w-1600-high", offset: 8.1, sill: SLAB_DEPTH + 1.4 },
{ id: "ff-bathroom-west", name: "Bathroom window", wall: "ff-west", type: "w-1000", offset: 1.0, sill: SLAB_DEPTH + 1.2 },
{ id: "ff-bedroom-1-west", name: "Bedroom slot window", wall: "ff-west", type: "w-slot-first", offset: 6.8, sill: SLAB_DEPTH },
];
export const DOORS: FillingSpec[] = [
{ id: "gf-entrance", name: "Front door", wall: "gf-north", type: "d-entrance", offset: 3.7, sill: 0 },
{ id: "gf-garden-slider-west", name: "Garden sliding door", wall: "gf-south", type: "d-slider-left", offset: 3.9, sill: 0 },
{ id: "gf-garden-slider-east", name: "Garden sliding door", wall: "gf-south", type: "d-slider-right", offset: 5.7, sill: 0 },
{ id: "gf-study-door", name: "Study door", wall: "gf-spine", type: "d-interior", offset: 2.5, sill: 0 },
{ id: "gf-utility-door", name: "Utility door", wall: "gf-spine", type: "d-interior", offset: 11.6, sill: 0 },
{ id: "gf-wc-door", name: "WC door", wall: "gf-wc-hall", type: "d-interior", offset: 1.3, sill: 0 },
{ id: "ff-terrace-slider", name: "Terrace sliding door", wall: "ff-east", type: "d-slider-2000", offset: 2.8, sill: SLAB_DEPTH },
{ id: "ff-bedroom-1-door", name: "Bedroom door", wall: "ff-corridor", type: "d-interior", offset: 2.2, sill: 0 },
{ id: "ff-bedroom-2-door", name: "Bedroom door", wall: "ff-corridor", type: "d-interior", offset: 5.6, sill: 0 },
{ id: "ff-main-bedroom-door", name: "Main bedroom door", wall: "ff-corridor", type: "d-interior", offset: 7.4, sill: 0 },
{ id: "ff-bathroom-door", name: "Bathroom door", wall: "ff-bath", type: "d-interior", offset: 2.3, sill: 0 },
{ id: "ff-wardrobe-door", name: "Wardrobe door", wall: "ff-wardrobe", type: "d-interior", offset: 0.5, sill: 0 },
{ id: "ff-study-door", name: "Study door", wall: STUDY_GLAZING.wall, type: "d-glass", offset: STUDY_GLAZING.x1 - STUDY_GLAZING.frame.width - GLASS_DOOR_WIDTH - STUDY_GLAZING.x0, sill: 0 },
];
export const PASSAGES: PassageSpec[] = [
{ id: "gf-hall-passage", name: "Hall passage", wall: "gf-spine", offset: 9.5, width: 1.0, height: 2.4 },
];
export const SURROUNDED_WINDOWS = ["ff-bedroom-1", "ff-bedroom-2", "ff-main-bedroom"];
export const SILLED_WINDOWS = ["gf-kitchen-ribbon", "gf-utility-east", "gf-utility-north", "gf-wc", "gf-study", "ff-study", "ff-bathroom-north", "ff-bathroom-west"];
const GARDEN_TERRACE = rect(-FACADE_DEPTH, -5.2, FIRST_INTERIOR.x1 + FACADE_DEPTH, -FACADE_DEPTH);
const ENTRANCE_PATH = rect(5.95, GY + FACADE_DEPTH, 7.35, 16);
const DRIVEWAY = rect(-4.4, 3.6, -FACADE_DEPTH, 16);
export const SLABS: SlabSpec[] = [
{ id: "gf-slab", name: "Ground floor slab", storey: "ground", outline: rect(-FACADE_DEPTH, -FACADE_DEPTH, TERRACE_EDGE, GY + FACADE_DEPTH), layerSet: "Insulated ground slab", topOffset: 0, kind: slabPredefinedTypeEnum.baseSlab, layer: "ground" },
{ id: "ff-slab", name: "First floor slab", storey: "first", outline: FIRST_OUTLINE, holes: [STAIR_VOID], layerSet: "Upper floor slab", topOffset: 0, kind: slabPredefinedTypeEnum.floor, layer: "first" },
{
id: "cantilever-soffit", name: "Insulated soffit", storey: "first", layerSet: "Insulated soffit", topOffset: -SLAB_DEPTH, kind: slabPredefinedTypeEnum.notDefined, layer: "first",
outline: rect(-TIMBER_WALL - CAVITY, FIRST_INTERIOR.y0 - TIMBER_WALL - CAVITY, FIRST_INTERIOR.x1 + TIMBER_WALL + CAVITY, -FACADE_DEPTH),
},
{ id: "terrace-slab", name: "Roof terrace slab", storey: "first", outline: rect(FIRST_FLOOR_EAST_FACE, 0, GX, GY), layerSet: "Terrace roof slab", topOffset: 0, kind: slabPredefinedTypeEnum.roof, layer: "first" },
{ id: "terrace-deck", name: "Roof terrace decking", storey: "first", outline: rect(FIRST_FLOOR_EAST_FACE, -FACADE_DEPTH, TERRACE_EDGE, GY + FACADE_DEPTH), layerSet: "Decking", topOffset: DECK_TOP, kind: slabPredefinedTypeEnum.floor, layer: "first" },
{ id: "garden-terrace", name: "Garden terrace", storey: "ground", outline: GARDEN_TERRACE, layerSet: "Garden paving", topOffset: 0, kind: slabPredefinedTypeEnum.floor, layer: "garden" },
{ id: "entrance-path", name: "Entrance path", storey: "ground", outline: ENTRANCE_PATH, layerSet: "Garden paving", topOffset: 0, kind: slabPredefinedTypeEnum.floor, layer: "garden" },
{ id: "driveway", name: "Driveway", storey: "ground", outline: DRIVEWAY, layerSet: "Driveway", topOffset: 0, kind: slabPredefinedTypeEnum.floor, layer: "garden" },
];
export const ROOF = {
id: "roof", name: "Warm flat roof", layerSet: "Warm flat roof", baseOffset: FIRST_FLOOR_WALL_TOP,
outline: rect(FIRST_INTERIOR.x0 - PARAPET_INSET, FIRST_INTERIOR.y0 - PARAPET_INSET, FIRST_INTERIOR.x1 + PARAPET_INSET, FIRST_INTERIOR.y1 + PARAPET_INSET),
};
export const CARPORT = {
x0: -4.4,
x1: -FACADE_DEPTH,
y0: 3.6,
y1: GY + FACADE_DEPTH,
clearance: 2.6,
upstand: 0.02,
beam: { width: 0.08, depth: 0.3 },
joist: { width: 0.07, depth: 0.18, count: 9 },
ledger: 0.06,
post: 0.1,
};
export function thicknessOf(layerSet: string): number {
return LAYER_SETS.find((candidate) => candidate.name === layerSet)?.layers.reduce((sum, layer) => sum + layer.thickness, 0) ?? 0;
}
export const CARPORT_DECK_BOTTOM = CARPORT.clearance + CARPORT.beam.depth - CARPORT.upstand - thicknessOf("Carport roof");
export const TERRAIN_OUTLINE = rect(-60, -60, 75, 65);
export const HARD_SURFACES: Point[] = [
[-FACADE_DEPTH, -5.2], [FIRST_INTERIOR.x1 + FACADE_DEPTH, -5.2], [FIRST_INTERIOR.x1 + FACADE_DEPTH, -FACADE_DEPTH], [TERRACE_EDGE, -FACADE_DEPTH],
[TERRACE_EDGE, GY + FACADE_DEPTH], [7.35, GY + FACADE_DEPTH], [7.35, 16], [5.95, 16], [5.95, GY + FACADE_DEPTH], [-FACADE_DEPTH, GY + FACADE_DEPTH],
[-FACADE_DEPTH, 16], [-4.4, 16], [-4.4, 3.6], [-FACADE_DEPTH, 3.6],
];
export const TREES: TreeSpec[] = [
{ id: "tree-birch-1", species: "Silver birch", position: [-11, -10], height: 11, crown: 2.4, trunk: 0.16, foliage: "Birch foliage", bark: "Birch bark" },
{ id: "tree-birch-2", species: "Silver birch", position: [-7.5, -15], height: 9, crown: 2.0, trunk: 0.13, foliage: "Birch foliage", bark: "Birch bark" },
{ id: "tree-maple-1", species: "Norway maple", position: [25, 2], height: 10, crown: 3.4, trunk: 0.24, foliage: "Maple foliage", bark: "Bark" },
{ id: "tree-maple-2", species: "Norway maple", position: [20, 18], height: 8.5, crown: 3.0, trunk: 0.2, foliage: "Maple foliage", bark: "Bark" },
{ id: "tree-pine-1", species: "Scots pine", position: [-14, 13], height: 13, crown: 2.3, trunk: 0.22, foliage: "Pine needles", bark: "Bark" },
{ id: "tree-pine-2", species: "Scots pine", position: [3, 25], height: 12, crown: 2.1, trunk: 0.2, foliage: "Pine needles", bark: "Bark" },
{ id: "tree-maple-3", species: "Norway maple", position: [-17, -1], height: 7.5, crown: 2.8, trunk: 0.18, foliage: "Maple foliage", bark: "Bark" },
];
export const STAIR = {
risers: 16,
riser: FIRST_FLOOR_ELEVATION / 16,
going: 0.26,
bottomEdge: 9.4,
sides: [5.11, 6.11] as const,
treadDepth: 0.3,
treadThickness: 0.06,
voidEdge: 5.48,
};
export const ROOMS: RoomSpec[] = [
{ id: "room-living", number: "0.01", name: "Living, dining and kitchen", storey: "ground", outline: rect(0, 0, 14.2, 4.94), height: 2.9 },
{ id: "room-study", number: "0.02", name: "Study", storey: "ground", outline: rect(0, 5.06, 3.6, 8.4), height: 2.9 },
{ id: "room-wc", number: "0.03", name: "Shower room", storey: "ground", outline: rect(3.72, 5.06, 5.3, 8.4), height: 2.9 },
{ id: "room-hall", number: "0.04", name: "Entrance hall", storey: "ground", outline: rect(5.42, 5.06, 10.54, 8.4), height: 2.9 },
{ id: "room-utility", number: "0.05", name: "Utility and pantry", storey: "ground", outline: rect(10.66, 5.06, 14.2, 8.4), height: 2.9 },
{ id: "room-bedroom-1", number: "1.01", name: "Bedroom", storey: "first", outline: rect(0, -2.2, 3.4, 3.54), height: 3.0 },
{ id: "room-bedroom-2", number: "1.02", name: "Bedroom", storey: "first", outline: rect(3.52, -2.2, 6.9, 3.54), height: 3.0 },
{ id: "room-main-bedroom", number: "1.03", name: "Main bedroom", storey: "first", outline: rect(7.02, -2.2, 10.6, 3.54), height: 3.0 },
{ id: "room-corridor", number: "1.04", name: "Corridor", storey: "first", outline: rect(0, 3.66, 10.6, 4.94), height: 3.0 },
{ id: "room-landing", number: "1.05", name: "Landing", storey: "first", outline: rect(3.52, 5.06, 5.48, 6.14), height: 3.0 },
{ id: "room-bathroom", number: "1.06", name: "Bathroom", storey: "first", outline: rect(0, 5.06, 3.4, 8.4), height: 3.0 },
{ id: "room-wardrobe", number: "1.07", name: "Wardrobe", storey: "first", outline: rect(3.52, 6.26, 5.4, 8.4), height: 3.0 },
{ id: "room-office", number: "1.08", name: "Home office", storey: "first", outline: rect(5.52, 6.26, 10.6, 8.4), height: 3.0 },
];
const BOOLEAN = "IfcBoolean";
const U_VALUE = "IfcThermalTransmittanceMeasure";
const EXTERNAL = { name: "IsExternal", value: true, type: BOOLEAN };
const INTERNAL = { name: "IsExternal", value: false, type: BOOLEAN };
const NOT_LOAD_BEARING = { name: "LoadBearing", value: false, type: BOOLEAN };
const LOAD_BEARING = { name: "LoadBearing", value: true, type: BOOLEAN };
function wallsOf(layerSet: string, external: boolean): string[] {
return WALL_RUNS.filter((run) => run.layerSet === layerSet && run.external === external).flatMap((run) => run.walls.map((wall) => wall.id));
}
function uValue(value: number): PropertySetSpec["properties"][number] {
return { name: "ThermalTransmittance", value, type: U_VALUE };
}
export const PROPERTY_SETS: PropertySetSpec[] = [
{ name: "Pset_WallCommon", elements: wallsOf("Rendered masonry wall", true), properties: [EXTERNAL, LOAD_BEARING, uValue(0.12)] },
{ name: "Pset_WallCommon", elements: wallsOf("Timber frame wall", true), properties: [EXTERNAL, LOAD_BEARING, uValue(0.11)] },
{ name: "Pset_WallCommon", elements: wallsOf("Parapet", true), properties: [EXTERNAL, NOT_LOAD_BEARING] },
{ name: "Pset_WallCommon", elements: wallsOf("Partition", false), properties: [INTERNAL, NOT_LOAD_BEARING, { name: "AcousticRating", value: "Rw 45 dB", type: "IfcLabel" }] },
{ name: "Pset_WallCommon", elements: [...wallsOf("Glass partition", false), ...wallsOf("Glass balustrade", false)], properties: [INTERNAL, NOT_LOAD_BEARING] },
{ name: "Pset_SlabCommon", elements: ["gf-slab", "terrace-slab"], properties: [EXTERNAL, LOAD_BEARING, uValue(0.13)] },
{ name: "Pset_SlabCommon", elements: ["cantilever-soffit"], properties: [EXTERNAL, NOT_LOAD_BEARING, uValue(0.19)] },
{ name: "Pset_RoofCommon", elements: [ROOF.id], properties: [EXTERNAL, uValue(0.07)] },
{ name: "Pset_WindowCommon", elements: WINDOWS.map((window) => window.id), properties: [EXTERNAL, uValue(0.75), { name: "GlazingAreaFraction", value: 0.88, type: "IfcPositiveRatioMeasure" }] },
{ name: "Pset_DoorCommon", elements: ["gf-entrance", "gf-garden-slider-west", "gf-garden-slider-east", "ff-terrace-slider"], properties: [EXTERNAL, uValue(0.8)] },
];
house/details.ts
import IFC = Bit.Inputs.IFC;
import Base = Bit.Inputs.Base;
import {
BOARD, CANTILEVER_DRIP, CARPORT, CARPORT_DECK_BOTTOM, CAVITY, COPING, DECK_TOP, GLASS_RAIL, RAILS, STUDY_GLAZING, DOOR_TYPES, DOORS, FACADE_DEPTH, FIRST_FLOOR_ELEVATION, FIRST_FLOOR_EAST_FACE, FIRST_FLOOR_WALL_TOP, FIRST_INTERIOR,
GROUND_INTERIOR, PARAPET_INSET, PARAPET_TOP, ROOF_DEPTH, SILLED_WINDOWS, SLAB_DEPTH, STAIR, SURROUNDED_WINDOWS, TIMBER_WALL, WALL_RUNS, WINDOW_TYPES, WINDOWS, railLine, thicknessOf,
} from "./house-plan";
import type { AssemblySpec, DetailSpec, FacadeLine, FacadeOpening, FacadeSide, FillingSpec, WallSide } from "./house-types";
const { memberPredefinedTypeEnum } = IFC;
const BOARD_PLANE = TIMBER_WALL + CAVITY + BOARD.thickness / 2;
const FLASHING = { height: 0.06, projection: 0.03 };
const ABOVE_FLASHING = -SLAB_DEPTH + FLASHING.height;
const SURROUND = { thickness: 0.03, projection: 0.15 };
const SILL = { thickness: 0.03, drip: 0.04 };
const FRAME_DEPTH = 0.09;
const MIN_PIECE = 0.02;
const CORNER_BOARD_LIMIT = FIRST_INTERIOR.y0 - TIMBER_WALL - CAVITY;
const ROOF_TOP = FIRST_FLOOR_WALL_TOP + ROOF_DEPTH;
const SOLAR = { width: 1.13, length: 1.72, thickness: 0.04, lift: 0.15, tilt: 10, gap: 0.05, rows: [-1.2, 1.6], columns: 7, firstColumn: 1.2 };
const SOLAR_RAIL = { width: 0.06, inset: 0.1, clearance: 0.003 };
const STRINGER = { thickness: 0.012, depth: 0.22, below: 0.08, past: 0.03 };
const HANDRAIL = { radius: 0.021, height: 0.9, wallGap: 0.02 };
const BALUSTER = { radius: 0.008, below: 0.05, underRail: 0.015 };
const DOWNPIPE = { radius: 0.05, standoff: 0.09, positions: [0.45, 10.5] };
const HOPPER = { height: 0.25, width: 0.22, depth: 0.17, belowCoping: 0.51 };
const CANOPY = {
from: 5.75, to: 8.35, projection: 1.4, underside: 2.75, cover: 0.02, upstand: 0.02,
fascia: { thickness: 0.03, depth: 0.14 },
slat: { width: 0.06, gap: 0.02, depth: 0.03, above: 0.03 },
lights: [6.665, 7.915], light: { radius: 0.035, depth: 0.01, out: 0.7 },
};
const SOLAR_ARRAY = "solar-array";
const FIRST_SIDES: FacadeSide[] = [
{ wall: "ff-south", along: "x", line: FIRST_INTERIOR.y0, outward: -1, from: -FACADE_DEPTH, to: FIRST_INTERIOR.x1 + FACADE_DEPTH, start: FIRST_INTERIOR.x0, direction: 1 },
{ wall: "ff-north", along: "x", line: FIRST_INTERIOR.y1, outward: 1, from: -FACADE_DEPTH, to: FIRST_INTERIOR.x1 + FACADE_DEPTH, start: FIRST_INTERIOR.x1, direction: -1 },
{ wall: "ff-west", along: "y", line: FIRST_INTERIOR.x0, outward: -1, from: CORNER_BOARD_LIMIT, to: FIRST_INTERIOR.y1 + TIMBER_WALL + CAVITY, start: FIRST_INTERIOR.y1, direction: -1 },
{ wall: "ff-east", along: "y", line: FIRST_INTERIOR.x1, outward: 1, from: CORNER_BOARD_LIMIT, to: FIRST_INTERIOR.y1 + TIMBER_WALL + CAVITY, start: FIRST_INTERIOR.y0, direction: 1 },
];
function sideOfWall(wall: string): WallSide {
const run = WALL_RUNS.find((candidate) => candidate.walls.some((spec) => spec.id === wall));
const spec = run?.walls.find((candidate) => candidate.id === wall);
if (!run || !spec) {
throw new Error(`The plan has no wall '${wall}'`);
}
const along = spec.start[1] === spec.end[1] ? "x" : "y";
const axis = along === "x" ? 0 : 1;
const across = along === "x" ? 1 : 0;
const direction = spec.end[axis] > spec.start[axis] ? 1 : -1;
const outward = along === "x" ? -direction as 1 | -1 : direction;
const thickness = run.layerSet === "Timber frame wall" ? TIMBER_WALL : FACADE_DEPTH;
return { wall, along, line: spec.start[across], outward, start: spec.start[axis], direction, thickness, baseOffset: run.baseOffset };
}
function sizeOf(filling: FillingSpec): { width: number; height: number } {
const type = WINDOW_TYPES.find((candidate) => candidate.id === filling.type) ?? DOOR_TYPES.find((candidate) => candidate.id === filling.type);
if (!type) {
throw new Error(`The plan has no type '${filling.type}'`);
}
return type;
}
function openingOf(filling: FillingSpec, side: FacadeLine & { baseOffset?: number }): FacadeOpening {
const { width, height } = sizeOf(filling);
const from = side.direction > 0 ? side.start + filling.offset : side.start - filling.offset - width;
const bottom = filling.sill + (side.baseOffset ?? -SLAB_DEPTH);
return { id: filling.id, from, to: from + width, bottom, top: bottom + height };
}
function point(side: FacadeLine, u: number, depth: number, z: number): Base.Point3 {
const across = side.line + side.outward * depth;
return side.along === "x" ? [u, across, z] : [across, u, z];
}
function boardBottom(side: FacadeSide, to: number): number {
if (side.wall === "ff-south") {
return CANTILEVER_DRIP;
}
if (side.wall === "ff-north") {
return to > FIRST_FLOOR_EAST_FACE ? DECK_TOP : ABOVE_FLASHING;
}
if (to <= -FACADE_DEPTH) {
return CANTILEVER_DRIP;
}
return side.wall === "ff-west" ? ABOVE_FLASHING : DECK_TOP;
}
function cutAround(bottom: number, top: number, from: number, to: number, openings: readonly FacadeOpening[]): [number, number][] {
let pieces: [number, number][] = [[bottom, top]];
for (const opening of openings) {
if (to <= opening.from || from >= opening.to) {
continue;
}
pieces = pieces.flatMap(([low, high]): [number, number][] => {
if (high <= opening.bottom || low >= opening.top) {
return [[low, high]];
}
const below: [number, number] = [low, Math.min(high, opening.bottom)];
const above: [number, number] = [Math.max(low, opening.top), high];
return [below, above].filter(([a, b]) => b - a > MIN_PIECE);
});
}
return pieces;
}
function facadeOpenings(side: FacadeSide): FacadeOpening[] {
return [...WINDOWS, ...DOORS].filter((filling) => filling.wall === side.wall).map((filling) => openingOf(filling, side));
}
function claddingOf(side: FacadeSide): string {
return `${side.wall}-cladding`;
}
function facingOf(side: FacadeSide): string {
const facing = side.wall.replace(/^ff-/, "");
return facing.charAt(0).toUpperCase() + facing.slice(1);
}
function claddingBoards(): DetailSpec[] {
const boards: DetailSpec[] = [];
const pitch = BOARD.width + BOARD.gap;
for (const side of FIRST_SIDES) {
const openings = facadeOpenings(side);
const count = Math.floor((side.to - side.from + BOARD.gap) / pitch);
const first = side.from + (side.to - side.from - (count * pitch - BOARD.gap)) / 2;
for (let index = 0; index < count; index++) {
const from = first + index * pitch;
const to = from + BOARD.width;
const middle = (from + to) / 2;
cutAround(boardBottom(side, to), PARAPET_TOP, from, to, openings).forEach(([bottom, top], piece) => {
boards.push({
id: `${side.wall}-board-${index}-${piece}`, name: "Cladding board", storey: "first", layer: "first", material: "Charred larch", assembly: claddingOf(side),
start: point(side, middle, BOARD_PLANE, bottom), end: point(side, middle, BOARD_PLANE, top),
width: BOARD.width, depth: BOARD.thickness, rotation: side.along === "y" ? 90 : 0, kind: memberPredefinedTypeEnum.plate,
});
});
}
}
return boards;
}
function flashings(): DetailSpec[] {
const outer = FACADE_DEPTH + FLASHING.projection;
const middle = (TIMBER_WALL + outer) / 2;
const width = outer - TIMBER_WALL;
const z = -SLAB_DEPTH + FLASHING.height / 2;
const [, north, west] = FIRST_SIDES as [FacadeSide, FacadeSide, FacadeSide, FacadeSide];
const flashing = (id: string, start: Base.Point3, end: Base.Point3): DetailSpec => ({
id, name: "Floor line flashing", storey: "first", layer: "first", material: "Anthracite aluminium", start, end, width, depth: FLASHING.height,
});
return [
flashing("flashing-north", point(north, -outer, middle, z), point(north, FIRST_FLOOR_EAST_FACE, middle, z)),
flashing("flashing-west", point(west, -FACADE_DEPTH, middle, z), point(west, FIRST_INTERIOR.y1 + TIMBER_WALL, middle, z)),
];
}
function windowSurrounds(): DetailSpec[] {
const t = SURROUND.thickness;
const inner = TIMBER_WALL / 2 + FRAME_DEPTH / 2;
const outer = FACADE_DEPTH + SURROUND.projection;
const middle = (inner + outer) / 2;
const width = outer - inner;
const [side] = FIRST_SIDES as [FacadeSide];
return WINDOWS.filter((window) => SURROUNDED_WINDOWS.includes(window.id)).flatMap((window) => {
const opening = openingOf(window, side);
const plate = (suffix: string, start: Base.Point3, end: Base.Point3, horizontal: boolean): DetailSpec => ({
id: `${window.id}-surround-${suffix}`, name: "Window surround", storey: "first", layer: "first", material: "Anthracite aluminium",
start, end, width: horizontal ? width : t, depth: horizontal ? t : width,
});
return [
plate("head", point(side, opening.from, middle, opening.top - t / 2), point(side, opening.to, middle, opening.top - t / 2), true),
plate("sill", point(side, opening.from, middle, opening.bottom + t / 2), point(side, opening.to, middle, opening.bottom + t / 2), true),
plate("left", point(side, opening.from + t / 2, middle, opening.bottom + t), point(side, opening.from + t / 2, middle, opening.top - t), false),
plate("right", point(side, opening.to - t / 2, middle, opening.bottom + t), point(side, opening.to - t / 2, middle, opening.top - t), false),
];
});
}
function windowSills(): DetailSpec[] {
return WINDOWS.filter((window) => SILLED_WINDOWS.includes(window.id)).map((window): DetailSpec => {
const side = sideOfWall(window.wall);
const opening = openingOf(window, side);
const inner = side.thickness / 2 + FRAME_DEPTH / 2;
const outer = FACADE_DEPTH + SILL.drip;
const z = opening.bottom + SILL.thickness / 2;
const storey = window.wall.startsWith("ff") ? "first" : "ground";
return {
id: `${window.id}-sill`, name: "Window sill", storey, layer: storey, material: "Anthracite aluminium",
start: point(side, opening.from, (inner + outer) / 2, z), end: point(side, opening.to, (inner + outer) / 2, z), width: outer - inner, depth: SILL.thickness,
};
});
}
function copings(): DetailSpec[] {
const inner = PARAPET_INSET - COPING.drip;
const outer = FACADE_DEPTH + COPING.drip;
const middle = (inner + outer) / 2;
const z = PARAPET_TOP + COPING.thickness / 2;
return FIRST_SIDES.map((side): DetailSpec => {
const [from, to] = side.along === "x" ? [-outer, FIRST_INTERIOR.x1 + outer] : [FIRST_INTERIOR.y0 - inner, FIRST_INTERIOR.y1 + inner];
return {
id: `${side.wall}-coping`, name: "Parapet coping", storey: "first", layer: "first", material: "Anthracite aluminium",
start: point(side, from, middle, z), end: point(side, to, middle, z), width: outer - inner, depth: COPING.thickness,
};
});
}
function solarArray(): DetailSpec[] {
const tilt = SOLAR.tilt * Math.PI / 180;
const slope = Math.tan(tilt);
const run = SOLAR.length * Math.cos(tilt);
const base = ROOF_TOP + SOLAR.lift;
const halfDepth = SOLAR.thickness / 2 / Math.cos(tilt);
const left = SOLAR.firstColumn - SOLAR.width / 2;
const right = SOLAR.firstColumn + (SOLAR.columns - 1) * (SOLAR.width + SOLAR.gap) + SOLAR.width / 2;
return SOLAR.rows.flatMap((y, row) => {
const rail = (id: string, near: number): DetailSpec => {
const top = base + (near - y) * slope - halfDepth - SOLAR_RAIL.clearance;
const middle = near + SOLAR_RAIL.width / 2;
return {
id, name: "Mounting rail", storey: "first", layer: "roof", material: "Anthracite aluminium", assembly: SOLAR_ARRAY,
start: [left, middle, (ROOF_TOP + top) / 2], end: [right, middle, (ROOF_TOP + top) / 2], width: SOLAR_RAIL.width, depth: top - ROOF_TOP,
};
};
const panels = Array.from({ length: SOLAR.columns }, (_, column): DetailSpec => {
const x = SOLAR.firstColumn + column * (SOLAR.width + SOLAR.gap);
return {
id: `solar-panel-${row}-${column}`, name: "Solar panel", storey: "first", layer: "roof", material: "Solar module", assembly: SOLAR_ARRAY,
start: [x, y, base], end: [x, y + run, base + run * slope], width: SOLAR.width, depth: SOLAR.thickness, kind: memberPredefinedTypeEnum.plate,
};
});
return [...panels, rail(`solar-rail-front-${row}`, y + SOLAR_RAIL.inset), rail(`solar-rail-back-${row}`, y + run - SOLAR_RAIL.inset - SOLAR_RAIL.width)];
});
}
function stairParts(): DetailSpec[] {
const [near, far] = STAIR.sides;
const slope = STAIR.riser / STAIR.going;
const nosing = (x: number): number => STAIR.riser + (STAIR.bottomEdge - x) * slope;
const treadMiddle = (step: number): number => STAIR.bottomEdge - (step - 0.5) * STAIR.going;
const steps = Array.from({ length: STAIR.risers - 1 }, (_, index) => index + 1);
const top = STAIR.voidEdge + 0.07;
const bottom = STAIR.bottomEdge + STRINGER.past;
const railBottom = STAIR.bottomEdge;
const railY = far + STRINGER.thickness + BALUSTER.radius;
const stringerY = far + STRINGER.thickness / 2;
const steel = { storey: "ground", layer: "ground", material: "Blackened steel" } as const;
return [
...steps.map((step): DetailSpec => ({
id: `stair-tread-${step}`, name: `Tread ${step}`, storey: "ground", layer: "ground", material: "Oak",
start: [treadMiddle(step), near, step * STAIR.riser - STAIR.treadThickness / 2], end: [treadMiddle(step), far, step * STAIR.riser - STAIR.treadThickness / 2],
width: STAIR.treadDepth, depth: STAIR.treadThickness,
})),
{
...steel, id: "stair-stringer", name: "Stair stringer", kind: memberPredefinedTypeEnum.stringer, width: STRINGER.thickness, depth: STRINGER.depth,
start: [bottom, stringerY, nosing(bottom) - STRINGER.below], end: [top, stringerY, nosing(top) - STRINGER.below],
},
...([["stair-handrail-wall", near + HANDRAIL.wallGap], ["stair-handrail-open", railY]] as const).map(([id, y]): DetailSpec => ({
...steel, id, name: "Handrail", radius: HANDRAIL.radius,
start: [railBottom, y, nosing(railBottom) + HANDRAIL.height], end: [top, y, nosing(top) + HANDRAIL.height],
})),
...steps.map((step): DetailSpec => ({
...steel, id: `stair-baluster-${step}`, name: "Baluster", radius: BALUSTER.radius, kind: memberPredefinedTypeEnum.post,
start: [treadMiddle(step), railY, step * STAIR.riser - BALUSTER.below],
end: [treadMiddle(step), railY, nosing(treadMiddle(step)) + HANDRAIL.height - HANDRAIL.radius - BALUSTER.underRail],
})),
];
}
function rainwater(): DetailSpec[] {
const y = GROUND_INTERIOR.y1 + FACADE_DEPTH + DOWNPIPE.standoff;
const hopperBottom = FIRST_FLOOR_ELEVATION + PARAPET_TOP - HOPPER.belowCoping;
const aluminium = { storey: "ground", layer: "ground", material: "Anthracite aluminium" } as const;
return DOWNPIPE.positions.flatMap((x, index): DetailSpec[] => [
{ ...aluminium, id: `downpipe-${index}`, name: "Downpipe", radius: DOWNPIPE.radius, start: [x, y, 0], end: [x, y, hopperBottom] },
{ ...aluminium, id: `rainwater-hopper-${index}`, name: "Rainwater hopper", start: [x, y, hopperBottom], end: [x, y, hopperBottom + HOPPER.height], width: HOPPER.width, depth: HOPPER.depth },
]);
}
function entranceCanopy(): DetailSpec[] {
const { from, to, fascia, slat, light } = CANOPY;
const wall = GROUND_INTERIOR.y1 + FACADE_DEPTH;
const front = wall + CANOPY.projection;
const t = fascia.thickness;
const fasciaZ = CANOPY.underside + fascia.depth / 2;
const coverZ = CANOPY.underside + fascia.depth - CANOPY.upstand - CANOPY.cover / 2;
const slatZ = CANOPY.underside + slat.above + slat.depth / 2;
const inner = { from: from + t, to: to - t, front: front - t };
const pitch = slat.width + slat.gap;
const count = Math.floor((inner.to - inner.from + slat.gap) / pitch);
const first = inner.from + (inner.to - inner.from - (count * pitch - slat.gap)) / 2 + slat.width / 2;
const metal = { storey: "ground", layer: "ground", material: "Anthracite aluminium" } as const;
return [
{ ...metal, id: "canopy-fascia-front", name: "Canopy fascia", start: [from, front - t / 2, fasciaZ], end: [to, front - t / 2, fasciaZ], width: t, depth: fascia.depth, beam: true },
{ ...metal, id: "canopy-fascia-left", name: "Canopy fascia", start: [from + t / 2, wall, fasciaZ], end: [from + t / 2, inner.front, fasciaZ], width: t, depth: fascia.depth, beam: true },
{ ...metal, id: "canopy-fascia-right", name: "Canopy fascia", start: [to - t / 2, wall, fasciaZ], end: [to - t / 2, inner.front, fasciaZ], width: t, depth: fascia.depth, beam: true },
{
...metal, id: "canopy-cover", name: "Canopy cover", kind: memberPredefinedTypeEnum.plate,
start: [inner.from, (wall + inner.front) / 2, coverZ], end: [inner.to, (wall + inner.front) / 2, coverZ], width: inner.front - wall, depth: CANOPY.cover,
},
...Array.from({ length: count }, (_, index): DetailSpec => ({
id: `canopy-slat-${index}`, name: "Canopy soffit slat", storey: "ground", layer: "ground", material: "Smoked oak",
start: [first + index * pitch, wall, slatZ], end: [first + index * pitch, inner.front, slatZ], width: slat.width, depth: slat.depth,
})),
...CANOPY.lights.map((x, index): DetailSpec => ({
id: `canopy-light-${index}`, name: "Downlight", storey: "ground", layer: "ground", material: "Brushed stainless steel", radius: light.radius,
start: [x, wall + light.out, CANOPY.underside + slat.above - light.depth], end: [x, wall + light.out, CANOPY.underside + slat.above],
})),
];
}
function carportFrame(): DetailSpec[] {
const { x0, x1, y0, y1, beam, joist } = CARPORT;
const beamZ = CARPORT.clearance + beam.depth / 2;
const joistZ = CARPORT_DECK_BOTTOM - joist.depth / 2;
const inner = { x0: x0 + beam.width, y0: y0 + beam.width, y1: y1 - beam.width };
const ledgerX = x1 - CARPORT.ledger / 2;
const spacing = (inner.y1 - inner.y0) / (joist.count + 1);
const steel = { storey: "ground", layer: "garden", material: "Blackened steel", beam: true } as const;
return [
{ ...steel, id: "carport-beam-outer", name: "Carport edge beam", start: [x0 + beam.width / 2, y0, beamZ], end: [x0 + beam.width / 2, y1, beamZ], width: beam.width, depth: beam.depth },
{ ...steel, id: "carport-beam-south", name: "Carport edge beam", start: [inner.x0, y0 + beam.width / 2, beamZ], end: [x1, y0 + beam.width / 2, beamZ], width: beam.width, depth: beam.depth },
{ ...steel, id: "carport-beam-north", name: "Carport edge beam", start: [inner.x0, y1 - beam.width / 2, beamZ], end: [x1, y1 - beam.width / 2, beamZ], width: beam.width, depth: beam.depth },
{ ...steel, id: "carport-ledger", name: "Carport wall ledger", start: [ledgerX, inner.y0, joistZ], end: [ledgerX, inner.y1, joistZ], width: CARPORT.ledger, depth: joist.depth },
...Array.from({ length: joist.count }, (_, index): DetailSpec => ({
id: `carport-joist-${index}`, name: "Carport joist", storey: "ground", layer: "garden", material: "Glulam spruce", beam: true,
start: [inner.x0, inner.y0 + (index + 1) * spacing, joistZ], end: [x1 - CARPORT.ledger, inner.y0 + (index + 1) * spacing, joistZ], width: joist.width, depth: joist.depth,
})),
];
}
function glassRails(): DetailSpec[] {
const { shoe, cap, glass } = GLASS_RAIL;
return RAILS.flatMap((rail) => rail.runs.flatMap((run): DetailSpec[] => {
const [shoeStart, shoeEnd] = railLine(run, shoe.width);
const [capStart, capEnd] = railLine(run, cap.width);
const shoeZ = rail.base + shoe.depth / 2;
const capZ = rail.base + shoe.depth + glass + cap.depth / 2;
return [
{
id: `${rail.id}-shoe-${run.id}`, name: "Balustrade base shoe", storey: "first", layer: "first", material: "Anthracite aluminium",
start: [...shoeStart, shoeZ], end: [...shoeEnd, shoeZ], width: shoe.width, depth: shoe.depth,
},
{
id: `${rail.id}-cap-${run.id}`, name: "Balustrade handrail", storey: "first", layer: "first", material: "Brushed stainless steel",
start: [...capStart, capZ], end: [...capEnd, capZ], width: cap.width, depth: cap.depth,
},
];
}));
}
function studyGlazing(): DetailSpec[] {
const { wall, line, x0, panes, frame } = STUDY_GLAZING;
const w = frame.width;
const studyDoor = DOORS.find((filling) => filling.wall === wall);
if (!studyDoor) {
throw new Error(`The plan has no door in '${wall}'`);
}
const door = openingOf(studyDoor, { wall, along: "x", line, outward: 1, start: x0, direction: 1, baseOffset: 0 });
const pane = (door.from - w - (x0 + w) - (panes - 1) * w) / panes;
const mullions = Array.from({ length: panes - 1 }, (_, index) => x0 + w + (index + 1) * pane + index * w);
const verticals = [x0, ...mullions, door.from - w, door.to];
const bays = verticals.flatMap((left, index) => {
const right = verticals[index + 1];
return right === undefined ? [] : [{ from: left + w, to: right, door: left + w === door.from }];
});
const faces = [line - thicknessOf("Glass partition") / 2 - frame.depth / 2, line + thicknessOf("Glass partition") / 2 + frame.depth / 2];
const steel = { storey: "first", layer: "first", material: "Blackened steel" } as const;
return faces.flatMap((y, face): DetailSpec[] => [
...verticals.map((left, index): DetailSpec => ({
...steel, id: `study-mullion-${face}-${index}`, name: "Glazing mullion", kind: memberPredefinedTypeEnum.mullion,
start: [left + w / 2, y, 0], end: [left + w / 2, y, FIRST_FLOOR_WALL_TOP], width: w, depth: frame.depth,
})),
...bays.flatMap((bay, index): DetailSpec[] => {
const rails: [string, number][] = [["transom", door.top + w / 2], ["head", FIRST_FLOOR_WALL_TOP - w / 2], ...(bay.door ? [] : [["sill", w / 2] as [string, number]])];
return rails.map(([kind, z]): DetailSpec => ({
...steel, id: `study-${kind}-${face}-${index}`, name: "Glazing rail", start: [bay.from, y, z], end: [bay.to, y, z], width: frame.depth, depth: w,
}));
}),
]);
}
export function houseAssemblies(): AssemblySpec[] {
return [
...FIRST_SIDES.map((side) => ({ id: claddingOf(side), name: `${facingOf(side)} cladding`, objectType: "Cladding" })),
{ id: SOLAR_ARRAY, name: "Solar array", objectType: "Solar array" },
];
}
export function houseDetails(): DetailSpec[] {
return [...claddingBoards(), ...flashings(), ...windowSurrounds(), ...windowSills(), ...copings(), ...solarArray(), ...stairParts(), ...rainwater(), ...entranceCanopy(), ...carportFrame(), ...glassRails(), ...studyGlazing()];
}
house/build-house.ts
import IFC = Bit.Inputs.IFC;
import Base = Bit.Inputs.Base;
import {
CARPORT, CARPORT_DECK_BOTTOM, DOOR_TYPES, DOORS, FIRST_FLOOR_ELEVATION, HARD_SURFACES, LAYER_SETS, MATERIALS, PASSAGES, PROPERTY_SETS, ROOF, ROOMS, SLABS, TERRAIN_OUTLINE, TREES, WALL_RUNS, WINDOW_TYPES, WINDOWS, rect,
} from "./house-plan";
import { houseAssemblies, houseDetails } from "./details";
import type { DetailSpec, HouseIfc, HouseModel, Layer } from "./house-types";
type Model = IFC.IfcModelPointer;
const FRAME = { frameMaterial: "Anthracite aluminium", glassMaterial: "Clear glass", frameThickness: 0.07, frameDepth: 0.09, glassThickness: 0.03 };
const LINING = { liningThickness: 0.06, liningDepth: 0.12, panelThickness: 0.05 };
export class HouseAuthor {
private model!: Model;
private readonly layers = new Map<string, Layer>();
private readonly ifc: HouseIfc;
private readonly progress: (message: string) => void;
constructor(ifc: HouseIfc, progress: (message: string) => void) {
this.ifc = ifc;
this.progress = progress;
}
async build(): Promise<HouseModel> {
const ifc = this.ifc;
this.progress("Setting up the project");
this.model = await ifc.model.create({ name: "Family house", lengthUnit: IFC.lengthUnitEnum.metre, seed: "ifc-family-house" });
this.model = await ifc.spatial.addStorey({ model: this.model, id: "ground", name: "Ground floor", elevation: 0 });
this.model = await ifc.spatial.addStorey({ model: this.model, id: "first", name: "First floor", elevation: FIRST_FLOOR_ELEVATION });
await this.addMaterials();
this.progress("Raising the walls");
await this.addWalls();
this.progress("Laying the floors");
await this.addSlabs();
this.progress("Fitting windows and doors");
await this.addWindowsAndDoors();
this.progress("Roofing and the carport");
await this.addRoof();
await this.addCarport();
this.progress("Cladding, flashings, the stair and the solar array");
const details = houseDetails();
await this.addDetails(details);
await this.addAssemblies(details);
this.progress("Shaping the garden");
await this.addSite();
this.progress("Naming the rooms");
await this.addRooms();
await this.addProperties();
this.progress("Measuring quantities");
this.model = await ifc.quantities.compute({ model: this.model });
return { model: this.model, layers: await this.layersByGlobalId(), rooms: ROOMS };
}
private remember(id: string, layer: Layer): string {
this.layers.set(id, layer);
return id;
}
private async layersByGlobalId(): Promise<Map<string, Layer>> {
const entries = await Promise.all([...this.layers].map(async ([id, layer]) => [await this.ifc.model.globalIdOf({ model: this.model, id }), layer] as const));
return new Map(entries);
}
private async addMaterials(): Promise<void> {
const ifc = this.ifc;
for (const material of MATERIALS) {
this.model = await ifc.materials.add({ model: this.model, ...material });
}
for (const layerSet of LAYER_SETS) {
this.model = await ifc.materials.addLayerSet({ model: this.model, ...layerSet });
}
for (const type of WINDOW_TYPES) {
this.model = await ifc.windows.addType({ model: this.model, ...type, ...FRAME });
}
for (const type of DOOR_TYPES) {
this.model = await ifc.doors.addType({
model: this.model, id: type.id, name: type.name, width: type.width, height: type.height, ...LINING, operation: type.operation,
liningMaterial: type.lining, panelMaterial: type.panel, handle: type.handle, handleMaterial: type.handleMaterial,
});
}
}
private async addWalls(): Promise<void> {
const ifc = this.ifc;
for (const run of WALL_RUNS) {
const layer: Layer = run.storey === "ground" ? "ground" : "first";
for (const wall of run.walls) {
this.model = await ifc.walls.add({
model: this.model, storey: run.storey, id: this.remember(wall.id, layer), name: wall.name, start: wall.start, end: wall.end,
height: wall.height ?? run.height, baseOffset: run.baseOffset, layerSet: run.layerSet, alignment: run.alignment,
predefinedType: run.external ? IFC.wallPredefinedTypeEnum.standard : IFC.wallPredefinedTypeEnum.partitioning,
});
}
for (const [wall, other] of run.joins) {
this.model = await ifc.walls.connect({ model: this.model, wall, other });
}
}
}
private async addSlabs(): Promise<void> {
for (const slab of SLABS) {
this.model = await this.ifc.slabs.add({
model: this.model, storey: slab.storey, id: this.remember(slab.id, slab.layer), name: slab.name,
outline: slab.outline, holes: slab.holes, layerSet: slab.layerSet, topOffset: slab.topOffset, predefinedType: slab.kind,
});
}
}
private layerOfWall(wall: string): Layer {
return this.layers.get(wall) ?? "ground";
}
private async addWindowsAndDoors(): Promise<void> {
const ifc = this.ifc;
for (const window of WINDOWS) {
this.model = await ifc.windows.add({ model: this.model, wall: window.wall, windowType: window.type, id: this.remember(window.id, this.layerOfWall(window.wall)), name: window.name, offset: window.offset, sill: window.sill });
}
for (const door of DOORS) {
this.model = await ifc.doors.add({ model: this.model, wall: door.wall, doorType: door.type, id: this.remember(door.id, this.layerOfWall(door.wall)), name: door.name, offset: door.offset, sill: door.sill });
}
for (const passage of PASSAGES) {
this.model = await ifc.openings.add({ model: this.model, wall: passage.wall, id: passage.id, name: passage.name, offset: passage.offset, sill: 0, width: passage.width, height: passage.height });
}
}
private async addRoof(): Promise<void> {
this.model = await this.ifc.roofs.add({
model: this.model, storey: "first", id: this.remember(ROOF.id, "roof"), name: ROOF.name,
outline: ROOF.outline, kind: IFC.roofKindEnum.flat, layerSet: ROOF.layerSet, baseOffset: ROOF.baseOffset,
});
}
private async addCarport(): Promise<void> {
const ifc = this.ifc;
const { x0, x1, y0, y1, beam, post } = CARPORT;
this.model = await ifc.roofs.add({
model: this.model, storey: "ground", id: this.remember("carport-roof", "garden"), name: "Carport roof",
outline: rect(x0 + beam.width, y0 + beam.width, x1, y1 - beam.width), kind: IFC.roofKindEnum.flat, layerSet: "Carport roof", baseOffset: CARPORT_DECK_BOTTOM,
});
const corners: Base.Point2[] = [[x0 + post / 2, y0 + post / 2], [x0 + post / 2, y1 - post / 2]];
for (const [index, position] of corners.entries()) {
this.model = await ifc.columns.add({
model: this.model, storey: "ground", id: this.remember(`carport-post-${index + 1}`, "garden"), name: "Carport post",
position, height: CARPORT.clearance, width: post, depth: post, material: "Blackened steel",
});
}
}
private async addDetails(details: DetailSpec[]): Promise<void> {
const { profileKindEnum, memberPredefinedTypeEnum } = IFC;
for (const detail of details) {
const common = {
model: this.model, storey: detail.storey, id: this.remember(detail.id, detail.layer), name: detail.name, start: detail.start, end: detail.end, material: detail.material,
profile: detail.radius === undefined ? profileKindEnum.rectangle : profileKindEnum.circle, width: detail.width, depth: detail.depth, radius: detail.radius, rotation: detail.rotation ?? 0,
};
this.model = detail.beam
? await this.ifc.beams.add(common)
: await this.ifc.members.add({ ...common, predefinedType: detail.kind ?? memberPredefinedTypeEnum.member });
}
}
private async addAssemblies(details: DetailSpec[]): Promise<void> {
const { elementAssemblyPredefinedTypeEnum } = IFC;
for (const assembly of houseAssemblies()) {
this.model = await this.ifc.assemblies.add({
model: this.model, id: assembly.id, name: assembly.name, parts: details.filter((detail) => detail.assembly === assembly.id).map((detail) => detail.id),
predefinedType: elementAssemblyPredefinedTypeEnum.userDefined, objectType: assembly.objectType,
});
}
}
private async addSite(): Promise<void> {
const site = this.ifc.site;
this.model = await site.addTerrain({ model: this.model, id: this.remember("terrain", "garden"), name: "Garden", outline: TERRAIN_OUTLINE, holes: [HARD_SURFACES], material: "Lawn" });
for (const tree of TREES) {
this.model = await site.addTree({
model: this.model, id: this.remember(tree.id, "garden"), name: tree.species, species: tree.species, position: tree.position,
height: tree.height, crownRadius: tree.crown, trunkRadius: tree.trunk, crownMaterial: tree.foliage, trunkMaterial: tree.bark,
});
}
}
private async addRooms(): Promise<void> {
for (const room of ROOMS) {
this.model = await this.ifc.spaces.add({ model: this.model, storey: room.storey, id: room.id, name: room.number, longName: room.name, outline: room.outline, height: room.height });
}
}
private async addProperties(): Promise<void> {
for (const set of PROPERTY_SETS) {
this.model = await this.ifc.properties.addSet({ model: this.model, elements: set.elements, name: set.name, properties: set.properties });
}
}
}
scene/draw-house.ts
import type { HouseModel } from "../house/house-types";
export interface DrawnElement {
readonly mesh: BABYLON.Mesh;
readonly glass: boolean;
readonly terrain: boolean;
}
const SHARP_ANGLE = 40;
const FALLBACK_COLOUR = "#d6d3cc";
const LEAST_OPACITY = 0.18;
function hexOf(rgba: readonly number[]): string {
return "#" + rgba.slice(0, 3).map((channel) => Math.round(channel * 255).toString(16).padStart(2, "0")).join("");
}
export async function drawHouse(house: HouseModel): Promise<DrawnElement[]> {
const recipe = await bitbybit.ifc.geometry.recipe({ model: house.model });
const solids = await bitbybit.manifold.recipes.build({ recipe, adjustZtoY: true, emptyWhenFailed: true });
const meshes = await bitbybit.manifold.decomposeManifoldsOrCrossSections({ manifoldsOrCrossSections: solids, minSharpAngle: SHARP_ANGLE });
const drawn = await Promise.all(recipe.roots.map(async (root, index): Promise<DrawnElement | undefined> => {
const mesh = meshes[index];
if (!mesh) {
return undefined;
}
const rgba = Array.isArray(root.tag["rgba"]) ? root.tag["rgba"] as number[] : undefined;
const alpha = rgba?.[3] ?? 1;
const options = new Bit.Inputs.Draw.DrawManifoldOrCrossSectionOptions();
options.faceColour = rgba ? hexOf(rgba) : FALLBACK_COLOUR;
options.faceOpacity = Math.max(LEAST_OPACITY, alpha);
options.drawTwoSided = false;
options.minSharpAngle = SHARP_ANGLE;
const drawnMesh = await bitbybit.draw.drawAnyAsync({ entity: mesh as Bit.Inputs.Manifold.DecomposedManifoldMeshDto, options }) as BABYLON.Mesh;
return { mesh: drawnMesh, glass: alpha < 1, terrain: root.tag["type"] === "IfcGeographicElement" };
}));
return drawn.filter((element): element is DrawnElement => element !== undefined);
}
scene/environment.ts
import type { DrawnElement } from "./draw-house";
const SUNRISE = 6;
const SUNSET = 20;
const START_TIME = 16.5;
const NOON_ELEVATION = 62;
const SKY_SIZE = 900;
const DAWN = new BABYLON.Color3(1, 0.62, 0.38);
const DAY = new BABYLON.Color3(1, 0.97, 0.92);
const AMBIENT_DUSK = new BABYLON.Color3(0.75, 0.62, 0.6);
const AMBIENT_DAY = new BABYLON.Color3(0.85, 0.9, 1);
const HAZE_DUSK = new BABYLON.Color3(0.55, 0.42, 0.4);
const HAZE_DAY = new BABYLON.Color3(0.72, 0.8, 0.9);
function clockOf(hours: number): string {
const minutes = Math.round(hours * 60);
return String(Math.floor(minutes / 60)).padStart(2, "0") + ":" + String(minutes % 60).padStart(2, "0");
}
function sunDirection(hours: number): BABYLON.Vector3 {
const day = (hours - SUNRISE) / (SUNSET - SUNRISE);
const elevation = BABYLON.Tools.ToRadians(Math.max(-6, Math.sin(Math.PI * day) * NOON_ELEVATION));
const azimuth = BABYLON.Tools.ToRadians(80 + 200 * day);
return new BABYLON.Vector3(Math.cos(elevation) * Math.sin(azimuth), Math.sin(elevation), -Math.cos(elevation) * Math.cos(azimuth));
}
export function setUpEnvironment(drawn: readonly DrawnElement[]): void {
const scene = bitbybit.babylon.scene.getScene();
scene.lights.slice().forEach((light) => { light.dispose(); });
const ambient = new BABYLON.HemisphericLight("ambient", new BABYLON.Vector3(0, 1, 0), scene);
ambient.groundColor = new BABYLON.Color3(0.32, 0.3, 0.27);
const sun = new BABYLON.DirectionalLight("sun", new BABYLON.Vector3(-1, -2, -1), scene);
sun.shadowMinZ = 1;
sun.shadowMaxZ = 300;
const shadows = new BABYLON.ShadowGenerator(4096, sun);
shadows.usePercentageCloserFiltering = true;
shadows.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
shadows.bias = 0.0005;
shadows.normalBias = 0.02;
shadows.darkness = 0.1;
drawn.forEach((element) => {
[element.mesh, ...element.mesh.getChildMeshes(false)].forEach((part) => {
part.receiveShadows = true;
if (!element.glass && !element.terrain) {
shadows.addShadowCaster(part, false);
}
});
});
const sky = bitbybit.babylon.material.skyMaterial.create({ luminance: 1, turbidity: 6, rayleigh: 2, mieCoefficient: 0.004, mieDirectionalG: 0.85, distance: 500, inclination: 0.3, azimuth: 0.25, useSunPosition: true, sunPosition: [0, 100, 0], cameraOffset: [0, 0, 0], up: [0, 1, 0], dithering: true });
const skybox = BABYLON.MeshBuilder.CreateBox("sky", { size: SKY_SIZE, sideOrientation: BABYLON.Mesh.BACKSIDE }, scene);
skybox.material = sky;
skybox.infiniteDistance = true;
skybox.applyFog = false;
const probe = new BABYLON.ReflectionProbe("sky-light", 256, scene);
probe.refreshRate = BABYLON.RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
probe.renderList?.push(skybox);
scene.environmentTexture = probe.cubeTexture;
scene.environmentIntensity = 0.55;
scene.imageProcessingConfiguration.toneMappingEnabled = true;
scene.imageProcessingConfiguration.toneMappingType = BABYLON.ImageProcessingConfiguration.TONEMAPPING_ACES;
scene.imageProcessingConfiguration.exposure = 1.1;
scene.imageProcessingConfiguration.contrast = 1.15;
scene.fogMode = BABYLON.Scene.FOGMODE_EXP2;
scene.fogDensity = 0.0025;
const setTimeOfDay = (hours: number): void => {
const toSun = sunDirection(hours);
const height = Math.max(0, toSun.y);
const warmth = Math.min(1, height * 3);
sky.sunPosition = toSun.scale(100);
sky.luminance = 0.25 + 0.75 * Math.min(1, height * 4);
sun.direction = toSun.negate();
sun.position = toSun.scale(80);
sun.intensity = 4.2 * Math.pow(height, 0.5);
sun.diffuse = BABYLON.Color3.Lerp(DAWN, DAY, warmth);
ambient.intensity = 0.15 + 0.3 * Math.min(1, height * 2.5);
ambient.diffuse = BABYLON.Color3.Lerp(AMBIENT_DUSK, AMBIENT_DAY, warmth);
scene.fogColor = BABYLON.Color3.Lerp(HAZE_DUSK, HAZE_DAY, warmth);
scene.clearColor = scene.fogColor.toColor4(1);
probe.cubeTexture.resetRefreshCounter();
};
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("time-of-day", true, scene);
const panel = new BABYLON.GUI.StackPanel("time-panel");
panel.width = "320px";
panel.height = "86px";
panel.paddingBottom = "20px";
panel.background = "#00000066";
panel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
ui.addControl(panel);
const clock = new BABYLON.GUI.TextBlock("time-label", "");
clock.height = "30px";
clock.color = "#ffffff";
clock.fontSize = 18;
panel.addControl(clock);
const slider = new BABYLON.GUI.Slider("time-slider");
slider.minimum = SUNRISE - 0.5;
slider.maximum = SUNSET + 0.5;
slider.value = START_TIME;
slider.height = "24px";
slider.width = "280px";
slider.color = "#f5c46b";
slider.background = "#ffffff55";
slider.thumbColor = "#ffffff";
slider.onValueChangedObservable.add((hours) => {
clock.text = "Time of day " + clockOf(hours);
setTimeOfDay(hours);
});
panel.addControl(slider);
clock.text = "Time of day " + clockOf(START_TIME);
setTimeOfDay(START_TIME);
}
The files of the project
The script is a TypeScript project of several files, which the editor shows in its file tree: open it to read and change any of them. The house itself is the house of the IFC example for Vite and three.js, examples/vite/threejs/ifc-house, file for file:
house/house-types.ts- the shapes of the plan's data: a wall run, a window or door, a slab, a detail.house/house-plan.ts- the plan, all in metres: the materials and the layered build-ups of walls, floors and the roof, the window and door types, the wall runs of both storeys, every window and door, the slabs, the rooms and their property sets.house/details.ts- the details worked out from the plan: cladding boards around the openings, flashings, window surrounds and sills, copings, the stair, rainwater pipes, the entrance canopy, the carport frame, glass rails and the solar array.house/build-house.ts-HouseAuthor, which writes all of it into one model withbitbybit.ifc: storeys, materials, walls, slabs, windows and doors, the roof, the carport, the details, assemblies, the garden, rooms, properties and quantities. The console shows each stage as it starts.scene/draw-house.ts- the geometry:ifc.geometry.recipedescribes the model as data,manifold.recipes.buildbuilds every element as a mesh solid, which is fast for a model this size, and each element is drawn in the colour of its material, glass translucent.scene/environment.ts- the setting, in BabylonJS: a sky material whose sun follows the time of day, a sun light with soft shadows, the sky reflected in every material, ACES tone mapping, a light haze, and the slider at the bottom of the view that moves the sun from morning to evening.main.ts- runs the three in turn and places the camera.
To export the model as an IFC file, give bitbybit.ifc.model.write the model HouseAuthor returns; IFC in the npm packages shows the same house in an app of your own.

