God-Files aufgeteilt (ResourceManager/generatePlan/toWalls3d/App.tsx-Handler), Totcode entfernt (geometry-Crate, OCCT/HLR-Spike), PDF/Bild-Import ergänzt.

ResourceManager.tsx, generatePlan.ts und toWalls3d.ts sind nach Bauteil-
Domäne in src/ui/resourceManager/, src/plan/generatePlan/ und
src/plan/toWalls3d/ aufgeteilt (reine Verschiebung, keine Verhaltens-
änderung). App.tsx verliert weitere Handler-Gruppen an eigene Hooks
(useExportHandlers, useImportHandling, useKeyboardShortcuts) sowie
Hit-Testing an src/viewport/planHitTest.ts.

Unbenutzte Rust-geometry-Crate (durch WASM-Joins abgelöst) sowie der alte
OCCT/HLR-Schnitt-Spike (hlr.ts/occt.ts, vite occt-Aliases) entfernt.

Neu: PDF-Import als Rasterbild (src/io/pdfImport.ts) + gemeinsamer
Bild-Asset-Helfer (src/io/imageAsset.ts) für Plan- und Layout-Bilder.

tsc -b sauber, vitest 891/891 grün.
This commit is contained in:
2026-08-19 22:45:41 +02:00
parent 06c0143d32
commit 1507c2bf00
85 changed files with 12461 additions and 11280 deletions
+312
View File
@@ -0,0 +1,312 @@
// Tab „Overrides" (grafische Overrides, Regel-Engine A1) — Master-Detail wie
// Linien/Wandstile: Liste links = Regeln in PRIORITÄTS-Reihenfolge (oben
// gewinnt pro Aktions-Feld) mit Aktiv-Checkbox, Auf/Ab und Löschen; Detail
// rechts = Bedingung (Kriterium/Operator/Wert) + optionale Aktionen (Farbe/
// Strichstärke/Linienstil). Reines Rendering-Overlay — die Auswertung liegt in
// src/overrides/engine.ts, der Einhängepunkt in plan/generatePlan.ts;
// Elementdaten bleiben unverändert.
import { useState } from "react";
import type { OverrideConditionType, OverrideOperator, OverrideRule, Project } from "../../model/types";
import { PEN_WEIGHTS } from "../../model/types";
import { t } from "../../i18n";
import { ColorField, DeleteButton, FieldRow, NumberField, SelectField, TextField } from "./fields";
/** Bedingungs-Typen (UI-Beschriftung übersetzt, Werte englisch). */
const OVERRIDE_TYPE_OPTIONS: { value: OverrideConditionType; labelKey: string }[] = [
{ value: "layer_name", labelKey: "resources.overrides.condType.layer" },
{ value: "object_name", labelKey: "resources.overrides.condType.object" },
];
/** Vergleichs-Operatoren (UI-Beschriftung übersetzt, Werte englisch). */
const OVERRIDE_OP_OPTIONS: { value: OverrideOperator; labelKey: string }[] = [
{ value: "equals", labelKey: "resources.overrides.op.equals" },
{ value: "contains", labelKey: "resources.overrides.op.contains" },
{ value: "starts_with", labelKey: "resources.overrides.op.startsWith" },
{ value: "not_equals", labelKey: "resources.overrides.op.notEquals" },
];
/** Kompakte Bedingungs-Zusammenfassung für die Listenzeile. */
function overrideConditionSummary(rule: OverrideRule): string {
const type = OVERRIDE_TYPE_OPTIONS.find((o) => o.value === rule.condition.type);
const op = OVERRIDE_OP_OPTIONS.find((o) => o.value === rule.condition.operator);
return `${type ? t(type.labelKey) : rule.condition.type} ${
op ? t(op.labelKey) : rule.condition.operator
} „${rule.condition.value}"`;
}
/** Optionale Aktion: Checkbox (an/aus) + Steuerelement, solange sie an ist. */
function OptionalAction({
label,
active,
onToggle,
children,
}: {
label: string;
active: boolean;
onToggle: (on: boolean) => void;
children?: React.ReactNode;
}) {
return (
<div className="res-field">
<span className="res-field-label">
<label className="res-ov-action-toggle">
<input
type="checkbox"
checked={active}
onChange={(e) => onToggle(e.target.checked)}
/>{" "}
{label}
</label>
</span>
<span className="res-field-control">
{active ? children : <span className="res-joint-none">–</span>}
</span>
</div>
);
}
/** Detail-Panel EINER Override-Regel (rechte Seite des Master-Detail-Layouts). */
function OverrideRuleDetail({
rule,
project,
onPatch,
onDelete,
}: {
rule: OverrideRule;
project: Project;
onPatch: (patch: Partial<OverrideRule>) => void;
onDelete: () => void;
}) {
const patchCondition = (patch: Partial<OverrideRule["condition"]>) =>
onPatch({ condition: { ...rule.condition, ...patch } });
const patchActions = (patch: Partial<OverrideRule["actions"]>) =>
onPatch({ actions: { ...rule.actions, ...patch } });
// Sentinel „—": kein Linienstil-Override.
const NONE = "__none__";
const lineOptions = [
{ value: NONE, label: t("resources.overrides.linetype.none") },
...project.lineStyles.map((l) => ({ value: l.id, label: l.name })),
];
return (
<div className="res-md-detail-inner">
<div className="res-md-head">
<input
className="res-input res-md-rename"
value={rule.name}
placeholder={t("resources.overrides.placeholder")}
onChange={(e) => onPatch({ name: e.target.value })}
/>
<DeleteButton onClick={onDelete} title={t("resources.overrides.delete")} />
</div>
<div className="res-hint">{t("resources.overrides.condition")}</div>
<FieldRow label={t("resources.overrides.condType")}>
<SelectField
value={rule.condition.type}
onChange={(type) => patchCondition({ type })}
options={OVERRIDE_TYPE_OPTIONS.map((o) => ({
value: o.value,
label: t(o.labelKey),
}))}
/>
</FieldRow>
<FieldRow label={t("resources.overrides.operator")}>
<SelectField
value={rule.condition.operator}
onChange={(operator) => patchCondition({ operator })}
options={OVERRIDE_OP_OPTIONS.map((o) => ({
value: o.value,
label: t(o.labelKey),
}))}
/>
</FieldRow>
<FieldRow label={t("resources.overrides.value")}>
<TextField
value={rule.condition.value}
placeholder={t("resources.overrides.value.placeholder")}
onChange={(value) => patchCondition({ value })}
/>
</FieldRow>
<div className="res-hint">{t("resources.overrides.actions")}</div>
{/* Vorschlagswerte für die Strichstärke (Standard-Stiftbreiten in mm). */}
<datalist id="ov-pen-weights">
{PEN_WEIGHTS.map((w) => (
<option key={w} value={w} />
))}
</datalist>
<OptionalAction
label={t("resources.overrides.action.color")}
active={rule.actions.color !== undefined}
onToggle={(on) => patchActions({ color: on ? "#808080" : undefined })}
>
<ColorField
color={rule.actions.color ?? "#808080"}
onChange={(color) => patchActions({ color })}
/>
</OptionalAction>
<OptionalAction
label={t("resources.overrides.action.lineweight")}
active={rule.actions.lineweight !== undefined}
onToggle={(on) => patchActions({ lineweight: on ? 0.35 : undefined })}
>
<NumberField
value={rule.actions.lineweight ?? 0.35}
onChange={(lineweight) => patchActions({ lineweight: Math.max(0, lineweight) })}
step={0.05}
min={0}
list="ov-pen-weights"
/>
</OptionalAction>
<OptionalAction
label={t("resources.overrides.action.linetype")}
active={rule.actions.linetypeId !== undefined}
onToggle={(on) =>
patchActions({
linetypeId: on ? project.lineStyles[0]?.id : undefined,
})
}
>
<SelectField
value={rule.actions.linetypeId ?? NONE}
onChange={(v) =>
patchActions({ linetypeId: v === NONE ? undefined : v })
}
options={lineOptions}
/>
</OptionalAction>
</div>
);
}
/** Tab „Overrides": Regelliste (Priorität von oben nach unten) + Detail. */
export function OverridesTab({
project,
onSetOverrideRules,
}: {
project: Project;
onSetOverrideRules?: (rules: OverrideRule[]) => void;
}) {
const rules = project.overrideRules ?? [];
const [selectedId, setSelectedId] = useState<string | null>(null);
const selected = rules.find((r) => r.id === selectedId) ?? rules[0];
const setRules = (next: OverrideRule[]) => onSetOverrideRules?.(next);
const patchRule = (id: string, patch: Partial<OverrideRule>) =>
setRules(rules.map((r) => (r.id === id ? { ...r, ...patch } : r)));
const addRule = () => {
const rule: OverrideRule = {
id: `ov-${Date.now()}`,
name: t("resources.overrides.defaultName"),
enabled: true,
condition: { type: "layer_name", operator: "contains", value: "" },
actions: {},
};
setRules([...rules, rule]);
setSelectedId(rule.id);
};
const deleteRule = (id: string) => setRules(rules.filter((r) => r.id !== id));
const moveRule = (id: string, dir: -1 | 1) => {
const i = rules.findIndex((r) => r.id === id);
const j = i + dir;
if (i < 0 || j < 0 || j >= rules.length) return;
const next = rules.slice();
[next[i], next[j]] = [next[j], next[i]];
setRules(next);
};
return (
<div className="res-tab">
<div className="res-hint">{t("resources.overrides.hint")}</div>
<div className="res-md">
<div className="res-md-list">
{rules.map((r) => (
<div
key={r.id}
role="button"
tabIndex={0}
className={`res-md-row res-ov-row${
selected?.id === r.id ? " active" : ""
}`}
style={{ display: "flex", alignItems: "center", gap: 6, cursor: "pointer" }}
onClick={() => setSelectedId(r.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") setSelectedId(r.id);
}}
>
<input
type="checkbox"
checked={r.enabled}
title={t("resources.overrides.enabled")}
onClick={(e) => e.stopPropagation()}
onChange={(e) => patchRule(r.id, { enabled: e.target.checked })}
/>
<span
className="res-md-row-name"
style={{ flex: 1, opacity: r.enabled ? 1 : 0.5 }}
>
{r.name || t("resources.overrides.placeholder")}
<span
style={{ display: "block", fontSize: "0.85em", opacity: 0.65 }}
>
{overrideConditionSummary(r)}
</span>
</span>
<button
type="button"
className="res-layer-btn"
title={t("resources.overrides.moveUp")}
disabled={rules[0]?.id === r.id}
onClick={(e) => {
e.stopPropagation();
moveRule(r.id, -1);
}}
>
▲
</button>
<button
type="button"
className="res-layer-btn"
title={t("resources.overrides.moveDown")}
disabled={rules[rules.length - 1]?.id === r.id}
onClick={(e) => {
e.stopPropagation();
moveRule(r.id, 1);
}}
>
▼
</button>
</div>
))}
{rules.length === 0 && (
<div className="res-md-empty">{t("resources.overrides.empty")}</div>
)}
<div className="res-md-listfoot">
<button
className="res-add"
onClick={addRule}
disabled={!onSetOverrideRules}
>
<span className="res-add-plus">+</span> {t("resources.add")}
</button>
</div>
</div>
<div className="res-md-detail">
{selected ? (
<OverrideRuleDetail
key={selected.id}
rule={selected}
project={project}
onPatch={(patch) => patchRule(selected.id, patch)}
onDelete={() => deleteRule(selected.id)}
/>
) : (
<div className="res-md-empty">{t("resources.overrides.empty")}</div>
)}
</div>
</div>
</div>
);
}