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 };
|
||||||
+508
-1
@@ -11,6 +11,14 @@
|
|||||||
|
|
||||||
export type Vec2 = { x: number; y: number };
|
export type Vec2 = { x: number; y: number };
|
||||||
|
|
||||||
|
// SIA-416-Blatt-Kategorie eines Raums. Der reine Rechenkern (geometry/roomArea)
|
||||||
|
// definiert den Typ; hier nur der Typ-Import (kein Zyklus zur Laufzeit).
|
||||||
|
import type { SiaCategory } from "../geometry/roomArea";
|
||||||
|
export type { SiaCategory } from "../geometry/roomArea";
|
||||||
|
// Rich-Text-Dokument für den Raum-Stempel (frei editierbarer Teil).
|
||||||
|
import type { RichTextDoc } from "../text/richText";
|
||||||
|
export type { RichTextDoc } from "../text/richText";
|
||||||
|
|
||||||
// ── Ressourcen-Bibliotheken (Vectorworks-/DOSSIER-Stil) ────────────────────
|
// ── Ressourcen-Bibliotheken (Vectorworks-/DOSSIER-Stil) ────────────────────
|
||||||
// Verwaltete, per id verwiesene Stil-Ressourcen. Verweis-Kette:
|
// Verwaltete, per id verwiesene Stil-Ressourcen. Verweis-Kette:
|
||||||
// Component → Hatch → LineStyle.
|
// Component → Hatch → LineStyle.
|
||||||
@@ -56,6 +64,33 @@ export interface HatchStyle {
|
|||||||
lineStyleId?: string;
|
lineStyleId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PBR-Material-Karten eines Bauteils (3D-Texturierung). Jede URL verweist auf
|
||||||
|
* ein Bild — entweder ein eingebautes Bibliotheks-Asset unter
|
||||||
|
* `/assets/materials/...` ODER eine per Nutzer-Upload erzeugte Object-/Data-URL.
|
||||||
|
* Fehlende Karten werden weggelassen (z. B. nur `color` bei einem Upload).
|
||||||
|
* `sizeM` ist die physische Kantenlänge EINER Texturkachel in Metern (für die
|
||||||
|
* korrekte Skalierung der Wiederholung), Default 1.0 m.
|
||||||
|
*/
|
||||||
|
export interface ComponentMaterial {
|
||||||
|
/** Optionaler Verweis auf ein Bibliotheks-Asset (`MaterialAsset.id`). */
|
||||||
|
libraryId?: string;
|
||||||
|
/** Albedo-/Farb-Karte (map). */
|
||||||
|
color?: string;
|
||||||
|
/** Normal-Karte (normalMap) — erzeugt die Oberflächentiefe. */
|
||||||
|
normal?: string;
|
||||||
|
/** Rauheits-Karte (roughnessMap). */
|
||||||
|
roughness?: string;
|
||||||
|
/** Metallizitäts-Karte (metalnessMap). */
|
||||||
|
metalness?: string;
|
||||||
|
/** Höhen-/Displacement-Karte (displacementMap, dezent angewandt). */
|
||||||
|
displacement?: string;
|
||||||
|
/** Ambient-Occlusion-Karte (aoMap). */
|
||||||
|
ao?: string;
|
||||||
|
/** Physische Kachelgröße in Metern (Default 1.0). */
|
||||||
|
sizeM?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ein Bauteil-Material (Component Manager) — vereint Plan-Schnittdarstellung
|
* Ein Bauteil-Material (Component Manager) — vereint Plan-Schnittdarstellung
|
||||||
* (Schraffur) und 3D-Erscheinung. Ersetzt das frühere `Material`.
|
* (Schraffur) und 3D-Erscheinung. Ersetzt das frühere `Material`.
|
||||||
@@ -69,6 +104,12 @@ export interface Component {
|
|||||||
hatchId: string;
|
hatchId: string;
|
||||||
/** Optionale 3D-Textur (vorerst ignoriert). */
|
/** Optionale 3D-Textur (vorerst ignoriert). */
|
||||||
texture3d?: string;
|
texture3d?: string;
|
||||||
|
/**
|
||||||
|
* Optionales echtes PBR-Material für die 3D-Texturierung (Bibliothek ODER
|
||||||
|
* Upload). Fehlt es, bleibt das heutige Verhalten (matte Farbe). Wirkt nur im
|
||||||
|
* Render-Modus „textured".
|
||||||
|
*/
|
||||||
|
material?: ComponentMaterial;
|
||||||
/** Verschneidungs-Rang: höher läuft am Stoß durch (Backbone). */
|
/** Verschneidungs-Rang: höher läuft am Stoß durch (Backbone). */
|
||||||
joinPriority: number;
|
joinPriority: number;
|
||||||
}
|
}
|
||||||
@@ -88,6 +129,182 @@ export interface WallType {
|
|||||||
layers: Layer[];
|
layers: Layer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Parametrische Wände (Parametric Walls) ────────────────────────────────
|
||||||
|
// Parametrische Wand-Regeln generieren automatisch Wall[]-Arrays — analog zu
|
||||||
|
// FreeCAD BIM. Sie leben in Project.resources.parametricWalls[] und werden
|
||||||
|
// durch `resolveParametricWall()` in `src/model/parametricWalls.ts` aufgelöst.
|
||||||
|
// Ausgabe: normale Wall[]-Objekte (kein neuer Elementtyp).
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine parametrische Wand-Regel — generiert automatisch Wall[]-Einträge für
|
||||||
|
* ein gegebenes Geschoss. Lebt in der Ressourcen-Bibliothek des Projekts.
|
||||||
|
* Wird über `resolveParametricWall()` aufgelöst, nicht zur Laufzeit gespeichert.
|
||||||
|
*/
|
||||||
|
export interface ParametricWall {
|
||||||
|
id: string;
|
||||||
|
/** Anzeigename, z. B. „Wohnbau-Raster 3m". */
|
||||||
|
name: string;
|
||||||
|
/** Optionale Beschreibung (für den Ressourcen-Manager). */
|
||||||
|
description?: string;
|
||||||
|
/**
|
||||||
|
* Geordnete Liste der anzuwendenden Regeln. Spätere Regeln können die
|
||||||
|
* Ausgabe früherer Regeln verfeinern (z. B. Dickenzuweisung nach Raster).
|
||||||
|
*/
|
||||||
|
rules: ParametricRule[];
|
||||||
|
/**
|
||||||
|
* Rückfall-Wandtyp, falls eine Regel keinen eigenen `wallTypeId` nennt.
|
||||||
|
* Muss auf einen gültigen WallType im Projekt verweisen.
|
||||||
|
*/
|
||||||
|
defaultWallTypeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine einzelne parametrische Regel — eine Strategie zur Wandplatzierung oder
|
||||||
|
* -verfeinerung. Regeln werden als Discriminated Union kodiert; der `type`-Tag
|
||||||
|
* bestimmt, welche Felder verfügbar sind.
|
||||||
|
*/
|
||||||
|
export type ParametricRule =
|
||||||
|
| GridRule
|
||||||
|
| ModuleRule
|
||||||
|
| ConditionalThicknessRule
|
||||||
|
| ReferenceLineRule
|
||||||
|
| SequenceRule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raster-Regel: generiert Wände entlang gleichmäßiger X-/Y-Achsen.
|
||||||
|
* Typischer Anwendungsfall: Tragwerks-Achsraster (z. B. 3 m Abstand).
|
||||||
|
*
|
||||||
|
* Ablauf (Engine):
|
||||||
|
* 1. Rasterachsen aus `spacing` oder (später) verlinkter Grid-Ressource.
|
||||||
|
* 2. Je Achse eine Wand von Rand zu Rand (begrenzt durch `boundaryId`).
|
||||||
|
* 3. Wandtyp, Referenzlinie und Höhe gemäß Regelfelder.
|
||||||
|
*/
|
||||||
|
export interface GridRule {
|
||||||
|
type: "grid";
|
||||||
|
/**
|
||||||
|
* Optionaler Verweis auf eine Grid-Ressource (Phase 3). Für MVP wird
|
||||||
|
* stattdessen `spacing` genutzt.
|
||||||
|
*/
|
||||||
|
gridId?: string;
|
||||||
|
/** Rasterabstand in Metern (Default: 3.0). Wird genutzt, wenn kein `gridId`. */
|
||||||
|
spacing?: number;
|
||||||
|
/**
|
||||||
|
* Achsrichtungen: „x" = nur horizontale Wände, „y" = nur vertikale,
|
||||||
|
* „both" = Vollraster.
|
||||||
|
*/
|
||||||
|
directions: "x" | "y" | "both";
|
||||||
|
/**
|
||||||
|
* Optionaler Verweis auf eine Drawing2D-Grenze (als Clipping-Polygon).
|
||||||
|
* Fehlt er, reicht das Raster über den sichtbaren Bereich des Geschosses.
|
||||||
|
*/
|
||||||
|
boundaryId?: string;
|
||||||
|
/** Optionale Wandtyp-Übersteuerung; sonst `defaultWallTypeId`. */
|
||||||
|
wallTypeId?: string;
|
||||||
|
/** Lage der Wandachse über die Dicke (Vectorworks-Stil). */
|
||||||
|
referenceLine?: WallReferenceLine;
|
||||||
|
/** Optionale Höhenübersteuerung in Metern; sonst Geschosshöhe. */
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modul-Regel: teilt eine Referenzspanne in proportionale Felder auf.
|
||||||
|
* Typischer Anwendungsfall: Tragwerks-Joche (z. B. 6 m-Module in einem Bauteil).
|
||||||
|
*
|
||||||
|
* Ablauf (Engine):
|
||||||
|
* 1. Gesamtspanne aus `referenceWallId` oder Geschossausdehnung ableiten.
|
||||||
|
* 2. In Module der Größe `moduleSize` unterteilen.
|
||||||
|
* 3. Querwände an jedem Modulteilungspunkt setzen.
|
||||||
|
*/
|
||||||
|
export interface ModuleRule {
|
||||||
|
type: "module";
|
||||||
|
/** Modulmaß in Metern (z. B. 6.0, 3.6). */
|
||||||
|
moduleSize: number;
|
||||||
|
/** Ausrichtung der Trennwände: „x" = quer zur X-Achse, „y" = quer zur Y-Achse. */
|
||||||
|
direction: "x" | "y";
|
||||||
|
/**
|
||||||
|
* Optionaler Verweis auf eine Referenzwand, die die Spannweite definiert.
|
||||||
|
* Fehlt er, wird die Geschoss-Ausdehnung genutzt (Phase 3: Achsen-Referenz).
|
||||||
|
*/
|
||||||
|
referenceWallId?: string;
|
||||||
|
/** Optionale Wandtyp-Übersteuerung; sonst `defaultWallTypeId`. */
|
||||||
|
wallTypeId?: string;
|
||||||
|
/** Lage der Wandachse über die Dicke. */
|
||||||
|
referenceLine?: WallReferenceLine;
|
||||||
|
/** Optionale Höhenübersteuerung in Metern. */
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bedingte-Dicken-Regel: weist bereits generierten Wänden einen anderen
|
||||||
|
* Wandtyp zu, wenn eine Bedingung erfüllt ist.
|
||||||
|
* Typischer Anwendungsfall: Außenwände erhalten einen anderen Aufbau als Innenwände.
|
||||||
|
*
|
||||||
|
* Ablauf (Engine):
|
||||||
|
* Bestehende Wände aus `existingWalls` filtern → `wallTypeId` ändern.
|
||||||
|
* Gibt modifizierte Kopien zurück (keine Mutation).
|
||||||
|
*/
|
||||||
|
export interface ConditionalThicknessRule {
|
||||||
|
type: "conditional-thickness";
|
||||||
|
/**
|
||||||
|
* Bedingung für den Treffer:
|
||||||
|
* • „exterior" — Wand liegt am Außenrand (ermittelt via Grenzpolygon).
|
||||||
|
* • „interior" — Wand liegt im Inneren.
|
||||||
|
* • „bearing" — tragende Wand (über Tag oder Wandtyp-Rang).
|
||||||
|
* • beliebiger String — benutzerdefiniertes Tag (Phase 3: Wall.tags[]).
|
||||||
|
*/
|
||||||
|
condition: "exterior" | "interior" | "bearing" | string;
|
||||||
|
/** Ziel-Wandtyp, der bei Treffer gesetzt wird. */
|
||||||
|
wallTypeId: string;
|
||||||
|
/**
|
||||||
|
* Verknüpfungslogik für mehrere Bedingungen (Phase 3: mehrere `condition`-Felder).
|
||||||
|
* Vorerst ungenutzt; Default ist implizites „or".
|
||||||
|
*/
|
||||||
|
logic?: "and" | "or";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Referenzlinien-Regel: setzt die `referenceLine`-Eigenschaft bei passenden
|
||||||
|
* Wänden einheitlich (Vectorworks-Stil).
|
||||||
|
* Typischer Anwendungsfall: Alle Außenwände auf „left" (linke Fläche = Fassade).
|
||||||
|
*
|
||||||
|
* Ablauf (Engine):
|
||||||
|
* Bestehende Wände aus `existingWalls` filtern → `referenceLine` setzen.
|
||||||
|
* Gibt modifizierte Kopien zurück.
|
||||||
|
*/
|
||||||
|
export interface ReferenceLineRule {
|
||||||
|
type: "reference-line";
|
||||||
|
/** Neue Lage der Wandachse, die einheitlich gesetzt wird. */
|
||||||
|
referenceLine: WallReferenceLine;
|
||||||
|
/**
|
||||||
|
* Filterziel:
|
||||||
|
* • „all" — alle Wände im aktuellen Satz.
|
||||||
|
* • „exterior" — nur Außenwände (wie bei ConditionalThicknessRule).
|
||||||
|
* • beliebiger String — benutzerdefiniertes Tag (Phase 3).
|
||||||
|
*/
|
||||||
|
target: "all" | "exterior" | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sequenz-Regel: fasst mehrere Unterregeln zusammen und wendet sie geordnet an.
|
||||||
|
* Jede Unterregel kann die Ausgabe der vorherigen verfeinern.
|
||||||
|
* Typischer Anwendungsfall: Raster → bedingte Dicke → Referenzlinie als atomare Einheit.
|
||||||
|
*
|
||||||
|
* Ablauf (Engine):
|
||||||
|
* Regeln in `rules` werden sequenziell ausgeführt; das Ergebnis jeder Regel wird
|
||||||
|
* als `existingWalls` der nächsten übergeben. `stopOnMatch` bricht ab, sobald
|
||||||
|
* eine Unterregel mindestens eine Wand generiert hat.
|
||||||
|
*/
|
||||||
|
export interface SequenceRule {
|
||||||
|
type: "sequence";
|
||||||
|
/** Unterregeln, in Ausführungsreihenfolge. */
|
||||||
|
rules: ParametricRule[];
|
||||||
|
/**
|
||||||
|
* Wenn `true`: Abbruch nach der ersten Unterregel, die mindestens eine Wand
|
||||||
|
* generiert/verändert. Ähnlich wie ein Short-Circuit-Fallback.
|
||||||
|
*/
|
||||||
|
stopOnMatch?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Art einer Zeichnungsebene.
|
* Art einer Zeichnungsebene.
|
||||||
* • "floor" — Geschoss, trägt Bauteile.
|
* • "floor" — Geschoss, trägt Bauteile.
|
||||||
@@ -206,9 +423,220 @@ export interface Wall {
|
|||||||
top?: VerticalAnchor;
|
top?: VerticalAnchor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine Decke (Slab) — ein geschossgebundenes, mehrschichtiges Flächenbauteil,
|
||||||
|
* definiert über einen GESCHLOSSENEN Umriss (Polygon) im Grundriss und eine
|
||||||
|
* Dicke. Analog zur Wand trägt sie ihren Schichtaufbau über einen `wallTypeId`
|
||||||
|
* (die Bauteil-/Schraffur-/Materialauflösung ist identisch); eine optionale
|
||||||
|
* `thickness` übersteuert die Gesamtdicke des Typs (Meter).
|
||||||
|
*
|
||||||
|
* Vertikale Lage: die OBERKANTE (OK) der Decke. Fehlt `top`, liegt die OK an der
|
||||||
|
* Oberkante des Geschosses (baseElevation + floorHeight — also bündig mit dem
|
||||||
|
* Wandkopf); die Decke wächst um `thickness` nach UNTEN. `top: custom` setzt eine
|
||||||
|
* absolute Z-Höhe, `top: floor` bindet die OK an ein Geschoss.
|
||||||
|
*/
|
||||||
|
export interface Ceiling {
|
||||||
|
id: string;
|
||||||
|
type: "ceiling";
|
||||||
|
/** Zugehörige Zeichnungsebene (Geschoss). */
|
||||||
|
floorId: string;
|
||||||
|
/** Grafik-Kategorie (Ebene), z. B. "30" für Decken. */
|
||||||
|
categoryCode: string;
|
||||||
|
/**
|
||||||
|
* Geschlossener Umriss im Grundriss (Meter). Der Schlusspunkt wird NICHT
|
||||||
|
* dupliziert (die letzte Kante läuft von outline[n-1] zu outline[0]).
|
||||||
|
*/
|
||||||
|
outline: Vec2[];
|
||||||
|
/** Verweis auf den (mehrschichtigen) Aufbau-Typ (wie WallType). */
|
||||||
|
wallTypeId: string;
|
||||||
|
/** Optionale Übersteuerung der Gesamtdicke in Metern (sonst Typ-Dicke). */
|
||||||
|
thickness?: number;
|
||||||
|
/**
|
||||||
|
* Optionale Übersteuerung der Strich-/Umrandungsfarbe; sonst gilt die
|
||||||
|
* Kategorie-Farbe.
|
||||||
|
*/
|
||||||
|
color?: string;
|
||||||
|
/**
|
||||||
|
* Vertikale Bindung der OBERKANTE (OK). Fehlt sie, sitzt die OK an der
|
||||||
|
* Oberkante des Geschosses (baseElevation + floorHeight).
|
||||||
|
*/
|
||||||
|
top?: VerticalAnchor;
|
||||||
|
}
|
||||||
|
|
||||||
/** Schwenkrichtung der Tür relativ zur Wandachse. */
|
/** Schwenkrichtung der Tür relativ zur Wandachse. */
|
||||||
export type SwingSide = "left" | "right";
|
export type SwingSide = "left" | "right";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine Öffnung (Fenster oder Tür), gehostet in einer Wand. Sie „kennt" ihre Wand
|
||||||
|
* (`hostWallId`) und liegt über `position` (Abstand vom Wand-Startpunkt entlang
|
||||||
|
* der Achse) relativ zur Wand — bewegt sich die Wand, folgt die Öffnung, weil die
|
||||||
|
* Weltkoordinaten beim Rendern IMMER aus der Wandachse abgeleitet werden.
|
||||||
|
*
|
||||||
|
* Vertikale Lage (relativ zur Wand-Unterkante, UK):
|
||||||
|
* • Tür — sitzt am Boden, `sillHeight` = 0, Höhe = lichte Türhöhe.
|
||||||
|
* • Fenster— Brüstung `sillHeight` > 0, Öffnung reicht von sillHeight bis
|
||||||
|
* sillHeight + height.
|
||||||
|
*
|
||||||
|
* Türspezifisch: `hinge` (Anschlagpfosten) + `swing` (Aufschlagseite) + optional
|
||||||
|
* `swingAngle` (Öffnungswinkel des Blatts in Grad, Default 90) + `openingDir`
|
||||||
|
* (Aufschlag nach innen/außen — kippt den Schwenkbogen auf die andere Achsseite).
|
||||||
|
* Fenster nutzen optional `frameDepth`/`frameThickness` für die 3D-Rahmenstärke.
|
||||||
|
*/
|
||||||
|
export interface Opening {
|
||||||
|
id: string;
|
||||||
|
type: "opening";
|
||||||
|
/** Wirts-Wand; das Geschoss ergibt sich aus der Wand. */
|
||||||
|
hostWallId: string;
|
||||||
|
/** Grafik-Kategorie (Ebene), z. B. "21" für Türen/Fenster. */
|
||||||
|
categoryCode: string;
|
||||||
|
/** Fenster oder Tür. */
|
||||||
|
kind: "window" | "door";
|
||||||
|
/** Abstand des ersten Pfostens vom Wand-Startpunkt entlang der Achse (Meter). */
|
||||||
|
position: number;
|
||||||
|
/** Lichte Öffnungsbreite in Metern. */
|
||||||
|
width: number;
|
||||||
|
/** Lichte Öffnungshöhe in Metern. */
|
||||||
|
height: number;
|
||||||
|
/** Brüstungshöhe (Unterkante der Öffnung) über der Wand-UK; 0 bei Türen. */
|
||||||
|
sillHeight: number;
|
||||||
|
/** Nur Tür: an welchem Pfosten das Scharnier sitzt. */
|
||||||
|
hinge?: "start" | "end";
|
||||||
|
/** Nur Tür: auf welche Seite der Wandachse das Blatt aufschlägt. */
|
||||||
|
swing?: SwingSide;
|
||||||
|
/** Nur Tür: Öffnungswinkel des Blatts in Grad (Default 90). */
|
||||||
|
swingAngle?: number;
|
||||||
|
/** Nur Tür: Aufschlagrichtung (nach innen/außen); Default "in". */
|
||||||
|
openingDir?: "in" | "out";
|
||||||
|
/** Optionale Rahmenstärke (quer zur Wand) in Metern für die 3D-Darstellung. */
|
||||||
|
frameThickness?: number;
|
||||||
|
/**
|
||||||
|
* Optionale Übersteuerung der Strich-/Symbolfarbe; sonst gilt die
|
||||||
|
* Kategorie-Farbe.
|
||||||
|
*/
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grundform einer Treppe (DOSSIER: gerade / L / Wendel).
|
||||||
|
* • "straight" — ein gerader Lauf (Lauflinie = Start → Richtung).
|
||||||
|
* • "L" — zwei rechtwinklige Läufe mit Zwischenpodest an der Ecke.
|
||||||
|
* • "spiral" — Wendeltreppe um ein Zentrum (keilförmige Tritte).
|
||||||
|
*/
|
||||||
|
export type StairShape = "straight" | "L" | "spiral";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine Treppe (Treppe/Stair) — ein geschossübergreifendes Bauteil, das über die
|
||||||
|
* Geschosshöhe (OKFF → OKFF des nächsten Geschosses) steigt. Definiert über eine
|
||||||
|
* Grundform (gerade / L / Wendel), eine Basis-Geometrie, die Laufbreite und die
|
||||||
|
* Stufung (Steigungshöhe/Auftrittstiefe, aus der Stufenanzahl abgeleitet).
|
||||||
|
*
|
||||||
|
* Basis-Geometrie je Grundform:
|
||||||
|
* • straight — `start` + `dir` (Einheitsrichtung) + `runLength` (Lauflänge).
|
||||||
|
* • L — `start` + `dir` (erster Lauf) + `runLength` (erster Lauf) +
|
||||||
|
* `run2Length` (zweiter Lauf) + `turn` (+1 = links, −1 = rechts abbiegen).
|
||||||
|
* Das Zwischenpodest sitzt am Ende des ersten Laufs (quadratisch, `width`).
|
||||||
|
* • spiral — `center` + `radius` (Innenradius zur Lauflinie) + `sweep`
|
||||||
|
* (Gesamtwinkel in Grad, +/− = Drehrichtung) + `start` (Startpunkt des ersten
|
||||||
|
* Tritts am äußeren Rand, definiert die Anfangsrichtung).
|
||||||
|
*
|
||||||
|
* Vertikale Lage: die UNTERKANTE (`baseZ`, abgeleitet aus dem Geschoss-OKFF) plus
|
||||||
|
* `totalRise` (Default = Geschosshöhe des zugehörigen Geschosses, sodass die
|
||||||
|
* Treppe genau ins nächste Geschoss steigt). `stepCount` Tritte/Setzstufen
|
||||||
|
* verteilen `totalRise` gleichmäßig; die Steigungshöhe = totalRise/stepCount, die
|
||||||
|
* Auftrittstiefe ergibt sich aus Lauflänge/(stepCount−1). Eine SIA-nahe
|
||||||
|
* Schrittregel (2·Steigung + Auftritt ≈ 0.63 m) liefert die Default-Stufenzahl.
|
||||||
|
*/
|
||||||
|
export interface Stair {
|
||||||
|
id: string;
|
||||||
|
type: "stair";
|
||||||
|
/** Zugehörige Zeichnungsebene (Geschoss), von dem die Treppe aufsteigt. */
|
||||||
|
floorId: string;
|
||||||
|
/** Grafik-Kategorie (Ebene), z. B. "40" für Treppen. */
|
||||||
|
categoryCode: string;
|
||||||
|
/** Grundform (gerade / L / Wendel). */
|
||||||
|
shape: StairShape;
|
||||||
|
/** Startpunkt der Lauflinie im Grundriss (Meter). */
|
||||||
|
start: Vec2;
|
||||||
|
/** Einheits-Laufrichtung des (ersten) Laufs im Grundriss. */
|
||||||
|
dir: Vec2;
|
||||||
|
/** Lauflänge des (ersten) Laufs in Metern (entlang `dir`). */
|
||||||
|
runLength: number;
|
||||||
|
/** Nur L: Lauflänge des zweiten Laufs in Metern. */
|
||||||
|
run2Length?: number;
|
||||||
|
/** Nur L: Abbiegerichtung des zweiten Laufs (+1 = links, −1 = rechts). */
|
||||||
|
turn?: 1 | -1;
|
||||||
|
/** Nur Wendel: Zentrum der Wendeltreppe (Meter). */
|
||||||
|
center?: Vec2;
|
||||||
|
/** Nur Wendel: Radius (Meter) von der Mitte zur Lauflinie. */
|
||||||
|
radius?: number;
|
||||||
|
/** Nur Wendel: Gesamt-Drehwinkel in Grad (+ = CCW, − = CW). */
|
||||||
|
sweep?: number;
|
||||||
|
/** Laufbreite in Metern (quer zur Laufrichtung). */
|
||||||
|
width: number;
|
||||||
|
/**
|
||||||
|
* Gesamt-Steighöhe in Metern (OKFF → OKFF nächstes Geschoss). Fehlt sie, gilt
|
||||||
|
* beim Auflösen die Geschosshöhe des zugehörigen Geschosses.
|
||||||
|
*/
|
||||||
|
totalRise?: number;
|
||||||
|
/** Anzahl der Steigungen (Setzstufen). Die Trittanzahl = stepCount (letzte =
|
||||||
|
* Austritt aufs obere Geschoss). Steigungshöhe = totalRise / stepCount. */
|
||||||
|
stepCount: number;
|
||||||
|
/**
|
||||||
|
* Laufrichtung „aufwärts": true = die Lauflinie steigt von `start` in Richtung
|
||||||
|
* `dir` (Default). false kehrt Auf-/Abpfeil um (Treppe steigt zum Start hin).
|
||||||
|
*/
|
||||||
|
up?: boolean;
|
||||||
|
/**
|
||||||
|
* Optionale Übersteuerung der Strich-/Umrandungsfarbe; sonst gilt die
|
||||||
|
* Kategorie-Farbe.
|
||||||
|
*/
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein Raum (Raum/Room) — eine SIA-416-Fläche auf einem Geschoss, definiert über
|
||||||
|
* einen GESCHLOSSENEN Umriss (Polygon, lichte Innenkontur) im Grundriss. Trägt
|
||||||
|
* eine SIA-416-Blatt-Kategorie (HNF/NNF/VF/FF/KGF), einen Namen und eine Farbe.
|
||||||
|
*
|
||||||
|
* Fläche/Umfang/Schwerpunkt werden NICHT gespeichert, sondern bei jedem Rendern
|
||||||
|
* aus `boundary` über den reinen Rechenkern (geometry/roomArea) abgeleitet —
|
||||||
|
* so sind sie nie veraltet. `stampAnchor` (optional) setzt den Ankerpunkt des
|
||||||
|
* Raum-Stempels; fehlt er, gilt der Flächenschwerpunkt (Centroid).
|
||||||
|
*/
|
||||||
|
export interface Room {
|
||||||
|
id: string;
|
||||||
|
type: "room";
|
||||||
|
/** Zugehörige Zeichnungsebene (Geschoss). */
|
||||||
|
floorId: string;
|
||||||
|
/** Grafik-Kategorie (Ebene), z. B. "45" für Räume. */
|
||||||
|
categoryCode: string;
|
||||||
|
/** SIA-416-Blatt-Kategorie (HNF/NNF/VF/FF/KGF). */
|
||||||
|
siaCategory: SiaCategory;
|
||||||
|
/** Raum-Name, z. B. „Wohnen". */
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Geschlossener Umriss (lichte Innenkontur) im Grundriss (Meter). Der
|
||||||
|
* Schlusspunkt wird NICHT dupliziert (die letzte Kante läuft von boundary[n-1]
|
||||||
|
* zu boundary[0]).
|
||||||
|
*/
|
||||||
|
boundary: Vec2[];
|
||||||
|
/** Strich-/Füllfarbe des Raums (hex). */
|
||||||
|
color: string;
|
||||||
|
/**
|
||||||
|
* Anker des Raum-Stempels (Meter). Wird bei der Erstellung EINMAL auf den
|
||||||
|
* Zentroid gesetzt und danach nie automatisch neu berechnet — der Stempel ist
|
||||||
|
* frei verschiebbar und bleibt beim Ändern der Kontur/Fläche an seiner Stelle.
|
||||||
|
* Fehlt er (Alt-Daten), gilt beim Rendern der Centroid.
|
||||||
|
*/
|
||||||
|
stampAnchor?: Vec2;
|
||||||
|
/**
|
||||||
|
* Frei editierbarer Rich-Text des Raum-Stempels (Name + Notizen, mit Fett/
|
||||||
|
* Kursiv/Grösse/Farbe/Ausrichtung). Fehlt er, gilt der Raum-Name als einfacher
|
||||||
|
* Text. Die LIVE-Flächenzeile wird beim Rendern separat darunter gesetzt.
|
||||||
|
*/
|
||||||
|
stampDoc?: RichTextDoc;
|
||||||
|
}
|
||||||
|
|
||||||
/** Eine Tür, gehostet in einer Wand. Ihr Geschoss ergibt sich aus der Wand. */
|
/** Eine Tür, gehostet in einer Wand. Ihr Geschoss ergibt sich aus der Wand. */
|
||||||
export interface Door {
|
export interface Door {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -282,9 +710,17 @@ export interface EdgeGrip {
|
|||||||
normal: Vec2;
|
normal: Vec2;
|
||||||
aIndex: number;
|
aIndex: number;
|
||||||
bIndex: number;
|
bIndex: number;
|
||||||
|
/**
|
||||||
|
* Freie Kanten-Verschiebung: gesetzt bei OFFENER Geometrie (Linie/offene
|
||||||
|
* Polylinie). Das Segment folgt dem vollen Cursor-Delta (nicht nur der Normale),
|
||||||
|
* beide Endpunkte wandern mit, die Nachbar-Segmente dehnen sich nach. Bei
|
||||||
|
* geschlossenen Formen/Wänden fehlt das Flag → senkrechte (parallel-)Verschiebung
|
||||||
|
* entlang der Außennormale wie bisher.
|
||||||
|
*/
|
||||||
|
free?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Element = Wall | Door | Drawing2D;
|
export type Element = Wall | Ceiling | Opening | Door | Stair | Room | Drawing2D;
|
||||||
|
|
||||||
// ── Kontext-Geometrie (importiert / abgeleitet, NICHT semantisch) ───────────
|
// ── Kontext-Geometrie (importiert / abgeleitet, NICHT semantisch) ───────────
|
||||||
// Importierte Geometrie ist „dumme" KONTEXT-Geometrie (Anzeige + späteres
|
// Importierte Geometrie ist „dumme" KONTEXT-Geometrie (Anzeige + späteres
|
||||||
@@ -362,7 +798,29 @@ export interface Project {
|
|||||||
/** Grafik-Kategorie-Baum (geschossübergreifend). */
|
/** Grafik-Kategorie-Baum (geschossübergreifend). */
|
||||||
layers: LayerCategory[];
|
layers: LayerCategory[];
|
||||||
walls: Wall[];
|
walls: Wall[];
|
||||||
|
/**
|
||||||
|
* Decken (Slabs) — geschossgebundene Flächenbauteile mit geschlossenem Umriss.
|
||||||
|
* Optional, damit bestehende Projekte/Tests ohne `ceilings` gültig bleiben
|
||||||
|
* (Default: leer behandeln).
|
||||||
|
*/
|
||||||
|
ceilings?: Ceiling[];
|
||||||
doors: Door[];
|
doors: Door[];
|
||||||
|
/**
|
||||||
|
* Öffnungen (Fenster/Türen), gehostet in Wänden. Optional, damit bestehende
|
||||||
|
* Projekte/Tests ohne `openings` gültig bleiben (Default: leer behandeln).
|
||||||
|
*/
|
||||||
|
openings?: Opening[];
|
||||||
|
/**
|
||||||
|
* Treppen (Treppe) — geschossübergreifende Bauteile (gerade/L/Wendel). Optional,
|
||||||
|
* damit bestehende Projekte/Tests ohne `stairs` gültig bleiben (Default: leer).
|
||||||
|
*/
|
||||||
|
stairs?: Stair[];
|
||||||
|
/**
|
||||||
|
* Räume (SIA-416-Flächen) — geschossgebundene Flächen mit geschlossenem Umriss.
|
||||||
|
* Optional, damit bestehende Projekte/Tests ohne `rooms` gültig bleiben
|
||||||
|
* (Default: leer behandeln).
|
||||||
|
*/
|
||||||
|
rooms?: Room[];
|
||||||
/** Freie 2D-Zeichengeometrie (Line/Polyline/Rect/Circle/Arc/Text). */
|
/** Freie 2D-Zeichengeometrie (Line/Polyline/Rect/Circle/Arc/Text). */
|
||||||
drawings2d: Drawing2D[];
|
drawings2d: Drawing2D[];
|
||||||
/**
|
/**
|
||||||
@@ -371,6 +829,13 @@ export interface Project {
|
|||||||
* Projekte/Tests ohne `context` gültig bleiben (Default: leer behandeln).
|
* Projekte/Tests ohne `context` gültig bleiben (Default: leer behandeln).
|
||||||
*/
|
*/
|
||||||
context?: ContextObject[];
|
context?: ContextObject[];
|
||||||
|
/**
|
||||||
|
* Bibliothek parametrischer Wand-Regelwerke. Optional, damit bestehende
|
||||||
|
* Projekte ohne `parametricWalls` gültig bleiben (Default: leer behandeln).
|
||||||
|
* Wird durch `resolveParametricWall()` in `src/model/parametricWalls.ts`
|
||||||
|
* aufgelöst — generiert Wall[]-Objekte bei Bedarf.
|
||||||
|
*/
|
||||||
|
parametricWalls?: ParametricWall[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helfer ───────────────────────────────────────────────────────────────
|
// ── Helfer ───────────────────────────────────────────────────────────────
|
||||||
@@ -381,6 +846,48 @@ export const getWallType = (project: Project, wall: Wall): WallType => {
|
|||||||
return wt;
|
return wt;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Liefert den Aufbau-Typ (WallType) einer Decke oder wirft. */
|
||||||
|
export const getCeilingType = (project: Project, ceiling: Ceiling): WallType => {
|
||||||
|
const wt = project.wallTypes.find((t) => t.id === ceiling.wallTypeId);
|
||||||
|
if (!wt) throw new Error(`Unbekannter Deckentyp: ${ceiling.wallTypeId}`);
|
||||||
|
return wt;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gesamtdicke einer Decke (Meter): eine explizite `thickness`-Übersteuerung hat
|
||||||
|
* Vorrang, sonst die Summe der Schichtdicken ihres Aufbau-Typs.
|
||||||
|
*/
|
||||||
|
export const ceilingThickness = (project: Project, ceiling: Ceiling): number => {
|
||||||
|
if (ceiling.thickness != null && ceiling.thickness > 0) return ceiling.thickness;
|
||||||
|
const wt = project.wallTypes.find((t) => t.id === ceiling.wallTypeId);
|
||||||
|
return wt ? wallTypeThickness(wt) : 0.2;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Alle Öffnungen einer Wand (leere Liste, wenn keine oder `openings` fehlt). */
|
||||||
|
export const openingsOfWall = (project: Project, wallId: string): Opening[] =>
|
||||||
|
(project.openings ?? []).filter((o) => o.hostWallId === wallId);
|
||||||
|
|
||||||
|
/** Menschenlesbarer Standardname einer Öffnung (Fenster/Tür + Breite×Höhe). */
|
||||||
|
export const openingLabel = (o: Opening): string =>
|
||||||
|
`${o.kind === "door" ? "Tür" : "Fenster"} ${(o.width * 100).toFixed(0)}×${(
|
||||||
|
o.height * 100
|
||||||
|
).toFixed(0)}`;
|
||||||
|
|
||||||
|
/** Alle Treppen eines Geschosses (leere Liste, wenn keine oder `stairs` fehlt). */
|
||||||
|
export const stairsOfFloor = (project: Project, floorId: string): Stair[] =>
|
||||||
|
(project.stairs ?? []).filter((s) => s.floorId === floorId);
|
||||||
|
|
||||||
|
/** Menschenlesbarer Standardname einer Treppe (Grundform + Stufenanzahl). */
|
||||||
|
export const stairLabel = (s: Stair): string => {
|
||||||
|
const shape =
|
||||||
|
s.shape === "straight" ? "Gerade" : s.shape === "L" ? "L-Treppe" : "Wendel";
|
||||||
|
return `${shape} ${s.stepCount} STG`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Alle Räume eines Geschosses (leere Liste, wenn keine oder `rooms` fehlt). */
|
||||||
|
export const roomsOfFloor = (project: Project, floorId: string): Room[] =>
|
||||||
|
(project.rooms ?? []).filter((r) => r.floorId === floorId);
|
||||||
|
|
||||||
/** Liefert ein Bauteil-Material (Component) per ID oder wirft. */
|
/** Liefert ein Bauteil-Material (Component) per ID oder wirft. */
|
||||||
export const getComponent = (project: Project, id: string): Component => {
|
export const getComponent = (project: Project, id: string): Component => {
|
||||||
const c = project.components.find((co) => co.id === id);
|
const c = project.components.find((co) => co.id === id);
|
||||||
|
|||||||
Reference in New Issue
Block a user