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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user