DXF-Import: CIRCLE/ARC als echte glatte Kreis-/Bogen-Formen (statt tesselliertem Vieleck)

This commit is contained in:
2026-07-05 15:06:25 +02:00
parent 45e19b7293
commit 4ac99d37cb
7 changed files with 200 additions and 15 deletions
+46
View File
@@ -3096,5 +3096,51 @@ function renderPrimitive(
</text>
);
}
case "drawingCircle": {
// Echtes `<circle>` — rund bei jedem Zoom (kein Vieleck). Füllung optional.
const c = toScreen(p.center);
const r = p.r * PX_PER_M;
return (
<circle
cx={c.x}
cy={c.y}
r={r}
fill={p.fill}
stroke={p.stroke}
strokeWidth={weight(p.weightMm)}
strokeDasharray={dashOf(p.dash)}
vectorEffect={vfx}
/>
);
}
case "drawingArc": {
// Glatter SVG-Bogen. Winkel in Radiant (CCW im Modell); Screen-Y ist
// gespiegelt → ein modell-CCW-Bogen läuft am Bildschirm im Uhrzeigersinn
// (SVG sweep-flag 1). large-arc aus der Spannweite (>π).
const TAU = Math.PI * 2;
const rPx = p.r * PX_PER_M;
const from = toScreen({
x: p.center.x + p.r * Math.cos(p.a0),
y: p.center.y + p.r * Math.sin(p.a0),
});
const to = toScreen({
x: p.center.x + p.r * Math.cos(p.a1),
y: p.center.y + p.r * Math.sin(p.a1),
});
const sweep = (((p.a1 - p.a0) % TAU) + TAU) % TAU;
const largeArc = sweep > Math.PI ? 1 : 0;
const d = `M ${from.x} ${from.y} A ${rPx} ${rPx} 0 ${largeArc} 1 ${to.x} ${to.y}`;
return (
<path
d={d}
fill="none"
stroke={p.stroke}
strokeWidth={weight(p.weightMm)}
strokeDasharray={dashOf(p.dash)}
strokeLinecap={dashHasDot(p.dash) ? "round" : undefined}
vectorEffect={vfx}
/>
);
}
}
}
+58 -13
View File
@@ -311,6 +311,35 @@ export type Primitive =
/** ID des 2D-Zeichenelements (für Links-Klick-Auswahl). */
drawingId?: string;
greyed?: boolean;
}
| {
// Glatter Kreis (aus Drawing2D `{shape:"circle"}`, z. B. DXF-CIRCLE) — als
// echtes `<circle>` gerendert (rund bei jedem Zoom), nicht als Vieleck.
kind: "drawingCircle";
center: Vec2;
/** Radius in Modell-Metern. */
r: number;
/** Vollton-Füllfarbe oder "none". */
fill: string;
stroke: string;
weightMm: number;
dash?: number[] | null;
drawingId?: string;
greyed?: boolean;
}
| {
// Glatter Bogen (aus Drawing2D `{shape:"arc"}`, z. B. DXF-ARC) — als
// SVG-Bogenpfad gerendert; Winkel in RADIANT (CCW im Modell).
kind: "drawingArc";
center: Vec2;
r: number;
a0: number;
a1: number;
stroke: string;
weightMm: number;
dash?: number[] | null;
drawingId?: string;
greyed?: boolean;
};
/**
@@ -1017,19 +1046,36 @@ function addDrawing2D(
seg(c4, c1);
break;
}
case "circle": {
// Kreis als geschlossene Polygon-Approximation (N Segmente) — nutzt die
// vorhandene Füll-/Strich-Pipeline, ohne ein eigenes Kreis-Primitiv.
const N = 64;
const pts: Vec2[] = [];
for (let i = 0; i < N; i++) {
const t = (i / N) * Math.PI * 2;
pts.push({ x: g.center.x + g.r * Math.cos(t), y: g.center.y + g.r * Math.sin(t) });
}
fillPoly(pts);
for (let i = 0; i < N; i++) seg(pts[i], pts[(i + 1) % N]);
case "circle":
// Echtes Kreis-Primitiv → glattes `<circle>` (rund bei jedem Zoom) mit
// optionaler Vollton-Füllung. Ersetzt die frühere 64-Ecken-Approximation.
out.push({
kind: "drawingCircle",
center: g.center,
r: g.r,
fill: background ?? d.fillColor ?? "none",
stroke: color,
weightMm,
dash,
greyed,
drawingId: d.id,
});
break;
case "arc":
// Echtes Bogen-Primitiv → glatter SVG-Bogenpfad (Winkel in Radiant).
out.push({
kind: "drawingArc",
center: g.center,
r: g.r,
a0: g.a0,
a1: g.a1,
stroke: color,
weightMm,
dash,
greyed,
drawingId: d.id,
});
break;
}
case "text":
out.push({
kind: "drawingText",
@@ -1042,7 +1088,6 @@ function addDrawing2D(
drawingId: d.id,
});
break;
// arc: Primitive-Erweiterung folgt später.
default:
break;
}
+15
View File
@@ -486,6 +486,21 @@ export function planToRenderScene(plan: Plan, paperScaleN: number = STAMP_DEFAUL
widthMm: p.weightMm,
dash: p.dash ?? null,
});
} else if (p.kind === "drawingCircle" || p.kind === "drawingArc") {
// Der native Renderer hat kein Kreis-/Bogen-Primitiv → hier tessellieren
// (der SVG-Pfad zeichnet sie glatt). Kreis = voller Umlauf, Bogen = a0..a1.
flushRun();
const col = withOpacity(toRgba(p.stroke, 1) ?? [0.1, 0.1, 0.1, 1], undefined, p.greyed);
const TAU = Math.PI * 2;
const a0 = p.kind === "drawingArc" ? p.a0 : 0;
const sweep = p.kind === "drawingArc" ? (((p.a1 - p.a0) % TAU) + TAU) % TAU : TAU;
const N = Math.max(8, Math.ceil(sweep / (Math.PI / 32)));
const pts: RPoint[] = [];
for (let i = 0; i <= N; i++) {
const t = a0 + (sweep * i) / N;
pts.push([p.center.x + p.r * Math.cos(t), p.center.y + p.r * Math.sin(t)]);
}
polylines.push({ pts, color: col, widthMm: p.weightMm, dash: p.dash ?? null, z: zc++ });
} else if (p.kind === "text") {
// "text": zeilenweise flachen — die ECHTEN Glyphen rastert der Rust-
// Renderer über einen GPU-Glyphen-Atlas (glyphon, dieselbe Font-Familie