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.amplitude": "Amplitude",
|
||||
"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.image": "Bild",
|
||||
"resources.lines.parallel": "Parallel",
|
||||
|
||||
@@ -561,6 +561,16 @@ export const en: Record<TranslationKey, string> = {
|
||||
"resources.field.rotation": "Rotation °",
|
||||
"resources.field.amplitude": "Amplitude",
|
||||
"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.image": "Image",
|
||||
"resources.lines.parallel": "Parallel",
|
||||
|
||||
+26
-1
@@ -29,7 +29,16 @@ export type { RichTextDoc } from "../text/richText";
|
||||
export interface LineStyle {
|
||||
id: 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;
|
||||
/** Farbe (hex). */
|
||||
color: string;
|
||||
@@ -112,6 +121,22 @@ export interface HatchStyle {
|
||||
color: string;
|
||||
/** Optionaler Linienstil für die Musterlinien (Line Manager). */
|
||||
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";
|
||||
/** Bild-Muster-Parameter (nur `kind==="image"`); durchgereicht aus dem HatchStyle. */
|
||||
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,
|
||||
lines: h.lines,
|
||||
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,
|
||||
baseAngleRad: number,
|
||||
seed: number,
|
||||
lenMin?: number,
|
||||
lenMax?: number,
|
||||
): HatchRun[] {
|
||||
if (poly.length < 3 || strokeLen <= 1e-9 || cell <= 1e-9) return [];
|
||||
const bb = bbox(poly);
|
||||
@@ -555,14 +557,22 @@ export function scatterStrokes(
|
||||
const nx = Math.max(1, Math.round(w / cell));
|
||||
const ny = Math.max(1, Math.round(h / cell));
|
||||
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 runs: HatchRun[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const cx = bb.minX + rnd() * w;
|
||||
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 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 dy = Math.sin(ang) * half;
|
||||
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[] {
|
||||
const s = scale ?? (hatch.scale > 1e-6 ? hatch.scale : 1);
|
||||
const cell = 8 * s * PX_TO_M; // Meter, wie der Parallel-Schar-Abstand
|
||||
const strokeLen = cell * 1.1; // kurzer Strich ~ Kachelmaß
|
||||
// Dichte: Multiplikator auf den Grundabstand (>1 = dichter → kleinere Zelle).
|
||||
// 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 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);
|
||||
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 ───────────────────────────────────────────────────
|
||||
|
||||
@@ -68,6 +68,25 @@ describe("Random-Vektor-Schraffur", () => {
|
||||
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)", () => {
|
||||
const poly: Primitive = {
|
||||
kind: "polygon",
|
||||
|
||||
+123
-43
@@ -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>
|
||||
|
||||
+18
-4
@@ -148,15 +148,29 @@ export function HatchSwatch({
|
||||
} else if (hatch.pattern === "none") {
|
||||
inner = null;
|
||||
} else if (hatch.lines === "random") {
|
||||
// Zufällig verteilte kurze Striche (deterministisch geseedet).
|
||||
const rnd = mulberry32(Math.round(scale * 1000) + Math.round(angle));
|
||||
const count = Math.round(18 * Math.min(2, scale)) + 6;
|
||||
const len = Math.max(3, 6 / Math.sqrt(scale));
|
||||
// Zufällig verteilte kurze Striche (deterministisch geseedet). Dichte, Länge
|
||||
// und der explizite Seed spiegeln die Plan-Parameter (HatchStyle) — so ändert
|
||||
// „Neu würfeln"/Dichte/Länge die Vorschau wie im Plan. Rein schematisch (px),
|
||||
// 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[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const cx = rnd() * w;
|
||||
const cy = rnd() * h;
|
||||
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 dy = (Math.sin(ang) * len) / 2;
|
||||
out.push(
|
||||
|
||||
Reference in New Issue
Block a user