Skip to content

Commit

Permalink
First attempt at exporting ArcPolygon only
Browse files Browse the repository at this point in the history
  • Loading branch information
Gutza committed Jan 2, 2021
1 parent b06ad6b commit b70d1e7
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 85 deletions.
5 changes: 3 additions & 2 deletions src/RegionEngine.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Circle } from "./geometry/Circle";
import { EDrawableEventType, ERegionDebugMode, TCircleRegions } from "./Types";
import { EDrawableEventType, ERegionDebugMode } from "./Types";
import GraphNode from "./topology/GraphNode";
import { RegionEngineBL } from "./RegionEngineBL";
import { DebugEngine } from "./DebugEngine";
import { RegionError } from "./geometry/utils/RegionError";
import { Point } from "./geometry/Point";
import { ArcPolygon } from "./geometry/ArcPolygon";

/**
* The main engine for computing regions resulted from intersecting circles.
Expand Down Expand Up @@ -98,7 +99,7 @@ export class RegionEngine extends RegionEngineBL {
* This method is the cheapest of all, if nothing changed since the last time it was called,
* or the most expensive of all, if everything changed.
*/
public computeRegions(): TCircleRegions {
public computeRegions(): ArcPolygon[] {
if (!this._staleRegions) {
return this._regions;
}
Expand Down
60 changes: 9 additions & 51 deletions src/RegionEngineBL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import GraphEdge from "./topology/GraphEdge";
import GraphNode from "./topology/GraphNode";

import {
TCircleRegions,
IGraphCycle,
TIntersectionType,
ETraversalDirection,
Expand All @@ -28,7 +27,7 @@ export class RegionEngineBL {
protected _nodes: GraphNode[] = [];
protected _edges: Map<string, GraphEdge> = new Map();
protected _circles: Circle[] = [];
protected _regions: TCircleRegions = [];
protected _regions: ArcPolygon[] = [];

/**
* True if the regions need to be recomputed, false if they're still current.
Expand Down Expand Up @@ -63,22 +62,7 @@ export class RegionEngineBL {
this._staleRegions = false;

// TODO: This is just sanity check; it can be removed when the code is mature enough.
let someArcPolygons = false;
for (let i = 0; i < this._regions.length; i++) {
const regionElement = this._regions[i];
if (regionElement instanceof Circle) {
if (regionElement.isOuterContour) {
return;
}
continue;
}

if (regionElement.regionType == ERegionType.outerContour) {
return;
}
someArcPolygons = true;
}
if (!someArcPolygons) {
if (this._regions.some(r => r.regionType == ERegionType.outerContour)) {
return;
}

Expand Down Expand Up @@ -162,10 +146,6 @@ export class RegionEngineBL {

protected removeStaleRegions = (staleCircles: Circle[]): void => {
this._regions = this._regions.filter(region => {
if (region instanceof Circle) {
return true;
}

if (region.arcs.every(arc => !staleCircles.includes(arc.circle))) {
return true;
}
Expand Down Expand Up @@ -359,38 +339,16 @@ export class RegionEngineBL {

// (5/5)
protected refreshRegions = (cycles: IGraphCycle[]): void => {
const deleteIndices: number[] = [];

// Deleting regions because they used to be stand-alone circles and now are
// not anymore stand-alone is statistically rare; we don't want to re-initialize
// the regions array unconditionally every time.
this._regions.forEach((region, index) => {
if (region instanceof ArcPolygon || region.vertices.length === 0) {
return;
}
deleteIndices.push(index);
});
if (deleteIndices.length > 0) {
this._regions = this._regions.filter((region, index) => {
if (!deleteIndices.includes(index)) {
return true;
}
this.emit(EDrawableEventType.delete, region);
return false;
});
}

this._circles.forEach(circle => {
circle.isRegion = circle.vertices.length === 0;
if (circle.isOuterContour) {
this._regions.push(circle);
return;
const isRegion = circle.vertices.length === 0;
if (isRegion && !this._regions.includes(circle.innerContour)) {
this._regions.push(circle.innerContour);
this.emit(EDrawableEventType.add, circle.innerContour);
}
if (circle.vertices.length !== 0 || this._regions.includes(circle)) {
return;
if ((isRegion || circle.isOuterContour) && !this._regions.includes(circle.outerContour)) {
this._regions.push(circle.outerContour);
this.emit(EDrawableEventType.add, circle.outerContour);
}
this._regions.push(circle);
this.emit(EDrawableEventType.add, circle);
});

cycles.forEach(cycle => {
Expand Down
2 changes: 0 additions & 2 deletions src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,6 @@ export interface IGraphCycle {
oEdges: IOrientedEdge[];
}

export type TCircleRegions = (ArcPolygon | Circle)[];

export interface INextTangentEdge {
edge: GraphEdge;
sameSide: boolean;
Expand Down
4 changes: 2 additions & 2 deletions src/geometry/ArcPolygon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { IDrawable, ERegionType } from "../Types";
import { CircleArc } from "./CircleArc";

/**
* Think of a polygon. Now imagine a polygon with edges made of circle arcs
* instead of line segments. That's an @see ArcPolygon.
* Imagine a polygon with edges made of circle arcs
* instead of line segments. This is that.
*/
export class ArcPolygon implements IDrawable {
public shape: object | undefined;
Expand Down
55 changes: 46 additions & 9 deletions src/geometry/Circle.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { EGeometryEventType, IBoundingBox, IDrawable, IRegion } from "../Types";
import { EGeometryEventType, ERegionType, IBoundingBox, IDrawable, IPoint, IRegion } from "../Types";
import CircleVertex from "./CircleVertex";
import GraphNode from "../topology/GraphNode";
import { round } from "./utils/numbers";
import { Point } from "./Point";
import { PureGeometry } from "./PureGeometry";
import { TWO_PI } from "./utils/angles";
import { xor } from "./utils/xor";
import { ArcPolygon } from "./ArcPolygon";
import { CircleArc } from "./CircleArc";

/**
* The main circle class. You can instantiate new circles either by calling
Expand All @@ -17,8 +19,6 @@ export class Circle extends PureGeometry implements IRegion, IDrawable {
private static internalCounter: number = 0;
protected _vertices: CircleVertex[] = [];
private _sortedVertices: boolean = true;
private _roundedBbox?: IBoundingBox;
private _roundedRadius?: number;

/**
* This is a stand-alone circle, and it should be rendered as two
Expand Down Expand Up @@ -62,11 +62,6 @@ export class Circle extends PureGeometry implements IRegion, IDrawable {
*/
private _area?: number;

/**
* Internal cache for the circle's perimeter.
*/
private _perimeter?: number;

private _bbox?: IBoundingBox;

/**
Expand Down Expand Up @@ -184,19 +179,23 @@ export class Circle extends PureGeometry implements IRegion, IDrawable {
}

/**
* Change this circle's radius.
* Resize this circle.
*/
set radius(radius: number) {
this._area = undefined;
this._radius = radius;
this._roundedRadius = undefined;
this._perimeter = undefined;
this._resetCommonGeometryCaches();
this.emit(EGeometryEventType.resize);
}

private _resetCommonGeometryCaches = () => {
this._bbox = undefined;
this._roundedBbox = undefined;
this._zeroPoint = undefined;
this._innerContour = undefined;
this._outerContour = undefined;
this._vertices = [];
this.setStale();
}
Expand Down Expand Up @@ -229,6 +228,7 @@ export class Circle extends PureGeometry implements IRegion, IDrawable {
};
}

private _roundedBbox?: IBoundingBox;
public get roundedBoundingBox(): IBoundingBox {
if (this._roundedBbox !== undefined) {
return this._roundedBbox;
Expand Down Expand Up @@ -292,6 +292,7 @@ export class Circle extends PureGeometry implements IRegion, IDrawable {
this.roundedRadius == that.roundedRadius
);

private _roundedRadius?: number;
public get roundedRadius(): number {
if (this._roundedRadius !== undefined) {
return this._roundedRadius;
Expand All @@ -304,6 +305,7 @@ export class Circle extends PureGeometry implements IRegion, IDrawable {
return this._internalId;
}

private _perimeter?: number;
public get perimeter(): number {
if (this._perimeter !== undefined) {
return this._perimeter;
Expand All @@ -312,4 +314,39 @@ export class Circle extends PureGeometry implements IRegion, IDrawable {
this._perimeter = TWO_PI * this.radius;
return this._perimeter;
}

private _zeroPoint?: IPoint;
public get zeroPoint(): IPoint {
if (this._zeroPoint !== undefined) {
return this._zeroPoint;
}
return this._zeroPoint = {
x: this.boundingBox.maxPoint.x,
y: this.center.y,
};
}

private _innerContour?: ArcPolygon;
public get innerContour(): ArcPolygon {
if (this._innerContour !== undefined) {
return this._innerContour;
}

return this._innerContour = new ArcPolygon(
[new CircleArc(this, 0, TWO_PI, this.zeroPoint, this.zeroPoint, false)],
ERegionType.region
);
}

private _outerContour?: ArcPolygon;
public get outerContour(): ArcPolygon {
if (this._outerContour !== undefined) {
return this._outerContour;
}

return this._outerContour = new ArcPolygon(
[new CircleArc(this, 0, TWO_PI, this.zeroPoint, this.zeroPoint, true)],
ERegionType.outerContour
);
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export { Circle } from "./geometry/Circle";
export { CircleArc} from "./geometry/CircleArc";
export { RegionEngine } from "./RegionEngine";
export { Point } from "./geometry/Point";
export { EDrawableEventType, ERegionType, IPoint } from "./Types";
export * as Types from "./Types";
export { ArcPolygon } from "./geometry/ArcPolygon";
export * as BezierHelper from "./geometry/helpers/BezierHelper";
export * as PolygonHelper from "./geometry/helpers/PolygonHelper";
29 changes: 11 additions & 18 deletions test/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,19 @@ describe("Removing circles", () => {

assert.strictEqual(engine.isStale, true, "Regions should be stale after adding circles");
assert.strictEqual(4, engine.computeRegions().length, "There should be 4 regions in total");
assert.strictEqual(0, engine.computeRegions().filter(region => region instanceof Circle).length, "There should be no circles");
assert.strictEqual(1, engine.computeRegions().filter(region => (region as ArcPolygon).regionType === ERegionType.outerContour).length, "There should be a single outer contour");
assert.strictEqual(1, engine.computeRegions().filter(region => region.regionType === ERegionType.outerContour).length, "There should be a single outer contour");

let midCircle = engine.add(0, 0, 1, "middle");

assert.strictEqual(engine.isStale, true, "Regions should be stale after adding circles");
assert.strictEqual(6, engine.computeRegions().length, "There should be 6 regions in total");
assert.strictEqual(0, engine.computeRegions().filter(region => region instanceof Circle).length, "There should be no circles");
assert.strictEqual(1, engine.computeRegions().filter(region => (region as ArcPolygon).regionType === ERegionType.outerContour).length, "There should be a single outer contour");
assert.strictEqual(1, engine.computeRegions().filter(region => region.regionType === ERegionType.outerContour).length, "There should be a single outer contour");

engine.removeCircle(midCircle);

assert.strictEqual(engine.isStale, true, "Regions should be stale after removing circles");
assert.strictEqual(4, engine.computeRegions().length, "There should be 4 regions in total");
assert.strictEqual(0, engine.computeRegions().filter(region => region instanceof Circle).length, "There should be no circles");
assert.strictEqual(1, engine.computeRegions().filter(region => (region as ArcPolygon).regionType === ERegionType.outerContour).length, "There should be a single outer contour");
assert.strictEqual(1, engine.computeRegions().filter(region => region.regionType === ERegionType.outerContour).length, "There should be a single outer contour");
});
});

Expand All @@ -42,8 +39,7 @@ describe("Four overlapping axis-centered circles", () => {
it("Basic region count and discrimination", () => {
assert.strictEqual(engine.isStale, true, "Regions should be stale after adding circles");
assert.strictEqual(14, engine.computeRegions().length, "There should be 14 regions in total");
assert.strictEqual(0, engine.computeRegions().filter(region => region instanceof Circle).length, "There should be no circles");
assert.strictEqual(1, engine.computeRegions().filter(region => (region as ArcPolygon).regionType === ERegionType.outerContour).length, "There should be a single outer contour");
assert.strictEqual(1, engine.computeRegions().filter(region => region.regionType === ERegionType.outerContour).length, "There should be a single outer contour");
});
});

Expand All @@ -55,9 +51,8 @@ describe("Canonical interior contour", () => {
it("Basic inner/outer contour discrimination", () => {
assert.strictEqual(engine.isStale, true, "Regions should be stale after adding circles");
assert.strictEqual(8, engine.computeRegions().length, "There should be 8 regions in total");
assert.strictEqual(0, engine.computeRegions().filter(region => region instanceof Circle).length, "There should be no circles");
assert.strictEqual(1, engine.computeRegions().filter(region => (region as ArcPolygon).regionType === ERegionType.outerContour).length, "There should be a single outer contour");
assert.strictEqual(1, engine.computeRegions().filter(region => (region as ArcPolygon).regionType === ERegionType.innerContour).length, "There should be a single inner contour");
assert.strictEqual(1, engine.computeRegions().filter(region => region.regionType === ERegionType.outerContour).length, "There should be a single outer contour");
assert.strictEqual(1, engine.computeRegions().filter(region => region.regionType === ERegionType.innerContour).length, "There should be a single inner contour");
});
});

Expand Down Expand Up @@ -92,9 +87,8 @@ describe("Crazy interior contour", () => {
it("Static inner/outer contour discrimination", () => {
assert.strictEqual(engine.isStale, true, "Regions should be stale after adding circles");
assert.strictEqual(54, engine.computeRegions().length, "There should be 54 regions in total"); // Yes, I actually counted them
assert.strictEqual(0, engine.computeRegions().filter(region => region instanceof Circle).length, "There should be no circles");
assert.strictEqual(1, engine.computeRegions().filter(region => (region as ArcPolygon).regionType === ERegionType.outerContour).length, "There should be a single outer contour");
assert.strictEqual(1, engine.computeRegions().filter(region => (region as ArcPolygon).regionType === ERegionType.innerContour).length, "There should be a single inner contour");
assert.strictEqual(1, engine.computeRegions().filter(region => region.regionType === ERegionType.outerContour).length, "There should be a single outer contour");
assert.strictEqual(1, engine.computeRegions().filter(region => region.regionType === ERegionType.innerContour).length, "There should be a single inner contour");
});
})

Expand Down Expand Up @@ -135,10 +129,9 @@ describe("Two-circle intersections must never produce inner regions", () => {
const dynamicCircle = new Circle(new Point(Math.cos(angle) * centerRad, Math.sin(angle) * centerRad), 1);
engine.addCircle(dynamicCircle);
const regions = engine.computeRegions();
assert.strictEqual(regions.filter(r => r instanceof ArcPolygon && r.regionType === ERegionType.outerContour).length, 1, `There should be exactly one outer contours at ${degAngle}°`);
assert.strictEqual(regions.filter(r => r instanceof ArcPolygon && r.regionType === ERegionType.region).length, 3, `There should be exactly three regions at ${degAngle}°`);
assert.strictEqual(regions.filter(r => r instanceof Circle).length, 0, `There should be no stand-alone circles at ${degAngle}°`);
assert.strictEqual(regions.filter(r => r instanceof ArcPolygon && r.regionType === ERegionType.innerContour).length, 0, `There should be no inner contours at ${degAngle}°`);
assert.strictEqual(regions.filter(r => r.regionType === ERegionType.outerContour).length, 1, `There should be exactly one outer contours at ${degAngle}°`);
assert.strictEqual(regions.filter(r => r.regionType === ERegionType.region).length, 3, `There should be exactly three regions at ${degAngle}°`);
assert.strictEqual(regions.filter(r => r.regionType === ERegionType.innerContour).length, 0, `There should be no inner contours at ${degAngle}°`);
}
});
});

0 comments on commit b70d1e7

Please sign in to comment.