Skip to content

Commit

Permalink
paper-clipper init
Browse files Browse the repository at this point in the history
  • Loading branch information
northamerican committed Sep 10, 2020
0 parents commit 53bb7f2
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
/lib
yarn.lock
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"printWidth": 120,
"trailingComma": "all",
"singleQuote": true
}
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "paper-clipper",
"version": "1.0.0",
"description": "Use Clipper's boolean and offsetting operations in paper.js",
"main": "index.js",
"scripts": {
"build": "tsc",
"format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
"lint": "tslint -p tsconfig.json"
},
"files": ["lib/**/*"],
"repository": "https://gitlab.com/northamerican/paper-clipper",
"author": "Chris Bitsakis",
"license": "MIT",
"private": false,
"dependencies": {
"js-angusj-clipper": "^1.1.0",
"paper": "^0.12.11",
"prettier": "^2.1.1",
"simplify-js": "^1.2.4",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0"
},
"devDependencies": {
"typescript": "^4.0.2"
}
}
176 changes: 176 additions & 0 deletions src/betterSimplify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import paper from 'paper'

const geomEpsilon = 1e-4

const clearData = (item: paper.Item) => {
item.data = {}
return item
}

const cloneWithoutData = (item: paper.Item) =>
clearData(item.clone({ insert: false }))

// Get part of a path from offset to offset
// Returns new path
const getPathPart = (targetPath: paper.Path, from: number, distance = Infinity) => {
const reverse = distance < 0
const path = cloneWithoutData(targetPath) as paper.Path
const pathPart = path.splitAt(from) || path

if (reverse) {
const pathLength = path.length
const reverseOffset = pathLength - Math.abs(distance)
const withinPath = reverseOffset > 0

if (withinPath) {
const pathPartReverse = path.splitAt(reverseOffset)

pathPartReverse.reverse()

return pathPartReverse
}

path.reverse()

return path
} else {
const withinPath = distance < pathPart.length

if (withinPath) {
pathPart.splitAt(distance)
}

return pathPart
}
}

// Must be a segment with no handles
const getSegmentAngle = (segment: paper.Segment) => {
if (!segment.path.closed && (segment.isFirst() || segment.isLast())) return null

const { handleIn, handleOut, point, path } = segment

const hasHandleIn = handleIn.length > geomEpsilon
const hasHandleOut = handleOut.length > geomEpsilon

const inPointAngleLocation = path.getLocationAt(segment.isFirst() ? path.length - 1 : segment.previous.location.offset)
const outPointAngleLocation = path.getLocationAt(segment.isLast() ? 1 : segment.next.location.offset)

if (!inPointAngleLocation || !outPointAngleLocation) return null

const inPointAngle = inPointAngleLocation.point.subtract(point).angle
const outPointAngle = outPointAngleLocation.point.subtract(point).angle

const inAngle = hasHandleIn ? handleIn.angle : inPointAngle
const outAngle = hasHandleOut ? handleOut.angle : outPointAngle

const angle = 180 - Math.abs(Math.abs(inAngle - outAngle) - 180)

return angle
}

const segmentIsAngled = (threshold = 1) => (segment: paper.Segment) => {
const angle = getSegmentAngle(segment) as number
const isAngled = angle > geomEpsilon && angle < (180 - threshold)

return isAngled
}

const removeDuplicateAdjacentSegments = (path: paper.Path): paper.Path => {
const { segments } = path
const segmentsBefore = segments.length

segments.forEach(segment => {
const { next } = segment

if (!next) return

const duplicateSegment = segment.point.isClose(next.point, geomEpsilon)

if (duplicateSegment) {
next.handleIn = segment.handleIn.clone()

segment.remove()
}
})

return segmentsBefore > segments.length ? removeDuplicateAdjacentSegments(path) : path
}

const splitAtOffsets = (path: paper.Path) => (offsets: number[]) => {
if (offsets.length === 0) return [path]
if (offsets.length === 1 && path.closed) return [path]

return offsets.reduce((pathParts: paper.Path[], offset, i, offsetsArr) => {
const prevOffset = offsetsArr[i - 1] || 0
const pathPart = getPathPart(path, prevOffset, offset - prevOffset)
const isLast = i === offsetsArr.length - 1

pathParts = pathParts.concat(pathPart)

if (isLast && !path.closed) {
const lastPathPart = getPathPart(path, offset, Infinity)

pathParts = pathParts.concat(lastPathPart)
}

return pathParts
}, [])
}

const joinPaths = (paths: paper.Path[]) => {
if (paths.length === 0) return null

return paths.reduce((path, pathPart, i) => {
if (i === 0) return pathPart

path.join(pathPart, geomEpsilon)
return path
})
}

const simplifyCopy = (tolerance: number) => (targetPathPart: paper.Path) => {
const pathPart = targetPathPart.clone({ insert: false }) as paper.Path

pathPart.simplify(tolerance)

const hasMoreSegments = pathPart.segments.length >= targetPathPart.segments.length

return hasMoreSegments ? targetPathPart : pathPart
}

const betterSimplify = (tolerance: number) => (targetPath: paper.Path): paper.Path => {
const path = removeDuplicateAdjacentSegments(targetPath)
const isClosed = path.closed

if (path.length === 0) return targetPath

if (isClosed) {
path.closed = false
path.addSegments([path.firstSegment.clone()])
}

const angledSegments = path.segments.filter(segmentIsAngled(45))
const angledSegmentOffsets = angledSegments.map(
segment => segment.location.offset
)

const pathParts = splitAtOffsets(path)(angledSegmentOffsets)
.map(removeDuplicateAdjacentSegments)

const simplifiedPathParts = pathParts
.map(simplifyCopy(tolerance))

const joinedPath = joinPaths(simplifiedPathParts)

if (!joinedPath) return targetPath

if (isClosed) {
joinedPath.join(joinedPath)
joinedPath.closed = true
}

return joinedPath
}

export default betterSimplify
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import clipperOffset from './offset'
import clipperUnite from './unite'

export {
clipperOffset,
clipperUnite
}
64 changes: 64 additions & 0 deletions src/offset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as clipperLib from 'js-angusj-clipper'
import paper from 'paper'
import simplify from 'simplify-js'
import betterSimplify from './betterSimplify'

// @ts-ignore
paper.setup()

enum EndTypes {
round = clipperLib.EndType.OpenRound,
square = clipperLib.EndType.OpenSquare,
butt = clipperLib.EndType.OpenButt,
closed = clipperLib.EndType.ClosedPolygon // clipperLib.EndType.ClosedLine
}

enum JoinTypes {
miter = clipperLib.JoinType.Miter,
round = clipperLib.JoinType.Round,
bevel = clipperLib.JoinType.Square,
}

const scale = 1000

const clipperOffset = (clipper: clipperLib.ClipperLibWrapper) => async (path: paper.Path, offset: number, tolerance: number = 0.5): Promise<paper.Path[]> => {
const { closed, strokeJoin, strokeCap } = path
const pathCopy = path.clone() as paper.Path
pathCopy.flatten(1)

const data = pathCopy.segments.map(({ point }) =>
({
x: Math.round(point.x * scale),
y: Math.round(point.y * scale)
})
)

const offsetPaths = clipper.offsetToPaths({
delta: offset * scale,
arcTolerance: 0.25 * scale,
offsetInputs: [{
// @ts-ignore
joinType: JoinTypes[strokeJoin],
// @ts-ignore
endType: closed ? EndTypes.closed : endTypes[strokeCap],
data
}]
})

if (!offsetPaths) return []

return offsetPaths
.map(offsetPath =>
new paper.Path({
closed,
segments: simplify(offsetPath.map(point => ({
x: point.x / scale,
y: point.y / scale
})), tolerance)
})
)
.map(betterSimplify(0.25))
.filter(offsetPath => offsetPath.length)
}

export default clipperOffset
41 changes: 41 additions & 0 deletions src/unite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as clipperLib from 'js-angusj-clipper'
import paper from 'paper'

// @ts-ignore
paper.setup()

enum FillTypes {
evenodd = clipperLib.PolyFillType.EvenOdd,
nonzero = clipperLib.PolyFillType.NonZero
}

const clipperOffset = (clipper: clipperLib.ClipperLibWrapper) => async (paths: paper.Path[]): Promise<paper.Path[]> => {
const scale = 1000
const data = paths.map(path =>
path.segments.map(({ point }) => ({ x: Math.round(point.x * scale), y: Math.round(point.y * scale) }))
)

const { closed, fillRule } = paths[0]

const unitedPaths = clipper.clipToPaths({
clipType: clipperLib.ClipType.Union,
// @ts-ignore
subjectFillType: FillTypes[fillRule],
subjectInputs: [{
closed,
data
}]
})

if (!unitedPaths) return []

return unitedPaths
.map(path =>
new paper.Path({
closed,
segments: path.map(point => ({ x: point.x / scale, y: point.y / scale }))
})
)
}

export default clipperOffset
12 changes: 12 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"declaration": true,
"outDir": "./lib",
"strict": true,
"esModuleInterop": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"]
}
3 changes: 3 additions & 0 deletions tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["tslint:recommended", "tslint-config-prettier"]
}

0 comments on commit 53bb7f2

Please sign in to comment.