Linien-/Random-Editor: Strich-Segmente, Vollinie, Random-Kontrollen; Weight raus

Linien-Editor: Umschalter Vollinie (dash null) / Strich mit editierbarer
Segment-Liste (mm, alternierend Strich/Luecke, hinzufuegen/entfernen), Live-
Vorschau. Das Strichstaerken-Feld ist aus dem Linien-Editor entfernt —
LineStyle.weight ist @deprecated (bleibt Render-Fallback), die echte
per-Element-Aufloesung (Attribut->Ebene->Default) folgt separat.

Random-Schraffur: additive HatchStyle-Parameter seed/density/lengthMin/Max +
Neu-wuerfeln-Button (neuer Seed nur zur Editzeit). buildRandomHatchRuns mischt
den expliziten Seed in den Hash und nutzt Dichte/Laenge — Determinismus
gewahrt (gleicher Seed+Flaeche => gleiche Streuung), ueber alle Renderpfade
konsistent, da alle aus derselben Funktion ziehen. resolveHatch/HatchRender
reichen die Parameter rein additiv durch. 2 neue Tests.
This commit is contained in:
2026-07-04 00:53:19 +02:00
parent 87ef7988d1
commit 07475cbe6c
8 changed files with 248 additions and 54 deletions
+123 -43
View File
@@ -56,29 +56,6 @@ const PATTERN_OPTIONS: { value: HatchPattern; labelKey: string }[] = [
{ value: "crosshatch", labelKey: "pattern.crosshatch" },
];
/**
* Benannte Strichmuster (mm). `null` = durchgezogen. Werte als Schlüssel
* serialisiert, damit das Dropdown den aktuellen Stil wiedererkennt.
*/
const DASH_OPTIONS: { key: string; labelKey: string; dash: number[] | null }[] = [
{ key: "solid", labelKey: "dash.solid", dash: null },
{ key: "dashed", labelKey: "dash.dashed", dash: [6, 4] },
{ key: "dotted", labelKey: "dash.dotted", dash: [1, 3] },
];
/** Findet den Dash-Schlüssel, der zu einem Strichmuster passt (sonst custom). */
function dashKey(dash: number[] | null): string {
const hit = DASH_OPTIONS.find(
(o) =>
(o.dash === null && dash === null) ||
(o.dash !== null &&
dash !== null &&
o.dash.length === dash.length &&
o.dash.every((v, i) => v === dash[i])),
);
return hit ? hit.key : "custom";
}
// ── Wiederverwendbare Feld-Bausteine (DOSSIER-Look) ────────────────────────
// Alle Steuerelemente füllen ihre Tabellenzelle (width:100%) und werden über
// die Spaltenbreite des Grids dimensioniert; Zahlenfelder sind rechtsbündig.
@@ -979,6 +956,14 @@ function HatchDetail({
r.readAsDataURL(file);
};
// ── Random-Vektor-Schraffur (Kies/Splitt) ───────────────────────────────────
// Dichte + Längenspanne + „Neu würfeln". Der Seed wird beim Klick gesetzt (KEIN
// Math.random() zur Renderzeit — die Streuung selbst ist deterministisch per
// Seed). Länge min/max in mm; bis der Nutzer sie berührt, bleibt der Default.
const lenMin = hatch.lengthMin ?? 1;
const lenMax = hatch.lengthMax ?? 3;
const reroll = () => onPatch({ seed: Math.floor(Math.random() * 0x7fffffff) });
return (
<div className="res-md-detail-inner">
<div className="res-md-head">
@@ -1064,6 +1049,39 @@ function HatchDetail({
options={lineOptions}
/>
</FieldRow>
{lines === "random" && (
<>
<FieldRow label={t("resources.field.density")}>
<NumberField
value={hatch.density ?? 1}
onChange={(density) => onPatch({ density })}
step={0.1}
min={0.1}
/>
</FieldRow>
<FieldRow label={t("resources.field.lengthMin")}>
<NumberField
value={lenMin}
onChange={(v) => onPatch({ lengthMin: v, lengthMax: hatch.lengthMax ?? lenMax })}
step={0.5}
min={0}
/>
</FieldRow>
<FieldRow label={t("resources.field.lengthMax")}>
<NumberField
value={lenMax}
onChange={(v) => onPatch({ lengthMax: v, lengthMin: hatch.lengthMin ?? lenMin })}
step={0.5}
min={0}
/>
</FieldRow>
<FieldRow label={t("resources.random.reroll")}>
<button type="button" className="res-seg-btn" onClick={reroll}>
{t("resources.random.reroll")}
</button>
</FieldRow>
</>
)}
</>
) : (
<>
@@ -1196,6 +1214,27 @@ function LineStyleDetail({
onPatch({ zigzag: { ...cur, ...part } });
};
// ── Strich (dash) ──────────────────────────────────────────────────────────
// „Vollinie" = `dash: null` (durchgezogen); „Strich" = editierbare Liste der
// Segmentlängen in mm (alternierend Strich/Lücke). Die Dicke gehört NICHT mehr
// hierher — sie wird per Element-Attribut aufgelöst (By-Object/By-Layer folgt).
const dash = style.dash;
const isSolid = dash === null;
const dashArr = dash ?? [];
const setStroke = (mode: "solid" | "dash") =>
onPatch({ dash: mode === "solid" ? null : dashArr.length ? dashArr : [1, 2] });
const setSeg = (i: number, v: number) => {
const next = dashArr.slice();
next[i] = Math.max(0, v);
onPatch({ dash: next });
};
const addSeg = () =>
onPatch({ dash: [...dashArr, dashArr.length ? dashArr[dashArr.length - 1] : 1] });
const removeSeg = (i: number) => {
const next = dashArr.filter((_, k) => k !== i);
onPatch({ dash: next.length ? next : null });
};
return (
<div className="res-md-detail-inner">
<div className="res-md-head">
@@ -1226,16 +1265,6 @@ function LineStyleDetail({
]}
/>
</FieldRow>
<FieldRow label={t("resources.col.weight")}>
<NumberField
value={style.weight}
onChange={(weight) => onPatch({ weight })}
step={0.05}
min={0.01}
max={2}
list="pen-weights"
/>
</FieldRow>
<FieldRow label={t("resources.col.color")}>
<ColorField color={style.color} onChange={(color) => onPatch({ color })} />
</FieldRow>
@@ -1259,16 +1288,67 @@ function LineStyleDetail({
</FieldRow>
</>
) : (
<FieldRow label={t("resources.col.stroke")}>
<SelectField
value={dashKey(style.dash)}
onChange={(key) => {
const opt = DASH_OPTIONS.find((o) => o.key === key);
if (opt) onPatch({ dash: opt.dash });
}}
options={DASH_OPTIONS.map((o) => ({ value: o.key, label: t(o.labelKey) }))}
/>
</FieldRow>
<>
<FieldRow label={t("resources.col.stroke")}>
<Segmented
value={isSolid ? "solid" : "dash"}
onChange={setStroke}
options={[
{ value: "solid", label: t("resources.stroke.solid") },
{ value: "dash", label: t("resources.stroke.dash") },
]}
/>
</FieldRow>
{!isSolid && (
<FieldRow
label={t("resources.dash.segments")}
hint={t("resources.dash.hint")}
>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: 4,
alignItems: "center",
}}
>
{dashArr.map((v, i) => (
<span
key={i}
style={{ display: "inline-flex", alignItems: "center", gap: 2 }}
>
<span style={{ width: 46 }}>
<NumberField
value={v}
onChange={(x) => setSeg(i, x)}
step={0.5}
min={0}
/>
</span>
<button
type="button"
className="res-seg-btn"
title={t("resources.dash.remove")}
onClick={() => removeSeg(i)}
style={{ padding: "2px 6px", lineHeight: 1 }}
>
×
</button>
</span>
))}
<button
type="button"
className="res-seg-btn"
title={t("resources.dash.add")}
onClick={addSeg}
style={{ padding: "2px 8px", lineHeight: 1 }}
>
+ {t("resources.dash.add")}
</button>
</div>
</FieldRow>
)}
</>
)}
</div>
</div>