Skip to content

Commit

Permalink
feat(geom): add new transform ops & helpers
Browse files Browse the repository at this point in the history
- add applyTransforms(), rotate(), scale()
- add internal helpers
- update transform() rect coercion (now => Quad, previous Polygon)
  • Loading branch information
postspectacular committed Jun 24, 2022
1 parent ccb40f1 commit cd8217c
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 2 deletions.
9 changes: 9 additions & 0 deletions packages/geom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@
"./api/triangle": {
"default": "./api/triangle.js"
},
"./apply-transforms": {
"default": "./apply-transforms.js"
},
"./arc-length": {
"default": "./arc-length.js"
},
Expand Down Expand Up @@ -310,6 +313,12 @@
"./resample": {
"default": "./resample.js"
},
"./rotate": {
"default": "./rotate.js"
},
"./scale": {
"default": "./scale.js"
},
"./scatter": {
"default": "./scatter.js"
},
Expand Down
68 changes: 68 additions & 0 deletions packages/geom/src/apply-transforms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { withoutKeysObj } from "@thi.ng/associative/without-keys";
import type { MultiFn1 } from "@thi.ng/defmulti";
import { DEFAULT, defmulti } from "@thi.ng/defmulti/defmulti";
import type { IHiccupShape, IShape } from "@thi.ng/geom-api";
import type { Group } from "./api/group.js";
import { __dispatch } from "./internal/dispatch.js";
import { rotate } from "./rotate.js";
import { scale } from "./scale.js";
import { transform } from "./transform.js";
import { translate } from "./translate.js";

/** @internal */
const __apply = ($: IShape) => {
let attribs = $.attribs;
if (!attribs) return $;
const { transform: tx, translate: t, rotate: r, scale: s } = attribs;
if (tx)
return transform(
$.withAttribs(withoutKeysObj(attribs, ["transform"])),
tx
);
if (!(t || r || s)) return $;
$ = $.withAttribs(
withoutKeysObj(attribs, ["translate", "rotate", "scale"])
);
if (r) $ = rotate($, r);
if (s) $ = scale($, s);
if (t) $ = translate($, t);
return $;
};

/**
* Applies any spatial transformation attributes defined (if any) for the given
* shape. If no such attributes exist, the original shape is returned as is.
*
* @remarks
* The following attributes are considered:
*
* - transform: A 2x3 (for 2D) or 4x4 (for 3D) transformation matrix
* - translate: Translation/offset vector
* - scale: A scale factor (scalar or vector)
* - rotate: Rotation angle (in radians)
*
* If the `transform` attrib is given, the others will be ignored. If any of the
* other 3 attribs is provided, the order of application is: rotate, scale,
* translate. Any of these relevant attributes will be removed from the
* transformed shapes to ensure idempotent behavior.
*
* For (@link group} shapes, the children are processed in depth-first order
* with any transformations to the group itself applied last.
*
* Note: Where possible, this function delegates to {@link rotate},
* {@link scale}, {@link translate} to realize individual/partial transformation
* aspects to increase the likelihodd of retaining original shape types. E.g.
* uniformly scaling a circle with a scalar factor retains a circle, but scaling
* non-uniformly will convert it to an ellipse... Similarly, rotating a rect
* will convert it to a quad etc.
*/
export const applyTransforms: MultiFn1<IShape, IShape> = defmulti<any, IShape>(
__dispatch,
{},
{
[DEFAULT]: __apply,

group: ($: Group) =>
__apply($.copyTransformed((x) => <IHiccupShape>applyTransforms(x))),
}
);
3 changes: 3 additions & 0 deletions packages/geom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export * from "./rect.js";
export * from "./text.js";
export * from "./triangle.js";

export * from "./apply-transforms.js";
export * from "./arc-length.js";
export * from "./area.js";
export * from "./as-cubic.js";
Expand All @@ -66,6 +67,8 @@ export * from "./offset.js";
export * from "./point-at.js";
export * from "./point-inside.js";
export * from "./resample.js";
export * from "./rotate.js";
export * from "./scale.js";
export * from "./scatter.js";
export * from "./simplify.js";
export * from "./split-at.js";
Expand Down
11 changes: 11 additions & 0 deletions packages/geom/src/internal/rotate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { PCLike, PCLikeConstructor } from "@thi.ng/geom-api";
import type { ReadonlyVec } from "@thi.ng/vectors";
import { rotate } from "@thi.ng/vectors/rotate";
import { __copyAttribs } from "./copy.js";

export const __rotatedPoints = (pts: ReadonlyVec[], delta: number) =>
pts.map((x) => rotate([], x, delta));

export const __rotatedShape =
(ctor: PCLikeConstructor) => ($: PCLike, delta: number) =>
new ctor(__rotatedPoints($.points, delta), __copyAttribs($));
18 changes: 18 additions & 0 deletions packages/geom/src/internal/scale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { isNumber } from "@thi.ng/checks/is-number";
import type { PCLike, PCLikeConstructor } from "@thi.ng/geom-api";
import type { ReadonlyVec } from "@thi.ng/vectors";
import { mul } from "@thi.ng/vectors/mul";
import { mulN } from "@thi.ng/vectors/muln";
import { __copyAttribs } from "./copy.js";

export const __scaledPoints = (
pts: ReadonlyVec[],
delta: number | ReadonlyVec
) =>
pts.map(
isNumber(delta) ? (x) => mulN([], x, delta) : (x) => mul([], x, delta)
);

export const __scaledShape =
(ctor: PCLikeConstructor) => ($: PCLike, delta: number | ReadonlyVec) =>
new ctor(__scaledPoints($.points, delta), __copyAttribs($));
95 changes: 95 additions & 0 deletions packages/geom/src/rotate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { MultiFn2 } from "@thi.ng/defmulti";
import { defmulti } from "@thi.ng/defmulti/defmulti";
import type { IHiccupShape, IShape } 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";
import { Cubic } from "./api/cubic.js";
import type { Ellipse } from "./api/ellipse.js";
import type { Group } from "./api/group.js";
import { Line } from "./api/line.js";
import { Path } from "./api/path.js";
import { Points } from "./api/points.js";
import { Polygon } from "./api/polygon.js";
import { Polyline } from "./api/polyline.js";
import { Quad } from "./api/quad.js";
import { Quadratic } from "./api/quadratic.js";
import { Ray } from "./api/ray.js";
import type { Rect } from "./api/rect.js";
import { Text } from "./api/text.js";
import { Triangle } from "./api/triangle.js";
import { asPath } from "./as-path.js";
import { asPolygon } from "./as-polygon.js";
import { __copyAttribs } from "./internal/copy.js";
import { __dispatch } from "./internal/dispatch.js";
import { __rotatedShape as tx } from "./internal/rotate.js";

export const rotate: MultiFn2<IShape, number, IShape> = defmulti<
any,
number,
IShape
>(
__dispatch,
{},
{
arc: ($: Arc, theta) => {
const a = $.copy();
$rotate(null, a.pos, theta);
return a;
},

circle: ($: Circle, theta) =>
new Circle($rotate([], $.pos, theta), $.r, __copyAttribs($)),

cubic: tx(Cubic),

ellipse: ($: Ellipse, theta) => rotate(asPath($), theta),

group: ($: Group, theta) =>
$.copyTransformed((x) => <IHiccupShape>rotate(x, theta)),

line: tx(Line),

path: ($: Path, theta) => {
return new Path(
$.segments.map((s) =>
s.geo
? {
type: s.type,
geo: <any>rotate(s.geo, theta),
}
: {
type: s.type,
point: $rotate([], s.point!, theta),
}
),
__copyAttribs($)
);
},

points: tx(Points),

poly: tx(Polygon),

polyline: tx(Polyline),

quad: tx(Quad),

quadratic: tx(Quadratic),

ray: ($: Ray, theta) => {
return new Ray(
$rotate([], $.pos, theta),
$rotate([], $.dir, theta),
__copyAttribs($)
);
},

rect: ($: Rect, theta) => rotate(asPolygon($), theta),

text: ($: Text, theta) =>
new Text($rotate([], $.pos, theta), $.body, __copyAttribs($)),

tri: tx(Triangle),
}
);
149 changes: 149 additions & 0 deletions packages/geom/src/scale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
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 { ReadonlyVec } from "@thi.ng/vectors";
import { mul2, mul3 } from "@thi.ng/vectors/mul";
import { mulN2, mulN3 } from "@thi.ng/vectors/muln";
import { normalize } from "@thi.ng/vectors/normalize";
import { AABB } from "./api/aabb.js";
import type { Arc } from "./api/arc.js";
import { Circle } from "./api/circle.js";
import { Cubic } from "./api/cubic.js";
import { Ellipse } from "./api/ellipse.js";
import type { Group } from "./api/group.js";
import { Line } from "./api/line.js";
import { Path } from "./api/path.js";
import { Points, Points3 } from "./api/points.js";
import { Polygon } from "./api/polygon.js";
import { Polyline } from "./api/polyline.js";
import { Quad } from "./api/quad.js";
import { Quadratic } from "./api/quadratic.js";
import { Ray } from "./api/ray.js";
import { Rect } from "./api/rect.js";
import { Sphere } from "./api/sphere.js";
import { Text } from "./api/text.js";
import { Triangle } from "./api/triangle.js";
import { __asVec } from "./internal/args.js";
import { __copyAttribs } from "./internal/copy.js";
import { __dispatch } from "./internal/dispatch.js";
import { __scaledShape as tx } from "./internal/scale.js";

export const scale: MultiFn2<IShape, number | ReadonlyVec, IShape> = defmulti<
any,
number | ReadonlyVec,
IShape
>(
__dispatch,
{},
{
aabb: ($: AABB, delta) => {
delta = __asVec(delta, 3);
return new AABB(
mul3([], $.pos, delta),
mul3([], $.size, delta),
__copyAttribs($)
);
},

arc: ($: Arc, delta) => {
delta = __asVec(delta);
const a = $.copy();
mul2(null, a.pos, delta);
mul2(null, a.r, delta);
return a;
},

circle: ($: Circle, delta) =>
isNumber(delta)
? new Circle(
mulN2([], $.pos, delta),
$.r * delta,
__copyAttribs($)
)
: new Ellipse(
mul2([], $.pos, delta),
mulN2([], delta, $.r),
__copyAttribs($)
),

cubic: tx(Cubic),

ellipse: ($: Ellipse, delta) => {
delta = __asVec(delta);
return new Ellipse(
mul2([], $.pos, delta),
mul2([], $.r, delta),
__copyAttribs($)
);
},

group: ($: Group, delta) =>
$.copyTransformed((x) => <IHiccupShape>scale(x, delta)),

line: tx(Line),

path: ($: Path, delta) => {
delta = __asVec(delta);
return new Path(
$.segments.map((s) =>
s.geo
? {
type: s.type,
geo: <any>scale(s.geo, delta),
}
: {
type: s.type,
point: mul2([], s.point!, <ReadonlyVec>delta),
}
),
__copyAttribs($)
);
},

points: tx(Points),

points3: tx(Points3),

poly: tx(Polygon),

polyline: tx(Polyline),

quad: tx(Quad),

quadratic: tx(Quadratic),

ray: ($: Ray, delta) => {
delta = __asVec(delta);
return new Ray(
mul2([], $.pos, delta),
normalize(null, mul2([], $.dir, delta)),
__copyAttribs($)
);
},

rect: ($: Rect, delta) => {
delta = __asVec(delta);
return new Rect(
mul2([], $.pos, delta),
mul2([], $.size, delta),
__copyAttribs($)
);
},

sphere: ($: Sphere, delta) =>
isNumber(delta)
? new Sphere(
mulN3([], $.pos, delta),
$.r * delta,
__copyAttribs($)
)
: unsupported("can't non-uniformly scale sphere"),

text: ($: Text, delta) =>
new Text(mul2([], $.pos, __asVec(delta)), $.body, __copyAttribs($)),

tri: tx(Triangle),
}
);
Loading

0 comments on commit cd8217c

Please sign in to comment.