Add parametric wall types and engine skeleton
This commit is contained in:
@@ -0,0 +1,569 @@
|
||||
// Parametrische Wand-Engine — löst ParametricWall-Regelwerke zu Wall[]-Arrays auf.
|
||||
//
|
||||
// Dieses Modul ist ABSICHTLICH frei von React-, Store- und App.tsx-Importen.
|
||||
// Es ist eine reine Modell-Schicht: Eingabe ist ein ParametricWall-Regelwerk +
|
||||
// Kontext (Geschoss, Grid-Achsen, bestehende Wände), Ausgabe sind neue Wall[]-
|
||||
// Objekte, die direkt in project.walls eingefügt werden können.
|
||||
//
|
||||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import type {
|
||||
ConditionalThicknessRule,
|
||||
DrawingLevel,
|
||||
GridRule,
|
||||
ModuleRule,
|
||||
ParametricRule,
|
||||
ParametricWall,
|
||||
ReferenceLineRule,
|
||||
SequenceRule,
|
||||
Vec2,
|
||||
Wall,
|
||||
WallType,
|
||||
} from "./types";
|
||||
|
||||
// ── Kontext-Typen ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Kontext, der dem Engine beim Auflösen übergeben wird.
|
||||
* Enthält alle Informationen, die für die Generierung von Wänden benötigt werden.
|
||||
*/
|
||||
export interface ParametricContext {
|
||||
/** Das Ziel-Geschoss. */
|
||||
floor: DrawingLevel;
|
||||
/**
|
||||
* Optionale Rasterachsen (Phase 3: verlinkter Grid-Ressource). Fehlen sie,
|
||||
* berechnet die Engine die Achsen aus `GridRule.spacing`.
|
||||
*/
|
||||
gridAxes?: { x: number[]; y: number[] };
|
||||
/**
|
||||
* Optionales Clipping-Polygon (Meter). Fehlt es, reicht das Raster über
|
||||
* einen Standardbereich (0…spacing*count).
|
||||
*/
|
||||
boundaryGeometry?: { boundary: Vec2[] };
|
||||
/**
|
||||
* Bereits im Projekt vorhandene Wände des Geschosses. Werden von
|
||||
* refinierenden Regeln (ConditionalThicknessRule, ReferenceLineRule) genutzt.
|
||||
* Optional — fehlt er, wird mit leerem Array gearbeitet.
|
||||
*/
|
||||
existingWalls?: Wall[];
|
||||
}
|
||||
|
||||
/** Interner Arbeitskontext, der durch den Rule-Dispatcher weitergereicht wird. */
|
||||
interface RuleCtx {
|
||||
floorId: string;
|
||||
defaultWallType: WallType;
|
||||
context: ParametricContext;
|
||||
/** Wände, die bisher durch frühere Regeln erzeugt wurden (veränderbar). */
|
||||
existingWalls: Wall[];
|
||||
}
|
||||
|
||||
// ── ID-Hilfsfunktion ──────────────────────────────────────────────────────
|
||||
|
||||
/** Einfacher Zähler für generierte Wand-IDs (sessionlokal, nicht persistent). */
|
||||
let _idCounter = 0;
|
||||
|
||||
/**
|
||||
* Erzeugt eine eindeutige ID für eine parametrisch generierte Wand.
|
||||
* Format: „pw-<parametricWallId>-<counter>".
|
||||
*/
|
||||
function makeWallId(prefix: string): string {
|
||||
_idCounter += 1;
|
||||
return `pw-${prefix}-${_idCounter}`;
|
||||
}
|
||||
|
||||
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Löst ein ParametricWall-Regelwerk zu einem Wall[]-Array für ein gegebenes
|
||||
* Geschoss auf.
|
||||
*
|
||||
* Ablauf:
|
||||
* 1. Regelwerk sequenziell ausführen; jede Regel erhält die Ausgabe der
|
||||
* vorherigen als `existingWalls` (ermöglicht Verfeinerung).
|
||||
* 2. Duplikate (gleicher Start-/Endpunkt innerhalb `tolerance`) entfernen.
|
||||
* 3. Bereinigte Wall[]-Liste zurückgeben.
|
||||
*
|
||||
* Die Ausgabe ist sofort bereit zur Einfügung in `project.walls`. Es werden
|
||||
* keine Seiteneffekte erzeugt — kein Store, kein Dispatch, kein React.
|
||||
*
|
||||
* @param parametricWall - Das Regelwerk (aus Project.parametricWalls[]).
|
||||
* @param floorId - ID des Ziel-Geschosses.
|
||||
* @param context - Kontext (Geschoss-Objekt, Grid-Achsen, Grenzen, …).
|
||||
* @param defaultWallType - Fallback-Wandtyp, wenn eine Regel keinen nennt.
|
||||
* @param tolerance - Näherungstoleranz für Duplikat-Erkennung (Meter, Default 0.01).
|
||||
* @returns Wall[]-Array, bereit zur Einfügung.
|
||||
*/
|
||||
export function resolveParametricWall(
|
||||
parametricWall: ParametricWall,
|
||||
floorId: string,
|
||||
context: ParametricContext,
|
||||
defaultWallType: WallType,
|
||||
tolerance = 0.01,
|
||||
): Wall[] {
|
||||
const ctx: RuleCtx = {
|
||||
floorId,
|
||||
defaultWallType,
|
||||
context,
|
||||
existingWalls: context.existingWalls ?? [],
|
||||
};
|
||||
|
||||
const generated: Wall[] = [];
|
||||
|
||||
for (const rule of parametricWall.rules) {
|
||||
const ruleWalls = applyRule(rule, {
|
||||
...ctx,
|
||||
existingWalls: [...ctx.existingWalls, ...generated],
|
||||
});
|
||||
generated.push(...ruleWalls);
|
||||
}
|
||||
|
||||
return deduplicateWalls(generated, tolerance);
|
||||
}
|
||||
|
||||
// ── Rule-Dispatcher ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Dispatcher: delegiert eine Regel an die passende Implementierung.
|
||||
* Alle Branches sind exhaustiv — unbekannte Typen geben leer zurück.
|
||||
*
|
||||
* @param rule - Die auszuführende Regel.
|
||||
* @param ctx - Interner Arbeitskontext.
|
||||
* @returns Wall[]-Array, das diese Regel erzeugt oder verändert hat.
|
||||
*/
|
||||
export function applyRule(rule: ParametricRule, ctx: RuleCtx): Wall[] {
|
||||
switch (rule.type) {
|
||||
case "grid":
|
||||
return applyGridRule(rule, ctx);
|
||||
case "module":
|
||||
return applyModuleRule(rule, ctx);
|
||||
case "conditional-thickness":
|
||||
return applyConditionalThicknessRule(rule, ctx);
|
||||
case "reference-line":
|
||||
return applyReferenceLineRule(rule, ctx);
|
||||
case "sequence":
|
||||
return applySequenceRule(rule, ctx);
|
||||
default: {
|
||||
// TypeScript exhaustiveness-Guard: niemals erreicht bei vollständiger Union.
|
||||
const _exhaustive: never = rule;
|
||||
void _exhaustive;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Regel-Implementierungen ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Raster-Regel: generiert Wände entlang gleichmäßiger X-/Y-Achsen.
|
||||
*
|
||||
* MVP-Verhalten:
|
||||
* - Rasterachsen aus `context.gridAxes` ODER gleichmäßigem `spacing`.
|
||||
* - Standardbereich: 0 … `defaultExtent` (10 Einheiten × Spacing), wenn keine
|
||||
* Grenzgeometrie vorhanden.
|
||||
* - Clipping durch `context.boundaryGeometry` (nur einfache Bounding-Box, MVP).
|
||||
* - Je Achse eine Wand senkrecht zur Richtung.
|
||||
*
|
||||
* @param rule - GridRule-Parameter.
|
||||
* @param ctx - Arbeitskontext (Geschoss, Wandtyp, …).
|
||||
* @returns Wall[]-Array mit generierten Rasterwänden.
|
||||
*/
|
||||
export function applyGridRule(rule: GridRule, ctx: RuleCtx): Wall[] {
|
||||
const walls: Wall[] = [];
|
||||
const spacing = rule.spacing ?? 3.0;
|
||||
const wallTypeId = rule.wallTypeId ?? ctx.defaultWallType.id;
|
||||
const referenceLine = rule.referenceLine;
|
||||
const height = rule.height ?? (ctx.context.floor.floorHeight ?? 2.6);
|
||||
const floorId = ctx.floorId;
|
||||
const categoryCode = "20";
|
||||
|
||||
// Bereich aus Grenzpolygon (Bounding-Box) oder Standardbereich ermitteln.
|
||||
const { minX, maxX, minY, maxY } = computeBounds(ctx.context, spacing);
|
||||
|
||||
// X-Richtung: Wände parallel zur Y-Achse (also senkrecht zur X-Richtung).
|
||||
if (rule.directions === "x" || rule.directions === "both") {
|
||||
const axes = rule.gridId != null && ctx.context.gridAxes
|
||||
? ctx.context.gridAxes.x
|
||||
: generateAxisPositions(minX, maxX, spacing);
|
||||
|
||||
for (const axisX of axes) {
|
||||
const wall: Wall = {
|
||||
id: makeWallId(`${floorId}-gx`),
|
||||
type: "wall",
|
||||
floorId,
|
||||
categoryCode,
|
||||
start: { x: axisX, y: minY },
|
||||
end: { x: axisX, y: maxY },
|
||||
wallTypeId,
|
||||
height,
|
||||
...(referenceLine != null ? { referenceLine } : {}),
|
||||
};
|
||||
walls.push(wall);
|
||||
}
|
||||
}
|
||||
|
||||
// Y-Richtung: Wände parallel zur X-Achse (also senkrecht zur Y-Richtung).
|
||||
if (rule.directions === "y" || rule.directions === "both") {
|
||||
const axes = rule.gridId != null && ctx.context.gridAxes
|
||||
? ctx.context.gridAxes.y
|
||||
: generateAxisPositions(minY, maxY, spacing);
|
||||
|
||||
for (const axisY of axes) {
|
||||
const wall: Wall = {
|
||||
id: makeWallId(`${floorId}-gy`),
|
||||
type: "wall",
|
||||
floorId,
|
||||
categoryCode,
|
||||
start: { x: minX, y: axisY },
|
||||
end: { x: maxX, y: axisY },
|
||||
wallTypeId,
|
||||
height,
|
||||
...(referenceLine != null ? { referenceLine } : {}),
|
||||
};
|
||||
walls.push(wall);
|
||||
}
|
||||
}
|
||||
|
||||
return walls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modul-Regel: unterteilt eine Referenzspanne proportional in Felder.
|
||||
*
|
||||
* MVP-Verhalten:
|
||||
* - Spanne aus Geschoss-Ausdehnung (Bounding-Box) oder Referenzwand.
|
||||
* - Teilungspunkte im `moduleSize`-Abstand.
|
||||
* - Querwände (senkrecht zur `direction`) an jedem Teilungspunkt.
|
||||
*
|
||||
* @param rule - ModuleRule-Parameter.
|
||||
* @param ctx - Arbeitskontext.
|
||||
* @returns Wall[]-Array mit Querwänden an Modul-Teilungspunkten.
|
||||
*/
|
||||
export function applyModuleRule(rule: ModuleRule, ctx: RuleCtx): Wall[] {
|
||||
const walls: Wall[] = [];
|
||||
const wallTypeId = rule.wallTypeId ?? ctx.defaultWallType.id;
|
||||
const referenceLine = rule.referenceLine;
|
||||
const height = rule.height ?? (ctx.context.floor.floorHeight ?? 2.6);
|
||||
const floorId = ctx.floorId;
|
||||
const categoryCode = "20";
|
||||
|
||||
const { minX, maxX, minY, maxY } = computeBounds(ctx.context, rule.moduleSize);
|
||||
|
||||
// Referenzwand: falls angegeben, Spanne aus dieser Wand ableiten.
|
||||
let spanStart: number;
|
||||
let spanEnd: number;
|
||||
let perpStart: number;
|
||||
let perpEnd: number;
|
||||
|
||||
if (rule.referenceWallId != null) {
|
||||
const refWall = ctx.existingWalls.find((w) => w.id === rule.referenceWallId);
|
||||
if (refWall != null) {
|
||||
// Spanne entlang der Hauptachse der Referenzwand.
|
||||
if (rule.direction === "x") {
|
||||
spanStart = Math.min(refWall.start.x, refWall.end.x);
|
||||
spanEnd = Math.max(refWall.start.x, refWall.end.x);
|
||||
perpStart = minY;
|
||||
perpEnd = maxY;
|
||||
} else {
|
||||
spanStart = Math.min(refWall.start.y, refWall.end.y);
|
||||
spanEnd = Math.max(refWall.start.y, refWall.end.y);
|
||||
perpStart = minX;
|
||||
perpEnd = maxX;
|
||||
}
|
||||
} else {
|
||||
// Referenzwand nicht gefunden: Fallback auf Bounding-Box.
|
||||
spanStart = rule.direction === "x" ? minX : minY;
|
||||
spanEnd = rule.direction === "x" ? maxX : maxY;
|
||||
perpStart = rule.direction === "x" ? minY : minX;
|
||||
perpEnd = rule.direction === "x" ? maxY : maxX;
|
||||
}
|
||||
} else {
|
||||
spanStart = rule.direction === "x" ? minX : minY;
|
||||
spanEnd = rule.direction === "x" ? maxX : maxY;
|
||||
perpStart = rule.direction === "x" ? minY : minX;
|
||||
perpEnd = rule.direction === "x" ? maxY : maxX;
|
||||
}
|
||||
|
||||
// Modul-Teilungspunkte (ohne Start und Ende der Spanne selbst).
|
||||
const positions = generateAxisPositions(spanStart, spanEnd, rule.moduleSize);
|
||||
// Ersten und letzten Punkt herausfiltern, da das dort bereits Außenwände gibt.
|
||||
const dividers = positions.filter((p) => p > spanStart + 1e-6 && p < spanEnd - 1e-6);
|
||||
|
||||
for (const pos of dividers) {
|
||||
const wall: Wall = rule.direction === "x"
|
||||
? {
|
||||
id: makeWallId(`${floorId}-mx`),
|
||||
type: "wall",
|
||||
floorId,
|
||||
categoryCode,
|
||||
start: { x: pos, y: perpStart },
|
||||
end: { x: pos, y: perpEnd },
|
||||
wallTypeId,
|
||||
height,
|
||||
...(referenceLine != null ? { referenceLine } : {}),
|
||||
}
|
||||
: {
|
||||
id: makeWallId(`${floorId}-my`),
|
||||
type: "wall",
|
||||
floorId,
|
||||
categoryCode,
|
||||
start: { x: perpStart, y: pos },
|
||||
end: { x: perpEnd, y: pos },
|
||||
wallTypeId,
|
||||
height,
|
||||
...(referenceLine != null ? { referenceLine } : {}),
|
||||
};
|
||||
walls.push(wall);
|
||||
}
|
||||
|
||||
return walls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bedingte-Dicken-Regel: weist bestehenden Wänden einen neuen Wandtyp zu,
|
||||
* wenn eine Bedingung erfüllt ist.
|
||||
*
|
||||
* Gibt MODIFIZIERTE KOPIEN der passenden Wände zurück (keine Mutation).
|
||||
* Die Originalwände in `existingWalls` bleiben unverändert.
|
||||
*
|
||||
* @param rule - ConditionalThicknessRule-Parameter.
|
||||
* @param ctx - Arbeitskontext (enthält die zu prüfenden Wände).
|
||||
* @returns Wall[]-Array mit geändertem `wallTypeId` für passende Wände.
|
||||
*/
|
||||
export function applyConditionalThicknessRule(
|
||||
rule: ConditionalThicknessRule,
|
||||
ctx: RuleCtx,
|
||||
): Wall[] {
|
||||
return ctx.existingWalls
|
||||
.filter((w) => matchesCondition(w, rule.condition, ctx))
|
||||
.map((w) => ({ ...w, id: makeWallId(`${ctx.floorId}-ct`), wallTypeId: rule.wallTypeId }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Referenzlinien-Regel: setzt `referenceLine` bei passenden Wänden einheitlich.
|
||||
*
|
||||
* Gibt MODIFIZIERTE KOPIEN der passenden Wände zurück.
|
||||
*
|
||||
* @param rule - ReferenceLineRule-Parameter.
|
||||
* @param ctx - Arbeitskontext.
|
||||
* @returns Wall[]-Array mit gesetzter `referenceLine`.
|
||||
*/
|
||||
export function applyReferenceLineRule(
|
||||
rule: ReferenceLineRule,
|
||||
ctx: RuleCtx,
|
||||
): Wall[] {
|
||||
return ctx.existingWalls
|
||||
.filter((w) => matchesTarget(w, rule.target, ctx))
|
||||
.map((w) => ({
|
||||
...w,
|
||||
id: makeWallId(`${ctx.floorId}-rl`),
|
||||
referenceLine: rule.referenceLine,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sequenz-Regel: führt Unterregeln in Reihenfolge aus.
|
||||
*
|
||||
* Jede Unterregel erhält das bisherige Ergebnis als `existingWalls`, sodass
|
||||
* spätere Regeln die früheren verfeinern können (z. B. Raster → Dicke → Linie).
|
||||
* Mit `stopOnMatch=true` bricht die Sequenz nach dem ersten produktiven Schritt ab.
|
||||
*
|
||||
* @param rule - SequenceRule-Parameter (enthält `rules[]`).
|
||||
* @param ctx - Arbeitskontext.
|
||||
* @returns Wall[]-Array (Summe aller Unterregel-Ausgaben, oder Abbruch bei stopOnMatch).
|
||||
*/
|
||||
export function applySequenceRule(rule: SequenceRule, ctx: RuleCtx): Wall[] {
|
||||
let result: Wall[] = [];
|
||||
|
||||
for (const subrule of rule.rules) {
|
||||
const subruleCtx: RuleCtx = {
|
||||
...ctx,
|
||||
existingWalls: [...ctx.existingWalls, ...result],
|
||||
};
|
||||
const subruleWalls = applyRule(subrule, subruleCtx);
|
||||
result = [...result, ...subruleWalls];
|
||||
|
||||
if (rule.stopOnMatch === true && subruleWalls.length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Hilfsfunktionen ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Entfernt doppelte Wände aus der generierten Liste.
|
||||
*
|
||||
* Zwei Wände gelten als Duplikat, wenn Start- und Endpunkt jeweils innerhalb
|
||||
* `tolerance` (Meter) übereinstimmen — sowohl in der gleichen als auch in der
|
||||
* umgekehrten Orientierung (A→B = B→A).
|
||||
*
|
||||
* Behält jeweils die ERSTE Instanz; spätere Duplikate werden verworfen.
|
||||
*
|
||||
* @param walls - Eingabe-Wall[]-Array (wird nicht mutiert).
|
||||
* @param tolerance - Abstandsschwelle in Metern (Default 0.01 m = 1 cm).
|
||||
* @returns Bereinigte Wall[]-Liste ohne Duplikate.
|
||||
*/
|
||||
export function deduplicateWalls(walls: Wall[], tolerance = 0.01): Wall[] {
|
||||
const unique: Wall[] = [];
|
||||
|
||||
for (const candidate of walls) {
|
||||
const isDuplicate = unique.some((existing) => {
|
||||
const fwd =
|
||||
dist2(candidate.start, existing.start) <= tolerance &&
|
||||
dist2(candidate.end, existing.end) <= tolerance;
|
||||
const rev =
|
||||
dist2(candidate.start, existing.end) <= tolerance &&
|
||||
dist2(candidate.end, existing.start) <= tolerance;
|
||||
return fwd || rev;
|
||||
});
|
||||
|
||||
if (!isDuplicate) {
|
||||
unique.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return unique;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob eine Wand die Bedingung einer ConditionalThicknessRule erfüllt.
|
||||
*
|
||||
* MVP-Implementierung:
|
||||
* • „exterior" — Wand liegt am Rand der Bounding-Box des Kontexts
|
||||
* (innerhalb einer großzügigen Toleranz).
|
||||
* • „interior" — Gegenteil von „exterior".
|
||||
* • „bearing" — Wand ist in der Primärrichtung (X oder Y) ausgerichtet
|
||||
* (einfaches Heuristikum für MVP).
|
||||
* • Sonstige — immer `false` (Phase 3: Wall.tags[]).
|
||||
*
|
||||
* @param wall - Zu prüfende Wand.
|
||||
* @param condition - Bedingungsstring aus der Regel.
|
||||
* @param ctx - Arbeitskontext (enthält Grenzgeometrie).
|
||||
* @returns `true`, wenn die Bedingung zutrifft.
|
||||
*/
|
||||
export function matchesCondition(
|
||||
wall: Wall,
|
||||
condition: string,
|
||||
ctx: RuleCtx,
|
||||
): boolean {
|
||||
if (condition === "exterior") {
|
||||
return isExteriorWall(wall, ctx);
|
||||
}
|
||||
if (condition === "interior") {
|
||||
return !isExteriorWall(wall, ctx);
|
||||
}
|
||||
if (condition === "bearing") {
|
||||
// Heuristikum: Wand ist „tragend", wenn sie in X- oder Y-Richtung läuft
|
||||
// (nahezu horizontal/vertikal). Phase 3: strukturelle Klassifizierung.
|
||||
const dx = Math.abs(wall.end.x - wall.start.x);
|
||||
const dy = Math.abs(wall.end.y - wall.start.y);
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
if (len < 1e-6) return false;
|
||||
const angleFromX = Math.atan2(dy, dx);
|
||||
// Gilt als tragend, wenn Winkel innerhalb 10° von X- oder Y-Achse liegt.
|
||||
const tenDeg = (10 * Math.PI) / 180;
|
||||
return (
|
||||
angleFromX <= tenDeg ||
|
||||
angleFromX >= Math.PI / 2 - tenDeg
|
||||
);
|
||||
}
|
||||
// Unbekannte Bedingung — Phase 3: Tag-Matching.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob eine Wand dem Ziel einer ReferenceLineRule entspricht.
|
||||
*
|
||||
* @param wall - Zu prüfende Wand.
|
||||
* @param target - Zielstring aus der Regel.
|
||||
* @param ctx - Arbeitskontext.
|
||||
* @returns `true`, wenn die Wand dem Ziel entspricht.
|
||||
*/
|
||||
export function matchesTarget(
|
||||
wall: Wall,
|
||||
target: string,
|
||||
ctx: RuleCtx,
|
||||
): boolean {
|
||||
if (target === "all") return true;
|
||||
if (target === "exterior") return isExteriorWall(wall, ctx);
|
||||
// Unbekanntes Ziel — Phase 3: Tag-Matching.
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Interne Hilfsfunktionen ───────────────────────────────────────────────
|
||||
|
||||
/** Euklidischer Abstand zweier 2D-Punkte. */
|
||||
function dist2(a: Vec2, b: Vec2): number {
|
||||
const dx = a.x - b.x;
|
||||
const dy = a.y - b.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Bounding-Box des Kontexts.
|
||||
* Nutzt `boundaryGeometry`, falls vorhanden; sonst einen Standardbereich,
|
||||
* der auf `spacing` basiert (0 … spacing × 10).
|
||||
*/
|
||||
function computeBounds(
|
||||
context: ParametricContext,
|
||||
spacing: number,
|
||||
): { minX: number; maxX: number; minY: number; maxY: number } {
|
||||
if (context.boundaryGeometry != null && context.boundaryGeometry.boundary.length >= 2) {
|
||||
const pts = context.boundaryGeometry.boundary;
|
||||
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
||||
for (const p of pts) {
|
||||
if (p.x < minX) minX = p.x;
|
||||
if (p.x > maxX) maxX = p.x;
|
||||
if (p.y < minY) minY = p.y;
|
||||
if (p.y > maxY) maxY = p.y;
|
||||
}
|
||||
return { minX, maxX, minY, maxY };
|
||||
}
|
||||
// Standardbereich: 10 Einheiten × Spacing.
|
||||
const extent = spacing * 10;
|
||||
return { minX: 0, maxX: extent, minY: 0, maxY: extent };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert gleichmäßig verteilte Achspositionen im Intervall [min, max].
|
||||
* Die erste Position liegt bei `min`, jede weitere `spacing` danach.
|
||||
* Enthält `max`, wenn dieser exakt auf einem Rasterpunkt liegt (±1e-6).
|
||||
*/
|
||||
function generateAxisPositions(min: number, max: number, spacing: number): number[] {
|
||||
if (spacing <= 0 || max <= min) return [];
|
||||
const positions: number[] = [];
|
||||
let pos = min;
|
||||
while (pos <= max + 1e-6) {
|
||||
positions.push(Math.round(pos * 1e6) / 1e6); // Rundungsfehler vermeiden
|
||||
pos += spacing;
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Klassifiziert eine Wand als „außen" (Rand der Bounding-Box) oder „innen".
|
||||
*
|
||||
* MVP: Eine Wand gilt als außen, wenn BEIDE ihrer Endpunkte auf oder nahe
|
||||
* am Rand der Bounding-Box des Kontexts liegen (±`edgeTolerance`).
|
||||
*/
|
||||
function isExteriorWall(wall: Wall, ctx: RuleCtx): boolean {
|
||||
const { minX, maxX, minY, maxY } = computeBounds(ctx.context, 1.0);
|
||||
const edgeTol = 0.1; // 10 cm Toleranz am Rand.
|
||||
|
||||
const isOnEdge = (p: Vec2): boolean =>
|
||||
Math.abs(p.x - minX) <= edgeTol ||
|
||||
Math.abs(p.x - maxX) <= edgeTol ||
|
||||
Math.abs(p.y - minY) <= edgeTol ||
|
||||
Math.abs(p.y - maxY) <= edgeTol;
|
||||
|
||||
return isOnEdge(wall.start) && isOnEdge(wall.end);
|
||||
}
|
||||
|
||||
// ── Re-Export der Kontexttypen für Unit-Tests / Integration ──────────────
|
||||
// (ermöglicht Import ohne Direktverweis auf interne RuleCtx)
|
||||
export type { ParametricContext as ResolveContext };
|
||||
|
||||
// DrawingLevel-Re-Export für Konsumenten, die keinen eigenen types-Import wollen.
|
||||
export type { DrawingLevel };
|
||||
Reference in New Issue
Block a user