Files
DOSSIER-STANDALONE/src/overrides/engine.ts
T
karim 1d939a939a Akkumulierten grünen Arbeitsstand landen (Basis für Weiterarbeit)
Bündelt den über mehrere Sessions gewachsenen, uncommitteten Stand in
einem Basis-Commit, damit Folge-Features isoliert darauf aufsetzen.
Verifikation: tsc --noEmit sauber, vitest 600/600 grün.

Enthalten (Details in PENDENZEN.md ✅-Liste / HANDOVER.md):
- truck-Integration: Profil-Extrusion + Verjüngung + Boolean-CSG (csgrs),
  Crate src-tauri/trucksolid, Werkzeug `extrude`, ExtrudedSolid-Modell.
- kernel2d-Port nach Rust/WASM (Phasen 1–5, Diff-Harness).
- render3d 3D-Live-Schnitt = 2D-Schnitt: geschichteter Bodenaufbau,
  Prioritäts-Verschneidung (section_boolean.rs), einstellbare
  Schichttrennlinien, per-Hatch-Strichstärke, relativeToWall-Orientierung.
- Interop-Export IFC4/STL/OBJ (Loch-Ausschnitt wallMeshCut), Schnellexport.
- Projektdatei .obp + OS-Lock (lock.rs, LockConflictDialog).
- Layout-Blätter (Modell/Editor/Panel/PDF), Ausschnitte, Override-Engine,
  Tragwerk-Stützen (Column), BIM-Tree-Panel.
- Bauteil-Typsystem (Tür/Fenster/Treppe-Typen), Betontreppe mit schräger
  Laufplatte, Text-/Textbox-Werkzeug, Mess-Werkzeug, 2D/3D-Griffe für
  Öffnungen/Treppen, Snap-Symbol-Restyle.
2026-07-09 00:57:29 +02:00

132 lines
4.8 KiB
TypeScript

// Grafische Overrides — reine Auswertungs-Engine (Regel-Engine, DOSSIER A1).
//
// Wertet die Regelliste `Project.overrideRules` für ein Element aus und
// liefert die effektiven Aktionen (Farbe/Strichstärke/Linienstil). Die Regeln
// werden ADDITIV angewendet: pro Aktions-Feld gewinnt die OBERSTE (erste)
// aktive Regel, deren Bedingung matcht; spätere Regeln füllen nur noch die
// verbleibenden Felder. Deaktivierte Regeln werden übersprungen.
//
// Reines Rendering-Overlay: die Engine kennt weder Project noch Renderer —
// der Aufrufer (plan/generatePlan.ts) baut das Subjekt aus Ebene + Element
// und legt das Ergebnis als oberste Schicht ÜBER die reguläre
// By-Layer/By-Object-Attribut-Auflösung. Elementdaten bleiben unverändert.
import type {
OverrideActions,
OverrideCondition,
OverrideRule,
} from "../model/types";
/**
* Das Subjekt einer Auswertung — die vergleichbaren Eigenschaften EINES
* Elements. `layer_name`-Bedingungen matchen gegen Namen ODER Code der Ebene
* (LayerCategory), `object_name`-Bedingungen gegen den Element-/Typ-Namen.
*/
export interface OverrideSubject {
/** Name der Ebene (LayerCategory.name) des Elements. */
layerName?: string;
/** Code der Ebene (LayerCategory.code / Element.categoryCode). */
layerCode?: string;
/** Element-/Typ-Name (z. B. Wandtyp-Name, Raum-Name, Öffnungs-Label). */
objectName?: string;
}
/** Leere Aktionen (kein Override) — geteilte Konstante für den Schnellpfad. */
export const EMPTY_OVERRIDES: OverrideActions = Object.freeze({});
/** Case-insensitiver Feld-Vergleich eines Operators. */
function matchValue(
operator: OverrideCondition["operator"],
candidate: string,
value: string,
): boolean {
switch (operator) {
case "equals":
return candidate === value;
case "contains":
return candidate.includes(value);
case "starts_with":
return candidate.startsWith(value);
case "not_equals":
return candidate !== value;
}
}
/**
* Prüft, ob eine Bedingung auf ein Subjekt zutrifft (case-insensitiv,
* Wert getrimmt). `layer_name` vergleicht gegen Ebenen-NAME und -CODE:
* positive Operatoren (equals/contains/starts_with) treffen, wenn EINER der
* beiden passt; `not_equals` trifft nur, wenn KEINER der beiden gleich ist
* (sonst würde der Code fast jede Namens-Ungleichheit erfüllen).
*/
export function matchesCondition(
condition: OverrideCondition,
subject: OverrideSubject,
): boolean {
const value = condition.value.trim().toLowerCase();
const candidates: string[] = [];
if (condition.type === "layer_name") {
if (subject.layerName != null) candidates.push(subject.layerName.trim().toLowerCase());
if (subject.layerCode != null) candidates.push(subject.layerCode.trim().toLowerCase());
} else {
if (subject.objectName != null) candidates.push(subject.objectName.trim().toLowerCase());
}
if (candidates.length === 0) {
// Subjekt trägt die Eigenschaft nicht: positive Operatoren können nicht
// treffen; „ungleich" ist trivial erfüllt.
return condition.operator === "not_equals";
}
if (condition.operator === "not_equals") {
return candidates.every((c) => c !== value);
}
return candidates.some((c) => matchValue(condition.operator, c, value));
}
/**
* Kollabiert die Regelliste für ein Subjekt zu den effektiven Aktionen.
* Additiv, oberste Regel gewinnt PRO FELD: die erste aktive, matchende Regel,
* die ein Feld setzt, bestimmt es; weitere Regeln füllen nur offene Felder.
* Ohne Treffer (oder ohne Regeln) kommt {@link EMPTY_OVERRIDES} zurück.
*/
export function effectiveOverrides(
rules: readonly OverrideRule[] | undefined,
subject: OverrideSubject,
): OverrideActions {
if (!rules || rules.length === 0) return EMPTY_OVERRIDES;
let out: OverrideActions | null = null;
for (const rule of rules) {
if (!rule.enabled) continue;
if (!matchesCondition(rule.condition, subject)) continue;
const a = rule.actions;
if (a.color === undefined && a.lineweight === undefined && a.linetypeId === undefined) {
continue;
}
if (!out) out = {};
if (out.color === undefined && a.color !== undefined) out.color = a.color;
if (out.lineweight === undefined && a.lineweight !== undefined) {
out.lineweight = a.lineweight;
}
if (out.linetypeId === undefined && a.linetypeId !== undefined) {
out.linetypeId = a.linetypeId;
}
// Alle Felder belegt → keine weitere Regel kann noch etwas beitragen.
if (
out.color !== undefined &&
out.lineweight !== undefined &&
out.linetypeId !== undefined
) {
break;
}
}
return out ?? EMPTY_OVERRIDES;
}
/** True, wenn mindestens eine Aktion gesetzt ist (Schnelltest für Aufrufer). */
export function hasOverrides(actions: OverrideActions): boolean {
return (
actions.color !== undefined ||
actions.lineweight !== undefined ||
actions.linetypeId !== undefined
);
}