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()
return new Polygon(...array(pathLength / lengthBetweenPts | 0).map(i => {
return PVector(pathElement.getPointAtLength(i * lengthBetweenPts))
}))
}
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
const CLIP = ClipperLib.PolyType.ptClip
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))))
}
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
}
}
getHatches(originalAngle = PI/2, spacing = 2, alternate = false) {
//bounding box
const originalBB = this.getBoundingBox()
const bb = {
origin: originalBB.origin.clone().sub(spacing * 2),
width: originalBB.width + 4 * spacing,
height: originalBB.height + 4 * spacing
}
// hashes
let angle = ((originalAngle % TAU) + TAU) % PI
if(angle > PI/2) angle -= PI
if(angle === 0 || angle === PI / 2) 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 = lineLineIntersect(
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 = lineLineIntersect(
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 segA = new Segment(PVector(bb.origin.x, startY), PVector(bb.origin.x, endY))
const segB = new Segment(leftPoint, rightPoint)
const step = hatchDir.clone().rotateBy(PI/2).setMag(spacing)
const stepProjection = hatchDir.clone().mult(angle < 0 ? -1 : 1).add(step)
const stepIntersection = lineLineIntersect(segA.a, segA.b, PVector.add(bb.origin, step), PVector.add(bb.origin, stepProjection))
const stepY = stepIntersection.y - bb.origin.y
const hatches = array(((endY - startY) / stepY | 0) + 1).map(i => {
const A = PVector(bb.origin.x, startY + i * stepY)
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) return null
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]))
}
return segments
}).filter(d => d).flat().filter(seg => this.contains(seg.pointAt(0.3)))
return hatches
}
}