-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 53bb7f2
Showing
9 changed files
with
338 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules/ | ||
/lib | ||
yarn.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"printWidth": 120, | ||
"trailingComma": "all", | ||
"singleQuote": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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__/*"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": ["tslint:recommended", "tslint-config-prettier"] | ||
} |