UNPKG

rpg-dialogue-js

Version:

A simple roleplay game dialogue engine and editor.

338 lines (310 loc) 13.3 kB
/** * @author Ikaros Kappler * @date 2020-10-05 * @modified 2021-02-08 Fixed a lot of es2015 compatibility issues. * @version 1.0.1 * @file CircleIntersections * @public **/ import { arrayFill } from "./arrayFill"; import { matrixFill } from "./matrixFill"; import { Circle } from "../../Circle"; import { CircleSector } from "../../CircleSector"; import { Line } from "../../Line"; import { CircularIntervalSet } from "../datastructures/CircularIntervalSet"; import { Interval, IndexPair, Matrix } from "../datastructures/interfaces"; /** * @classdesc A script for finding the intersection points of two or * multiple circles (the 'radical lines'). * * Based on the C++ implementation by Robert King * https://stackoverflow.com/questions/3349125/circle-circle-intersection-points * and the 'Circles and spheres' article by Paul Bourke. * http://paulbourke.net/geometry/circlesphere/ * * @requires arrayFill * @requires matrixFill * @requires Circle * @requires IndexPair * @requires Matrix * @requires Interval * @requires Line * @requires CirularIntervalSet */ export class CircleIntersections { /** * Find all connected outer path partitions. * * @method findOuterPartitions * @static * @memberof CircleIntersections * @param {Array<Circle>} circles - The circles to find intersections for. * @param {Array<CircularIntervalSet>} intervalSets - The determined interval sets (see `findOuterCircleIntervals`). * @return {Array<Array<IndexPair>>} An array of paths, each defined by a sequence of IndexPairs adressing circle i and interval j. **/ static findOuterPartitions( circles:Array<Circle>, intervalSets:Array<CircularIntervalSet> ) : Array<Array<IndexPair>> { // For tracking which interval we already used for detecting the partition // we need a matrix; find the maximal interval length. let maxSetLength : number = 0; for( var i = 0; i < intervalSets.length; i++ ) { maxSetLength = Math.max( maxSetLength, intervalSets[i].intervals.length ); } const usedIntervals : Matrix<boolean> = matrixFill<boolean>( intervalSets.length, maxSetLength, false ); var path : Array<IndexPair>|null = null; var pathList : Array<Array<IndexPair>> = []; while( (path = CircleIntersections.findOuterPartition(circles,intervalSets,usedIntervals)) != null ) { pathList.push( path ); } return pathList; }; /** * Find all connected outer path partitions (as CircleSectors). * * @method findOuterPartitionsAsSectors * @static * @memberof CircleIntersections * @param {Array<Circle>} circles - The circles to find intersections for. * @param {Array<CircularIntervalSet>} intervalSets - The determined interval sets (see `findOuterCircleIntervals`). * @return {Array<Array<CircleSector>>} An array of paths, each defined by a sequence of SircleSectors. **/ static findOuterPartitionsAsSectors( circles:Array<Circle>, intervalSets:Array<CircularIntervalSet> ) : Array<Array<CircleSector>> { const partitions : Array<Array<IndexPair>> = CircleIntersections.findOuterPartitions( circles, intervalSets ); const partitionsAsArcs : Array<Array<CircleSector>> = []; for( var p = 0; p < partitions.length; p++ ) { const path : Array<IndexPair> = partitions[p]; const pathAsArcs : Array<CircleSector> = []; for( var i = 0; i < path.length; i++ ) { const circleIndex : number = path[i].i; const circle : Circle = circles[ circleIndex ]; var interval = intervalSets[ path[i].i ].intervals[ path[i].j ]; // Params: circle, startAngle, endAngle pathAsArcs.push( new CircleSector( circle, interval[0], interval[1] ) ); } // END for partitionsAsArcs.push( pathAsArcs ); } // END for return partitionsAsArcs; }; /** * Build the n*n intersection matrix: contains the radical line at (i,j) if circle i and circle j do intersect; * conatins null at (i,j) otherwise. * * Note that this matrix is symmetrical: if circles (i,j) intersect with line (A,B), then also circles (j,i) intersect * with line (B,A). * * The returned two-dimensional matrix (array) has exactly as many entries as the passed circle array. * * @method buildRadicalLineMatrix * @static * @memberof CircleIntersections * @param {Array<Circle>} circles - The circles to find intersections for. * @return {Array<Array<Line>>} A 2d-matrix containing the radical lines where circles intersect. **/ static buildRadicalLineMatrix( circles:Array<Circle> ) : Matrix<Line> { var radicalLines:Array<Array<Line>> = []; for( var i = 0; i < circles.length; i++ ) { if( !radicalLines[i] ) radicalLines[i] = arrayFill<Line>( circles.length, null ); // Array<Line>( circles.length ); for( var j = 0; j < circles.length; j++ ) { if( i == j ) continue; if( radicalLines[i][j] ) continue; radicalLines[i][j] = circles[i].circleIntersection( circles[j] ); // Build symmetrical matrix if( radicalLines[i][j] ) { if( !radicalLines[j] ) radicalLines[j] = arrayFill<Line>( circles.length, null ); // Array<Line>( circles.length ); // Use reverse line radicalLines[j][i] = new Line( radicalLines[i][j].b, radicalLines[i][j].a ); } } } return radicalLines; }; /** * Find all circles (indices) which are completely located inside another circle. * * The returned array conatins the array indices. * * @method findInnerCircles * @static * @memberof CircleIntersections * @param {Array<Circle>} circles - The circles to find intersections for. * @return {Array<number>} **/ static findInnerCircles( circles : Array<Circle> ) : Array<number> { const innerCircleIndices : Array<number> = []; for( var i = 0; i < circles.length; i++ ) { for( var j = 0; j < circles.length; j++ ) { if( i == j ) continue; if( circles[j].containsCircle(circles[i]) ) { innerCircleIndices.push( i ); } } } return innerCircleIndices; }; /** * Calculate all outer circle intervals (sections that belong to the outermost line), dermined by the given * circles and their radical lines. * * The returned array contains IntervalSets - one for each circle - indicating the remaining circle sections. * * @method findOuterCircleIntervals * @static * @memberof CircleIntersections * @param {Array<Circle>} circles - The circles to find intersections for. * @param {Array<Line>} intersectionMatrix * @return {Array<number>} **/ static findOuterCircleIntervals( circles:Array<Circle>, intersectionMatrix: Matrix<Line> ) : Array<CircularIntervalSet> { const intervalSets : Array<CircularIntervalSet> = []; for( var i = 0; i < circles.length; i++ ) { intervalSets[i] = new CircularIntervalSet( 0, 2*Math.PI ); for( var j = 0; j < circles.length; j++ ) { if( i == j ) continue; if( intersectionMatrix[i][j] !== null ) { // const interval : Interval = CircleIntersections.radicalLineToInterval( circle, radicalLine ); CircleIntersections.handleCircleInterval( circles[i], intersectionMatrix[i][j], intervalSets[i] ); } else if( circles[j].containsCircle(circles[i]) ) { intervalSets[i].clear(); } } } return intervalSets; }; /** * Calculate the next connected partition from the given set of circles and outer path intervals. The function * will pick a random unused circle interval and detect all adjacent intervals until a closed partition * was found. * * If an interval (circle section) was already visited will be stored in the `usedIntervalSetRecords` matrix (thus * is must be large enough to map all sections). * * The returned array contains IndexPairs (i,j) - one for each circle i - indicating the used circle section j. * * @method findOuterPartition * @static * @memberof CircleIntersections * @param {Array<Circle>} circles - The circles to find intersections for. * @param {Array<CircularIntervalSet>} intervalSets - The circle intervals that form the intersection outline. * @param {Matrix<boolean>} usedIntervals - A matrix for remembering which circle intervals were always used. * @return {Array<Indexpair>|null} The next partition or `null` if no more can be found. **/ static findOuterPartition( circles:Array<Circle>, intervalSets:Array<CircularIntervalSet>, usedIntervals:Matrix<boolean> ) : Array<IndexPair>|null { let intLocation : IndexPair = CircleIntersections.randomUnusedInterval( intervalSets, usedIntervals ); const path : Array<IndexPair> = []; while( intLocation != null ) { path.push( intLocation ); usedIntervals[ intLocation.i ][ intLocation.j ] = true; intLocation = CircleIntersections.findAdjacentInterval( circles, intLocation, intervalSets, usedIntervals, 0.001 ); }; return path.length == 0 ? null : path; }; /** * Convert a radical line (belonging to a circle) into an interval: start angle and end angle. * * @method radicalLineToInterval * @static * @private * @memberof CircleIntersections * @param {Circle} circle - The circle itself. * @param {Line} radicaLine - The radical line to convert (must have two intersection points on the circle). * @return {Interval} The interval `[startAngle,endAngle]` determined by the radical line. **/ private static radicalLineToInterval( circle:Circle, radicalLine:Line ) : Interval { // Get angle sections in the circles const lineA : Line = new Line( circle.center, radicalLine.a ); const lineB : Line = new Line( circle.center, radicalLine.b ); let angleA : number = lineA.angle(); let angleB : number = lineB.angle(); // Map angles to [0 ... 2*PI] // (the angle() function might return negative angles in [-PI .. 0 .. PI]) if( angleA < 0 ) angleA = Math.PI*2 + angleA; if( angleB < 0 ) angleB = Math.PI*2 + angleB; return [angleA, angleB]; }; /** * This is a helper fuction used by `findOuterCircleIntervals`. * * It applies the passed radical line by intersecting the remaining circle sections with the new section. * * @method handleCircleInterval * @static * @private * @memberof CircleIntersections * @param {Circle} circle - The circles to find intersections for. * @param {Line} radicalLine - The radical line to apply. * @param {CircularIntervalSet} intervalSet - The CircularIntervalSet to use (must have left and right border: 0 and 2*PI). * @return {void} **/ private static handleCircleInterval( circle : Circle, radicalLine : Line, intervalSet : CircularIntervalSet ) : void { const interval : Interval = CircleIntersections.radicalLineToInterval( circle, radicalLine ); intervalSet.intersect( interval[1], interval[0] ); }; /** * Pick a random unused circle interval. This function is used by the `findOuterPartition` function, which * starts the detection with any random section. * * @method randomUnusedInterval * @static * @private * @memberof CircleIntersections * @param {Array<CircularIntervalSet>} intervalSets - An array of all available interval sets/intervals. * @param {Matrix<boolean>} usedIntervals - A matrix indicating which intervals have already been used/visited by the algorithm * @return {IndexPair|null} **/ private static randomUnusedInterval( intervalSets:Array<CircularIntervalSet>, usedIntervals:Matrix<boolean> ) : IndexPair|null { for( var i = 0; i < intervalSets.length; i++ ) { for( var j = 0; j < intervalSets[i].intervals.length; j++ ) { if( !usedIntervals[i][j] ) { return { i : i, j : j }; } } } return null; }; /** * Find the next adjacent circle interval for the given interval. * starts the detection with any random section. * * @method randomUnusedInterval * @static * @private * @memberof CircleIntersections * @param {Array<CircularIntervalSet>} intervalSets - An array of all available interval sets/intervals. * @param {Matrix<boolean>} usedIntervals - A matrix indicating which intervals have already been used/visited by the algorithm * @return {IndexPair|null} **/ private static findAdjacentInterval( circles:Array<Circle>, intLocation:IndexPair, intervalSets:Array<CircularIntervalSet>, usedIntervalSetRecords:Matrix<boolean>, epsilon:number ) { var curInterval = intervalSets[ intLocation.i ].intervals[ intLocation.j ]; var curEndPoint = circles[ intLocation.i ].vertAt( curInterval[1] ); for( var i = 0; i < intervalSets.length; i++ ) { for( var j = 0; j < intervalSets[i].intervals.length; j++ ) { if( usedIntervalSetRecords[i][j] ) { continue; } var interval = intervalSets[i].intervals[j]; var startPoint = circles[i].vertAt( interval[0] ); if( curEndPoint.distance(startPoint) < epsilon ) return { i : i, j : j }; } } return null; }; };