class Polygon {
static fromPath(path, lengthBetweenPts = 5) {
const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path')
pathElement.setAttribute('d', path)
const pathLength = pathElement.getTotalLength()
const poly = new Polygon(...array(pathLength / lengthBetweenPts | 0).map(i => {
return PVector(pathElement.getPointAtLength(i * lengthBetweenPts))
}))
poly.originalPath = path
return poly
}
static intersection(p1, p2) {
const INTERSECTION = ClipperLib.ClipType.ctIntersection
return Polygon.boolOp(INTERSECTION, p1, p2)
}
static union(p1, p2) {
const UNION = ClipperLib.ClipType.ctUnion
return Polygon.boolOp(UNION, p1, p2)
}
static diff(p1, p2) {
const DIFF = ClipperLib.ClipType.ctDifference
return Polygon.boolOp(DIFF, p1, p2)
}
static xor(p1, p2) {
const XOR = ClipperLib.ClipType.ctXor
return Polygon.boolOp(XOR, p1, p2)
}
static boolOp(type, p1, p2) {
const polygonToClipperPath = p => p.pts.map(pt => ({ X: pt.x * 100, Y: pt.y * 100 }))
const subj_paths = [polygonToClipperPath(p1)]
const clip_paths = [polygonToClipperPath(p2)]
const clipper = new ClipperLib.Clipper();
const SUBJECT = ClipperLib.PolyType.ptSubject // 0
const CLIP = ClipperLib.PolyType.ptClip // 1
clipper.AddPaths(subj_paths, SUBJECT, true)
clipper.AddPaths(clip_paths, CLIP, true)
const result = [];
const EVEN_ODD = ClipperLib.PolyFillType.pftEvenOdd // 0
const NON_ZERO = ClipperLib.PolyFillType.pftNonZero // 1
const POSITIVE = ClipperLib.PolyFillType.pftPositive // 2
const NEGATIVE = ClipperLib.PolyFillType.pftNegative // 3
clipper.Execute(type, result, NON_ZERO, NON_ZERO)
return result.map(pts => new Polygon(...pts.map(pt => PVector(pt.X / 100, pt.Y / 100))))
}
// offset done using ClipperLib
// https://sourceforge.net/p/jsclipper/wiki/Home%206/#b-offsetting-paths
static offset(p, delta, type = 'round') {
// scale up coords since Clipper is using integer
const scale = 1000
let paths = [p.pts.map(pt => ({ X: pt.x * scale, Y: pt.y * scale }))]
// Simplifying
paths = ClipperLib.Clipper.SimplifyPolygons(paths, ClipperLib.PolyFillType.pftNonZero)
// Cleaning
var cleandelta = 0.1 // 0.1 should be the appropriate delta in different cases
paths = ClipperLib.JS.Clean(paths, cleandelta * scale)
// Create an instance of ClipperOffset object
var co = new ClipperLib.ClipperOffset()
const jointType = type === 'miter' ? ClipperLib.JoinType.jtMiter :
type === 'square' ? ClipperLib.JoinType.jtSquare :
ClipperLib.JoinType.jtRound
// Add paths
co.AddPaths(paths, jointType, ClipperLib.EndType.etClosedPolygon)
// Create an empty solution and execute the offset operation
let offsetted_paths = new ClipperLib.Paths()
co.Execute(offsetted_paths, delta * scale)
ClipperLib.JS.ScaleDownPaths(offsetted_paths, scale)
if(!(offsetted_paths[0] && offsetted_paths[0].length > 0)) return null
return new Polygon(...offsetted_paths[0].map(pt => PVector(pt.X, pt.Y)))
}
constructor(...pts) {
this.pts = pts.map(pt => pt.clone())
this.segments = pts.map((pt, i) => new Segment(pt, pts[(i + 1 ) % pts.length]))
this.center = new PVector(pts.reduce((acc, d) => PVector.add(acc, d), PVector())).div(pts.length)
}
// adapted from https://www.codeproject.com/tips/84226/is-a-point-inside-a-polygon
contains(pt) {
const pts = this.pts
let c = false
for (let i = 0, j = pts.length-1; i < pts.length; j = i++) {
if (
((pts[i].y > pt.y) != (pts[j].y > pt.y)) &&
(pt.x < (pts[j].x-pts[i].x) * (pt.y-pts[i].y) / (pts[j].y-pts[i].y) + pts[i].x)
) {
c = !c
}
}
return c
}
getBoundingBox() {
const xs = this.pts.map(pt => pt.x)
const ys = this.pts.map(pt => pt.y)
const minX = Math.min(...xs)
const maxX = Math.max(...xs)
const minY = Math.min(...ys)
const maxY = Math.max(...ys)
return {
origin: PVector(minX, minY),
width: maxX - minX,
height: maxY - minY
}
}
getHatchesParametric(originalAngle = PI/2, startSpacing = 1, endSpacing = 10, func = t => t, offset = 0, alternate = false) {
if(offset !== 0) return Polygon.offset(this, -offset).getHatchesParametric(originalAngle, startSpacing, endSpacing, func, 0, alternate)
//bounding box
const originalBB = this.getBoundingBox()
const bb = {
origin: originalBB.origin.clone().sub(Math.min(startSpacing, endSpacing) * 2),
width: originalBB.width + 4 * Math.min(startSpacing, endSpacing),
height: originalBB.height + 4 * Math.min(startSpacing, endSpacing)
}
// hatches
let angle = ((originalAngle % TAU) + TAU) % PI
if(angle > PI/2) angle -= PI
if(modularDist(angle, PI/2) < 0.00001) angle += 0.00001
const hatchDirOriginal = PVector.fromAngle(originalAngle).setMag(1000)
const hatchDir = PVector.fromAngle(angle).setMag(1000)
let startY, endY
let leftPoint, rightPoint
if(angle < 0) {
rightPoint = bb.origin.clone().add(PVector(bb.width, bb.height))
startY = bb.origin.y
endY = lineLineIntersection(
bb.origin,
bb.origin.clone().addY(bb.height),
rightPoint,
rightPoint.clone().sub(hatchDir)
).y
leftPoint = PVector(bb.origin.x, endY)
}
else {
rightPoint = bb.origin.clone().addX(bb.width)
startY = lineLineIntersection(
bb.origin,
bb.origin.clone().addY(bb.height),
rightPoint,
rightPoint.clone().sub(hatchDir)
).y
endY = bb.origin.y + bb.height
leftPoint = PVector(bb.origin.x, startY)
}
const hatches = []
for(let y = startY, stepY = startSpacing, i = 0; y < endY; y += stepY) {
const segA = new Segment(PVector(bb.origin.x, startY), PVector(bb.origin.x, endY))
const segB = new Segment(leftPoint, rightPoint)
const t = (y - startY) / (endY - startY)
const d = startSpacing + (endSpacing - startSpacing) * func(t)
const step = hatchDir.clone().rotateBy(PI/2).setMag(d)
const stepProjection = hatchDir.clone().mult(angle < 0 ? -1 : 1).add(step)
const stepIntersection = lineLineIntersection(segA.a, segA.b, PVector.add(bb.origin, step), PVector.add(bb.origin, stepProjection))
stepY = stepIntersection.y - bb.origin.y
const A = PVector(bb.origin.x, y)
const B = A.clone().add(hatchDir)
const ray = new Ray(A, hatchDir)
const intersections = this.segments.map(seg => raySegIntersection(ray, seg))
.filter(d => d)
.sort((a, b) => a.y > b.y ? 1 :
a.y < b.y ? -1 :
a.x > b.x ? 1 :
a.x < b.x ? -1 : 0)
if(intersections.length < 2) continue;
else {
if(alternate && i % 2) intersections.reverse()
const segments = []
for(let i = 0; i < intersections.length && intersections[i + 1]; i += 2) {
segments.push(new Segment(intersections[i], intersections[i + 1]))
}
hatches.push(segments)
i++
}
}
return hatches.filter(d => d).flat().filter(seg => this.contains(seg.pointAt(0.3)))
}
getHatches(originalAngle = PI/2, spacing = 2, offset = 0, alternate = false) {
return this.getHatchesParametric(originalAngle, spacing, spacing, t => t, offset, alternate)
}
getHatches2(originalAngle = PI/2, startSpacing = 1, endSpacing = 10, power = 1, offset = 0, alternate = false) {
return this.getHatchesParametric(originalAngle, startSpacing, endSpacing, t => t ** power, offset, alternate)
}
}