Skip to content

Commit

Permalink
fix bug in drawing line to infinity in case of shadows
Browse files Browse the repository at this point in the history
  • Loading branch information
emilefokkema committed Apr 15, 2023
1 parent 013bbbf commit 97f26ad
Show file tree
Hide file tree
Showing 48 changed files with 483 additions and 248 deletions.
1 change: 1 addition & 0 deletions src/areas/area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export interface Area{
expandToIncludePoint(point: Point): Area;
expandToIncludeInfinityInDirection(direction: Point): Area;
transform(transformation: Transformation): Area;
getVertices(): Point[]
}
3 changes: 3 additions & 0 deletions src/areas/empty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { Ray } from "./line/ray";
import { Line } from "./line/line";

class Empty implements Area{
public getVertices(): Point[]{
return []
}
public intersectWith(area: Area): Area {
return this;
}
Expand Down
3 changes: 3 additions & 0 deletions src/areas/line/line-segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export class LineSegment extends SubsetOfLine implements Area{
constructor(public point1: Point, public point2: Point){
super(point1, point2.minus(point1));
}
public getVertices(): Point[]{
return [this.point1, this.point2]
}
public join(area: Area): Area{
return area.expandToIncludePoint(this.point1).expandToIncludePoint(this.point2)
}
Expand Down
3 changes: 3 additions & 0 deletions src/areas/line/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { HalfPlaneLineIntersection } from "../polygons/half-plane-line-intersect
import { HalfPlane } from "../polygons/half-plane";

export class Line extends SubsetOfLine implements Area{
public getVertices(): Point[]{
return []
}
public intersectWith(area: Area): Area {
return area.intersectWithLine(this);
}
Expand Down
3 changes: 3 additions & 0 deletions src/areas/line/ray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { Line } from "./line";
import { HalfPlane } from '../polygons/half-plane'

export class Ray extends SubsetOfLine implements Area{
public getVertices(): Point[]{
return [this.base]
}
public intersectWith(area: Area): Area {
return area.intersectWithRay(this);
}
Expand Down
3 changes: 3 additions & 0 deletions src/areas/plane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { Ray } from "./line/ray";
import { Line } from "./line/line";

class Plane implements Area{
public getVertices(): Point[]{
return []
}
public expandToIncludePoint(point: Point): Area {
return this;
}
Expand Down
69 changes: 31 additions & 38 deletions src/areas/polygons/convex-polygon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,54 +99,47 @@ export class ConvexPolygon implements Area{
}
return true;
}

public getPointInFrontInDirection(point: Point, direction: Point): Point{
let along: number = 0;
let frontMostVertex: Point;
for(let vertex of this.vertices){
const newAlong: number = vertex.point.minus(point).dot(direction);
if(newAlong > along){
frontMostVertex = vertex.point;
along = newAlong;
}
}
if(frontMostVertex){
return point.plus(frontMostVertex.minus(point).projectOn(direction));
}
return point;
public getVertices(): Point[]{
return this.vertices.map(v => v.point);
}
public expandToIncludePoint(point: Point): ConvexPolygon{
if(this.containsPoint(point)){
return this;
}
let newHalfPlanes: HalfPlane[] = [];
const newVertices: PolygonVertex[] = [];
for(let halfPlane of this.halfPlanes){
if(this.hasAtMostOneVertex(halfPlane) && !halfPlane.containsPoint(point)){
newHalfPlanes.push(halfPlane.expandToIncludePoint(point));
const newHalfPlanesSet: Set<HalfPlane> = new Set<HalfPlane>(this.halfPlanes);
for(const halfPlane of this.halfPlanes){
if(!this.hasAtMostOneVertex(halfPlane) || halfPlane.containsPoint(point)){
continue;
}
const replacement = halfPlane.expandToIncludePoint(point);
newHalfPlanesSet.delete(halfPlane)
newHalfPlanesSet.add(replacement)
}
if(this.vertices.length === 0){
return new ConvexPolygon(newHalfPlanes.concat(this.halfPlanes.filter(hp => hp.containsPoint(point))));
}
for(let vertex of this.vertices){
if(vertex.containsPoint(point)){
newVertices.push(vertex);
for(const vertex of this.vertices){
const firstContainsPoint = vertex.halfPlane1.containsPoint(point);
const secondContainsPoint = vertex.halfPlane2.containsPoint(point);
if(firstContainsPoint && secondContainsPoint){
continue;
}
else if(vertex.isExpandableToContainPoint(point)){
const expansion: {newHalfPlane: HalfPlane, newVertex: PolygonVertex} = vertex.expandToContainPoint(point);
newHalfPlanes.push(expansion.newHalfPlane);
newVertices.push(expansion.newVertex);
}else if(vertex.isInSameHalfPlaneAs(point)){
newHalfPlanes.push(vertex.getHalfPlaneContaining(point));
if(!firstContainsPoint){
newHalfPlanesSet.delete(vertex.halfPlane1)
}
if(!secondContainsPoint){
newHalfPlanesSet.delete(vertex.halfPlane2)
}
if(!firstContainsPoint && !secondContainsPoint){
continue;
}
let newNormal = point.minus(vertex.point).getPerpendicular();
if(!vertex.isContainedByHalfPlaneWithNormal(newNormal)){
newNormal = newNormal.scale(-1);
}
const replacement = new HalfPlane(point, newNormal);
newHalfPlanesSet.add(replacement)
}
if(newHalfPlanes.length !== 2){
throw new Error("expected two new half planes");
}
newVertices.push(new PolygonVertex(point, newHalfPlanes[0], newHalfPlanes[1]));
newHalfPlanes = ConvexPolygon.getHalfPlanes(newVertices);
return new ConvexPolygon(newHalfPlanes, newVertices);
const newHalfPlanes: HalfPlane[] = [];
newHalfPlanesSet.forEach((v) => newHalfPlanes.push(v))
return new ConvexPolygon(ConvexPolygon.getHalfPlanesNotContainingAnyOther(newHalfPlanes))
}
public expandToIncludeInfinityInDirection(direction: Point): Area{
if(this.containsInfinityInDirection(direction)){
Expand Down
22 changes: 0 additions & 22 deletions src/areas/polygons/polygon-vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,6 @@ export class PolygonVertex{
this.normal1 = halfPlane1.normalTowardInterior;
this.normal2 = halfPlane2.normalTowardInterior;
}
public isExpandableToContainPoint(point: Point): boolean{
return this.halfPlane1.interiorContainsPoint(point) || this.halfPlane2.interiorContainsPoint(point);
}
public isInSameHalfPlaneAs(point: Point): boolean{
return this.halfPlane1.containsPoint(point) || this.halfPlane2.containsPoint(point);
}
public getHalfPlaneContaining(point: Point): HalfPlane{
return this.halfPlane1.containsPoint(point) ? this.halfPlane1 : this.halfPlane2;
}
public expandToContainPoint(point: Point): {newHalfPlane: HalfPlane, newVertex: PolygonVertex}{
const halfPlaneToKeep: HalfPlane = this.halfPlane1.interiorContainsPoint(point) ? this.halfPlane1 : this.halfPlane2;
let directionOfNewHalfPlane: Point = point.minus(this.point).getPerpendicular();
if(!this.isContainedByHalfPlaneWithNormal(directionOfNewHalfPlane)){
directionOfNewHalfPlane = directionOfNewHalfPlane.scale(-1);
}
const newHalfPlane: HalfPlane = new HalfPlane(this.point, directionOfNewHalfPlane);
return {
newHalfPlane: newHalfPlane,
newVertex: new PolygonVertex(this.point, halfPlaneToKeep, newHalfPlane)
};

}
public isContainedByHalfPlaneWithNormal(normal: Point): boolean{
return normal.isInSmallerAngleBetweenPoints(this.normal1, this.normal2);
}
Expand Down
17 changes: 17 additions & 0 deletions src/geometry/get-point-in-front-in-direction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Point } from "./point";

export function getPointInFrontInDirection(shape: Point[], point: Point, direction: Point){
let along: number = 0;
let frontMostVertex: Point;
for(let shapePoint of shape){
const newAlong: number = shapePoint.minus(point).dot(direction);
if(newAlong > along){
frontMostVertex = shapePoint;
along = newAlong;
}
}
if(frontMostVertex){
return point.plus(frontMostVertex.minus(point).projectOn(direction));
}
return point;
}
4 changes: 2 additions & 2 deletions src/infinite-canvas-instruction-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { rectangleIsPlane } from "./geometry/rectangle-is-plane";
import { plane } from "./areas/plane";
import { ConvexPolygon } from "./areas/polygons/convex-polygon";
import { CanvasRectangle } from "./rectangle/canvas-rectangle";
import { StateAndInstruction } from "./instructions/state-and-instruction";
import { ExecutableInstructionWithState } from "./instructions/executable-instruction-with-state";
import { InstructionsWithPositiveDrawnArea } from "./instructions/instructions-with-positive-drawn-area";
import { DrawingInstruction } from "./drawing-instruction";

Expand Down Expand Up @@ -122,7 +122,7 @@ export class InfiniteCanvasInstructionSet{
}
this.incorporateDrawingInstruction(DrawingInstruction.create({
instruction,
build: (_, instruction) => new InstructionsWithPositiveDrawnArea(StateAndInstruction.create(state, instruction, this.rectangle), area),
build: (_, instruction) => new InstructionsWithPositiveDrawnArea(ExecutableInstructionWithState.create(state, instruction, this.rectangle), area),
transformationKind,
state,
tempState,
Expand Down
50 changes: 30 additions & 20 deletions src/infinite-canvas-viewbox-infinity.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import {ViewboxInfinity} from "./interfaces/viewbox-infinity";
import {Point} from "./geometry/point";
import {Transformation} from "./transformation";
import {ConvexPolygon} from "./areas/polygons/convex-polygon";
import { right, left, down, up } from "./geometry/points-at-infinity";
import {CanvasRectangle} from "./rectangle/canvas-rectangle";
import {InfiniteCanvasState} from "./state/infinite-canvas-state";
import { DrawnStrokeProperties } from "./interfaces/drawn-stroke-properties";
import { Area } from "./areas/area";
import { getPointInFrontInDirection } from "./geometry/get-point-in-front-in-direction";

function getPointInFront(area: Area, point: Point, direction: Point): Point{
return getPointInFrontInDirection(area.getVertices(), point, direction);
}

export class InfiniteCanvasViewboxInfinity implements ViewboxInfinity{
constructor(
Expand All @@ -16,36 +21,41 @@ export class InfiniteCanvasViewboxInfinity implements ViewboxInfinity{
public addPathAroundViewbox(context: CanvasRenderingContext2D): void{
this.rectangle.addPathAroundViewbox(context, this.drawnStroke.lineWidth);
}
private getTransformedViewbox(): ConvexPolygon{
const margin = this.drawnStroke.lineWidth * this.rectangle.transformation.scale;
private getTransformedViewbox(): Area{
const bitmapTransformationToTransformedInfiniteCanvasContext: Transformation = this.state.current.transformation.before(this.rectangle.getBitmapTransformationToInfiniteCanvasContext());
return this.rectangle.polygon.expandByDistance(margin).transform(bitmapTransformationToTransformedInfiniteCanvasContext.inverse())
let rectangle: Area = this.rectangle.polygon;
rectangle = rectangle.transform(bitmapTransformationToTransformedInfiniteCanvasContext.inverse()).expandByDistance(this.drawnStroke.lineWidth)
for(const shadowOffset of this.drawnStroke.shadowOffsets){
const offsetRectangle = rectangle.transform(Transformation.translation(-shadowOffset.x, -shadowOffset.y));
rectangle = rectangle.join(offsetRectangle)
}
return rectangle;
}
public clearRect(context: CanvasRenderingContext2D, transformation: Transformation, x: number, y: number, width: number, height: number): void{
const transformedViewbox: ConvexPolygon = this.getTransformedViewbox();
const transformedViewbox: Area = this.getTransformedViewbox();
const {a, b, c, d, e, f} = transformation;
context.save();
context.transform(a, b, c, d, e, f);
const xStart: number = Number.isFinite(x) ? x : transformedViewbox.getPointInFrontInDirection(new Point(0, 0), x > 0 ? right.direction : left.direction).x;
const xEnd: number = Number.isFinite(width) ? x + width : transformedViewbox.getPointInFrontInDirection(new Point(0, 0), width > 0 ? right.direction : left.direction).x;
const yStart: number = Number.isFinite(y) ? y : transformedViewbox.getPointInFrontInDirection(new Point(0, 0), y > 0 ? down.direction : up.direction).y;
const yEnd: number = Number.isFinite(height) ? y + height : transformedViewbox.getPointInFrontInDirection(new Point(0, 0), height > 0 ? down.direction : up.direction).y;
const xStart: number = Number.isFinite(x) ? x : getPointInFront(transformedViewbox, new Point(0, 0), x > 0 ? right.direction : left.direction).x;
const xEnd: number = Number.isFinite(width) ? x + width : getPointInFront(transformedViewbox, new Point(0, 0), width > 0 ? right.direction : left.direction).x;
const yStart: number = Number.isFinite(y) ? y : getPointInFront(transformedViewbox, new Point(0, 0), y > 0 ? down.direction : up.direction).y;
const yEnd: number = Number.isFinite(height) ? y + height : getPointInFront(transformedViewbox, new Point(0, 0), height > 0 ? down.direction : up.direction).y;
context.clearRect(xStart, yStart, xEnd - xStart, yEnd - yStart);
context.restore();
}
public moveToInfinityFromPointInDirection(context: CanvasRenderingContext2D, transformation: Transformation, fromPoint: Point, direction: Point): void{
const pointInFront: Point = this.getTransformedViewbox().getPointInFrontInDirection(fromPoint, direction);
const pointInFront: Point = getPointInFront(this.getTransformedViewbox(), fromPoint, direction);
this.moveToTransformed(context, pointInFront, transformation);
}
public drawLineToInfinityFromInfinityFromPoint(context: CanvasRenderingContext2D, transformation: Transformation, point: Point, fromDirection: Point, toDirection: Point): void{
const transformedViewbox: ConvexPolygon = this.getTransformedViewbox();
const startingPoint: Point = transformedViewbox.getPointInFrontInDirection(point, fromDirection);
const destinationPoint: Point = transformedViewbox.getPointInFrontInDirection(point, toDirection);
let polygonToCircumscribe: ConvexPolygon = transformedViewbox
const transformedViewbox: Area = this.getTransformedViewbox();
const startingPoint: Point = getPointInFront(transformedViewbox, point, fromDirection);
const destinationPoint: Point = getPointInFront(transformedViewbox, point, toDirection);
let polygonToCircumscribe: Area = transformedViewbox
.expandToIncludePoint(point)
.expandToIncludePoint(startingPoint)
.expandToIncludePoint(destinationPoint);
const verticesInBetween: Point[] = polygonToCircumscribe.vertices.map(v => v.point).filter(p => !p.equals(startingPoint) && !p.equals(destinationPoint) && !p.equals(point) && p.minus(point).isInSmallerAngleBetweenPoints(fromDirection, toDirection));
const verticesInBetween: Point[] = polygonToCircumscribe.getVertices().filter(p => !p.equals(startingPoint) && !p.equals(destinationPoint) && !p.equals(point) && p.minus(point).isInSmallerAngleBetweenPoints(fromDirection, toDirection));
verticesInBetween.sort((p1, p2) => {
if(p1.minus(point).isInSmallerAngleBetweenPoints(p2.minus(point), fromDirection)){
return -1;
Expand All @@ -64,21 +74,21 @@ export class InfiniteCanvasViewboxInfinity implements ViewboxInfinity{
this.ensureDistanceCoveredIsMultipleOfLineDashPeriod(context, transformation, distanceCovered, destinationPoint, toDirection);
}
public drawLineFromInfinityFromPointToInfinityFromPoint(context: CanvasRenderingContext2D, transformation: Transformation, point1: Point, point2: Point, direction: Point): void{
const transformedViewbox: ConvexPolygon = this.getTransformedViewbox();
const fromPoint: Point = transformedViewbox.getPointInFrontInDirection(point1, direction);
const toPoint: Point = transformedViewbox.getPointInFrontInDirection(point2, direction);
const transformedViewbox: Area = this.getTransformedViewbox();
const fromPoint: Point = getPointInFront(transformedViewbox, point1, direction);
const toPoint: Point = getPointInFront(transformedViewbox, point2, direction);
this.lineToTransformed(context, toPoint, transformation);
const distanceCovered: number = toPoint.minus(fromPoint).mod();
this.ensureDistanceCoveredIsMultipleOfLineDashPeriod(context, transformation, distanceCovered, toPoint, direction);
}
public drawLineFromInfinityFromPointToPoint(context: CanvasRenderingContext2D, transformation: Transformation, point: Point, direction: Point): void{
const fromPoint: Point = this.getTransformedViewbox().getPointInFrontInDirection(point, direction);
const fromPoint: Point = getPointInFront(this.getTransformedViewbox(), point, direction);
const distanceToCover: number = point.minus(fromPoint).mod();
this.ensureDistanceCoveredIsMultipleOfLineDashPeriod(context, transformation, distanceToCover, fromPoint, direction);
this.lineToTransformed(context, point, transformation);
}
public drawLineToInfinityFromPointInDirection(context: CanvasRenderingContext2D, transformation: Transformation, fromPoint: Point, direction: Point): void{
const toPoint: Point = this.getTransformedViewbox().getPointInFrontInDirection(fromPoint, direction);
const toPoint: Point = getPointInFront(this.getTransformedViewbox(), fromPoint, direction);
this.lineToTransformed(context, toPoint, transformation);
const distanceCovered: number = toPoint.minus(fromPoint).mod();
this.ensureDistanceCoveredIsMultipleOfLineDashPeriod(context, transformation, distanceCovered, toPoint, direction);
Expand Down
4 changes: 2 additions & 2 deletions src/instructions/clear-rect-with-state.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StateAndInstruction } from "./state-and-instruction";
import { ExecutableInstructionWithState } from "./executable-instruction-with-state";
import { StateChangingInstructionSetWithArea } from "../interfaces/state-changing-instruction-set-with-area";
import { InfiniteCanvasState } from "../state/infinite-canvas-state";
import { Instruction } from "./instruction";
Expand All @@ -9,7 +9,7 @@ import { CanvasRectangle } from "../rectangle/canvas-rectangle";
import { DrawingArea } from "../areas/drawing-area";
import { NegativeDrawingArea } from "../areas/negative-drawing-area";

export class ClearRectWithState extends StateAndInstruction implements StateChangingInstructionSetWithArea{
export class ClearRectWithState extends ExecutableInstructionWithState implements StateChangingInstructionSetWithArea{
public drawingArea: DrawingArea;
constructor(initialState: InfiniteCanvasState, state: InfiniteCanvasState, instruction: Instruction, stateConversion: Instruction, private readonly area: Area, rectangle: CanvasRectangle){
super(initialState, state, instruction, stateConversion, rectangle);
Expand Down
9 changes: 4 additions & 5 deletions src/instructions/clipped-paths.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { StateChangingInstructionSet } from "../interfaces/state-changing-instruction-set";
import { StateChangingInstructionSetWithCurrentPath } from "../interfaces/state-changing-instruction-set-with-current-path";
import { InstructionsToClip } from "../interfaces/instructions-to-clip";
import { Area } from "../areas/area";
import { InfiniteCanvasState } from "../state/infinite-canvas-state";
import { Instruction } from "./instruction";
import { Transformation } from "../transformation";
import { CanvasRectangle } from "../rectangle/canvas-rectangle";

export class ClippedPaths {
constructor(public area: Area, public latestClippedPath: StateChangingInstructionSet, public readonly previouslyClippedPaths?: ClippedPaths){}
public withClippedPath(latestClippedPath: StateChangingInstructionSetWithCurrentPath): ClippedPaths{
const newArea: Area = latestClippedPath.getClippedArea(this.area);
constructor(public area: Area, public latestClippedPath: InstructionsToClip, public readonly previouslyClippedPaths?: ClippedPaths){}
public withClippedPath(latestClippedPath: InstructionsToClip): ClippedPaths{
const newArea: Area = latestClippedPath.area.intersectWith(this.area);
return new ClippedPaths(newArea, latestClippedPath, this);
}
public get initialState(): InfiniteCanvasState{
Expand Down
Loading

0 comments on commit 97f26ad

Please sign in to comment.