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:
@@ -566,6 +566,16 @@ export const de = {
|
|||||||
"resources.field.rotation": "Rotation °",
|
"resources.field.rotation": "Rotation °",
|
||||||
"resources.field.amplitude": "Amplitude",
|
"resources.field.amplitude": "Amplitude",
|
||||||
"resources.field.wavelength": "Wellenlänge",
|
"resources.field.wavelength": "Wellenlänge",
|
||||||
|
"resources.field.density": "Dichte",
|
||||||
|
"resources.field.lengthMin": "Länge min (mm)",
|
||||||
|
"resources.field.lengthMax": "Länge max (mm)",
|
||||||
|
"resources.random.reroll": "Neu würfeln",
|
||||||
|
"resources.stroke.solid": "Vollinie",
|
||||||
|
"resources.stroke.dash": "Strich",
|
||||||
|
"resources.dash.segments": "Segmente (mm)",
|
||||||
|
"resources.dash.hint": "Alternierend Strich/Lücke in mm (z. B. 1, 2 = 1 mm Strich, 2 mm Lücke).",
|
||||||
|
"resources.dash.add": "Segment",
|
||||||
|
"resources.dash.remove": "Segment entfernen",
|
||||||
"resources.hatchKind.vector": "Vektor",
|
"resources.hatchKind.vector": "Vektor",
|
||||||
"resources.hatchKind.image": "Bild",
|
"resources.hatchKind.image": "Bild",
|
||||||
"resources.lines.parallel": "Parallel",
|
"resources.lines.parallel": "Parallel",
|
||||||
|
|||||||
@@ -561,6 +561,16 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"resources.field.rotation": "Rotation °",
|
"resources.field.rotation": "Rotation °",
|
||||||
"resources.field.amplitude": "Amplitude",
|
"resources.field.amplitude": "Amplitude",
|
||||||
"resources.field.wavelength": "Wavelength",
|
"resources.field.wavelength": "Wavelength",
|
||||||
|
"resources.field.density": "Density",
|
||||||
|
"resources.field.lengthMin": "Length min (mm)",
|
||||||
|
"resources.field.lengthMax": "Length max (mm)",
|
||||||
|
"resources.random.reroll": "Re-roll",
|
||||||
|
"resources.stroke.solid": "Solid",
|
||||||
|
"resources.stroke.dash": "Dash",
|
||||||
|
"resources.dash.segments": "Segments (mm)",
|
||||||
|
"resources.dash.hint": "Alternating dash/gap in mm (e.g. 1, 2 = 1 mm dash, 2 mm gap).",
|
||||||
|
"resources.dash.add": "Segment",
|
||||||
|
"resources.dash.remove": "Remove segment",
|
||||||
"resources.hatchKind.vector": "Vector",
|
"resources.hatchKind.vector": "Vector",
|
||||||
"resources.hatchKind.image": "Image",
|
"resources.hatchKind.image": "Image",
|
||||||
"resources.lines.parallel": "Parallel",
|
"resources.lines.parallel": "Parallel",
|
||||||
|
|||||||
+26
-1
@@ -29,7 +29,16 @@ export type { RichTextDoc } from "../text/richText";
|
|||||||
export interface LineStyle {
|
export interface LineStyle {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
/** Strichstärke in Millimetern (≙ Rhino PlotWeight). */
|
/**
|
||||||
|
* @deprecated Die Strichstärke gehört NICHT mehr in den Linienstil — es gibt nur
|
||||||
|
* „Vollinie/Strich", die tatsächliche Dicke wird per Element-Attribut (bzw.
|
||||||
|
* Ebene/Default) aufgelöst. Das Feld bleibt für die Backward-Compat-Auflösung
|
||||||
|
* erhalten und dient den Renderern noch als Fallback für die Muster-/Fugen-Stärke
|
||||||
|
* (siehe `resolveHatch` → `HatchRender.lineWeight`). Der Linien-Detail-Editor
|
||||||
|
* zeigt es nicht mehr an; neue Linienstile bekommen einen Haarlinien-Default.
|
||||||
|
* Die echte per-Element-Strichstärken-Auflösung (Attribut → Ebene → Default)
|
||||||
|
* folgt separat mit dem By-Layer/By-Object-Refactor — NICHT hier.
|
||||||
|
*/
|
||||||
weight: number;
|
weight: number;
|
||||||
/** Farbe (hex). */
|
/** Farbe (hex). */
|
||||||
color: string;
|
color: string;
|
||||||
@@ -112,6 +121,22 @@ export interface HatchStyle {
|
|||||||
color: string;
|
color: string;
|
||||||
/** Optionaler Linienstil für die Musterlinien (Line Manager). */
|
/** Optionaler Linienstil für die Musterlinien (Line Manager). */
|
||||||
lineStyleId?: string;
|
lineStyleId?: string;
|
||||||
|
/**
|
||||||
|
* Steuer-Parameter der Random-Vektor-Schraffur (`kind`/"vector" + `lines`===
|
||||||
|
* "random", z. B. Kies/Splitt). ALLE additiv & optional — fehlen sie, gilt das
|
||||||
|
* bestehende deterministische Streu-Verhalten (Seed aus der Flächen-Bounding-Box,
|
||||||
|
* Dichte/Länge aus `scale`). Die Streuung bleibt bei gleichem `seed` + gleicher
|
||||||
|
* Fläche reproduzierbar über ALLE Renderpfade (single render truth); KEIN
|
||||||
|
* `Math.random()` zur Renderzeit.
|
||||||
|
*/
|
||||||
|
/** Expliziter Streu-Seed. Wird in den Flächen-Seed eingemischt; „Neu würfeln" setzt einen neuen Wert. */
|
||||||
|
seed?: number;
|
||||||
|
/** Streudichte als Multiplikator auf den Grundabstand (Default 1; >1 = dichter). */
|
||||||
|
density?: number;
|
||||||
|
/** Minimale Strichlänge in mm (Papier). Fehlt es, greift die `scale`-abhängige Default-Länge. */
|
||||||
|
lengthMin?: number;
|
||||||
|
/** Maximale Strichlänge in mm (Papier). Fehlt es, greift die `scale`-abhängige Default-Länge. */
|
||||||
|
lengthMax?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -135,6 +135,15 @@ export interface HatchRender {
|
|||||||
lines?: "parallel" | "random";
|
lines?: "parallel" | "random";
|
||||||
/** Bild-Muster-Parameter (nur `kind==="image"`); durchgereicht aus dem HatchStyle. */
|
/** Bild-Muster-Parameter (nur `kind==="image"`); durchgereicht aus dem HatchStyle. */
|
||||||
image?: { src: string; scaleX: number; scaleY: number; rotation: number };
|
image?: { src: string; scaleX: number; scaleY: number; rotation: number };
|
||||||
|
/**
|
||||||
|
* Steuer-Parameter der Random-Vektor-Streuung (nur `lines==="random"`), 1:1 aus
|
||||||
|
* dem HatchStyle durchgereicht. Alle optional — die eigentliche Erzeugung/
|
||||||
|
* Default-Auflösung liegt zentral in `glPlan/glPlanHatch.ts` (single render truth).
|
||||||
|
*/
|
||||||
|
seed?: number;
|
||||||
|
density?: number;
|
||||||
|
lengthMin?: number;
|
||||||
|
lengthMax?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -307,6 +316,11 @@ export function resolveHatch(
|
|||||||
kind: h.kind,
|
kind: h.kind,
|
||||||
lines: h.lines,
|
lines: h.lines,
|
||||||
image: h.image,
|
image: h.image,
|
||||||
|
// Random-Streu-Parameter unverändert durchreichen (Erzeugung in glPlanHatch).
|
||||||
|
seed: h.seed,
|
||||||
|
density: h.density,
|
||||||
|
lengthMin: h.lengthMin,
|
||||||
|
lengthMax: h.lengthMax,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -546,6 +546,8 @@ export function scatterStrokes(
|
|||||||
cell: number,
|
cell: number,
|
||||||
baseAngleRad: number,
|
baseAngleRad: number,
|
||||||
seed: number,
|
seed: number,
|
||||||
|
lenMin?: number,
|
||||||
|
lenMax?: number,
|
||||||
): HatchRun[] {
|
): HatchRun[] {
|
||||||
if (poly.length < 3 || strokeLen <= 1e-9 || cell <= 1e-9) return [];
|
if (poly.length < 3 || strokeLen <= 1e-9 || cell <= 1e-9) return [];
|
||||||
const bb = bbox(poly);
|
const bb = bbox(poly);
|
||||||
@@ -555,14 +557,22 @@ export function scatterStrokes(
|
|||||||
const nx = Math.max(1, Math.round(w / cell));
|
const nx = Math.max(1, Math.round(w / cell));
|
||||||
const ny = Math.max(1, Math.round(h / cell));
|
const ny = Math.max(1, Math.round(h / cell));
|
||||||
const count = Math.min(20000, Math.max(4, nx * ny));
|
const count = Math.min(20000, Math.max(4, nx * ny));
|
||||||
|
// Explizite Längen-Spanne (falls gesetzt) validieren; sonst die heutige
|
||||||
|
// scale-abhängige Variation (±40 % um `strokeLen`) beibehalten.
|
||||||
|
const hasRange =
|
||||||
|
lenMin != null && lenMax != null && lenMin > 1e-9 && lenMax >= lenMin;
|
||||||
const rnd = mulberry32(seed);
|
const rnd = mulberry32(seed);
|
||||||
const runs: HatchRun[] = [];
|
const runs: HatchRun[] = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const cx = bb.minX + rnd() * w;
|
const cx = bb.minX + rnd() * w;
|
||||||
const cy = bb.minY + rnd() * h;
|
const cy = bb.minY + rnd() * h;
|
||||||
// Orientierung frei (± π um die Basisrichtung), Länge um ±40 % variiert.
|
// Orientierung frei (± π um die Basisrichtung).
|
||||||
const ang = baseAngleRad + (rnd() * 2 - 1) * Math.PI;
|
const ang = baseAngleRad + (rnd() * 2 - 1) * Math.PI;
|
||||||
const half = (strokeLen * 0.5) * (0.6 + 0.8 * rnd());
|
// Länge: explizite Spanne [lenMin,lenMax] oder Default (±40 % um strokeLen).
|
||||||
|
const len = hasRange
|
||||||
|
? lenMin! + rnd() * (lenMax! - lenMin!)
|
||||||
|
: strokeLen * (0.6 + 0.8 * rnd());
|
||||||
|
const half = len * 0.5;
|
||||||
const dx = Math.cos(ang) * half;
|
const dx = Math.cos(ang) * half;
|
||||||
const dy = Math.sin(ang) * half;
|
const dy = Math.sin(ang) * half;
|
||||||
const a: Vec2 = { x: cx - dx, y: cy - dy };
|
const a: Vec2 = { x: cx - dx, y: cy - dy };
|
||||||
@@ -581,12 +591,24 @@ export function scatterStrokes(
|
|||||||
*/
|
*/
|
||||||
export function buildRandomHatchRuns(poly: Vec2[], hatch: HatchRender, scale?: number): HatchRun[] {
|
export function buildRandomHatchRuns(poly: Vec2[], hatch: HatchRender, scale?: number): HatchRun[] {
|
||||||
const s = scale ?? (hatch.scale > 1e-6 ? hatch.scale : 1);
|
const s = scale ?? (hatch.scale > 1e-6 ? hatch.scale : 1);
|
||||||
const cell = 8 * s * PX_TO_M; // Meter, wie der Parallel-Schar-Abstand
|
// Dichte: Multiplikator auf den Grundabstand (>1 = dichter → kleinere Zelle).
|
||||||
const strokeLen = cell * 1.1; // kurzer Strich ~ Kachelmaß
|
// Fehlt/ungültig ⇒ 1 ⇒ heutiges Verhalten.
|
||||||
|
const density = hatch.density != null && hatch.density > 1e-6 ? hatch.density : 1;
|
||||||
|
const cell = (8 * s * PX_TO_M) / density; // Meter, wie der Parallel-Schar-Abstand
|
||||||
|
const strokeLen = cell * 1.1; // Default-Strich ~ Kachelmaß (wenn keine Längenspanne)
|
||||||
const bb = bbox(poly);
|
const bb = bbox(poly);
|
||||||
const seed = hashSeed(bb.minX, bb.minY, bb.maxX - bb.minX, bb.maxY - bb.minY, hatch.angle, s);
|
// Flächen-Seed (gerundete Bounding-Box + Muster-Parameter) MIT dem expliziten
|
||||||
|
// `hatch.seed` gemischt: „Neu würfeln" (neuer seed) ⇒ andere Streuung, aber
|
||||||
|
// gleicher seed + gleiche Fläche ⇒ identische Geometrie über alle Renderpfade.
|
||||||
|
const seed = hashSeed(
|
||||||
|
bb.minX, bb.minY, bb.maxX - bb.minX, bb.maxY - bb.minY,
|
||||||
|
hatch.angle, s, density, hatch.seed ?? 0,
|
||||||
|
);
|
||||||
const base = toModelAngleRad(hatch.angle);
|
const base = toModelAngleRad(hatch.angle);
|
||||||
return scatterStrokes(poly, strokeLen, cell, base, seed);
|
// Explizite Längenspanne (mm-Papier → Modell-Meter, wie das Dash-Muster).
|
||||||
|
const lenMin = hatch.lengthMin != null ? hatch.lengthMin * DASH_MM_TO_M : undefined;
|
||||||
|
const lenMax = hatch.lengthMax != null ? hatch.lengthMax * DASH_MM_TO_M : undefined;
|
||||||
|
return scatterStrokes(poly, strokeLen, cell, base, seed, lenMin, lenMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Zickzack-/Wellen-Linie ───────────────────────────────────────────────────
|
// ── Zickzack-/Wellen-Linie ───────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -68,6 +68,25 @@ describe("Random-Vektor-Schraffur", () => {
|
|||||||
expect(JSON.stringify(random)).not.toEqual(JSON.stringify(parallel));
|
expect(JSON.stringify(random)).not.toEqual(JSON.stringify(parallel));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("expliziter Seed ist reproduzierbar und aendert die Streuung (Neu wuerfeln)", () => {
|
||||||
|
const base = { kind: "vector", lines: "random", scale: 1 } as const;
|
||||||
|
const s7a = buildHatchRuns(SQUARE, hatch({ ...base, seed: 7 }));
|
||||||
|
const s7b = buildHatchRuns(SQUARE, hatch({ ...base, seed: 7 }));
|
||||||
|
const s8 = buildHatchRuns(SQUARE, hatch({ ...base, seed: 8 }));
|
||||||
|
expect(s7a).toEqual(s7b); // gleicher Seed ⇒ identische Geometrie
|
||||||
|
expect(JSON.stringify(s7a)).not.toEqual(JSON.stringify(s8)); // neuer Seed ⇒ neue Streuung
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Dichte verdichtet die Streuung, Längenspanne wird angewandt", () => {
|
||||||
|
const base = { kind: "vector", lines: "random", scale: 1, seed: 3 } as const;
|
||||||
|
const sparse = buildHatchRuns(SQUARE, hatch({ ...base }));
|
||||||
|
const dense = buildHatchRuns(SQUARE, hatch({ ...base, density: 3 }));
|
||||||
|
expect(dense.length).toBeGreaterThan(sparse.length);
|
||||||
|
// Längenspanne greift ohne zu werfen und bleibt deterministisch.
|
||||||
|
const ranged = buildHatchRuns(SQUARE, hatch({ ...base, lengthMin: 1, lengthMax: 2 }));
|
||||||
|
expect(ranged).toEqual(buildHatchRuns(SQUARE, hatch({ ...base, lengthMin: 1, lengthMax: 2 })));
|
||||||
|
});
|
||||||
|
|
||||||
it("erscheint als Streu-Polylinien in der RenderScene (deterministisch)", () => {
|
it("erscheint als Streu-Polylinien in der RenderScene (deterministisch)", () => {
|
||||||
const poly: Primitive = {
|
const poly: Primitive = {
|
||||||
kind: "polygon",
|
kind: "polygon",
|
||||||
|
|||||||
+120
-40
@@ -56,29 +56,6 @@ const PATTERN_OPTIONS: { value: HatchPattern; labelKey: string }[] = [
|
|||||||
{ value: "crosshatch", labelKey: "pattern.crosshatch" },
|
{ 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) ────────────────────────
|
// ── Wiederverwendbare Feld-Bausteine (DOSSIER-Look) ────────────────────────
|
||||||
// Alle Steuerelemente füllen ihre Tabellenzelle (width:100%) und werden über
|
// Alle Steuerelemente füllen ihre Tabellenzelle (width:100%) und werden über
|
||||||
// die Spaltenbreite des Grids dimensioniert; Zahlenfelder sind rechtsbündig.
|
// die Spaltenbreite des Grids dimensioniert; Zahlenfelder sind rechtsbündig.
|
||||||
@@ -979,6 +956,14 @@ function HatchDetail({
|
|||||||
r.readAsDataURL(file);
|
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 (
|
return (
|
||||||
<div className="res-md-detail-inner">
|
<div className="res-md-detail-inner">
|
||||||
<div className="res-md-head">
|
<div className="res-md-head">
|
||||||
@@ -1064,6 +1049,39 @@ function HatchDetail({
|
|||||||
options={lineOptions}
|
options={lineOptions}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</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 } });
|
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 (
|
return (
|
||||||
<div className="res-md-detail-inner">
|
<div className="res-md-detail-inner">
|
||||||
<div className="res-md-head">
|
<div className="res-md-head">
|
||||||
@@ -1226,16 +1265,6 @@ function LineStyleDetail({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</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")}>
|
<FieldRow label={t("resources.col.color")}>
|
||||||
<ColorField color={style.color} onChange={(color) => onPatch({ color })} />
|
<ColorField color={style.color} onChange={(color) => onPatch({ color })} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
@@ -1259,16 +1288,67 @@ function LineStyleDetail({
|
|||||||
</FieldRow>
|
</FieldRow>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
<FieldRow label={t("resources.col.stroke")}>
|
<FieldRow label={t("resources.col.stroke")}>
|
||||||
<SelectField
|
<Segmented
|
||||||
value={dashKey(style.dash)}
|
value={isSolid ? "solid" : "dash"}
|
||||||
onChange={(key) => {
|
onChange={setStroke}
|
||||||
const opt = DASH_OPTIONS.find((o) => o.key === key);
|
options={[
|
||||||
if (opt) onPatch({ dash: opt.dash });
|
{ value: "solid", label: t("resources.stroke.solid") },
|
||||||
}}
|
{ value: "dash", label: t("resources.stroke.dash") },
|
||||||
options={DASH_OPTIONS.map((o) => ({ value: o.key, label: t(o.labelKey) }))}
|
]}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+18
-4
@@ -148,15 +148,29 @@ export function HatchSwatch({
|
|||||||
} else if (hatch.pattern === "none") {
|
} else if (hatch.pattern === "none") {
|
||||||
inner = null;
|
inner = null;
|
||||||
} else if (hatch.lines === "random") {
|
} else if (hatch.lines === "random") {
|
||||||
// Zufällig verteilte kurze Striche (deterministisch geseedet).
|
// Zufällig verteilte kurze Striche (deterministisch geseedet). Dichte, Länge
|
||||||
const rnd = mulberry32(Math.round(scale * 1000) + Math.round(angle));
|
// und der explizite Seed spiegeln die Plan-Parameter (HatchStyle) — so ändert
|
||||||
const count = Math.round(18 * Math.min(2, scale)) + 6;
|
// „Neu würfeln"/Dichte/Länge die Vorschau wie im Plan. Rein schematisch (px),
|
||||||
const len = Math.max(3, 6 / Math.sqrt(scale));
|
// NICHT deckungsgleich mit der Modell-Streuung, aber qualitativ konsistent.
|
||||||
|
const density = hatch.density != null && hatch.density > 0 ? hatch.density : 1;
|
||||||
|
const seed = (Math.round(scale * 1000) + Math.round(angle) + Math.round(hatch.seed ?? 0)) >>> 0;
|
||||||
|
const rnd = mulberry32(seed);
|
||||||
|
const count = Math.min(400, Math.max(4, Math.round((18 * Math.min(2, scale) + 6) * density)));
|
||||||
|
// mm-Papier → Vorschau-px (wie das Dash-Muster im LineSwatch: ·4).
|
||||||
|
const PX_PER_MM = 4;
|
||||||
|
const hasRange =
|
||||||
|
hatch.lengthMin != null && hatch.lengthMax != null &&
|
||||||
|
hatch.lengthMin > 0 && hatch.lengthMax >= hatch.lengthMin;
|
||||||
|
const baseLen = Math.max(3, 6 / Math.sqrt(scale));
|
||||||
const out: React.ReactNode[] = [];
|
const out: React.ReactNode[] = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const cx = rnd() * w;
|
const cx = rnd() * w;
|
||||||
const cy = rnd() * h;
|
const cy = rnd() * h;
|
||||||
const ang = rnd() * Math.PI;
|
const ang = rnd() * Math.PI;
|
||||||
|
const lr = rnd();
|
||||||
|
const len = hasRange
|
||||||
|
? (hatch.lengthMin! + lr * (hatch.lengthMax! - hatch.lengthMin!)) * PX_PER_MM
|
||||||
|
: baseLen;
|
||||||
const dx = (Math.cos(ang) * len) / 2;
|
const dx = (Math.cos(ang) * len) / 2;
|
||||||
const dy = (Math.sin(ang) * len) / 2;
|
const dy = (Math.sin(ang) * len) / 2;
|
||||||
out.push(
|
out.push(
|
||||||
|
|||||||
Reference in New Issue
Block a user