Skip to content

Commit

Permalink
Improve approximations for SVG export
Browse files Browse the repository at this point in the history
  • Loading branch information
sgenoud committed Dec 20, 2024
1 parent 31b2b51 commit 361756a
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 32 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
"eslint": "^8.1.0",
"lerna": "7.1.5",
"prettier": "^2.4.1"
}
},
"packageManager": "[email protected]+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a"
}
15 changes: 4 additions & 11 deletions packages/replicad/src/blueprints/Blueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
Curve2D,
samePoint,
isPoint2D,
approximateAsSvgCompatibleCurve,
} from "../lib2d";
import { assembleWire } from "../shapeHelpers";
import { Face } from "../shapes";
Expand Down Expand Up @@ -199,17 +200,9 @@ export default class Blueprint implements DrawingInterface {
const r = GCWithScope();
const bp = this.clone().mirror([1, 0], [0, 0], "plane");

const path = bp.curves.flatMap((c) => {
if (
(c.geomType === "ELLIPSE" || c.geomType === "CIRCLE") &&
samePoint(c.firstPoint, c.lastPoint)
) {
const [c1, c2] = c.splitAt([0.5]);
return [
adaptedCurveToPathElem(r(c1.adaptor()), c1.lastPoint),
adaptedCurveToPathElem(r(c2.adaptor()), c2.lastPoint),
];
}
const compatibleCurves = approximateAsSvgCompatibleCurve(bp.curves);

const path = compatibleCurves.flatMap((c) => {
return adaptedCurveToPathElem(r(c.adaptor()), c.lastPoint);
});

Expand Down
28 changes: 28 additions & 0 deletions packages/replicad/src/blueprints/approximations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
approximateAsSvgCompatibleCurve,
ApproximationOptions,
} from "../lib2d";
import Blueprint from "./Blueprint";
import Blueprints from "./Blueprints";
import { Shape2D } from "./boolean2D";
import CompoundBlueprint from "./CompoundBlueprint";

export function approximateForSVG<T extends Shape2D>(
bp: T,
options: ApproximationOptions
): T {
if (bp instanceof Blueprint) {
return new Blueprint(
approximateAsSvgCompatibleCurve(bp.curves, options)
) as T;
} else if (bp instanceof CompoundBlueprint) {
return new CompoundBlueprint(
bp.blueprints.map((b) => approximateForSVG(b, options))
) as T;
} else if (bp instanceof Blueprints) {
return new Blueprints(
bp.blueprints.map((b) => approximateForSVG(b, options))
) as T;
}
return bp;
}
12 changes: 12 additions & 0 deletions packages/replicad/src/draw.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ApproximationOptions,
BoundingBox2d,
make2dCircle,
make2dEllipse,
Expand Down Expand Up @@ -33,6 +34,7 @@ import { CornerFinder } from "./finders/cornerFinder";
import { fillet2D, chamfer2D } from "./blueprints/customCorners";
import { edgeToCurve } from "./curves";
import { BSplineApproximationConfig } from "./shapeHelpers";
import { approximateForSVG } from "./blueprints/approximations";

export class Drawing implements DrawingInterface {
private innerShape: Shape2D;
Expand Down Expand Up @@ -168,6 +170,16 @@ export class Drawing implements DrawingInterface {
return new Drawing(offset(this.innerShape, distance));
}

approximate(
target: "svg" | "arcs",
options: ApproximationOptions = {}
): Drawing {
if (target !== "svg") {
throw new Error("Only 'svg' is supported for now");
}
return new Drawing(approximateForSVG(this.innerShape, options));
}

get blueprint(): Blueprint {
if (!(this.innerShape instanceof Blueprint)) {
if (
Expand Down
77 changes: 73 additions & 4 deletions packages/replicad/src/lib2d/approximations.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
import { Geom2dAdaptor_Curve } from "replicad-opencascadejs";
import { Geom2dAdaptor_Curve, GeomAbs_Shape } from "replicad-opencascadejs";
import { findCurveType } from "../definitionMaps";
import { getOC } from "../oclib";
import { GCWithScope } from "../register";
import { Curve2D } from "./Curve2D";
import { samePoint } from "./vectorOperations";

export const approximateAsBSpline = (
adaptor: Geom2dAdaptor_Curve,
tolerance = 1e-8
tolerance = 1e-4,
continuity: "C0" | "C1" | "C2" | "C3" = "C0",
maxSegments = 200
): Curve2D => {
const oc = getOC();
const r = GCWithScope();

const continuities: Record<string, GeomAbs_Shape> = {
C0: oc.GeomAbs_Shape.GeomAbs_C0 as GeomAbs_Shape,
C1: oc.GeomAbs_Shape.GeomAbs_C1 as GeomAbs_Shape,
C2: oc.GeomAbs_Shape.GeomAbs_C2 as GeomAbs_Shape,
C3: oc.GeomAbs_Shape.GeomAbs_C3 as GeomAbs_Shape,
};

const convert = r(
new oc.Geom2dConvert_ApproxCurve_2(
adaptor.ShallowCopy(),
tolerance,
oc.GeomAbs_Shape.GeomAbs_C0 as any,
30,
continuities[continuity],
maxSegments,
3
)
);

return new Curve2D(convert.Curve());
};

Expand All @@ -46,3 +57,61 @@ export const BSplineToBezier = (adaptor: Geom2dAdaptor_Curve): Curve2D[] => {
convert.delete();
return curves;
};

export interface ApproximationOptions {
tolerance?: number;
continuity?: "C0" | "C1" | "C2" | "C3";
maxSegments?: number;
}

export function approximateAsSvgCompatibleCurve(
curves: Curve2D[],
options: ApproximationOptions = {
tolerance: 1e-4,
continuity: "C0",
maxSegments: 300,
}
): Curve2D[] {
const r = GCWithScope();

return curves.flatMap((curve) => {
const adaptor = r(curve.adaptor());
const curveType = findCurveType(adaptor.GetType());

if (
curveType === "ELLIPSE" ||
(curveType === "CIRCLE" && samePoint(curve.firstPoint, curve.lastPoint))
) {
return curve.splitAt([0.5]);
}

if (["LINE", "ELLIPSE", "CIRCLE"].includes(curveType)) {
return curve;
}

if (curveType === "BEZIER_CURVE") {
const b = adaptor.Bezier().get();
const deg = b.Degree();

if ([1, 2, 3].includes(deg)) {
return curve;
}
}

if (curveType === "BSPLINE_CURVE") {
const c = BSplineToBezier(adaptor);
return approximateAsSvgCompatibleCurve(c, options);
}

const bspline = approximateAsBSpline(
adaptor,
options.tolerance,
options.continuity,
options.maxSegments
);
return approximateAsSvgCompatibleCurve(
BSplineToBezier(r(bspline.adaptor())),
options
);
});
}
17 changes: 1 addition & 16 deletions packages/replicad/src/lib2d/svgPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { findCurveType } from "../definitionMaps";
import { getOC } from "../oclib";
import round2 from "../utils/round2";
import round5 from "../utils/round5";
import { approximateAsBSpline, BSplineToBezier } from "./approximations";
import { Point2D } from "./definitions";

const fromPnt = (pnt: gp_Pnt2d) => `${round2(pnt.X())} ${round2(pnt.Y())}`;
Expand Down Expand Up @@ -82,19 +81,5 @@ export const adaptedCurveToPathElem = (
} ${curve.IsDirect() ? "1" : "0"} ${end}`;
}

if (curveType === "BSPLINE_CURVE") {
const deg = adaptor.BSpline().get().Degree();
if (deg < 4) {
const bezierCurves = BSplineToBezier(adaptor);
return bezierCurves
.map((c) => adaptedCurveToPathElem(c.adaptor(), c.lastPoint))
.join(" ");
}
}

const bspline = approximateAsBSpline(adaptor);
const bezierCurves = BSplineToBezier(bspline.adaptor());
return bezierCurves
.map((c) => adaptedCurveToPathElem(c.adaptor(), c.lastPoint))
.join(" ");
throw new Error(`Unsupported curve type: ${curveType}`);
};

0 comments on commit 361756a

Please sign in to comment.