Skip to content

Commit

Permalink
feat(geom): update Path to support sub-paths (holes), update impls (#464
Browse files Browse the repository at this point in the history
)

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()
  • Loading branch information
postspectacular committed May 2, 2024
1 parent ded007c commit 9329d27
Show file tree
Hide file tree
Showing 12 changed files with 176 additions and 126 deletions.
59 changes: 36 additions & 23 deletions packages/geom/src/api/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathSegment>, public attribs?: Attribs) {
constructor(
segments?: Iterable<PathSegment>,
subPaths?: Iterable<PathSegment[]>,
public attribs?: Attribs
) {
this.segments = segments ? ensureArray(segments) : [];
this.subPaths = subPaths ? ensureArray(subPaths) : [];
this.closed = this.subPaths.length > 0;
}

get type() {
Expand All @@ -28,20 +34,16 @@ 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 = <any>s.geo.copy());
return d;
}),
this.segments.map(__copySegment),
this.subPaths.map((sub) => sub.map(__copySegment)),
__copyAttribs(this)
);
p.closed = this.closed;
return p;
}

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;
}
Expand All @@ -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];
}
}
18 changes: 11 additions & 7 deletions packages/geom/src/bounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -113,15 +114,18 @@ export const bounds: MultiFn1O<IShape, number, Maybe<AABBLike>> = 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
);
Expand Down
11 changes: 10 additions & 1 deletion packages/geom/src/internal/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -36,3 +37,11 @@ export const __copyShape = <T extends PCLike>(
ctor: PCLikeConstructor,
inst: T
) => <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 = <any>s.geo.copy());
return d;
};
18 changes: 9 additions & 9 deletions packages/geom/src/path-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -60,21 +60,21 @@ 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",
});
return this;
}

lineTo(p: Vec, relative = false): PathBuilder {
this.curr.add({
this.curr.addSegments({
geo: new Line([copy(this.currP), this.updateCurrent(p, relative)]),
type: "l",
});
Expand Down Expand Up @@ -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),
Expand All @@ -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",
});
Expand All @@ -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",
});
Expand All @@ -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,
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/geom/src/path-from-svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 15 additions & 7 deletions packages/geom/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,40 @@ import { Path } from "./api/path.js";
import { asCubic } from "./as-cubic.js";
import { PathBuilder } from "./path-builder.js";

export const path = (segments: Iterable<PathSegment>, attribs?: Attribs) =>
new Path(segments, attribs);
export const path = (
segments: Iterable<PathSegment>,
subPaths: Iterable<PathSegment[]> = [],
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 });
}
return path;
};

export const normalizedPath = (path: Path) =>
new Path(
mapcat(
export const normalizedPath = (path: Path) => {
const $normalize = (segments: PathSegment[]) => [
...mapcat(
(s) =>
s.geo
? map<Cubic, PathSegment>(
(c) => ({ type: "c", geo: c }),
asCubic(s.geo)
)
: [{ ...s }],
path.segments
segments
),
];
return new Path(
$normalize(path.segments),
path.subPaths.map($normalize),
path.attribs
);
};

export const roundedRect = (
pos: Vec,
Expand Down
11 changes: 7 additions & 4 deletions packages/geom/src/rotate.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -77,8 +77,8 @@ export const rotate: MultiFn2<IShape, number, IShape> = 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,
Expand All @@ -88,7 +88,10 @@ export const rotate: MultiFn2<IShape, number, IShape> = defmulti<
type: s.type,
point: $rotate([], s.point!, theta),
}
),
);
return new Path(
$rotateSegments($.segments),
$.subPaths.map($rotateSegments),
__copyAttribs($)
);
},
Expand Down
11 changes: 7 additions & 4 deletions packages/geom/src/scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -118,8 +118,8 @@ export const scale: MultiFn2<IShape, number | ReadonlyVec, IShape> = 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,
Expand All @@ -129,7 +129,10 @@ export const scale: MultiFn2<IShape, number | ReadonlyVec, IShape> = defmulti<
type: s.type,
point: mul2([], s.point!, <ReadonlyVec>delta),
}
),
);
return new Path(
$scaleSegments($.segments),
$.subPaths.map($scaleSegments),
__copyAttribs($)
);
},
Expand Down

0 comments on commit 9329d27

Please sign in to comment.