SPB Git

spb/uqo-eval

Public
26.7 KB · 233 lines typescript
1// UQO Éval — Métré : générateur paramétrique de gabarits de plans (bungalows, maisons 2 étages, plex, rangée, chalets, garages) + fiche technique2// par défaut pour la méthode du relevé des quantités. Module PUR : produit des objets de plan (m) par niveau ; le service les convertit en pixels.3import type { ObjectKind, ObjectProps, Point } from "./types";45/* ------------------------------------------------------------- fiche QS */67export type BasementKind = "none" | "crawl" | "unfinished" | "finished";8export type FoundationKind = "poured_concrete" | "concrete_block" | "slab_on_grade" | "piers";9export type RoofKind = "asphalt_shingle" | "metal" | "membrane";10export type RoofGeom = "gable" | "hip" | "flat";11export type HeatingKind = "electric_baseboard" | "heat_pump" | "furnace_electric" | "furnace_gas";12export type Quality = "economy" | "standard" | "superior" | "prestige";1314/** Fiche technique d'un projet — tout ce que le plan ne montre pas mais que le relevé des quantités exige. */15export interface BuildingSpecQS {16  units: number; // logements17  basement: BasementKind;18  basementFinishedPct: number; // 0-119  foundation: FoundationKind;20  foundationWallHeightM: number; // 2.44 sous-sol, 1.2 vide sanitaire / mur hors gel21  roofType: RoofKind;22  roofGeometry: RoofGeom;23  roofPitch: number; // po/1224  siding: Partial<Record<"vinyl" | "brick" | "fiber_cement" | "wood" | "stone" | "steel", number>>;25  windows: "pvc" | "hybrid" | "aluminum" | "wood";26  heating: HeatingKind;27  hasAirConditioning: boolean;28  hasAirExchanger: boolean;29  quality: Quality;30  kitchenQuality: Quality;31  bathroomQuality: Quality;32  flooring: Partial<Record<"hardwood" | "engineered" | "vinyl_plank" | "ceramic" | "laminate" | "carpet", number>>;33  ceilingHeightM: number;34  garage: "none" | "attached" | "detached" | "integrated";35  deckM2: number;36  drivewayM2: number;37  driveway: "asphalt" | "pavers" | "gravel" | "none";38  fenceM: number;39  landscapingM2: number;40  notes: string | null;41}4243export function defaultSpec(p: Partial<BuildingSpecQS> = {}): BuildingSpecQS {44  return {45    units: 1, basement: "unfinished", basementFinishedPct: 0, foundation: "poured_concrete", foundationWallHeightM: 2.44, roofType: "asphalt_shingle", roofGeometry: "gable", roofPitch: 6,46    siding: { vinyl: 0.7, brick: 0.3 }, windows: "pvc", heating: "electric_baseboard", hasAirConditioning: false, hasAirExchanger: true, quality: "standard", kitchenQuality: "standard", bathroomQuality: "standard",47    flooring: { hardwood: 0.6, ceramic: 0.25, vinyl_plank: 0.15 }, ceilingHeightM: 2.44, garage: "none", deckM2: 0, drivewayM2: 0, driveway: "none", fenceM: 0, landscapingM2: 0, notes: null, ...p,48  };49}5051/* -------------------------------------------------------- objets générés */5253export interface GenObject { kind: ObjectKind; name: string; points: Point[]; closed: boolean; props: ObjectProps; openings?: GenOpening[] }54export interface GenOpening { name: string; kind: "door" | "window" | "opening"; at: Point; widthM: number; heightM: number; swing?: number }55export interface GenLevel { level: string; widthM: number; heightM: number; objects: GenObject[] }56export interface Template { key: string; kind: "bungalow" | "cottage" | "plex" | "row" | "chalet" | "garage" | "condo" | "mobile"; fr: string; en: string; footprint: string; storeys: number; units: number; areaM2: number; spec: Partial<BuildingSpecQS>; levels: GenLevel[] }5758const T_EXT = 0.3, T_INT = 0.12, H = 2.44;59const rect = (x: number, y: number, w: number, h: number): Point[] => [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }];60const r2 = (v: number) => Math.round(v * 100) / 100;6162/** Programme d'une bande de pièces : [usage, nom, fraction de la largeur]. */63type Band = [string, string, number][];64interface UnitProgram { front: Band; back: Band; frontDepth: number; stairs?: boolean; entrance?: "front" | "side" }6566const PROGRAMS: Record<string, UnitProgram> = {67  bungalow2: { front: [["séjour", "Séjour", 0.55], ["cuisine", "Cuisine / salle à manger", 0.45]], back: [["chambre", "Chambre principale", 0.38], ["chambre", "Chambre 2", 0.3], ["salle de bain", "Salle de bain", 0.17], ["hall", "Hall / escalier", 0.15]], frontDepth: 0.5, stairs: true },68  bungalow3: { front: [["séjour", "Séjour", 0.45], ["cuisine", "Cuisine / salle à manger", 0.4], ["hall", "Hall / escalier", 0.15]], back: [["chambre", "Chambre principale", 0.34], ["chambre", "Chambre 2", 0.25], ["chambre", "Chambre 3", 0.23], ["salle de bain", "Salle de bain", 0.18]], frontDepth: 0.5, stairs: true },69  cottageRdc: { front: [["séjour", "Salon", 0.5], ["salle à manger", "Salle à manger", 0.3], ["hall", "Hall / escalier", 0.2]], back: [["cuisine", "Cuisine", 0.45], ["bureau", "Bureau / salle familiale", 0.35], ["salle d'eau", "Salle d'eau", 0.2]], frontDepth: 0.5, stairs: true },70  cottageEtage: { front: [["chambre", "Chambre principale", 0.45], ["salle de bain", "Salle de bain", 0.25], ["hall", "Hall / escalier", 0.3]], back: [["chambre", "Chambre 2", 0.37], ["chambre", "Chambre 3", 0.33], ["chambre", "Chambre 4", 0.3]], frontDepth: 0.5, stairs: true },71  cottageEtage3: { front: [["chambre", "Chambre principale", 0.5], ["salle de bain", "Salle de bain", 0.25], ["hall", "Hall / escalier", 0.25]], back: [["chambre", "Chambre 2", 0.5], ["chambre", "Chambre 3", 0.5]], frontDepth: 0.5, stairs: true },72  apartment: { front: [["séjour", "Séjour", 0.5], ["cuisine", "Cuisine / salle à manger", 0.5]], back: [["chambre", "Chambre 1", 0.4], ["chambre", "Chambre 2", 0.35], ["salle de bain", "Salle de bain", 0.25]], frontDepth: 0.5 },73  apartment1: { front: [["séjour", "Séjour / cuisine", 1]], back: [["chambre", "Chambre", 0.6], ["salle de bain", "Salle de bain", 0.4]], frontDepth: 0.5 },74  rowRdc: { front: [["séjour", "Salon", 0.65], ["hall", "Hall / escalier", 0.35]], back: [["cuisine", "Cuisine / salle à manger", 0.7], ["salle d'eau", "Salle d'eau", 0.3]], frontDepth: 0.5, stairs: true },75  rowEtage: { front: [["chambre", "Chambre principale", 0.65], ["hall", "Hall / escalier", 0.35]], back: [["chambre", "Chambre 2", 0.55], ["salle de bain", "Salle de bain", 0.45]], frontDepth: 0.5, stairs: true },76  chalet: { front: [["séjour", "Séjour / cuisine", 1]], back: [["chambre", "Chambre 1", 0.4], ["chambre", "Chambre 2", 0.35], ["salle de bain", "Salle de bain", 0.25]], frontDepth: 0.55 },77  mobile: { front: [["séjour", "Séjour / cuisine", 0.45], ["chambre", "Chambre 2", 0.2], ["salle de bain", "Salle de bain", 0.12], ["chambre", "Chambre principale", 0.23]], back: [], frontDepth: 1 },78};7980/** Dispose un logement rectangulaire (x,y,W,D en m) : cloisons, pièces, ouvertures sur les murs extérieurs exposés. */81function layoutUnit(x: number, y: number, W: number, D: number, prog: UnitProgram, exposed: { front: boolean; back: boolean; left: boolean; right: boolean }, unitLabel: string, out: GenObject[], openings: GenOpening[]): void {82  const inner = (ox: number, oy: number, w: number, h: number): Point[] => rect(ox + 0.08, oy + 0.08, w - 0.16, h - 0.16);83  const frontD = prog.back.length ? D * prog.frontDepth : D;84  const prefix = unitLabel ? `${unitLabel} — ` : "";85  if (prog.back.length) out.push({ kind: "wall", name: `${prefix}Cloison centrale`, points: [{ x, y: y + frontD }, { x: x + W, y: y + frontD }], closed: false, props: { thickness: T_INT, height: H } });86  const band = (rooms: Band, by: number, bh: number, isFront: boolean) => {87    let cx = x;88    rooms.forEach(([usage, name, frac], i) => {89      const w = W * frac;90      out.push({ kind: "room", name: `${prefix}${name}`, points: inner(cx, by, w, bh), closed: true, props: { usage, height: H } });91      if (i < rooms.length - 1) out.push({ kind: "wall", name: `${prefix}Cloison ${name}`, points: [{ x: cx + w, y: by }, { x: cx + w, y: by + bh }], closed: false, props: { thickness: T_INT, height: H } });92      // fenêtre sur la face exposée (avant ou arrière)93      const edgeY = isFront ? y : y + D; const exposedEdge = isFront ? exposed.front : exposed.back;94      if (exposedEdge && usage !== "hall" && usage !== "salle d'eau") openings.push({ name: `Fenêtre ${name}`, kind: "window", at: { x: cx + w / 2, y: edgeY }, widthM: usage === "séjour" || usage === "cuisine" ? 1.8 : usage === "salle de bain" ? 0.9 : 1.2, heightM: usage === "salle de bain" ? 0.9 : 1.3 });95      if (usage === "hall" && prog.stairs) out.push({ kind: "stairs", name: `${prefix}Escalier`, points: rect(cx + 0.3, by + 0.3, Math.min(1.0, w - 0.6), Math.min(3.6, bh - 0.6)), closed: true, props: {} });96      cx += w;97    });98  };99  band(prog.front, y, frontD, true);100  if (prog.back.length) band(prog.back, y + frontD, D - frontD, false);101  // porte d'entrée (face avant, premier tiers) et porte-patio (face arrière, côté cuisine) si exposées102  if (exposed.front) openings.push({ name: `${prefix}Porte d'entrée`, kind: "door", at: { x: x + W * 0.12, y }, widthM: 0.9, heightM: 2.1, swing: 1 });103  else if (exposed.left) openings.push({ name: `${prefix}Porte d'entrée`, kind: "door", at: { x, y: y + 1.2 }, widthM: 0.9, heightM: 2.1, swing: 1 });104  if (exposed.back && prog.back.length) openings.push({ name: `${prefix}Porte-patio`, kind: "door", at: { x: x + W * 0.78, y: y + D }, widthM: 1.8, heightM: 2.1, swing: -1 });105  // fenêtres latérales (une par face exposée)106  if (exposed.left) openings.push({ name: "Fenêtre latérale", kind: "window", at: { x, y: y + D * 0.3 }, widthM: 1.2, heightM: 1.3 });107  if (exposed.right) openings.push({ name: "Fenêtre latérale", kind: "window", at: { x: x + W, y: y + D * 0.7 }, widthM: 1.2, heightM: 1.3 });108}109110/** Niveau complet : enveloppe + logements côte à côte (nUnitsAcross) + garage attenant facultatif. */111function level(name: string, W: number, D: number, programs: UnitProgram[], opts: { garage?: { w: number; d: number } | null; unitLabels?: string[]; basement?: boolean } = {}): GenLevel {112  const margin = 2.5; const objects: GenObject[] = []; const openings: GenOpening[] = [];113  const x0 = margin, y0 = margin;114  objects.push({ kind: "wall", name: "Murs extérieurs", points: rect(x0, y0, W, D), closed: true, props: { thickness: T_EXT, height: H, exterior: true } });115  objects.push({ kind: "footprint", name: "Empreinte", points: rect(x0 - T_EXT / 2, y0 - T_EXT / 2, W + T_EXT, D + T_EXT), closed: true, props: {} });116  if (opts.basement) {117    objects.push({ kind: "room", name: "Sous-sol", points: rect(x0 + 0.1, y0 + 0.1, W - 0.2, D - 0.2), closed: true, props: { usage: "sous-sol", height: 2.3 } });118    objects.push({ kind: "wall", name: "Cloison salle mécanique", points: [{ x: x0 + 3, y: y0 }, { x: x0 + 3, y: y0 + 3 }, { x: x0, y: y0 + 3 }], closed: false, props: { thickness: T_INT, height: 2.3 } });119    objects.push({ kind: "room", name: "Salle mécanique", points: rect(x0 + 0.1, y0 + 0.1, 2.8, 2.8), closed: true, props: { usage: "salle mécanique", height: 2.3 } });120    objects.push({ kind: "stairs", name: "Escalier", points: rect(x0 + W - 1.3, y0 + 0.3, 1.0, 3.6), closed: true, props: {} });121    openings.push({ name: "Fenêtre de sous-sol", kind: "window", at: { x: x0 + W * 0.3, y: y0 + D }, widthM: 0.8, heightM: 0.5 }, { name: "Fenêtre de sous-sol", kind: "window", at: { x: x0 + W * 0.7, y: y0 + D }, widthM: 0.8, heightM: 0.5 }, { name: "Fenêtre de sous-sol", kind: "window", at: { x: x0, y: y0 + D * 0.5 }, widthM: 0.8, heightM: 0.5 });122  } else {123    const n = programs.length; const uw = W / n;124    programs.forEach((p, i) => {125      if (i > 0) objects.push({ kind: "wall", name: `Mur mitoyen ${i}`, points: [{ x: x0 + uw * i, y: y0 }, { x: x0 + uw * i, y: y0 + D }], closed: false, props: { thickness: 0.25, height: H } });126      layoutUnit(x0 + uw * i, y0, uw, D, p, { front: true, back: true, left: i === 0, right: i === n - 1 }, opts.unitLabels?.[i] ?? "", objects, openings);127    });128  }129  if (opts.garage) {130    const g = opts.garage; const gx = x0 + W, gy = y0 + D - g.d;131    objects.push({ kind: "wall", name: "Murs du garage", points: [{ x: gx, y: gy }, { x: gx + g.w, y: gy }, { x: gx + g.w, y: gy + g.d }, { x: gx, y: gy + g.d }], closed: false, props: { thickness: T_EXT, height: 3.0, exterior: true }, openings: [{ name: "Porte de garage", kind: "door", at: { x: gx + g.w / 2, y: gy + g.d }, widthM: g.w >= 6 ? 4.9 : 2.7, heightM: 2.1, swing: -2 }, { name: "Fenêtre garage", kind: "window", at: { x: gx + g.w, y: gy + g.d / 2 }, widthM: 0.9, heightM: 0.8 }] });132    objects.push({ kind: "room", name: "Garage", points: rect(gx + 0.1, gy + 0.1, g.w - 0.2, g.d - 0.2), closed: true, props: { usage: "garage", height: 3.0 } });133    objects.push({ kind: "footprint", name: "Empreinte garage", points: rect(gx, gy - T_EXT / 2, g.w + T_EXT / 2, g.d + T_EXT), closed: true, props: {} });134  }135  const ext = objects[0]; ext.openings = openings;136  return { level: name, widthM: r2(W + 2 * margin + (opts.garage?.w ?? 0)), heightM: r2(D + 2 * margin), objects };137}138139/* ------------------------------------------------------------ catalogue */140141interface Def { key: string; kind: Template["kind"]; fr: string; en: string; W: number; D: number; storeys: number; units?: number; basement?: BasementKind; garage?: { w: number; d: number } | null; programs: (lvl: number) => UnitProgram[]; labels?: (lvl: number) => string[]; spec?: Partial<BuildingSpecQS>; levelNames?: string[] }142143const B = (key: string, fr: string, en: string, W: number, D: number, prog: string, extra: Partial<Def> = {}): Def => ({ key, kind: "bungalow", fr, en, W, D, storeys: 1, basement: "unfinished", garage: null, programs: () => [PROGRAMS[prog]], ...extra });144const C = (key: string, fr: string, en: string, W: number, D: number, upper = "cottageEtage", extra: Partial<Def> = {}): Def => ({ key, kind: "cottage", fr, en, W, D, storeys: 2, basement: "unfinished", garage: null, programs: (l) => [l === 0 ? PROGRAMS.cottageRdc : PROGRAMS[upper]], ...extra });145const P = (key: string, fr: string, en: string, W: number, D: number, storeys: number, across: number, prog = "apartment", extra: Partial<Def> = {}): Def => ({ key, kind: "plex", fr, en, W, D, storeys, units: storeys * across, basement: "unfinished", garage: null, programs: () => Array.from({ length: across }, () => PROGRAMS[prog]), labels: (l) => Array.from({ length: across }, (_, i) => `Logement ${l * across + i + 1}`), spec: { units: storeys * across, siding: { brick: 0.8, vinyl: 0.2 }, roofGeometry: "flat", roofType: "membrane", roofPitch: 0, heating: "electric_baseboard" }, ...extra });146147const DEFS: Def[] = [148  B("bungalow-a", "Bungalow 2 chambres 10,4 × 8,5 m", "Bungalow 2 bedrooms 10.4 × 8.5 m", 10.4, 8.5, "bungalow2"),149  B("bungalow-b", "Bungalow 2 chambres 11,6 × 9,1 m", "Bungalow 2 bedrooms 11.6 × 9.1 m", 11.6, 9.1, "bungalow2"),150  B("bungalow-c", "Bungalow 3 chambres 12,2 × 9,1 m", "Bungalow 3 bedrooms 12.2 × 9.1 m", 12.2, 9.1, "bungalow3"),151  B("bungalow-d", "Bungalow 3 chambres 13,4 × 9,8 m", "Bungalow 3 bedrooms 13.4 × 9.8 m", 13.4, 9.8, "bungalow3"),152  B("bungalow-e", "Bungalow 3 chambres 14,6 × 10,4 m", "Bungalow 3 bedrooms 14.6 × 10.4 m", 14.6, 10.4, "bungalow3", { spec: { quality: "superior", kitchenQuality: "superior" } }),153  B("bungalow-garage", "Bungalow 3 ch. 12,2 × 9,1 m + garage attenant", "Bungalow 3 bdr 12.2 × 9.1 m + attached garage", 12.2, 9.1, "bungalow3", { garage: { w: 4.3, d: 6.7 }, spec: { garage: "attached" } }),154  B("bungalow-garage-2", "Bungalow 3 ch. 13,4 × 9,8 m + garage double", "Bungalow 3 bdr 13.4 × 9.8 m + double garage", 13.4, 9.8, "bungalow3", { garage: { w: 6.7, d: 6.7 }, spec: { garage: "attached", quality: "superior" } }),155  B("bungalow-fini", "Bungalow 3 ch. 12,2 × 9,1 m, sous-sol fini", "Bungalow 3 bdr 12.2 × 9.1 m, finished basement", 12.2, 9.1, "bungalow3", { basement: "finished", spec: { basement: "finished", basementFinishedPct: 0.8 } }),156  B("bungalow-dalle", "Bungalow 2 ch. 11,6 × 9,1 m sur dalle (sans sous-sol)", "Bungalow 2 bdr 11.6 × 9.1 m on slab (no basement)", 11.6, 9.1, "bungalow2", { basement: "none", spec: { basement: "none", foundation: "slab_on_grade", foundationWallHeightM: 1.2 } }),157  B("bungalow-vs", "Bungalow 2 ch. 10,4 × 8,5 m sur vide sanitaire", "Bungalow 2 bdr 10.4 × 8.5 m on crawl space", 10.4, 8.5, "bungalow2", { basement: "crawl", spec: { basement: "crawl", foundationWallHeightM: 1.2 } }),158  C("cottage-a", "Maison 2 étages 9,1 × 7,9 m (3 chambres)", "Two-storey 9.1 × 7.9 m (3 bedrooms)", 9.1, 7.9, "cottageEtage3"),159  C("cottage-b", "Maison 2 étages 10,4 × 8,5 m (3 chambres)", "Two-storey 10.4 × 8.5 m (3 bedrooms)", 10.4, 8.5, "cottageEtage3"),160  C("cottage-c", "Maison 2 étages 11,6 × 9,1 m (4 chambres)", "Two-storey 11.6 × 9.1 m (4 bedrooms)", 11.6, 9.1),161  C("cottage-d", "Maison 2 étages 12,2 × 9,8 m (4 chambres)", "Two-storey 12.2 × 9.8 m (4 bedrooms)", 12.2, 9.8, "cottageEtage", { spec: { quality: "superior", kitchenQuality: "superior", bathroomQuality: "superior", siding: { brick: 0.6, fiber_cement: 0.4 } } }),162  C("cottage-garage", "Maison 2 étages 11,6 × 9,1 m + garage attenant", "Two-storey 11.6 × 9.1 m + attached garage", 11.6, 9.1, "cottageEtage", { garage: { w: 4.3, d: 6.7 }, spec: { garage: "attached" } }),163  C("cottage-garage-2", "Maison 2 étages 12,2 × 9,8 m + garage double", "Two-storey 12.2 × 9.8 m + double garage", 12.2, 9.8, "cottageEtage", { garage: { w: 6.7, d: 7.3 }, spec: { garage: "attached", quality: "superior", heating: "heat_pump", hasAirConditioning: true } }),164  C("cottage-fini", "Maison 2 étages 11,6 × 9,1 m, sous-sol fini", "Two-storey 11.6 × 9.1 m, finished basement", 11.6, 9.1, "cottageEtage", { basement: "finished", spec: { basement: "finished", basementFinishedPct: 0.8 } }),165  C("cottage-prestige", "Maison 2 étages 13,4 × 10,4 m, prestige, garage double", "Two-storey 13.4 × 10.4 m, prestige, double garage", 13.4, 10.4, "cottageEtage", { garage: { w: 6.7, d: 7.3 }, spec: { garage: "attached", quality: "prestige", kitchenQuality: "prestige", bathroomQuality: "prestige", siding: { brick: 0.5, stone: 0.3, fiber_cement: 0.2 }, windows: "hybrid", heating: "heat_pump", hasAirConditioning: true } }),166  P("duplex", "Duplex superposé 11,6 × 10,4 m (2 logements)", "Stacked duplex 11.6 × 10.4 m (2 units)", 11.6, 10.4, 2, 1),167  P("duplex-b", "Duplex superposé 12,8 × 11 m (2 logements)", "Stacked duplex 12.8 × 11 m (2 units)", 12.8, 11, 2, 1),168  P("triplex", "Triplex 11,6 × 10,4 m (3 logements)", "Triplex 11.6 × 10.4 m (3 units)", 11.6, 10.4, 3, 1),169  P("triplex-b", "Triplex 12,8 × 11 m (3 logements)", "Triplex 12.8 × 11 m (3 units)", 12.8, 11, 3, 1),170  P("quadruplex", "Quadruplex 2 étages 18,3 × 10,4 m (4 logements)", "Fourplex 2 storeys 18.3 × 10.4 m (4 units)", 18.3, 10.4, 2, 2),171  P("quadruplex-b", "Quadruplex 2 étages 20,1 × 11 m (4 logements)", "Fourplex 2 storeys 20.1 × 11 m (4 units)", 20.1, 11, 2, 2),172  P("sixplex", "Sixplex 3 étages 18,3 × 10,4 m (6 logements)", "Sixplex 3 storeys 18.3 × 10.4 m (6 units)", 18.3, 10.4, 3, 2),173  P("sixplex-b", "Sixplex 3 étages 20,1 × 11 m (6 logements)", "Sixplex 3 storeys 20.1 × 11 m (6 units)", 20.1, 11, 3, 2),174  P("huitplex", "8 logements 3 étages 24,4 × 11 m (3 + 3 + 2)", "8 units 3 storeys 24.4 × 11 m", 24.4, 11, 3, 3, "apartment", { units: 9, spec: { units: 9 } }),175  P("studio-plex", "Triplex de studios 9,1 × 9,1 m (3 logements 1 ch.)", "Triplex of one-bedroom units 9.1 × 9.1 m", 9.1, 9.1, 3, 1, "apartment1"),176  { key: "rangee", kind: "row", fr: "Maison en rangée 6,1 × 11 m (2 étages)", en: "Row house 6.1 × 11 m (2 storeys)", W: 6.1, D: 11, storeys: 2, basement: "unfinished", garage: null, programs: (l) => [l === 0 ? PROGRAMS.rowRdc : PROGRAMS.rowEtage], spec: { siding: { brick: 0.9, vinyl: 0.1 } } },177  { key: "rangee-b", kind: "row", fr: "Maison en rangée 7,3 × 12,2 m (2 étages, sous-sol fini)", en: "Row house 7.3 × 12.2 m (2 storeys, finished basement)", W: 7.3, D: 12.2, storeys: 2, basement: "finished", garage: null, programs: (l) => [l === 0 ? PROGRAMS.rowRdc : PROGRAMS.rowEtage], spec: { basement: "finished", basementFinishedPct: 0.8, siding: { brick: 0.9, vinyl: 0.1 } } },178  { key: "jumele", kind: "cottage", fr: "Jumelé 2 étages 7,9 × 10,4 m (une unité)", en: "Semi-detached 2 storeys 7.9 × 10.4 m (one unit)", W: 7.9, D: 10.4, storeys: 2, basement: "unfinished", garage: null, programs: (l) => [l === 0 ? PROGRAMS.rowRdc : PROGRAMS.rowEtage], spec: { siding: { brick: 0.5, vinyl: 0.5 } } },179  { key: "chalet", kind: "chalet", fr: "Chalet 8,5 × 7,3 m sur pieux", "en": "Cottage cabin 8.5 × 7.3 m on piers", W: 8.5, D: 7.3, storeys: 1, basement: "none", garage: null, programs: () => [PROGRAMS.chalet], spec: { basement: "none", foundation: "piers", roofType: "metal", roofPitch: 8, siding: { wood: 1 }, heating: "electric_baseboard", hasAirExchanger: false } },180  { key: "chalet-b", kind: "chalet", fr: "Chalet 4 saisons 10,4 × 8,5 m, vide sanitaire", en: "Four-season cottage 10.4 × 8.5 m, crawl space", W: 10.4, D: 8.5, storeys: 1, basement: "crawl", garage: null, programs: () => [PROGRAMS.bungalow2], spec: { basement: "crawl", foundationWallHeightM: 1.2, roofType: "metal", roofPitch: 8, siding: { wood: 0.7, stone: 0.3 }, heating: "heat_pump" } },181  { key: "mobile", kind: "mobile", fr: "Maison mobile 4,3 × 18,3 m", en: "Mobile home 4.3 × 18.3 m", W: 18.3, D: 4.3, storeys: 1, basement: "none", garage: null, programs: () => [PROGRAMS.mobile], spec: { basement: "none", foundation: "piers", roofType: "metal", roofGeometry: "flat", roofPitch: 2, siding: { steel: 1 }, hasAirExchanger: false, quality: "economy", kitchenQuality: "economy", bathroomQuality: "economy" } },182  { key: "condo", kind: "condo", fr: "Unité de copropriété 10,4 × 8,5 m (intérieur seulement)", en: "Condo unit 10.4 × 8.5 m (interior only)", W: 10.4, D: 8.5, storeys: 1, basement: "none", garage: null, programs: () => [PROGRAMS.apartment], spec: { basement: "none", foundation: "slab_on_grade", roofGeometry: "flat", roofType: "membrane", roofPitch: 0, siding: { brick: 1 }, heating: "electric_baseboard" } },183  { key: "garage-1", kind: "garage", fr: "Garage détaché simple 4,3 × 6,7 m", en: "Detached single garage 4.3 × 6.7 m", W: 4.3, D: 6.7, storeys: 1, basement: "none", garage: null, programs: () => [], spec: { basement: "none", foundation: "slab_on_grade", foundationWallHeightM: 1.2, siding: { vinyl: 1 }, heating: "electric_baseboard", hasAirExchanger: false, garage: "detached" } },184  { key: "garage-2", kind: "garage", fr: "Garage détaché double 6,7 × 6,7 m", en: "Detached double garage 6.7 × 6.7 m", W: 6.7, D: 6.7, storeys: 1, basement: "none", garage: null, programs: () => [], spec: { basement: "none", foundation: "slab_on_grade", foundationWallHeightM: 1.2, siding: { vinyl: 1 }, heating: "electric_baseboard", hasAirExchanger: false, garage: "detached" } },185  { key: "garage-3", kind: "garage", fr: "Garage détaché 7,3 × 9,1 m avec atelier", en: "Detached garage 7.3 × 9.1 m with workshop", W: 7.3, D: 9.1, storeys: 1, basement: "none", garage: null, programs: () => [], spec: { basement: "none", foundation: "slab_on_grade", foundationWallHeightM: 1.2, siding: { steel: 1 }, heating: "electric_baseboard", hasAirExchanger: false, garage: "detached" } },186];187188const LEVEL_NAMES = ["Rez-de-chaussée", "Étage", "2e étage", "3e étage"];189190export function listTemplates(): Omit<Template, "levels">[] { return DEFS.map((d) => { const t = buildTemplate(d.key)!; const { levels, ...rest } = t; void levels; return rest; }); }191192export function buildTemplate(key: string): Template | null {193  const d = DEFS.find((x) => x.key === key); if (!d) return null;194  const levels: GenLevel[] = [];195  if (d.kind === "garage") {196    const margin = 2; const objs: GenObject[] = [197      { kind: "wall", name: "Murs du garage", points: rect(margin, margin, d.W, d.D), closed: true, props: { thickness: 0.2, height: 3.0, exterior: true }, openings: [{ name: "Porte de garage", kind: "door", at: { x: margin + d.W / 2, y: margin }, widthM: d.W >= 6 ? 4.9 : 2.7, heightM: 2.1, swing: 1 }, { name: "Porte de service", kind: "door", at: { x: margin + d.W, y: margin + d.D * 0.7 }, widthM: 0.9, heightM: 2.1 }, { name: "Fenêtre", kind: "window", at: { x: margin + d.W * 0.5, y: margin + d.D }, widthM: 1.0, heightM: 0.8 }] },198      { kind: "room", name: "Garage", points: rect(margin + 0.1, margin + 0.1, d.W - 0.2, d.D - 0.2), closed: true, props: { usage: "garage", height: 3.0 } },199      { kind: "slab", name: "Dalle 125 mm", points: rect(margin, margin, d.W, d.D), closed: true, props: { thickness: 0.125 } },200      { kind: "footprint", name: "Empreinte", points: rect(margin - 0.1, margin - 0.1, d.W + 0.2, d.D + 0.2), closed: true, props: {} },201    ];202    levels.push({ level: "Rez-de-chaussée", widthM: d.W + 2 * margin, heightM: d.D + 2 * margin, objects: objs });203  } else {204    if (d.basement && d.basement !== "none") levels.push(level("Sous-sol", d.W, d.D, [], { basement: true }));205    for (let l = 0; l < d.storeys; l++) levels.push(level(d.levelNames?.[l] ?? LEVEL_NAMES[l], d.W, d.D, d.programs(l), { garage: l === 0 ? d.garage ?? null : null, unitLabels: d.labels?.(l) }));206  }207  const units = d.units ?? 1;208  const areaM2 = r2(d.W * d.D * d.storeys);209  const spec: Partial<BuildingSpecQS> = { units, basement: d.basement ?? "none", ...(d.spec ?? {}) };210  return { key: d.key, kind: d.kind, fr: d.fr, en: d.en, footprint: `${d.W} × ${d.D} m`, storeys: d.storeys, units, areaM2, spec, levels };211}212213export const TEMPLATE_KINDS: { key: Template["kind"]; fr: string; en: string }[] = [214  { key: "bungalow", fr: "Bungalows", en: "Bungalows" }, { key: "cottage", fr: "Maisons 2 étages / jumelés", en: "Two-storey / semi-detached" }, { key: "plex", fr: "Plex (2 à 9 logements)", en: "Plexes (2 to 9 units)" },215  { key: "row", fr: "Maisons en rangée", en: "Row houses" }, { key: "chalet", fr: "Chalets", en: "Cottages" }, { key: "mobile", fr: "Maisons mobiles", en: "Mobile homes" }, { key: "condo", fr: "Copropriétés", en: "Condos" }, { key: "garage", fr: "Garages détachés", en: "Detached garages" },216];217218/* ------------------------------------------------- matérialisation (px) */219220import type { PlanObject } from "./types";221222/** Convertit un niveau généré (mètres) en objets de plan (pixels) avec identifiants temporaires ; les ouvertures référencent leur mur. */223export function materializeLevel(lv: GenLevel, pxPerMeter: number): PlanObject[] {224  const k = pxPerMeter; let n = 0; const out: PlanObject[] = [];225  const px = (p: Point): Point => ({ x: Math.round(p.x * k * 100) / 100, y: Math.round(p.y * k * 100) / 100 });226  for (const o of lv.objects) {227    const id = `tmp-${++n}`;228    out.push({ id, pageId: "", kind: o.kind, layer: o.kind === "wall" ? "murs" : o.kind === "room" ? "pièces" : o.kind === "footprint" || o.kind === "lot" ? "site" : o.kind === "slab" ? "fondations" : "default", name: o.name, points: o.points.map(px), closed: o.closed, props: { ...o.props }, createdAt: "", updatedAt: "" });229    for (const op of o.openings ?? []) out.push({ id: `tmp-${++n}`, pageId: "", kind: "opening", layer: "ouvertures", name: op.name, points: [px(op.at)], closed: false, props: { openingKind: op.kind, openingWidth: op.widthM, openingHeight: op.heightM, count: 1, deductFrom: id, swing: op.swing ?? 1 }, createdAt: "", updatedAt: "" });230  }231  return out;232}233
Files · main
Menu

Theme