UNPKG

@jscad/modeling

Version:

Constructive Solid Geometry (CSG) Library for JSCAD

171 lines (155 loc) 6.01 kB
const { EPS, TAU } = require('../../maths/constants') const intersect = require('../../maths/utils/intersect') const line2 = require('../../maths/line2') const vec2 = require('../../maths/vec2') const area = require('../../maths/utils/area') /* * Create a set of offset points from the given points using the given options (if any). * @param {Object} options - options for offset * @param {Float} [options.delta=1] - delta of offset (+ to exterior, - from interior) * @param {String} [options.corners='edge'] - type corner to create during of expansion; edge, chamfer, round * @param {Integer} [options.segments=16] - number of segments when creating round corners * @param {Integer} [options.closed=false] - is the last point connected back to the first point? * @param {Array} points - array of 2D points * @returns {Array} new set of offset points, plus points for each rounded corner */ const offsetFromPoints = (options, points) => { const defaults = { delta: 1, corners: 'edge', closed: false, segments: 16 } let { delta, corners, closed, segments } = Object.assign({ }, defaults, options) if (Math.abs(delta) < EPS) return points let rotation = options.closed ? area(points) : 1.0 // + counter clockwise, - clockwise if (rotation === 0) rotation = 1.0 // use right hand normal? const orientation = ((rotation > 0) && (delta >= 0)) || ((rotation < 0) && (delta < 0)) delta = Math.abs(delta) // sign is no longer required let previousSegment = null let newPoints = [] const newCorners = [] const of = vec2.create() const n = points.length for (let i = 0; i < n; i++) { const j = (i + 1) % n const p0 = points[i] const p1 = points[j] // calculate the unit normal orientation ? vec2.subtract(of, p0, p1) : vec2.subtract(of, p1, p0) vec2.normal(of, of) vec2.normalize(of, of) // calculate the offset vector vec2.scale(of, of, delta) // calculate the new points (edge) const n0 = vec2.add(vec2.create(), p0, of) const n1 = vec2.add(vec2.create(), p1, of) const currentSegment = [n0, n1] if (previousSegment != null) { if (closed || (!closed && j !== 0)) { // check for intersection of new line segments const ip = intersect(previousSegment[0], previousSegment[1], currentSegment[0], currentSegment[1]) if (ip) { // adjust the previous points newPoints.pop() // adjust current points currentSegment[0] = ip } else { newCorners.push({ c: p0, s0: previousSegment, s1: currentSegment }) } } } previousSegment = [n0, n1] if (j === 0 && !closed) continue newPoints.push(currentSegment[0]) newPoints.push(currentSegment[1]) } // complete the closure if required if (closed && previousSegment != null) { // check for intersection of closing line segments const n0 = newPoints[0] const n1 = newPoints[1] const ip = intersect(previousSegment[0], previousSegment[1], n0, n1) if (ip) { // adjust the previous points newPoints[0] = ip newPoints.pop() } else { const p0 = points[0] const cursegment = [n0, n1] newCorners.push({ c: p0, s0: previousSegment, s1: cursegment }) } } // generate corners if necessary if (corners === 'edge') { // map for fast point index lookup const pointIndex = new Map() // {point: index} newPoints.forEach((point, index) => pointIndex.set(point, index)) // create edge corners const line0 = line2.create() const line1 = line2.create() newCorners.forEach((corner) => { line2.fromPoints(line0, corner.s0[0], corner.s0[1]) line2.fromPoints(line1, corner.s1[0], corner.s1[1]) const ip = line2.intersectPointOfLines(line0, line1) if (Number.isFinite(ip[0]) && Number.isFinite(ip[1])) { const p0 = corner.s0[1] const i = pointIndex.get(p0) newPoints[i] = ip newPoints[(i + 1) % newPoints.length] = undefined } else { // paralell segments, drop one const p0 = corner.s1[0] const i = pointIndex.get(p0) newPoints[i] = undefined } }) newPoints = newPoints.filter((p) => p !== undefined) } if (corners === 'round') { // create rounded corners let cornersegments = Math.floor(segments / 4) const v0 = vec2.create() newCorners.forEach((corner) => { // calculate angle of rotation let rotation = vec2.angle(vec2.subtract(v0, corner.s1[0], corner.c)) rotation -= vec2.angle(vec2.subtract(v0, corner.s0[1], corner.c)) if (orientation && rotation < 0) { rotation = rotation + Math.PI if (rotation < 0) rotation = rotation + Math.PI } if ((!orientation) && rotation > 0) { rotation = rotation - Math.PI if (rotation > 0) rotation = rotation - Math.PI } if (rotation !== 0.0) { // generate the segments cornersegments = Math.floor(segments * (Math.abs(rotation) / TAU)) const step = rotation / cornersegments const start = vec2.angle(vec2.subtract(v0, corner.s0[1], corner.c)) const cornerpoints = [] for (let i = 1; i < cornersegments; i++) { const radians = start + (step * i) const point = vec2.fromAngleRadians(vec2.create(), radians) vec2.scale(point, point, delta) vec2.add(point, point, corner.c) cornerpoints.push(point) } if (cornerpoints.length > 0) { const p0 = corner.s0[1] let i = newPoints.findIndex((point) => vec2.equals(p0, point)) i = (i + 1) % newPoints.length newPoints.splice(i, 0, ...cornerpoints) } } else { // paralell segments, drop one const p0 = corner.s1[0] const i = newPoints.findIndex((point) => vec2.equals(p0, point)) newPoints.splice(i, 1) } }) } return newPoints } module.exports = offsetFromPoints