DXF-Import: CIRCLE/ARC als echte glatte Kreis-/Bogen-Formen (statt tesselliertem Vieleck)
This commit is contained in:
@@ -471,6 +471,52 @@ describe("textsToDrawings", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("parseDxf — Kurven behalten echte Geometrie (curve)", () => {
|
||||||
|
it("CIRCLE trägt curve {kind:circle}", () => {
|
||||||
|
const c = onlyContour(dxf(circle(10, 20, 5, 4)));
|
||||||
|
expect(c.curve?.kind).toBe("circle");
|
||||||
|
expect(c.curve?.cx).toBeCloseTo(10, 9);
|
||||||
|
expect(c.curve?.cy).toBeCloseTo(20, 9);
|
||||||
|
expect(c.curve?.r).toBeCloseTo(4, 9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ARC trägt curve {kind:arc} mit Winkeln (Radiant)", () => {
|
||||||
|
const c = onlyContour(dxf(arc(0, 0, 10, 0, 90)));
|
||||||
|
expect(c.curve?.kind).toBe("arc");
|
||||||
|
expect(c.curve?.r).toBeCloseTo(10, 9);
|
||||||
|
expect(c.curve?.a0).toBeCloseTo(0, 9);
|
||||||
|
expect(c.curve?.a1).toBeCloseTo(Math.PI / 2, 9);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("contoursToDrawings — Kurven → glatte Formen", () => {
|
||||||
|
it("curve circle → {shape:\"circle\"}", () => {
|
||||||
|
const set: ContourSet = {
|
||||||
|
id: "s", type: "contourSet", name: "c",
|
||||||
|
contours: [{ z: 0, closed: true, pts: [{ x: 0, y: 0 }], curve: { kind: "circle", cx: 1, cy: 2, r: 3, a0: 0, a1: Math.PI * 2 } }],
|
||||||
|
};
|
||||||
|
const [d] = contoursToDrawings([set], "lvl", "active", "C", (s) => s);
|
||||||
|
expect(d.geom.shape).toBe("circle");
|
||||||
|
if (d.geom.shape === "circle") {
|
||||||
|
expect(d.geom.r).toBeCloseTo(3, 9);
|
||||||
|
expect(d.geom.center.x).toBeCloseTo(1, 9);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("curve arc → {shape:\"arc\"}", () => {
|
||||||
|
const set: ContourSet = {
|
||||||
|
id: "s", type: "contourSet", name: "c",
|
||||||
|
contours: [{ z: 0, closed: false, pts: [{ x: 0, y: 0 }], curve: { kind: "arc", cx: 0, cy: 0, r: 5, a0: 0, a1: 1 } }],
|
||||||
|
};
|
||||||
|
const [d] = contoursToDrawings([set], "lvl", "active", "C", (s) => s);
|
||||||
|
expect(d.geom.shape).toBe("arc");
|
||||||
|
if (d.geom.shape === "arc") {
|
||||||
|
expect(d.geom.r).toBeCloseTo(5, 9);
|
||||||
|
expect(d.geom.a1).toBeCloseTo(1, 9);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("contoursToDrawings — HATCH-Füllung", () => {
|
describe("contoursToDrawings — HATCH-Füllung", () => {
|
||||||
it("gefüllte Kontur → Drawing2D mit fillColor und polyline-Form", () => {
|
it("gefüllte Kontur → Drawing2D mit fillColor und polyline-Form", () => {
|
||||||
const set: ContourSet = {
|
const set: ContourSet = {
|
||||||
|
|||||||
+14
-2
@@ -444,7 +444,13 @@ function arcContour(e: DxfEntity): Contour | null {
|
|||||||
const t = start + (sweep * i) / segs;
|
const t = start + (sweep * i) / segs;
|
||||||
pts.push({ x: cx + r * Math.cos(t), y: cy + r * Math.sin(t) });
|
pts.push({ x: cx + r * Math.cos(t), y: cy + r * Math.sin(t) });
|
||||||
}
|
}
|
||||||
return { z, pts, closed: false, layer: e.layer };
|
return {
|
||||||
|
z,
|
||||||
|
pts,
|
||||||
|
closed: false,
|
||||||
|
layer: e.layer,
|
||||||
|
curve: { kind: "arc", cx, cy, r, a0: start, a1: start + sweep },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CIRCLE → geschlossener Kreis-Linienzug (voller Umlauf, letzter Punkt weggelassen). */
|
/** CIRCLE → geschlossener Kreis-Linienzug (voller Umlauf, letzter Punkt weggelassen). */
|
||||||
@@ -471,7 +477,13 @@ function circleContour(e: DxfEntity): Contour | null {
|
|||||||
const t = (Math.PI * 2 * i) / segs;
|
const t = (Math.PI * 2 * i) / segs;
|
||||||
pts.push({ x: cx + r * Math.cos(t), y: cy + r * Math.sin(t) });
|
pts.push({ x: cx + r * Math.cos(t), y: cy + r * Math.sin(t) });
|
||||||
}
|
}
|
||||||
return { z, pts, closed: true, layer: e.layer };
|
return {
|
||||||
|
z,
|
||||||
|
pts,
|
||||||
|
closed: true,
|
||||||
|
layer: e.layer,
|
||||||
|
curve: { kind: "circle", cx, cy, r, a0: 0, a1: Math.PI * 2 },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -102,6 +102,14 @@ export function textsToDrawings(
|
|||||||
|
|
||||||
/** Eine Kontur → 2D-Geometrie (line bei 2 offenen Punkten, sonst polyline). */
|
/** Eine Kontur → 2D-Geometrie (line bei 2 offenen Punkten, sonst polyline). */
|
||||||
function contourGeom(contour: Contour): Drawing2DGeom | null {
|
function contourGeom(contour: Contour): Drawing2DGeom | null {
|
||||||
|
// Echte Kurve (CIRCLE/ARC) → GLATTE Kreis-/Bogen-Form statt tesselliertem Vieleck.
|
||||||
|
const c = contour.curve;
|
||||||
|
if (c) {
|
||||||
|
if (c.kind === "circle") {
|
||||||
|
return { shape: "circle", center: { x: c.cx, y: c.cy }, r: c.r };
|
||||||
|
}
|
||||||
|
return { shape: "arc", center: { x: c.cx, y: c.cy }, r: c.r, a0: c.a0, a1: c.a1 };
|
||||||
|
}
|
||||||
const pts = contour.pts;
|
const pts = contour.pts;
|
||||||
if (pts.length < 2) return null;
|
if (pts.length < 2) return null;
|
||||||
if (pts.length === 2 && !contour.closed) {
|
if (pts.length === 2 && !contour.closed) {
|
||||||
|
|||||||
@@ -1098,6 +1098,19 @@ export interface Contour {
|
|||||||
* geschlossene, gefüllte 2D-Form (Vollton-Füllung); sonst nur ein Umriss.
|
* geschlossene, gefüllte 2D-Form (Vollton-Füllung); sonst nur ein Umriss.
|
||||||
*/
|
*/
|
||||||
filled?: boolean;
|
filled?: boolean;
|
||||||
|
/**
|
||||||
|
* Wahre Kurvengeometrie (aus CIRCLE/ARC). `pts` bleibt tesselliert (Kontext/
|
||||||
|
* 3D), aber der Drawing-Import baut daraus eine GLATTE `{shape:"circle"|"arc"}`-
|
||||||
|
* Form statt eines Vielecks. Winkel in RADIANT (CCW); Kreis = a0..a1 über 2π.
|
||||||
|
*/
|
||||||
|
curve?: {
|
||||||
|
kind: "circle" | "arc";
|
||||||
|
cx: number;
|
||||||
|
cy: number;
|
||||||
|
r: number;
|
||||||
|
a0: number;
|
||||||
|
a1: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3096,5 +3096,51 @@ function renderPrimitive(
|
|||||||
</text>
|
</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
@@ -311,6 +311,35 @@ export type Primitive =
|
|||||||
/** ID des 2D-Zeichenelements (für Links-Klick-Auswahl). */
|
/** ID des 2D-Zeichenelements (für Links-Klick-Auswahl). */
|
||||||
drawingId?: string;
|
drawingId?: string;
|
||||||
greyed?: boolean;
|
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);
|
seg(c4, c1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "circle": {
|
case "circle":
|
||||||
// Kreis als geschlossene Polygon-Approximation (N Segmente) — nutzt die
|
// Echtes Kreis-Primitiv → glattes `<circle>` (rund bei jedem Zoom) mit
|
||||||
// vorhandene Füll-/Strich-Pipeline, ohne ein eigenes Kreis-Primitiv.
|
// optionaler Vollton-Füllung. Ersetzt die frühere 64-Ecken-Approximation.
|
||||||
const N = 64;
|
out.push({
|
||||||
const pts: Vec2[] = [];
|
kind: "drawingCircle",
|
||||||
for (let i = 0; i < N; i++) {
|
center: g.center,
|
||||||
const t = (i / N) * Math.PI * 2;
|
r: g.r,
|
||||||
pts.push({ x: g.center.x + g.r * Math.cos(t), y: g.center.y + g.r * Math.sin(t) });
|
fill: background ?? d.fillColor ?? "none",
|
||||||
}
|
stroke: color,
|
||||||
fillPoly(pts);
|
weightMm,
|
||||||
for (let i = 0; i < N; i++) seg(pts[i], pts[(i + 1) % N]);
|
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;
|
break;
|
||||||
}
|
|
||||||
case "text":
|
case "text":
|
||||||
out.push({
|
out.push({
|
||||||
kind: "drawingText",
|
kind: "drawingText",
|
||||||
@@ -1042,7 +1088,6 @@ function addDrawing2D(
|
|||||||
drawingId: d.id,
|
drawingId: d.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
// arc: Primitive-Erweiterung folgt später.
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,6 +486,21 @@ export function planToRenderScene(plan: Plan, paperScaleN: number = STAMP_DEFAUL
|
|||||||
widthMm: p.weightMm,
|
widthMm: p.weightMm,
|
||||||
dash: p.dash ?? null,
|
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") {
|
} else if (p.kind === "text") {
|
||||||
// "text": zeilenweise flachen — die ECHTEN Glyphen rastert der Rust-
|
// "text": zeilenweise flachen — die ECHTEN Glyphen rastert der Rust-
|
||||||
// Renderer über einen GPU-Glyphen-Atlas (glyphon, dieselbe Font-Familie
|
// Renderer über einen GPU-Glyphen-Atlas (glyphon, dieselbe Font-Familie
|
||||||
|
|||||||
Reference in New Issue
Block a user