From a44c85d2309bb17797312f1e3a2467b482f307b3 Mon Sep 17 00:00:00 2001 From: Bryan Parker Date: Tue, 30 Apr 2024 09:48:50 -0500 Subject: [PATCH] 1) moves code to create plots into `Plot` class, 2) fixes `TangentLine` --- src/shapes/composed/axes.ts | 106 +---------------- src/shapes/composed/plot.ts | 127 ++++++++++++++++++--- src/shapes/composed/tangent_line.ts | 30 +++-- src/shapes/derived/circle.ts | 20 ++-- src/shapes/primitives/point_shape.ts | 114 +++++++++++++----- test/script_test.ts | 92 ++++++++++++++- test/shapes/primitives/point_shape.test.ts | 18 +++ test/test_utils.ts | 14 +++ 8 files changed, 350 insertions(+), 171 deletions(-) diff --git a/src/shapes/composed/axes.ts b/src/shapes/composed/axes.ts index cb308d6..082c8b8 100644 --- a/src/shapes/composed/axes.ts +++ b/src/shapes/composed/axes.ts @@ -196,104 +196,7 @@ class Axes extends ComposedShape { } plot(fn: (x: number) => number | null): Plot { - const stepSize = (this._xRange[1] - this._xRange[0]) / MIN_NUM_STEPS; - // const origin = this.origin(); - - let lastValidPoint: Point | null = null; - const [yLow, yHigh] = this._yRange; - - const interpolate = (x: number, y: number, lastY: number, targetY: number): Point | null => { - if (lastValidPoint === null) { - return null; - } - - const [lastX, _] = lastValidPoint; - let slope = (y - lastY) / (x - lastX); - - if (slope === 0) return null; - - let b = lastY - (slope * lastX); - let interpolatedX = (targetY - b) / slope; - - if (interpolatedX >= this._xRange[0] && interpolatedX <= this._xRange[1]) { - return [interpolatedX, targetY]; - } else { - return null; - } - }; - - const origFn = (x: number) => { - const y = fn(x); - return [x, y] as [number, number | null]; - }; - - const plot = new Plot(origFn); - let segmentPoints: Point[] = []; - - const finalizeSegment = () => { - if (segmentPoints.length > 0) { - const transformedPoints = segmentPoints.map(pt => { - // const [pX, pY] = math.addVec(pt, origin); - return this.point(...pt); - }); - - plot.addSubplot(new PointShape({ points: transformedPoints, smooth: true, })); - segmentPoints = []; - } - }; - - const addPoint = (x: number) => { - let y = fn(x); - - // TODO: add hole - if (!Number.isFinite(y) || y === null) { - lastValidPoint = null; - finalizeSegment(); - return; - } - - if (y >= yLow && y <= yHigh) { - /* The point is within the Y-range, but the previous point was outside. In order to avoid leaving a gap between the top of the Y-range and the - * start of the plot, we interpolate a point between the last valid point and the current point. */ - if (lastValidPoint === null) { - // Last point was invalid - const lastX = x - stepSize; - const lastY = fn(lastX); - - if (lastY !== null) { - const tY = lastY > yHigh ? yHigh : yLow; - const slope = (y - lastY) / stepSize; - let b = lastY - (slope * lastX); - let interpolatedX = (tY - b) / slope; - if (interpolatedX >= this._xRange[0] && interpolatedX <= this._xRange[1]) { - segmentPoints.push([interpolatedX, tY] as Point); - } - } - } - - const point = [x, y] as Point; - segmentPoints.push(point); - lastValidPoint = point; - } else if (lastValidPoint) { - const tY = y > yHigh ? yHigh : yLow; - const interPoint = interpolate(x, y, lastValidPoint[1], tY); - if (interPoint) { - segmentPoints.push(interPoint); - } - finalizeSegment(); - lastValidPoint = null; - } - }; - - for (let x = this._xRange[0]; x <= this._xRange[1]; x += stepSize) { - addPoint(x); - } - - // If the step size doesn't land directly on the end of the range, add the last point in the range - addPoint(this._xRange[1]); - finalizeSegment(); - - return plot; + return new Plot(fn, this._xRange, this._yRange, this.point.bind(this)); } area(plot: Plot, range?: [number, number]): Shape { @@ -302,10 +205,9 @@ class Axes extends ComposedShape { const points = [this.point(rS, 0)]; const stepSize = (rE - rS) / MIN_NUM_STEPS; for (let x = rS; x <= rE; x += stepSize) { - const y = plot.valueAtX(x); - - if (y !== null) { - points.push(this.point(x, y)); + const pt = plot.pointAtX(x); + if (pt) { + points.push(pt); } } diff --git a/src/shapes/composed/plot.ts b/src/shapes/composed/plot.ts index 4c69916..6c8c8db 100644 --- a/src/shapes/composed/plot.ts +++ b/src/shapes/composed/plot.ts @@ -1,40 +1,133 @@ -import { BezierSegment } from '@/base'; +import { BezierSegment, Point } from '@/base'; import { ComposedShape } from './composed_shape'; import { PointShape } from '@/shapes/primitives/point_shape'; +// The number of discrete points that make up the plot function +const MIN_NUM_STEPS = 200; + + class Plot extends ComposedShape { - private _subplots: PointShape[] = []; - private _fn: (x: number) => [number, number | null]; + // private _subplots: PointShape[] = []; + private _fn: (x: number) => number | null; + private _xRange: [number, number]; + private _yRange: [number, number]; + private _pointTransformer: (x: number, y: number) => Point; - constructor(fn: (x: number) => [number, number | null]) { + constructor(fn: (x: number) => number | null, xRange: [number, number], yRange: [number, number], pointTransformer: (x: number, y: number) => Point) { super(); this._fn = fn; - } - - addSubplot(shape: PointShape): void { - this._subplots.push(shape); + this._xRange = xRange; + this._yRange = yRange; + this._pointTransformer = pointTransformer; } compose(): this { - for (const subplot of this._subplots) { - this.add(subplot); + const stepSize = (this._xRange[1] - this._xRange[0]) / MIN_NUM_STEPS; + const [yLow, yHigh] = this._yRange; + + let lastValidPoint: Point | null = null; + + const interpolate = (x: number, y: number, lastY: number, targetY: number): Point | null => { + if (lastValidPoint === null) { + return null; + } + + const [lastX, _] = lastValidPoint; + let slope = (y - lastY) / (x - lastX); + + if (slope === 0) return null; + + let b = lastY - (slope * lastX); + let interpolatedX = (targetY - b) / slope; + + if (interpolatedX >= this._xRange[0] && interpolatedX <= this._xRange[1]) { + return [interpolatedX, targetY]; + } else { + return null; + } + }; + + let segmentPoints: Point[] = []; + + const finalizeSegment = () => { + if (segmentPoints.length > 0) { + const transformedPoints = segmentPoints.map(pt => this._pointTransformer(pt[0], pt[1])); + this.add(new PointShape({ points: transformedPoints, smooth: true, })); + segmentPoints = []; + } + }; + + const addPoint = (x: number) => { + let y = this._fn(x); + + // TODO: add hole + if (!Number.isFinite(y) || y === null) { + lastValidPoint = null; + finalizeSegment(); + return; + } + + if (y >= yLow && y <= yHigh) { + /* The point is within the Y-range, but the previous point was outside. In order to avoid leaving a gap between the top of the Y-range and the + * start of the plot, we interpolate a point between the last valid point and the current point. */ + if (lastValidPoint === null) { + // Last point was invalid + const lastX = x - stepSize; + const lastY = this._fn(lastX); + + if (lastY !== null) { + const tY = lastY > yHigh ? yHigh : yLow; + const slope = (y - lastY) / stepSize; + let b = lastY - (slope * lastX); + let interpolatedX = (tY - b) / slope; + if (interpolatedX >= this._xRange[0] && interpolatedX <= this._xRange[1]) { + segmentPoints.push([interpolatedX, tY] as Point); + } + } + } + + const point = [x, y] as Point; + segmentPoints.push(point); + lastValidPoint = point; + } else if (lastValidPoint) { + const tY = y > yHigh ? yHigh : yLow; + const interPoint = interpolate(x, y, lastValidPoint[1], tY); + if (interPoint) { + segmentPoints.push(interPoint); + } + finalizeSegment(); + lastValidPoint = null; + } + }; + + for (let x = this._xRange[0]; x <= this._xRange[1]; x += stepSize) { + addPoint(x); } + // If the step size doesn't land directly on the end of the range, add the last point in the range + addPoint(this._xRange[1]); + finalizeSegment(); + return this; } valueAtX(x: number): number | null { - return this._fn(x)[1]; + return this._fn(x); } - *points(): Generator { - for (const subplot of this._subplots) { - for (const point of subplot.points()) { - yield point; - } - } + pointAtX(x: number): Point | null { + const y = this._fn(x); + return y !== null ? this._pointTransformer(x, y) : null; } + + // *points(): Generator { + // for (const subplot of this._subplots) { + // for (const point of subplot.points()) { + // yield point; + // } + // } + // } } diff --git a/src/shapes/composed/tangent_line.ts b/src/shapes/composed/tangent_line.ts index 468e8f8..241b37f 100644 --- a/src/shapes/composed/tangent_line.ts +++ b/src/shapes/composed/tangent_line.ts @@ -24,24 +24,30 @@ class TangentLine extends ComposedShape { } compose(): this { + // Before transforming the points, we need to calculate the slope const y1 = this._plot.valueAtX(this._x); const y2 = this._plot.valueAtX(this._x + this._delta); - if (y1 !== null && y2 !== null) { - const m = (y2 - y1) / this._delta; + if (y1 === null || y2 === null) { + // Find other points? + return this; + } - const from = [ - this._x + (this._length / (2 * Math.sqrt(1 + m ** 2))), - m * (this._length / (2 * Math.sqrt(1 + m ** 2))) + y1 - ] as Point; + const m = (y2 - y1) / this._delta; - const to = [ - this._x - (this._length / (2 * Math.sqrt(1 + m ** 2))), - y1 - m * (this._length / (2 * Math.sqrt(1 + m ** 2))) - ] as Point; + const [tx1, ty1] = this._plot.pointAtX(this._x)!; - this.add(new Line({ from, to, ...this.styles() })); - } + const from = [ + tx1 - (this._length / (2 * Math.sqrt(1 + m ** 2))), + ty1 - m * (this._length / (2 * Math.sqrt(1 + m ** 2))) + ] as Point; + + const to = [ + tx1 + (this._length / (2 * Math.sqrt(1 + m ** 2))), + m * (this._length / (2 * Math.sqrt(1 + m ** 2))) + ty1 + ] as Point; + + this.add(new Line({ from, to, ...this.styles() })); return this; } diff --git a/src/shapes/derived/circle.ts b/src/shapes/derived/circle.ts index 06f8209..77828dc 100644 --- a/src/shapes/derived/circle.ts +++ b/src/shapes/derived/circle.ts @@ -1,6 +1,6 @@ import { ORIGIN, Prettify } from '@/base'; import { Arc } from '../primitives/arc'; -import { Locatable, ShapeStyles } from '@/shapes/shape'; +import { Locatable, ShapeStyles, isLocatable } from '@/shapes/shape'; type CircleArgs = { center?: Locatable; radius?: number; selectable?: boolean; }; @@ -13,13 +13,19 @@ class Circle extends Arc { constructor(radius: number); constructor(args: Prettify); constructor(args?: Locatable | number | Prettify) { - if (args === undefined) { - super(defaultCircleArgs); - } else if (typeof args === 'number') { - super(Object.assign({}, defaultCircleArgs, { radius: args })); - } else { - super(Object.assign({}, defaultCircleArgs, args)); + let circleArgs = structuredClone(defaultCircleArgs); + + if (args !== undefined) { + if (isLocatable(args)) { + circleArgs.center = args; + } else if (typeof args === 'number') { + circleArgs.radius = args; + } else { + circleArgs = { ...circleArgs, ...args }; + } } + + super(circleArgs); } } diff --git a/src/shapes/primitives/point_shape.ts b/src/shapes/primitives/point_shape.ts index 133708f..50fe9fe 100644 --- a/src/shapes/primitives/point_shape.ts +++ b/src/shapes/primitives/point_shape.ts @@ -46,13 +46,38 @@ class PointShape implements Shape, SelectableShape { } center(): Point { - return this.centerWithSampling(); + // return this.centerWithSampling(); + // return this.meanCenter(); + return this.geometricCenter(); } - private centerWithSampling(): Point { - let xSum = 0, ySum = 0; - let sampleCount = 0; - const samplesPerCurve = 100; + private geometricCenter(): Point { + let totalMinX = Infinity, totalMaxX = -Infinity, totalMinY = Infinity, totalMaxY = -Infinity; + + function linerizeBezier(segment: BezierSegment, divisions: number): Point[] { + const [start, cp1, cp2, end] = segment; + const points = []; + + for (let i = 0; i <= divisions; i++) { + const t = i / divisions; + points.push(math.evalBezier(start!, cp1, cp2, end, t)); + } + + return points; + } + + function boundingBox(points: Point[]): { minX: number, maxX: number, minY: number, maxY: number } { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + + for (const [x, y] of points) { + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + + return { minX, maxX, minY, maxY }; + } for (let i = 0; i < this._points.length; i++) { let [start, cp1, cp2, end] = this._points[i]; @@ -67,39 +92,70 @@ class PointShape implements Shape, SelectableShape { start = this._points[i - 1][3]; } - for (let j = 0; j <= samplesPerCurve; j++) { - const t = j / samplesPerCurve; - const [x, y] = math.evalBezier(start, cp1, cp2, end, t); - - xSum += x; - ySum += y; - } + const points = linerizeBezier([start, cp1, cp2, end], 100); + const box = boundingBox(points); - sampleCount += samplesPerCurve + 1; + if (box.minX < totalMinX) totalMinX = box.minX; + if (box.maxX > totalMaxX) totalMaxX = box.maxX; + if (box.minY < totalMinY) totalMinY = box.minY; + if (box.maxY > totalMaxY) totalMaxY = box.maxY; } - return [xSum / sampleCount, ySum / sampleCount]; + return [(totalMinX + totalMaxX) / 2, (totalMinY + totalMaxY) / 2]; } - /*private meanCenter(): Point { - let xMean = 0; - let yMean = 0; + // private centerWithSampling(): Point { + // let xSum = 0, ySum = 0; + // let sampleCount = 0; + // const samplesPerCurve = 100; - for (let i = 0, count = 0; i < this._points.length; i++) { - const pt = this._points[i]; + // for (let i = 0; i < this._points.length; i++) { + // let [start, cp1, cp2, end] = this._points[i]; - for (let j = 0; j < pt.length; j++) { - if (pt[j] !== null) { - xMean += (pt[j]![0] - xMean) / (count + 1); - yMean += (pt[j]![1] - yMean) / (count + 1); + // // If the start point is null, make sure this isn't the first point + // if (start === null) { + // if (i === 0) { + // throw new Error('Invalid bezier curve. Expect the initial point to define a starting point'); + // } - count++; - } - } - } + // // Set the start point of the current curve to the end point of the previous curve + // start = this._points[i - 1][3]; + // } + + // for (let j = 0; j <= samplesPerCurve; j++) { + // const t = j / samplesPerCurve; + // const [x, y] = math.evalBezier(start, cp1, cp2, end, t); + + // xSum += x; + // ySum += y; + // } + + // sampleCount += samplesPerCurve + 1; + // } + + // console.log(xSum / sampleCount, ySum / sampleCount); + // return [xSum / sampleCount, ySum / sampleCount]; + // } + + // private meanCenter(): Point { + // let xMean = 0; + // let yMean = 0; + + // for (let i = 0, count = 0; i < this._points.length; i++) { + // const pt = this._points[i]; + + // for (let j = 0; j < pt.length; j++) { + // if (pt[j] !== null) { + // xMean += (pt[j]![0] - xMean) / (count + 1); + // yMean += (pt[j]![1] - yMean) / (count + 1); - return [xMean, yMean]; - }*/ + // count++; + // } + // } + // } + + // return [xMean, yMean]; + // } top(): Point { const { minX, maxX, maxY } = this.boundingBox(); diff --git a/test/script_test.ts b/test/script_test.ts index 898ede6..7d37db8 100644 --- a/test/script_test.ts +++ b/test/script_test.ts @@ -20,7 +20,7 @@ // import { PointShape, Dot, Circle } from '../src/shapes' // import { GrowFromCenter } from '../src/animation/grow_from_center'; // import math from '../src/math'; -import { Scene, BaseAnimation, GridLines, Square, Text, RIGHT, LEFT, UP, DOWN, UR, DR, DL, UL, Tex, Axes, Triangle, NumberLine, Colors, Dot, Line, Arrow, Circle, Group } from '../src'; +import { Scene, BaseAnimation, GridLines, Square, Text, RIGHT, LEFT, UP, DOWN, UR, DR, DL, UL, Tex, Axes, Triangle, NumberLine, Colors, Dot, Line, Arrow, Circle, Group, TangentLine, PointShape, Point } from '../src'; import math from '../src/math'; @@ -274,10 +274,94 @@ class TestScene extends Scene { // this.add(new Text({ text: 'f(x) = x^2 - 2x + 3', size, tex: true }).moveTo([3, 3])) - const s = new Square(); - const c = new Circle(); + const axes = this.add(new Axes({ + xRange: [0, 2], + yRange: [0, 2], + xLength: 6, + yLength: 6, + xStep: 0.5, + // showTicks: false, + // showLabels: false, + })); + + const fn = x => 3 * Math.pow(x - 1, 3) + 2 * Math.pow(x - 1, 2) + 0.5; + const p = this.add(axes.plot(x => fn(x))); + const pt = axes.point(1.25, fn(1.25)); + const area = this.add(axes.area( p, [0.25, 0.75]).changeColor(Colors.red({ opacity: 0.3 })).changeLineColor(Colors.transparent())); + const tangentLine = this.add(new TangentLine({ plot: p, x: 1.25, lineColor: Colors.blue(), length: 2.5 })); + this.add(new Text({ text: 'Area' }).moveTo(area.center())); + this.add(new Text({ text: 'Tangent line', align: 'left' }).nextTo(pt, RIGHT(), 0.3)); + + this.add( + new Dot(pt), + new Text('P').nextTo(pt, LEFT(), 0.3), + new Text('a').nextTo(area, DL()), + new Text('b').nextTo(area, DR()) + ); + + // const pts = [ + // [-2.25,-3], + // [-2.25,-1.921875], [-2.2424999999999997,-1.906495171875],[-2.2350000000000003,-1.8912926249999993],[-2.2275,-1.8762665156249998],[-2.2199999999999998,-1.861416], + // [-2.2125000000000004,-1.846740234375],[-2.205,-1.8322383749999995],[-2.1975,-1.8179095781250003],[-2.1899999999999995,-1.8037530000000002],[-2.1825,-1.789767796875], + // [-2.1750000000000003,-1.775953125],[-2.1674999999999995,-1.7623081406249996],[-2.16,-1.748832],[-2.1525,-1.735523859375],[-2.1449999999999996,-1.722382875], + // [-2.1374999999999997,-1.7094082031249993],[-2.13,-1.6965989999999993],[-2.1225,-1.6839544218750004],[-2.115,-1.671473625],[-2.1075,-1.6591557656249996], + // [-2.0999999999999996,-1.6469999999999998],[-2.0925000000000002,-1.6350054843750002],[-2.0849999999999995,-1.623171375],[-2.0774999999999997,-1.611496828124999], + // [-2.0700000000000003,-1.5999809999999999],[-2.0625,-1.588623046875],[-2.0549999999999997,-1.5774221249999998],[-2.0474999999999994,-1.566377390625], + // [-2.04,-1.5554879999999998],[-2.0324999999999998,-1.5447531093749998],[-2.0249999999999995,-1.5341718749999995],[-2.0175,-1.5237434531249994],[-2.01,-1.513467], + // [-2.0024999999999995,-1.5033416718750003],[-1.9949999999999999,-1.4933666249999997],[-1.9874999999999998,-1.4835410156249997],[-1.9799999999999995,-1.4738639999999996], + // [-1.9725,-1.4643347343749995],[-1.9649999999999999,-1.4549523749999997],[-1.9574999999999996,-1.4457160781249991],[-1.9499999999999995,-1.4366249999999996], + // [-1.9425,-1.4276782968749997],[-1.935,-1.4188751249999996],[-1.9274999999999995,-1.4102146406249998],[-1.92,-1.4016959999999994],[-1.9124999999999996,-1.3933183593749998], + // [-1.9049999999999996,-1.3850808749999992],[-1.8974999999999995,-1.376982703124999],[-1.8899999999999997,-1.369022999999999],[-1.8824999999999998,-1.3612009218749999], + // [-1.8749999999999998,-1.3535156249999998],[-1.8674999999999997,-1.3459662656249998],[-1.8599999999999994,-1.338552],[-1.8524999999999998,-1.3312719843749996], + // [-1.8449999999999993,-1.3241253749999995],[-1.8374999999999995,-1.3171113281249998],[-1.8299999999999998,-1.3102289999999996],[-1.8224999999999998,-1.3034775468749995], + // [-1.8149999999999995,-1.2968561249999997],[-1.8074999999999994,-1.2903638906249997],[-1.7999999999999998,-1.2839999999999996],[-1.7924999999999995,-1.2777636093749996], + // [-1.7849999999999995,-1.271653875],[-1.7774999999999999,-1.2656699531249997],[-1.7699999999999996,-1.2598109999999998],[-1.7624999999999993,-1.2540761718749998], + // [-1.7549999999999997,-1.2484646249999996],[-1.7474999999999996,-1.2429755156249997],[-1.7399999999999993,-1.2376079999999998],[-1.7324999999999997,-1.2323612343749997], + // [-1.7249999999999996,-1.227234375],[-1.7174999999999994,-1.2222265781249995],[-1.7099999999999993,-1.2173369999999997],[-1.7024999999999997,-1.2125647968750002], + // [-1.6949999999999998,-1.2079091249999996],[-1.6874999999999993,-1.2033691406249998],[-1.6799999999999997,-1.1989439999999998],[-1.6724999999999994,-1.1946328593749997], + // [-1.6649999999999994,-1.1904348749999996],[-1.6574999999999993,-1.1863492031249994],[-1.6499999999999995,-1.1823749999999997],[-1.6424999999999996,-1.1785114218750001], + // [-1.6349999999999996,-1.1747576249999998],[-1.6274999999999995,-1.1711127656249993],[-1.6199999999999992,-1.167576],[-1.6124999999999996,-1.164146484375], + // [-1.604999999999999,-1.1608233749999997],[-1.5974999999999993,-1.157605828125],[-1.5899999999999996,-1.1544929999999995],[-1.5824999999999996,-1.1514840468749998], + // [-1.5749999999999993,-1.1485781249999996],[-1.5674999999999992,-1.145774390625],[-1.5599999999999996,-1.1430719999999999],[-1.5524999999999993,-1.140470109375], + // [-1.5449999999999993,-1.1379678749999995],[-1.5374999999999996,-1.1355644531250002],[-1.5299999999999994,-1.1332589999999994],[-1.522499999999999,-1.1310506718750002], + // [-1.5149999999999995,-1.1289386249999998],[-1.5074999999999994,-1.1269220156249997],[-1.4999999999999991,-1.125],[-1.4924999999999997,-1.1231717343749994], + // [-1.4849999999999994,-1.121436375],[-1.4775,-1.1197930781250003],[-1.4699999999999998,-1.1182409999999996],[-1.4625000000000004,-1.116779296875],[-1.455,-1.1154071250000004], + // [-1.4475000000000007,-1.1141236406250004],[-1.4400000000000004,-1.1129280000000001],[-1.432500000000001,-1.1118193593750003],[-1.4250000000000007,-1.1107968750000001], + // [-1.4175000000000013,-1.1098597031250002],[-1.410000000000001,-1.109007],[-1.4025000000000016,-1.1082379218750003],[-1.3950000000000014,-1.1075516250000002], + // [-1.387500000000002,-1.1069472656250001],[-1.3800000000000017,-1.1064240000000003],[-1.3725000000000023,-1.105980984375],[-1.365000000000002,-1.1056173750000005], + // [-1.3575000000000026,-1.1053323281249998],[-1.3500000000000023,-1.1051249999999997],[-1.342500000000003,-1.104994546875],[-1.3350000000000026,-1.1049401250000004], + // [-1.3275000000000032,-1.1049608906249997],[-1.320000000000003,-1.1050560000000003],[-1.3125000000000036,-1.105224609375],[-1.3050000000000033,-1.105465875], + // [-1.2975000000000039,-1.1057789531250002],[-1.2900000000000036,-1.1061629999999996],[-1.2825000000000042,-1.1066171718749997],[-1.275000000000004,-1.1071406249999995], + // [-1.2675000000000045,-1.107732515625],[-1.2600000000000042,-1.1083919999999996],[-1.2525000000000048,-1.109118234375],[-1.2450000000000045,-1.1099103749999992], + // [-1.2375000000000052,-1.1107675781249995],[-1.2300000000000049,-1.1116889999999993],[-1.2225000000000055,-1.1126737968749991],[-1.2150000000000052,-1.1137211249999994], + // [-1.2075000000000058,-1.1148301406249992],[-1.2000000000000055,-1.1159999999999988],[-1.192500000000006,-1.1172298593749987],[-1.1850000000000058,-1.1185188749999992], + // [-1.1775000000000064,-1.1198662031249988],[-1.1700000000000061,-1.1212709999999992],[-1.1625000000000068,-1.1227324218749988],[-1.1550000000000065,-1.124249624999999], + // [-1.147500000000007,-1.1258217656249985],[-1.1400000000000068,-1.1274479999999985],[-1.1325000000000074,-1.1291274843749988],[-1.125000000000007,-1.1308593749999982], + // [-1.1175000000000077,-1.1326428281249985],[-1.1100000000000074,-1.134476999999998],[-1.102500000000008,-1.1363610468749985],[-1.0950000000000077,-1.138294124999998], + // [-1.0875000000000083,-1.140275390624998],[-1.080000000000008,-1.1423039999999975],[-1.0725000000000087,-1.1443791093749973],[-1.0650000000000084,-1.1464998749999975], + // [-1.057500000000009,-1.1486654531249973],[-1.0500000000000087,-1.1508749999999974],[-1.0425000000000093,-1.1531276718749972],[-1.035000000000009,-1.1554226249999968], + // [-1.0275000000000096,-1.1577590156249968],[-1.0200000000000093,-1.1601359999999965],[-1.01250000000001,-1.1625527343749966],[-1.0050000000000097,-1.165008374999997], + // [-0.9975000000000103,-1.167502078124997],[-0.99000000000001,-1.1700329999999965],[-0.9825000000000106,-1.1726002968749965],[-0.9750000000000103,-1.1752031249999961], + // [-0.9675000000000109,-1.1778406406249964],[-0.9600000000000106,-1.1805119999999965],[-0.9525000000000112,-1.183216359374996],[-0.9450000000000109,-1.1859528749999961], + // [-0.9375000000000115,-1.188720703124996],[-0.9300000000000113,-1.1915189999999958],[-0.9225000000000119,-1.1943469218749956],[-0.9150000000000116,-1.1972036249999958], + // [-0.9075000000000122,-1.2000882656249952],[-0.9000000000000119,-1.2029999999999956],[-0.8925000000000125,-1.2059379843749953],[-0.8850000000000122,-1.2089013749999948], + // [-0.8775000000000128,-1.211889328124995],[-0.8700000000000125,-1.214900999999995],[-0.8625000000000131,-1.2179355468749948],[-0.8550000000000129,-1.2209921249999949], + // [-0.8475000000000135,-1.2240698906249943],[-0.8400000000000132,-1.227167999999995],[-0.8325000000000138,-1.2302856093749948],[-0.8250000000000135,-1.2334218749999941], + // [-0.8175000000000141,-1.2365759531249942],[-0.8100000000000138,-1.2397469999999942],[-0.8025000000000144,-1.2429341718749942],[-0.7950000000000141,-1.2461366249999934], + // [-0.7875000000000147,-1.2493535156249935],[-0.7800000000000145,-1.2525839999999935],[-0.7725000000000151,-1.2558272343749932],[-0.7650000000000148,-1.2590823749999933], + // [-0.7575000000000154,-1.2623485781249935],[-0.7500000000000151,-1.2656249999999931],[-0.75,-3], + // [-2.25,-3]]; + // const p = this.add(new PointShape({ points: pts })); + // this.add(new Dot(p.center())); + + // const c = this.add(new Circle([2, 0])); + // this.add(new Dot(c.center())); + // const points = [[1, 1], [1, -1], [-1, -1], [-1, 1], [0, 2], [1, 1]] as Point[]; + // const shape = new PointShape({ points }); - this.add(new Group(s, c).arrange(RIGHT())); + // this.add(shape); + // this.add(new Dot(shape.center())); + // console.log(shape.center()); } } diff --git a/test/shapes/primitives/point_shape.test.ts b/test/shapes/primitives/point_shape.test.ts index c1f7ea5..82cc57c 100644 --- a/test/shapes/primitives/point_shape.test.ts +++ b/test/shapes/primitives/point_shape.test.ts @@ -1,6 +1,7 @@ import { beforeEach, expect, test } from 'vitest'; import { PointShape } from '../../../src/shapes/primitives/point_shape'; import { Point } from '../../../src/base'; +import { expectArraysToBeClose } from '../../test_utils'; test('should convert points to bezier segments', () => { @@ -35,3 +36,20 @@ test('should convert a mixture of points and bezier segments to bezier segments' [null, [-1, 1], [1, 1], [1, 1]] ]); }); + + +test('should get geometric center of a square', () => { + const points = [[1, 1], [1, -1], [-1, -1], [-1, 1], [1, 1]] as Point[]; + const shape = new PointShape({ points }); + + expect(shape.center()).toMatchObject([0, 0]); +}); + + +test('should get geometric center of a non-regular shape', () => { + // const points = [[1, 1], [1, -1], [-1, -1], [-1, 1], [1, 1]] as Point[]; + const points = [[1, 1], [1, -1], [-1, -1], [-1, 1], [0, 2], [1, 1]] as Point[]; + const shape = new PointShape({ points }); + + expectArraysToBeClose(shape.center(), [0, 0.5]); +}); diff --git a/test/test_utils.ts b/test/test_utils.ts index 709ccbd..2edc99e 100644 --- a/test/test_utils.ts +++ b/test/test_utils.ts @@ -2,6 +2,7 @@ import { Line } from '../src/shapes/primitives/line'; import { Canvas } from '../src/canvas'; import { BezierCurve, Shape, PointShape } from '../src/shapes'; import { Arc } from '../src/shapes/primitives/arc'; +import { expect } from 'vitest'; export function getTestCanvas() { @@ -28,3 +29,16 @@ export function getTestCanvas() { } }; } + + +export function expectArraysToBeClose(arr1: number[] | number[][], arr2: number[] | number[][], precision = 2) { + expect(arr1.length).toEqual(arr2.length); + + arr1.forEach((value: number | number[], index: number) => { + if (Array.isArray(value) && Array.isArray(arr2[index])) { + expectArraysToBeClose(value, arr2[index] as number[], precision); + } else { + expect(value).toBeCloseTo(arr2[index] as number, precision); + } + }); +}