From 361756ac874a9ea485450ab5e356442ef01b5f81 Mon Sep 17 00:00:00 2001 From: Steve Genoud Date: Fri, 20 Dec 2024 11:45:22 +0100 Subject: [PATCH] Improve approximations for SVG export --- package.json | 3 +- packages/replicad/src/blueprints/Blueprint.ts | 15 +--- .../replicad/src/blueprints/approximations.ts | 28 +++++++ packages/replicad/src/draw.ts | 12 +++ packages/replicad/src/lib2d/approximations.ts | 77 ++++++++++++++++++- packages/replicad/src/lib2d/svgPath.ts | 17 +--- 6 files changed, 120 insertions(+), 32 deletions(-) create mode 100644 packages/replicad/src/blueprints/approximations.ts diff --git a/package.json b/package.json index ad97974..f4d39a7 100644 --- a/package.json +++ b/package.json @@ -14,5 +14,6 @@ "eslint": "^8.1.0", "lerna": "7.1.5", "prettier": "^2.4.1" - } + }, + "packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a" } diff --git a/packages/replicad/src/blueprints/Blueprint.ts b/packages/replicad/src/blueprints/Blueprint.ts index c91fa7f..c986297 100644 --- a/packages/replicad/src/blueprints/Blueprint.ts +++ b/packages/replicad/src/blueprints/Blueprint.ts @@ -18,6 +18,7 @@ import { Curve2D, samePoint, isPoint2D, + approximateAsSvgCompatibleCurve, } from "../lib2d"; import { assembleWire } from "../shapeHelpers"; import { Face } from "../shapes"; @@ -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); }); diff --git a/packages/replicad/src/blueprints/approximations.ts b/packages/replicad/src/blueprints/approximations.ts new file mode 100644 index 0000000..2d42465 --- /dev/null +++ b/packages/replicad/src/blueprints/approximations.ts @@ -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( + 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; +} diff --git a/packages/replicad/src/draw.ts b/packages/replicad/src/draw.ts index 401d2b6..4d2ae6a 100644 --- a/packages/replicad/src/draw.ts +++ b/packages/replicad/src/draw.ts @@ -1,4 +1,5 @@ import { + ApproximationOptions, BoundingBox2d, make2dCircle, make2dEllipse, @@ -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; @@ -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 ( diff --git a/packages/replicad/src/lib2d/approximations.ts b/packages/replicad/src/lib2d/approximations.ts index f2b30cd..b03dd2c 100644 --- a/packages/replicad/src/lib2d/approximations.ts +++ b/packages/replicad/src/lib2d/approximations.ts @@ -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 = { + 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()); }; @@ -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 + ); + }); +} diff --git a/packages/replicad/src/lib2d/svgPath.ts b/packages/replicad/src/lib2d/svgPath.ts index 896542a..b5c2c89 100644 --- a/packages/replicad/src/lib2d/svgPath.ts +++ b/packages/replicad/src/lib2d/svgPath.ts @@ -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())}`; @@ -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}`); };