From 9329d271aa2829fa38e7f492ee0b70c672c9999a Mon Sep 17 00:00:00 2001 From: Karsten Schmidt Date: Thu, 2 May 2024 19:58:32 +0200 Subject: [PATCH] feat(geom): update Path to support sub-paths (holes), update impls (#464) BREAKING CHANGE: update path related ctors & functions - add `subPaths` argument for Path ctor/factory fn - rename `Path.add()` => `Path.addSegments()` - add `Path.addSubPaths()` - update `Path.toHiccup()` to include sub-paths - update `pathFromSvg()` to always return a single path only - sub-paths are included in main path now - update impls for following ops to also process sub-paths: - bounds() - rotate() - scale() - simplify() - translate() - transform() - transformVertices() --- packages/geom/src/api/path.ts | 59 +++++++++++++++---------- packages/geom/src/bounds.ts | 18 +++++--- packages/geom/src/internal/copy.ts | 11 ++++- packages/geom/src/path-builder.ts | 18 ++++---- packages/geom/src/path-from-svg.ts | 4 +- packages/geom/src/path.ts | 22 ++++++--- packages/geom/src/rotate.ts | 11 +++-- packages/geom/src/scale.ts | 11 +++-- packages/geom/src/simplify.ts | 58 +++++++++++++----------- packages/geom/src/transform-vertices.ts | 37 ++++++++-------- packages/geom/src/transform.ts | 37 ++++++++-------- packages/geom/src/translate.ts | 16 ++++--- 12 files changed, 176 insertions(+), 126 deletions(-) diff --git a/packages/geom/src/api/path.ts b/packages/geom/src/api/path.ts index b178497fba..bc63643b49 100644 --- a/packages/geom/src/api/path.ts +++ b/packages/geom/src/api/path.ts @@ -3,15 +3,21 @@ import { ensureArray } from "@thi.ng/arrays/ensure-array"; import { equiv } from "@thi.ng/equiv"; import { illegalState } from "@thi.ng/errors/illegal-state"; import type { Attribs, IHiccupShape, PathSegment } from "@thi.ng/geom-api"; -import { copy } from "@thi.ng/vectors/copy"; -import { __copyAttribs } from "../internal/copy.js"; +import { __copyAttribs, __copySegment } from "../internal/copy.js"; export class Path implements IClear, IHiccupShape { segments: PathSegment[]; + subPaths: PathSegment[][]; closed = false; - constructor(segments?: Iterable, public attribs?: Attribs) { + constructor( + segments?: Iterable, + subPaths?: Iterable, + public attribs?: Attribs + ) { this.segments = segments ? ensureArray(segments) : []; + this.subPaths = subPaths ? ensureArray(subPaths) : []; + this.closed = this.subPaths.length > 0; } get type() { @@ -28,12 +34,8 @@ export class Path implements IClear, IHiccupShape { copy(): Path { const p = new Path( - this.segments.map((s) => { - const d: PathSegment = { type: s.type }; - s.point && (d.point = copy(s.point)); - s.geo && (d.geo = s.geo.copy()); - return d; - }), + this.segments.map(__copySegment), + this.subPaths.map((sub) => sub.map(__copySegment)), __copyAttribs(this) ); p.closed = this.closed; @@ -41,7 +43,7 @@ export class Path implements IClear, IHiccupShape { } withAttribs(attribs: Attribs): Path { - const res = new Path(this.segments, attribs); + const res = new Path(this.segments, this.subPaths, attribs); res.closed = this.closed; return res; } @@ -50,28 +52,39 @@ export class Path implements IClear, IHiccupShape { return o instanceof Path && equiv(this.segments, o.segments); } - add(...segments: PathSegment[]) { - if (this.closed) illegalState("path already closed"); + isComplex() { + return this.subPaths.length; + } + + addSegments(...segments: PathSegment[]) { + this.closed && illegalState("path already closed"); this.segments.push(...segments); + return this; + } + + addSubPaths(...paths: PathSegment[][]) { + this.subPaths.push(...paths); + return this; } toHiccup() { - let dest: any[] = []; - const segments = this.segments; - const n = segments.length; - if (n > 1) { - for (let i = 0; i < n; i++) { + const acc: any[] = []; + const $hiccupSegments = (segments: PathSegment[]) => { + for (let i = 0, n = segments.length; i < n; i++) { const s = segments[i]; if (s.geo) { - dest = dest.concat(s.geo!.toHiccupPathSegments()); + acc.push(...s.geo!.toHiccupPathSegments()); } else if (s.point) { - dest.push(["M", s.point]); + acc.push(["M", s.point]); } } - if (this.closed) { - dest.push(["Z"]); - } + }; + + if (this.segments.length > 1) { + $hiccupSegments(this.segments); + this.closed && acc.push(["Z"]); } - return ["path", this.attribs || {}, dest]; + for (let p of this.subPaths) $hiccupSegments(p); + return ["path", this.attribs || {}, acc]; } } diff --git a/packages/geom/src/bounds.ts b/packages/geom/src/bounds.ts index 241c4636e2..72d3707fb4 100644 --- a/packages/geom/src/bounds.ts +++ b/packages/geom/src/bounds.ts @@ -10,6 +10,7 @@ import { comp } from "@thi.ng/transducers/comp"; import { filter } from "@thi.ng/transducers/filter"; import { iterator1 } from "@thi.ng/transducers/iterator"; import { map } from "@thi.ng/transducers/map"; +import { mapcat } from "@thi.ng/transducers/mapcat"; import { addN2 } from "@thi.ng/vectors/addn"; import { max } from "@thi.ng/vectors/max"; import { min } from "@thi.ng/vectors/min"; @@ -113,15 +114,18 @@ export const bounds: MultiFn1O> = defmulti< rectFromMinMaxWithMargin(min([], a, b), max([], a, b), margin), path: (path: Path, margin = 0) => { + const $segmentGeo = (segments: PathSegment[]) => + iterator1( + comp( + map((s: PathSegment) => s.geo!), + filter((s) => !!s) + ), + segments + ); const b = __collBounds( [ - ...iterator1( - comp( - map((s: PathSegment) => s.geo!), - filter((s) => !!s) - ), - path.segments - ), + ...$segmentGeo(path.segments), + ...mapcat($segmentGeo, path.subPaths), ], bounds ); diff --git a/packages/geom/src/internal/copy.ts b/packages/geom/src/internal/copy.ts index db261f0928..1aecd59993 100644 --- a/packages/geom/src/internal/copy.ts +++ b/packages/geom/src/internal/copy.ts @@ -5,8 +5,9 @@ import type { IShape, PCLike, PCLikeConstructor, + PathSegment, } from "@thi.ng/geom-api"; -import { copyVectors } from "@thi.ng/vectors/copy"; +import { copy, copyVectors } from "@thi.ng/vectors/copy"; /** * Creates a shallow copy of shape's attribs. Any `exclude` keys will be removed @@ -36,3 +37,11 @@ export const __copyShape = ( ctor: PCLikeConstructor, inst: T ) => new ctor(copyVectors(inst.points), __copyAttribs(inst)); + +/** @internal */ +export const __copySegment = (s: PathSegment) => { + const d: PathSegment = { type: s.type }; + s.point && (d.point = copy(s.point)); + s.geo && (d.geo = s.geo.copy()); + return d; +}; diff --git a/packages/geom/src/path-builder.ts b/packages/geom/src/path-builder.ts index 6e8d9d8910..edec04c704 100644 --- a/packages/geom/src/path-builder.ts +++ b/packages/geom/src/path-builder.ts @@ -51,7 +51,7 @@ export class PathBuilder { } newPath() { - this.curr = new Path([], this.attribs); + this.curr = new Path([], [], this.attribs); this.paths.push(this.curr); this.currP = zeroes(2); this.bezierP = zeroes(2); @@ -60,13 +60,13 @@ export class PathBuilder { moveTo(p: Vec, relative = false): PathBuilder { if (this.opts.autoSplit !== false && this.curr.segments.length > 0) { - this.curr = new Path([], this.attribs); + this.curr = new Path([], [], this.attribs); this.paths.push(this.curr); } p = this.updateCurrent(p, relative); set2(this.startP, p); set2(this.bezierP, p); - this.curr.add({ + this.curr.addSegments({ point: p, type: "m", }); @@ -74,7 +74,7 @@ export class PathBuilder { } lineTo(p: Vec, relative = false): PathBuilder { - this.curr.add({ + this.curr.addSegments({ geo: new Line([copy(this.currP), this.updateCurrent(p, relative)]), type: "l", }); @@ -132,7 +132,7 @@ export class PathBuilder { return this.lineTo(p, relative); } const prev = copy(this.currP); - this.curr.add({ + this.curr.addSegments({ geo: arcFrom2Points( prev, this.updateCurrent(p, relative), @@ -148,7 +148,7 @@ export class PathBuilder { } closePath() { - this.curr.add({ + this.curr.addSegments({ geo: new Line([copy(this.currP), copy(this.startP)]), type: "l", }); @@ -169,7 +169,7 @@ export class PathBuilder { const prev = copy(this.currP); this.currP[i] = relative ? this.currP[i] + p : p; set2(this.bezierP, this.currP); - this.curr.add({ + this.curr.addSegments({ geo: new Line([prev, copy(this.currP)]), type: "l", }); @@ -178,7 +178,7 @@ export class PathBuilder { protected addCubic(cp1: Vec, cp2: Vec, p: Vec, relative: boolean) { cp2 = this.absPoint(cp2, relative); set2(this.bezierP, cp2); - this.curr.add({ + this.curr.addSegments({ geo: new Cubic([ copy(this.currP), cp1, @@ -191,7 +191,7 @@ export class PathBuilder { protected addQuadratic(cp: Vec, p: Vec, relative: boolean) { set2(this.bezierP, cp); - this.curr.add({ + this.curr.addSegments({ geo: new Quadratic([ copy(this.currP), cp, diff --git a/packages/geom/src/path-from-svg.ts b/packages/geom/src/path-from-svg.ts index 624ca1edfc..0e700f1799 100644 --- a/packages/geom/src/path-from-svg.ts +++ b/packages/geom/src/path-from-svg.ts @@ -75,7 +75,9 @@ export const pathFromSvg = (svg: string) => { ); } } - return b.paths; + return b.paths[0].addSubPaths( + ...b.paths.slice(1).map((p) => p.segments) + ); } catch (e) { throw e instanceof Error ? e diff --git a/packages/geom/src/path.ts b/packages/geom/src/path.ts index 0028351f07..683d4ba7c1 100644 --- a/packages/geom/src/path.ts +++ b/packages/geom/src/path.ts @@ -9,11 +9,14 @@ import { Path } from "./api/path.js"; import { asCubic } from "./as-cubic.js"; import { PathBuilder } from "./path-builder.js"; -export const path = (segments: Iterable, attribs?: Attribs) => - new Path(segments, attribs); +export const path = ( + segments: Iterable, + subPaths: Iterable = [], + attribs?: Attribs +) => new Path(segments, subPaths, attribs); export const pathFromCubics = (cubics: Cubic[], attribs?: Attribs) => { - const path = new Path([], attribs || cubics[0].attribs); + const path = new Path([], [], attribs || cubics[0].attribs); path.segments.push({ type: "m", point: cubics[0].points[0] }); for (let c of cubics) { path.segments.push({ type: "c", geo: c }); @@ -21,9 +24,9 @@ export const pathFromCubics = (cubics: Cubic[], attribs?: Attribs) => { return path; }; -export const normalizedPath = (path: Path) => - new Path( - mapcat( +export const normalizedPath = (path: Path) => { + const $normalize = (segments: PathSegment[]) => [ + ...mapcat( (s) => s.geo ? map( @@ -31,10 +34,15 @@ export const normalizedPath = (path: Path) => asCubic(s.geo) ) : [{ ...s }], - path.segments + segments ), + ]; + return new Path( + $normalize(path.segments), + path.subPaths.map($normalize), path.attribs ); +}; export const roundedRect = ( pos: Vec, diff --git a/packages/geom/src/rotate.ts b/packages/geom/src/rotate.ts index 92e147b2b6..638314eed1 100644 --- a/packages/geom/src/rotate.ts +++ b/packages/geom/src/rotate.ts @@ -1,6 +1,6 @@ import type { MultiFn2 } from "@thi.ng/defmulti"; import { defmulti } from "@thi.ng/defmulti/defmulti"; -import type { IHiccupShape, IShape } from "@thi.ng/geom-api"; +import type { IHiccupShape, IShape, PathSegment } from "@thi.ng/geom-api"; import { rotate as $rotate } from "@thi.ng/vectors/rotate"; import type { Arc } from "./api/arc.js"; import { Circle } from "./api/circle.js"; @@ -77,8 +77,8 @@ export const rotate: MultiFn2 = defmulti< line: tx(Line), path: ($: Path, theta) => { - return new Path( - $.segments.map((s) => + const $rotateSegments = (segments: PathSegment[]) => + segments.map((s) => s.geo ? { type: s.type, @@ -88,7 +88,10 @@ export const rotate: MultiFn2 = defmulti< type: s.type, point: $rotate([], s.point!, theta), } - ), + ); + return new Path( + $rotateSegments($.segments), + $.subPaths.map($rotateSegments), __copyAttribs($) ); }, diff --git a/packages/geom/src/scale.ts b/packages/geom/src/scale.ts index 7c2386820b..075a99461d 100644 --- a/packages/geom/src/scale.ts +++ b/packages/geom/src/scale.ts @@ -2,7 +2,7 @@ import { isNumber } from "@thi.ng/checks/is-number"; import type { MultiFn2 } from "@thi.ng/defmulti"; import { defmulti } from "@thi.ng/defmulti/defmulti"; import { unsupported } from "@thi.ng/errors/unsupported"; -import type { IHiccupShape, IShape } from "@thi.ng/geom-api"; +import type { IHiccupShape, IShape, PathSegment } from "@thi.ng/geom-api"; import type { ReadonlyVec } from "@thi.ng/vectors"; import { mul2, mul3 } from "@thi.ng/vectors/mul"; import { mulN2, mulN3 } from "@thi.ng/vectors/muln"; @@ -118,8 +118,8 @@ export const scale: MultiFn2 = defmulti< path: ($: Path, delta) => { delta = __asVec(delta); - return new Path( - $.segments.map((s) => + const $scaleSegments = (segments: PathSegment[]) => + segments.map((s) => s.geo ? { type: s.type, @@ -129,7 +129,10 @@ export const scale: MultiFn2 = defmulti< type: s.type, point: mul2([], s.point!, delta), } - ), + ); + return new Path( + $scaleSegments($.segments), + $.subPaths.map($scaleSegments), __copyAttribs($) ); }, diff --git a/packages/geom/src/simplify.ts b/packages/geom/src/simplify.ts index 82caab1d80..d583c0b005 100644 --- a/packages/geom/src/simplify.ts +++ b/packages/geom/src/simplify.ts @@ -40,37 +40,43 @@ export const simplify: MultiFn2 = defmulti< {}, { path: ($: Path, eps = 0) => { - const res: PathSegment[] = []; - const orig = $.segments; - const n = orig.length; - let points!: Vec[] | null; - let lastP!: Vec; - for (let i = 0; i < n; i++) { - const s = orig[i]; - if (s.type === "l" || s.type === "p") { - points = points - ? points.concat(vertices(s.geo!)) - : vertices(s.geo!); - lastP = peek(points); - } else if (points) { + const $simplifySegments = (segments: PathSegment[]) => { + const res: PathSegment[] = []; + const n = segments.length; + let points!: Vec[] | null; + let lastP!: Vec; + for (let i = 0; i < n; i++) { + const s = segments[i]; + if (s.type === "l" || s.type === "p") { + points = points + ? points.concat(vertices(s.geo!)) + : vertices(s.geo!); + lastP = peek(points); + } else if (points) { + points.push(lastP); + res.push({ + geo: new Polyline(_simplify(points, eps)), + type: "p", + }); + points = null; + } else { + res.push({ ...s }); + } + } + if (points) { points.push(lastP); res.push({ - geo: new Polyline(_simplify(points, eps)), + geo: new Polyline(points), type: "p", }); - points = null; - } else { - res.push({ ...s }); } - } - if (points) { - points.push(lastP); - res.push({ - geo: new Polyline(points), - type: "p", - }); - } - return new Path(res, __copyAttribs($)); + return res; + }; + return new Path( + $simplifySegments($.segments), + $.subPaths.map($simplifySegments), + __copyAttribs($) + ); }, poly: ($: Polygon, eps = 0) => diff --git a/packages/geom/src/transform-vertices.ts b/packages/geom/src/transform-vertices.ts index 3fcd8b709d..69f23eab2b 100644 --- a/packages/geom/src/transform-vertices.ts +++ b/packages/geom/src/transform-vertices.ts @@ -4,7 +4,6 @@ import { defmulti } from "@thi.ng/defmulti/defmulti"; import type { IHiccupShape, IShape, PathSegment } from "@thi.ng/geom-api"; import type { ReadonlyMat } from "@thi.ng/matrices"; import { mulV } from "@thi.ng/matrices/mulv"; -import { map } from "@thi.ng/transducers/map"; import type { ReadonlyVec } from "@thi.ng/vectors"; import { Cubic } from "./api/cubic.js"; import type { Group } from "./api/group.js"; @@ -84,25 +83,25 @@ export const transformVertices: MultiFn2< line: tx(Line), - path: ($: Path, fn) => - new Path( - [ - ...map( - (s) => - s.type === "m" - ? { - type: s.type, - point: mulV([], fn(s.point!), s.point!), - } - : { - type: s.type, - geo: transformVertices(s.geo!, fn), - }, - $.segments - ), - ], + path: ($: Path, fn) => { + const $transformSegments = (segments: PathSegment[]) => + segments.map((s) => + s.type === "m" + ? { + type: s.type, + point: mulV([], fn(s.point!), s.point!), + } + : { + type: s.type, + geo: transformVertices(s.geo!, fn), + } + ); + return new Path( + $transformSegments($.segments), + $.subPaths.map($transformSegments), __copyAttribs($) - ), + ); + }, points: tx(Points), diff --git a/packages/geom/src/transform.ts b/packages/geom/src/transform.ts index 8f2ea49595..f384238ab0 100644 --- a/packages/geom/src/transform.ts +++ b/packages/geom/src/transform.ts @@ -3,7 +3,6 @@ import { defmulti } from "@thi.ng/defmulti/defmulti"; import type { IHiccupShape, IShape, PathSegment } from "@thi.ng/geom-api"; import type { ReadonlyMat } from "@thi.ng/matrices"; import { mulV } from "@thi.ng/matrices/mulv"; -import { map } from "@thi.ng/transducers/map"; import { Cubic } from "./api/cubic.js"; import type { Group } from "./api/group.js"; import { Line } from "./api/line.js"; @@ -82,25 +81,25 @@ export const transform: MultiFn2 = defmulti< line: tx(Line), - path: ($: Path, mat) => - new Path( - [ - ...map( - (s) => - s.type === "m" - ? { - type: s.type, - point: mulV([], mat, s.point!), - } - : { - type: s.type, - geo: transform(s.geo!, mat), - }, - $.segments - ), - ], + path: ($: Path, mat) => { + const $transformSegments = (segments: PathSegment[]) => + segments.map((s) => + s.type === "m" + ? { + type: s.type, + point: mulV([], mat, s.point!), + } + : { + type: s.type, + geo: transform(s.geo!, mat), + } + ); + return new Path( + $transformSegments($.segments), + $.subPaths.map($transformSegments), __copyAttribs($) - ), + ); + }, points: tx(Points), diff --git a/packages/geom/src/translate.ts b/packages/geom/src/translate.ts index e98e067f3b..6cf8d12cee 100644 --- a/packages/geom/src/translate.ts +++ b/packages/geom/src/translate.ts @@ -1,6 +1,6 @@ import type { MultiFn2 } from "@thi.ng/defmulti"; import { defmulti } from "@thi.ng/defmulti/defmulti"; -import type { IHiccupShape, IShape } from "@thi.ng/geom-api"; +import type { IHiccupShape, IShape, PathSegment } from "@thi.ng/geom-api"; import type { ReadonlyVec } from "@thi.ng/vectors"; import { add2, add3 } from "@thi.ng/vectors/add"; import { set2, set3 } from "@thi.ng/vectors/set"; @@ -93,9 +93,9 @@ export const translate: MultiFn2 = defmulti< line: tx(Line), - path: ($: Path, delta: ReadonlyVec) => - new Path( - $.segments.map((s) => + path: ($: Path, delta: ReadonlyVec) => { + const $translateSegments = (segments: PathSegment[]) => + segments.map((s) => s.geo ? { type: s.type, @@ -105,9 +105,13 @@ export const translate: MultiFn2 = defmulti< type: s.type, point: add2([], s.point!, delta), } - ), + ); + return new Path( + $translateSegments($.segments), + $.subPaths.map($translateSegments), __copyAttribs($) - ), + ); + }, points: tx(Points),