UNPKG

svg-kit

Version:

A SVG toolkit, with its own Vector Graphics structure, multiple renderers (svg text, DOM svg, canvas), and featuring Flowing Text.

254 lines (172 loc) 7.35 kB
/* SVG Kit Copyright (c) 2017 - 2024 Cédric Ronvel The MIT License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ "use strict" ; const BoundingBox = require( './BoundingBox.js' ) ; function Polygon( params ) { this.points = [] ; this.sides = [] ; this.boundingBox = new BoundingBox( null ) ; this.totalAngle = 0 ; if ( params ) { this.set( params ) ; } } module.exports = Polygon ; Polygon.prototype.__prototypeUID__ = 'svg-kit/Polygon' ; Polygon.prototype.__prototypeVersion__ = require( '../package.json' ).version ; // Circular... const Path = require( './Path/Path.js' ) ; Polygon.prototype.set = function( params ) { if ( params.points ) { this.setPoints( params.points ) ; } } ; Polygon.prototype.setPoints = function( points ) { this.points.length = 0 ; this.sides.length = 0 ; this.boundingBox.nullify() ; if ( ! Array.isArray( points ) ) { return ; } for ( let point of points ) { this.addPoint( point , true ) ; } if ( this.addClosingSide ) { this.addClosingSide() ; } this.computeTotalAngle() ; } ; Polygon.prototype.addPoint = function( point , noExtraCompute = false ) { if ( ! point || typeof point !== 'object' ) { return ; } var pointClone = { x: + point.x || 0 , y: + point.y || 0 } ; this.points.push( pointClone ) ; this.boundingBox.ensurePoint( pointClone ) ; this.addLastSide() ; if ( noExtraCompute ) { return ; } if ( this.addClosingSide ) { this.addClosingSide() ; } this.computeTotalAngle() ; } ; Polygon.prototype.addLastSide = function() { var count = this.points.length ; if ( count <= 1 ) { return ; } this.sides[ count - 2 ] = Polygon.getParametricLineParameters( this.points[ count - 2 ] , this.points[ count - 1 ] ) ; } ; Polygon.prototype.addClosingSide = function() { var count = this.points.length ; if ( count < 3 ) { return ; } this.sides[ count - 1 ] = Polygon.getParametricLineParameters( this.points[ count - 1 ] , this.points[ 0 ] ) ; } ; Polygon.prototype.export = function( data = {} ) { data.points = this.points.map( p => ( { x: p.x , y: p.y } ) ) ; return data ; } ; Polygon.prototype.toD = function( invertY = false ) { return Path.polygonPointsToD( this.points , invertY ) ; } ; Polygon.prototype.isInside = function( coords ) { if ( ! this.sides.length ) { return false ; } var count = 0 ; // Cast rays on each side, and count the intersection for ( let side of this.sides ) { if ( Polygon.castRayOnSide( side , coords ) ) { count ++ ; } } //console.log( "Polygon.isInside: count =" , count ) ; // Odd count means it's inside, even count means it's outside return !! ( count % 2 ) ; } ; Polygon.prototype.computeTotalAngle = function() { this.totalAngle = 0 ; for ( let i = 0 ; i < this.sides.length ; i ++ ) { let angleDelta = this.sides[ ( i + 1 ) % this.sides.length ].angle - this.sides[ i ].angle ; if ( angleDelta > Math.PI ) { angleDelta -= 2 * Math.PI ; } if ( angleDelta < - Math.PI ) { angleDelta += 2 * Math.PI ; } this.totalAngle += angleDelta ; } } ; Polygon.computeTotalAngle = function( points ) { let totalAngle = 0 ; for ( let i = 0 ; i < points.length ; i ++ ) { let p1 = points[ i ] , p2 = points[ ( i + 1 ) % points.length ] , p3 = points[ ( i + 2 ) % points.length ] , angle1 = Math.atan2( p2.y - p1.y , p2.x - p1.x ) , angle2 = Math.atan2( p3.y - p2.y , p3.x - p2.x ) , angleDelta = angle2 - angle1 ; if ( angleDelta > Math.PI ) { angleDelta -= 2 * Math.PI ; } if ( angleDelta < - Math.PI ) { angleDelta += 2 * Math.PI ; } totalAngle += angleDelta ; } return totalAngle ; } ; // Ensure the side turns in the correct orientation, can be useful for third-party like 3D, to create faces // with the correct face-normal (e.g. for polygon extrusion) Polygon.prototype.ensureOrientation = function( sign ) { sign = Math.sign( + sign || 1 ) ; if ( Math.sign( this.totalAngle ) === sign ) { return ; } console.warn( "Reversing the orientation (Polygon.prototype.ensureOrientation)" ) ; this.setPoints( this.points.slice().reverse() ) ; } ; Polygon.ensureOrientation = function( points , sign ) { sign = Math.sign( + sign || 1 ) ; var totalAngle = Polygon.computeTotalAngle( points ) ; if ( Math.sign( totalAngle ) === sign ) { return ; } console.warn( "Reversing the orientation (Polygon.ensureOrientation)" ) ; points.reverse() ; } ; /* Get the line equation's parameters. The equation is: ax + by + c The result is =0 on the line, >0 on the left-side (point 1 toward point 2), <0 on the right-side. Also get the parametric line segment parameters. For 0 ≤ t ≤ 1 : x = x0 + dx * t y = y0 + dy * t */ Polygon.getParametricLineParameters = function( point1 , point2 ) { var dx = point2.x - point1.x , dy = point2.y - point1.y ; return { // Line equation a: - dy , b: dx , c: - dx * point1.y + dy * point1.x , // Parametric x0: point1.x , y0: point1.y , dx , dy , // Misc angle: Math.atan2( dy , dx ) } ; } ; // Apply the line equation on a point/coords, result is =0 if on the line, >0 if on the left-side, <0 if on the right-side Polygon.testSideLineEquation = function( side , coords ) { return side.a * coords.x + side.b * coords.y + side.c ; } ; // Cast an horizontal ray, and return true if it intersect with a side. // The ray is infinite on the left-side (x<0) and stop ath the precise ray.x and ray.y coordinates. Polygon.castRayOnSide = function( side , ray ) { // Tangential rays are always considered not intersecting, so horizontal line segments don't intersect if ( ! side.dy ) { return false ; } // Compute the parametric t value for the side's line-segment for the Y horizontal line // y = y0 + dy * t <=> t = ( y - y0 ) / dy let t = ( ray.y - side.y0 ) / side.dy ; // If we don't have 0 < t < 1, then the ray is not in the correct Y-axis range // To avoid degenerate cases, we consider edge case to be OUTSIDE, no intersection. if ( t <= 0 || t >= 1 ) { return false ; } // Now get the X coord of the intersection // x = x0 + dx * t let x = side.x0 + side.dx * t ; // The ray is infinite on the left and stop at ray's (x,y), so for the ray to intersect, // its X coord should be greater than the X coord of the line intersection. // Again, to avoid degenerate cases, we consider edge case to be OUTSIDE, no intersection. return ray.x > x ; } ;