From 9b4df2e0c70f04f052fda999b43c90bd09b23d6b Mon Sep 17 00:00:00 2001 From: Karsten Schmidt Date: Thu, 16 May 2024 21:46:08 +0200 Subject: [PATCH] feat(geom): add/update asCubic()/asPath() impls/types/signatures BREAKING CHANGE: update asCubic/asPath() to use new CubicOpts - add support for more shape types, incl. 3D --- packages/geom/src/as-cubic.ts | 275 ++++++++++++++++++++++++---------- packages/geom/src/as-path.ts | 217 ++++++++++++++++----------- 2 files changed, 332 insertions(+), 160 deletions(-) diff --git a/packages/geom/src/as-cubic.ts b/packages/geom/src/as-cubic.ts index b4772243c1..2263470833 100644 --- a/packages/geom/src/as-cubic.ts +++ b/packages/geom/src/as-cubic.ts @@ -1,7 +1,15 @@ import type { Maybe } from "@thi.ng/api"; import type { MultiFn1O } from "@thi.ng/defmulti"; import { defmulti } from "@thi.ng/defmulti/defmulti"; -import type { CubicOpts, IShape, PCLike } from "@thi.ng/geom-api"; +import { unsupported } from "@thi.ng/errors/unsupported"; +import type { + Attribs, + CubicOpts, + IShape, + IShape2, + IShape3, + PCLike, +} from "@thi.ng/geom-api"; import { closedCubicFromBreakPoints, openCubicFromBreakPoints, @@ -10,139 +18,256 @@ import { closedCubicFromControlPoints, openCubicFromControlPoints, } from "@thi.ng/geom-splines/cubic-from-controlpoints"; +import { cubicHobby2 } from "@thi.ng/geom-splines/cubic-hobby"; import { TAU } from "@thi.ng/math/api"; import { concat } from "@thi.ng/transducers/concat"; import { flatten1 } from "@thi.ng/transducers/flatten1"; import { mapcat } from "@thi.ng/transducers/mapcat"; import type { ReadonlyVec, Vec } from "@thi.ng/vectors"; +import type { BPatch } from "./api/bpatch.js"; import type { Circle } from "./api/circle.js"; import type { ComplexPolygon } from "./api/complex-polygon.js"; import { Cubic } from "./api/cubic.js"; +import { Cubic3 } from "./api/cubic3.js"; import type { Group } from "./api/group.js"; import type { Line } from "./api/line.js"; import type { Path } from "./api/path.js"; import type { Polygon } from "./api/polygon.js"; +import type { Polygon3 } from "./api/polygon3.js"; import type { Polyline } from "./api/polyline.js"; +import type { Polyline3 } from "./api/polyline3.js"; import type { Quadratic } from "./api/quadratic.js"; +import type { Quadratic3 } from "./api/quadratic3.js"; import type { Rect } from "./api/rect.js"; import { arc } from "./arc.js"; import { asPolygon } from "./as-polygon.js"; import { cubicFromArc, cubicFromLine, cubicFromQuadratic } from "./cubic.js"; -import { __copyAttribs } from "./internal/copy.js"; +import { cubicFromLine3, cubicFromQuadratic3 } from "./cubic3.js"; +import { __copyAttribsRaw } from "./internal/copy.js"; import { __dispatch } from "./internal/dispatch.js"; +export interface AsCubicOpts extends CubicOpts { + attribs: boolean; +} + +export type AsCubicFn = { + (shape: T, opts?: Partial): Cubic[]; + (shape: T, opts?: Partial): Cubic3[]; +} & MultiFn1O, (Cubic | Cubic3)[]>; + /** - * Converts given shape into an array of {@link Cubic} curves. For some shapes - * (see below) the conversion supports optionally provided {@link CubicOpts}. + * Converts given shape into an array of {@link Cubic} or {@link Cubic3} curves. + * For some shapes (see below) the conversion supports optionally provided + * {@link CubicOpts}. * * @remarks * Currently implemented for: * * - {@link Arc} + * - {@link BPatch} * - {@link Circle} * - {@link ComplexPolygon} * - {@link Cubic} + * - {@link Cubic3} * - {@link Ellipse} * - {@link Group} + * - {@link Group3} * - {@link Line} + * - {@link Line3} * - {@link Path} + * - {@link Path3} * - {@link Polygon} + * - {@link Polygon3} * - {@link Polyline} + * - {@link Polyline3} * - {@link Quad} + * - {@link Quad3} * - {@link Quadratic} + * - {@link Quadratic3} * - {@link Rect} * - {@link Triangle} + * - {@link Triangle3} * * Shape types supporting custom conversion options (see * [@thi.ng/geom-splines](https://github.com/thi-ng/umbrella/tree/develop/packages/geom-splines#cubic-curve-conversion-from-polygons--polylines) * for more details): * * - {@link Group} (only used for eligible children) + * - {@link Group3} (only used for eligible children) * - {@link ComplexPolygon} * - {@link Polygon} + * - {@link Polygon3} * - {@link Polyline} + * - {@link Polyline3} * - {@link Quad} - * - {@link Quadratic} + * - {@link Quad3} * - {@link Rect} * - {@link Triangle} + * - {@link Triangle3} * * @param shape * @param opts */ -export const asCubic: MultiFn1O, Cubic[]> = defmulti< - any, - Maybe>, - Cubic[] ->( - __dispatch, - { - ellipse: "circle", - quad: "poly", - tri: "poly", - }, - { - arc: cubicFromArc, - - circle: ($: Circle) => asCubic(arc($.pos, $.r, 0, 0, TAU, true, true)), - - complexpoly: ($: ComplexPolygon, opts = {}) => [ - ...mapcat((x) => asCubic(x, opts), [$.boundary, ...$.children]), - ], - - cubic: ($: Cubic) => [$], - - group: ($: Group, opts) => [ - ...mapcat((x) => asCubic(x, opts), $.children), - ], - - line: ({ attribs, points }: Line) => [ - cubicFromLine(points[0], points[1], { ...attribs }), - ], - - path: ($: Path) => [ - ...mapcat( - (segment) => (segment.geo ? asCubic(segment.geo) : null), - concat($.segments, flatten1($.subPaths)) - ), - ], - - poly: ($: Polygon, opts = {}) => - __polyCubic( - $, - opts, - closedCubicFromBreakPoints, - closedCubicFromControlPoints - ), - - polyline: ($: Polyline, opts = {}) => - __polyCubic( - $, - opts, - openCubicFromBreakPoints, - openCubicFromControlPoints - ), - - quadratic: ({ attribs, points }: Quadratic) => [ - cubicFromQuadratic(points[0], points[1], points[2], { ...attribs }), - ], - - rect: ($: Rect, opts) => asCubic(asPolygon($)[0], opts), - } +export const asCubic = ( + defmulti>, (Cubic | Cubic3)[]>( + __dispatch, + { + cubic3: "cubic", + ellipse: "circle", + group3: "group", + quad: "poly", + quad3: "poly3", + rect: "$aspoly", + tri: "poly", + }, + { + $aspoly: ($, opts) => asCubic(asPolygon($)[0], opts), + + arc: cubicFromArc, + + bpatch: ({ points, attribs }: BPatch, opts) => + [ + [0, 4], + [12, 1], + [15, -4], + [3, -1], + ].map( + ([i, s]) => + new Cubic( + [ + points[i], + points[i + s], + points[i + 2 * s], + points[i + 3 * s], + ], + __attribs(opts, attribs) + ) + ), + + circle: ($: Circle, opts) => + asCubic( + arc( + $.pos, + $.r, + 0, + 0, + TAU, + true, + true, + __attribs(opts, $.attribs) + ) + ), + + complexpoly: ( + { boundary, children, attribs }: ComplexPolygon, + opts + ) => [ + ...mapcat( + (x) => + asCubic(x, opts).map( + (x) => ((x.attribs = __attribs(opts, attribs)), x) + ), + [boundary, ...children] + ), + ], + + cubic: ($: Cubic, opts) => { + const res = $.copy(); + if (opts?.attribs === false) res.attribs = undefined; + return [res]; + }, + + group: ($: Group, opts) => [ + ...mapcat((x) => asCubic(x, opts), $.children), + ], + + line: ({ points, attribs }: Line, opts) => [ + cubicFromLine(points[0], points[1], __attribs(opts, attribs)), + ], + + line3: ({ points, attribs }: Line, opts) => [ + cubicFromLine3(points[0], points[1], __attribs(opts, attribs)), + ], + + path: ($: Path) => [ + ...mapcat( + (segment) => (segment.geo ? asCubic(segment.geo) : null), + concat($.segments, flatten1($.subPaths)) + ), + ], + + poly: ($: Polygon, opts) => + __polyCubic(Cubic, $, opts, { + default: closedCubicFromControlPoints, + breakpoints: closedCubicFromBreakPoints, + hobby: (pts, scale) => cubicHobby2(pts, true, scale), + }), + + poly3: ($: Polygon3, opts) => + __polyCubic(Cubic3, $, opts, { + default: closedCubicFromControlPoints, + breakpoints: closedCubicFromBreakPoints, + }), + + polyline: ($: Polyline, opts) => + __polyCubic(Cubic, $, opts, { + default: openCubicFromControlPoints, + breakpoints: openCubicFromBreakPoints, + hobby: (pts, scale) => cubicHobby2(pts, false, scale), + }), + + polyline3: ($: Polyline3, opts) => + __polyCubic(Cubic3, $, opts, { + default: openCubicFromControlPoints, + breakpoints: openCubicFromBreakPoints, + }), + + quadratic: ({ points, attribs }: Quadratic, opts) => [ + cubicFromQuadratic( + points[0], + points[1], + points[2], + __attribs(opts, attribs) + ), + ], + + quadratic3: ({ points, attribs }: Quadratic3, opts) => [ + cubicFromQuadratic3( + points[0], + points[1], + points[2], + __attribs(opts, attribs) + ), + ], + } + ) ); +type CubicConstructor = { + new (pts: Vec[], attribs?: Attribs): T; +}; + +type CubicConversions = Record< + CubicOpts["mode"], + (points: ReadonlyVec[], scale?: number, uniform?: boolean) => Vec[][] +>; + /** * @internal */ -// prettier-ignore -const __polyCubic = ( - $: PCLike, - opts: Partial, - breakPoints: (pts: ReadonlyVec[], t?: number, uniform?: boolean) => Vec[][], - controlPoints: (pts: ReadonlyVec[], t?: number, uniform?: boolean) => Vec[][] +const __polyCubic = ( + ctor: CubicConstructor, + { points, attribs }: PCLike, + opts: Maybe>, + conversions: Partial ) => { - opts = { breakPoints: false, scale: 1 / 3, uniform: false, ...opts }; - return (opts.breakPoints - ? breakPoints($.points, opts.scale, opts.uniform) - : controlPoints($.points, opts.scale, opts.uniform) - ).map((pts) => new Cubic(pts, __copyAttribs($))); + opts = { mode: "default", uniform: false, scale: 1 / 3, ...opts }; + const fn = conversions[opts.mode!]; + if (!fn) unsupported(`conversion mode: ${opts.mode}`); + return fn(points, opts.scale, opts.uniform).map( + (pts) => new ctor(pts, __attribs(opts, attribs)) + ); }; + +const __attribs = (opts?: Partial, attribs?: Attribs) => + attribs && opts?.attribs !== false ? __copyAttribsRaw(attribs) : undefined; diff --git a/packages/geom/src/as-path.ts b/packages/geom/src/as-path.ts index 5e5be6e09e..c2677d40c6 100644 --- a/packages/geom/src/as-path.ts +++ b/packages/geom/src/as-path.ts @@ -1,22 +1,39 @@ import type { Maybe } from "@thi.ng/api"; -import type { MultiFn2O } from "@thi.ng/defmulti"; +import type { MultiFn1O } from "@thi.ng/defmulti"; import { DEFAULT, defmulti } from "@thi.ng/defmulti/defmulti"; -import type { Attribs, CubicOpts, IShape, PathSegment } from "@thi.ng/geom-api"; +import type { + CubicOpts, + IShape, + IShape2, + IShape3, + PCLikeConstructor, + PathSegment, + PathSegment2, + PathSegment3, +} from "@thi.ng/geom-api"; import type { ReadonlyVec } from "@thi.ng/vectors"; import { copy } from "@thi.ng/vectors/copy"; import type { APC } from "./api/apc.js"; import type { Arc } from "./api/arc.js"; import type { ComplexPolygon } from "./api/complex-polygon.js"; import { Line } from "./api/line.js"; +import { Line3 } from "./api/line3.js"; import { Path } from "./api/path.js"; +import { Path3 } from "./api/path3.js"; import type { Polygon } from "./api/polygon.js"; -import type { Polyline } from "./api/polyline.js"; +import { Polyline } from "./api/polyline.js"; import { asCubic } from "./as-cubic.js"; import { asPolygon } from "./as-polygon.js"; import { asPolyline } from "./as-polyline.js"; -import { __copyAttribs } from "./internal/copy.js"; +import { __copyAttribs, __copyAttribsRaw } from "./internal/copy.js"; import { __dispatch } from "./internal/dispatch.js"; import { pathFromCubics } from "./path.js"; +import { pathFromCubics3 } from "./path3.js"; + +export type AsPathFn = { + (shape: IShape2, opts?: Partial): Path; + (shape: IShape3, opts?: Partial): Path3; +} & MultiFn1O, Path | Path3>; export interface AsPathOpts extends CubicOpts { /** @@ -37,98 +54,128 @@ export interface AsPathOpts extends CubicOpts { * {@link asPolyline} with default opts. * * @param src - * @param attribs + * @param opts */ -export const asPath: (( - shape: IShape, - opts?: Partial, - attribs?: Attribs -) => Path) & - MultiFn2O>, Attribs, Path> = defmulti< - any, - Maybe>, - Maybe, - Path ->( - __dispatch, - { - line: "polyline", - quad: "poly", - tri: "poly", - }, - { - [DEFAULT]: ( - src: IShape, - opts?: Partial, - attribs?: Attribs - ) => - opts?.linear - ? asPath(asPolygon(src)[0], opts, attribs || __copyAttribs(src)) - : __defaultImpl(src, opts, attribs), +export const asPath = ( + defmulti>, Path | Path3>( + __dispatch, + { + line: "polyline", + quad: "poly", + tri: "poly", + }, + { + [DEFAULT]: (src: IShape2, opts?: Partial) => + opts?.linear + ? asPath(asPolygon(src)[0], opts) + : __defaultImpl(src, opts), - arc: ($: Arc, opts, attribs) => - opts?.linear - ? asPath(asPolyline($)[0], opts, attribs || __copyAttribs($)) - : __defaultImpl($, opts, attribs), + arc: ($: Arc, opts) => + opts?.linear + ? asPath(asPolyline($)[0], opts) + : __defaultImpl($, opts), - complexpoly: ($: ComplexPolygon, opts, attribs) => { - attribs = attribs || __copyAttribs($); - if (opts?.linear) { - return __linearPath( - $.boundary, - $.children.map((c) => __lineSegments(c.points, true)), - attribs, - true - ); - } - const res = pathFromCubics(asCubic($.boundary, opts), attribs); - for (let child of $.children) { - res.addSubPaths(pathFromCubics(asCubic(child, opts)).segments); - } - return res; - }, + complexpoly: ( + { boundary, children, attribs }: ComplexPolygon, + opts + ) => { + attribs = __copyAttribsRaw(attribs); + if (opts?.linear) { + return __linearPath( + boundary, + children.map((c) => + __lineSegments(Line, c.points, true) + ), + true + ); + } + const res = pathFromCubics(asCubic(boundary, opts), attribs); + for (let child of children) { + res.addSubPaths( + pathFromCubics(asCubic(child, opts)).segments + ); + } + return res; + }, - poly: ($: Polygon, opts, attribs) => - opts?.linear - ? __linearPath($, [], attribs, true) - : __defaultImpl($, opts, attribs), + poly: ($: Polygon, opts) => + opts?.linear + ? __linearPath($, [], true) + : __defaultImpl($, opts), - polyline: ($: Polyline, opts, attribs) => - opts?.linear - ? __linearPath($, [], attribs, false) - : __defaultImpl($, opts, attribs), - } + polyline: ($: Polyline, opts) => + opts?.linear + ? __linearPath($, [], false) + : __defaultImpl($, opts), + } + ) ); -/** @internal */ -const __defaultImpl = ( - src: IShape, - opts?: Partial, - attribs?: Attribs -) => pathFromCubics(asCubic(src, opts), attribs || __copyAttribs(src)); +/** + * TODO update to support Path3 + * + * @internal + */ +const __defaultImpl = (src: IShape, opts?: Partial) => + (src.dim === 2 ? pathFromCubics : pathFromCubics3)( + asCubic(src, { attribs: false, ...opts }), + __copyAttribs(src) + ); -/** @internal */ -const __lineSegments = (points: ReadonlyVec[], closed: boolean) => { - if (!points.length) return []; +/** + * TODO update to support Path3 + * + * @internal + */ +function __lineSegments( + ctor: PCLikeConstructor, + points: ReadonlyVec[], + closed: boolean +): PathSegment2[]; +function __lineSegments( + ctor: PCLikeConstructor, + points: ReadonlyVec[], + closed: boolean +): PathSegment3[]; +function __lineSegments( + ctor: PCLikeConstructor, + points: ReadonlyVec[], + closed: boolean +): PathSegment[] { + const n = points.length; + if (!n) return []; const segments: PathSegment[] = [{ type: "m", point: copy(points[0]) }]; - for (let i = 1, n = points.length; i < n; i++) + for (let i = 1; i < n; i++) segments.push({ type: "l", - geo: new Line([copy(points[i - 1]), copy(points[i])]), + geo: new ctor([copy(points[i - 1]), copy(points[i])]), }); - if (closed) segments.push({ type: "z" }); + if (closed) { + segments.push( + { + type: "l", + geo: new ctor([copy(points[n - 1]), copy(points[0])]), + }, + { type: "z" } + ); + } return segments; -}; +} -/** @internal */ -const __linearPath = ( - shape: APC, - subPaths: PathSegment[][], - attribs?: Attribs, - closed = false -) => - new Path( - __lineSegments(shape.points, closed), - subPaths, - attribs || __copyAttribs(shape) - ); +/** + * TODO update to support Path3 + * + * @internal + */ +const __linearPath = (shape: APC, subPaths: PathSegment[][], closed = false) => + shape.dim === 2 + ? new Path( + __lineSegments(Line, shape.points, closed), + subPaths, + __copyAttribs(shape) + ) + : new Path3( + __lineSegments(Line3, shape.points, closed), + subPaths, + __copyAttribs(shape) + );