SPB Git

spb/uqo-eval

Public
4.6 KB · 101 lines typescript
1// UQO Éval — Métré : géométrie plane des objets de plan et calibration d'échelle. Module PUR.2import type { Calibration, LengthUnit, Point } from "./types";3import { lengthToMeters } from "./units";45export const GEOMETRY_VERSION = "metre-geo-1.0";67/** précision de clic supposée pour l'incertitude de calibration (pixels) */8export const CLICK_SIGMA_PX = 1.5;910export function dist(a: Point, b: Point): number {11  return Math.hypot(b.x - a.x, b.y - a.y);12}1314/** Longueur d'une polyligne (pixels). `closed` ajoute le segment de fermeture. */15export function polylineLengthPx(points: Point[], closed = false): number {16  if (points.length < 2) return 0;17  let s = 0;18  for (let i = 1; i < points.length; i++) s += dist(points[i - 1], points[i]);19  if (closed && points.length > 2) s += dist(points[points.length - 1], points[0]);20  return s;21}2223/** Aire d'un polygone simple (formule du lacet), en pixels². Toujours positive. */24export function polygonAreaPx(points: Point[]): number {25  if (points.length < 3) return 0;26  let s = 0;27  for (let i = 0; i < points.length; i++) {28    const a = points[i], b = points[(i + 1) % points.length];29    s += a.x * b.y - b.x * a.y;30  }31  return Math.abs(s) / 2;32}3334/** Vrai si deux segments (a,b) et (c,d) se croisent strictement. */35function segmentsCross(a: Point, b: Point, c: Point, d: Point): boolean {36  const o = (p: Point, q: Point, r: Point) => Math.sign((q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x));37  const o1 = o(a, b, c), o2 = o(a, b, d), o3 = o(c, d, a), o4 = o(c, d, b);38  return o1 !== o2 && o3 !== o4 && o1 !== 0 && o2 !== 0 && o3 !== 0 && o4 !== 0;39}4041/** Détecte un polygone auto-intersectant (aire du lacet non fiable). */42export function isSelfIntersecting(points: Point[]): boolean {43  const n = points.length;44  if (n < 4) return false;45  for (let i = 0; i < n; i++) {46    for (let j = i + 2; j < n; j++) {47      if (i === 0 && j === n - 1) continue; // segments adjacents (fermeture)48      if (segmentsCross(points[i], points[(i + 1) % n], points[j], points[(j + 1) % n])) return true;49    }50  }51  return false;52}5354export function bounds(points: Point[]): { minX: number; minY: number; maxX: number; maxY: number } {55  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;56  for (const p of points) { minX = Math.min(minX, p.x); minY = Math.min(minY, p.y); maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y); }57  return { minX, minY, maxX, maxY };58}5960/* ---------------------------------------------------------- calibration */6162/**63 * Construit une calibration à partir de deux points et de la distance réelle64 * connue. L'incertitude relative combine l'erreur de clic (σ ≈ 1,5 px par65 * point, √2 pour deux points) et l'incertitude de la longueur saisie (0 par66 * défaut). Jamais inventée : si la distance en pixels est trop courte, l'erreur67 * monte mécaniquement et l'interface l'affiche.68 */69export function makeCalibration(input: { id: string; pageId: string; revision: number; p1: Point; p2: Point; realLength: number; unit: LengthUnit; createdBy: string; createdAt: string; lengthUncertaintyPct?: number }): Calibration {70  const px = dist(input.p1, input.p2);71  if (!(px > 0)) throw new Error("Les deux points de calibration sont confondus.");72  if (!(input.realLength > 0)) throw new Error("La longueur réelle doit être positive.");73  const meters = lengthToMeters(input.realLength, input.unit);74  const pxPerMeter = px / meters;75  const clickPct = (CLICK_SIGMA_PX * Math.SQRT2) / px * 100;76  const uncertaintyPct = Math.sqrt(clickPct ** 2 + (input.lengthUncertaintyPct ?? 0) ** 2);77  return { id: input.id, pageId: input.pageId, revision: input.revision, p1: input.p1, p2: input.p2, realLength: input.realLength, unit: input.unit, pxPerMeter, uncertaintyPct: Math.round(uncertaintyPct * 100) / 100, createdBy: input.createdBy, createdAt: input.createdAt };78}7980/** pixels → mètres. */81export function pxToMeters(px: number, cal: Calibration): number { return px / cal.pxPerMeter; }82/** pixels² → m². */83export function px2ToM2(px2: number, cal: Calibration): number { return px2 / cal.pxPerMeter ** 2; }8485/* --------------------------------------------------------------- toiture */8687/**88 * Facteur de pente : surface réelle = surface projetée × √(1 + (pente/12)²).89 * La pente est la hausse en pouces par 12 pouces de course (6/12 ≈ 1,118).90 */91export function roofSlopeFactor(pitchRise12: number): number {92  const p = Math.max(0, pitchRise12 || 0) / 12;93  return Math.sqrt(1 + p * p);94}9596/** Facteur de pente depuis un angle en degrés. */97export function roofSlopeFactorFromDegrees(deg: number): number {98  const r = (Math.max(0, Math.min(89, deg)) * Math.PI) / 180;99  return 1 / Math.cos(r);100}101
Files · main
Menu

Theme