├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-interactive-tools.cjs └── releases │ └── yarn-3.4.1.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── __snapshots__ │ └── tests.spec.ts.snap ├── external-libs │ └── clipper.js ├── pureJs.ts ├── tests.spec.ts ├── tsconfig.json └── utils.ts ├── build ├── compile-asm.ts └── tsconfig.json ├── docs ├── apiReference │ ├── clipping │ │ ├── ClipInput.md │ │ ├── ClipParams.md │ │ ├── ClipType.md │ │ ├── SubjectInput.md │ │ └── clipTo.md │ ├── index.md │ ├── libInit │ │ ├── NativeClipperLibLoadedFormat.md │ │ ├── NativeClipperLibRequestedFormat.md │ │ └── loadNativeClipperLibInstanceAsync.md │ ├── offsetting │ │ ├── EndType.md │ │ ├── JoinType.md │ │ ├── OffsetInput.md │ │ ├── OffsetParams.md │ │ └── offsetTo.md │ └── shared │ │ ├── ClipperLibWrapper.md │ │ ├── IntPoint.md │ │ ├── Path.md │ │ ├── Paths.md │ │ ├── PointInPolygonResult.md │ │ ├── PolyFillType.md │ │ ├── PolyNode.md │ │ └── PolyTree.md ├── faq │ └── index.md └── overview │ └── index.md ├── jest.config.js ├── package.json ├── src ├── Clipper.ts ├── ClipperError.ts ├── ClipperOffset.ts ├── IntPoint.ts ├── IntRect.ts ├── Path.ts ├── Paths.ts ├── PolyNode.ts ├── PolyTree.ts ├── clipFunctions.ts ├── constants.ts ├── enums.ts ├── functions.ts ├── index.ts ├── native │ ├── NativeClipper.ts │ ├── NativeClipperBase.ts │ ├── NativeClipperLibInstance.ts │ ├── NativeClipperOffset.ts │ ├── NativeDeletable.ts │ ├── NativeIntPoint.ts │ ├── NativeIntRect.ts │ ├── NativePath.ts │ ├── NativePaths.ts │ ├── NativePolyNode.ts │ ├── NativePolyTree.ts │ ├── NativeVector.ts │ ├── PathToNativePath.ts │ ├── PathsToNativePaths.ts │ ├── mem.ts │ ├── nativeEnumConversion.ts │ └── nativeEnums.ts ├── nativeFinalizationRegistry.ts ├── offsetFunctions.ts └── wasm │ ├── binding.hpp │ ├── clipper-wasm.js │ ├── clipper.cpp │ ├── clipper.hpp │ └── clipper.js ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | end_of_line = lf 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /**/*.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | "prettier", 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:import/recommended", 12 | "plugin:import/typescript", 13 | ], 14 | parser: "@typescript-eslint/parser", 15 | parserOptions: { 16 | project: "tsconfig.json", 17 | sourceType: "module", 18 | }, 19 | plugins: ["eslint-plugin-import", "@typescript-eslint", "prettier"], 20 | rules: { 21 | "@typescript-eslint/adjacent-overload-signatures": "error", 22 | "@typescript-eslint/consistent-type-assertions": "error", 23 | "@typescript-eslint/dot-notation": "error", 24 | "@typescript-eslint/explicit-member-accessibility": [ 25 | "error", 26 | { 27 | accessibility: "no-public", 28 | }, 29 | ], 30 | "@typescript-eslint/no-empty-function": "error", 31 | "@typescript-eslint/no-empty-interface": "error", 32 | "@typescript-eslint/no-misused-new": "error", 33 | "@typescript-eslint/no-namespace": "error", 34 | "@typescript-eslint/no-shadow": [ 35 | "error", 36 | { 37 | hoist: "all", 38 | }, 39 | ], 40 | "@typescript-eslint/no-this-alias": "error", 41 | "@typescript-eslint/no-unused-expressions": "error", 42 | "@typescript-eslint/prefer-for-of": "error", 43 | "@typescript-eslint/prefer-function-type": "error", 44 | "@typescript-eslint/prefer-namespace-keyword": "error", 45 | "@typescript-eslint/unified-signatures": "error", 46 | }, 47 | overrides: [ 48 | { 49 | files: "__tests__/**/*.+(ts|tsx)", 50 | parserOptions: { 51 | project: "./__tests__/tsconfig.json", 52 | }, 53 | }, 54 | ], 55 | }; 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .idea 3 | /dist/ 4 | /node_modules/ 5 | /src/wasm/*.js 6 | /universal/ 7 | /web/ 8 | .yarn/* 9 | !.yarn/patches 10 | !.yarn/releases 11 | !.yarn/plugins 12 | !.yarn/sdks 13 | !.yarn/versions 14 | .pnp.* 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /universal 2 | /web 3 | __tests__/external-libs 4 | docs 5 | README.md 6 | src/wasm/clipper*.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "node" 5 | 6 | cache: 7 | yarn: true 8 | directories: 9 | - node_modules 10 | 11 | sudo: required 12 | 13 | services: 14 | - docker 15 | 16 | script: yarn travis 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest Current File", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": ["--ci", "-i", "${fileBasenameNoExtension}"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "windows": { 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 18 | }, 19 | "skipFiles": ["${workspaceFolder}/node_modules/**/*.js", "/**/*.js"], 20 | "cwd": "${workspaceFolder}" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } 4 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: "@yarnpkg/plugin-interactive-tools" 6 | 7 | yarnPath: .yarn/releases/yarn-3.4.1.cjs 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v1.3.1 4 | 5 | - Fixed `scalePath`/`scalePaths` from reversing the path. 6 | - Fixed the type definitions of `clipToPaths` and `clipToPolyTree`. The did not return undefined. 7 | 8 | ## v1.3.0 9 | 10 | - Made it work with the latest version of emscripten 11 | - Now using yarn v3 12 | - Updated all dependencies 13 | - Exported the `SubjectInput` type. 14 | 15 | ## v1.2.1 16 | 17 | - Use direct requires so bundlers have an easier time. 18 | 19 | ## v1.2.0 20 | 21 | - Updated dependencies. 22 | - Compiled with the latest version of emscripten and in modularize mode (which should be more compatible with node). 23 | 24 | ## v1.1.0 25 | 26 | - Compiled with the latest version of emscripten. 27 | - Will now use `FinalizationRegistry` when provided by the runtime to avoid mem leaks whenever calling `dispose()` is forgotten. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 xaviergonz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-angusj-clipper 2 | 3 | #### _Polygon and line clipping and offsetting library for Javascript/Typescript_ 4 | 5 | _a port of Angus Johnson's clipper to WebAssembly/Asm.js_ 6 | 7 | [![npm version](https://badge.fury.io/js/js-angusj-clipper.svg)](https://badge.fury.io/js/js-angusj-clipper) 8 | [![Build Status](https://travis-ci.org/xaviergonz/js-angusj-clipper.svg?branch=master)](https://travis-ci.org/xaviergonz/js-angusj-clipper) 9 | 10 | --- 11 | 12 | Install it with `npm install --save js-angusj-clipper` 13 | 14 | **To support this project star it on [github](https://github.com/xaviergonz/js-angusj-clipper)!** 15 | 16 | --- 17 | 18 | ### What is this? 19 | 20 | A library to make polygon clipping (boolean operations) and offsetting **fast** on Javascript thanks 21 | to WebAssembly with a fallback to Asm.js, based on the excellent Polygon Clipping (also known as Clipper) library by 22 | Angus Johnson. 23 | 24 | --- 25 | 26 | ### Why? 27 | 28 | Because sometimes performance does matter and I could not find a javascript library 29 | as fast or as rock solid as the C++ version of [Clipper](https://sourceforge.net/projects/polyclipping/). 30 | 31 | As an example, the results of the benchmarks included on the test suite when running on my machine (node 17.9.0) are: 32 | 33 | _Note, pureJs is [jsclipper](https://sourceforge.net/projects/jsclipper/), a pure JS port of the same library_ 34 | 35 | ``` 36 | 500 boolean operations over two circles of 5000 points each 37 | clipType: intersection, subjectFillType: evenOdd 38 | ✓ wasm (212 ms) 39 | ✓ asmJs (598 ms) 40 | ✓ pureJs (573 ms) 41 | clipType: union, subjectFillType: evenOdd 42 | ✓ wasm (267 ms) 43 | ✓ asmJs (666 ms) 44 | ✓ pureJs (663 ms) 45 | clipType: difference, subjectFillType: evenOdd 46 | ✓ wasm (232 ms) 47 | ✓ asmJs (575 ms) 48 | ✓ pureJs (573 ms) 49 | clipType: xor, subjectFillType: evenOdd 50 | ✓ wasm (296 ms) 51 | ✓ asmJs (681 ms) 52 | ✓ pureJs (779 ms) 53 | 10000 boolean operations over two circles of 100 points each 54 | clipType: intersection, subjectFillType: evenOdd 55 | ✓ wasm (143 ms) 56 | ✓ asmJs (347 ms) 57 | ✓ pureJs (255 ms) 58 | clipType: union, subjectFillType: evenOdd 59 | ✓ wasm (181 ms) 60 | ✓ asmJs (417 ms) 61 | ✓ pureJs (265 ms) 62 | clipType: difference, subjectFillType: evenOdd 63 | ✓ wasm (159 ms) 64 | ✓ asmJs (339 ms) 65 | ✓ pureJs (239 ms) 66 | clipType: xor, subjectFillType: evenOdd 67 | ✓ wasm (186 ms) 68 | ✓ asmJs (404 ms) 69 | ✓ pureJs (262 ms) 70 | 100 offset operations over a circle of 5000 points 71 | joinType: miter, endType: closedPolygon, delta: 5 72 | ✓ wasm (129 ms) 73 | ✓ asmJs (390 ms) 74 | ✓ pureJs (702 ms) 75 | joinType: miter, endType: closedPolygon, delta: 0 76 | ✓ wasm (34 ms) 77 | ✓ asmJs (140 ms) 78 | ✓ pureJs (108 ms) 79 | joinType: miter, endType: closedPolygon, delta: -5 80 | ✓ wasm (146 ms) 81 | ✓ asmJs (386 ms) 82 | ✓ pureJs (770 ms) 83 | 5000 offset operations over a circle of 100 points 84 | joinType: miter, endType: closedPolygon, delta: 5 85 | ✓ wasm (74 ms) 86 | ✓ asmJs (161 ms) 87 | ✓ pureJs (278 ms) 88 | joinType: miter, endType: closedPolygon, delta: 0 89 | ✓ wasm (61 ms) 90 | ✓ asmJs (138 ms) 91 | ✓ pureJs (162 ms) 92 | joinType: miter, endType: closedPolygon, delta: -5 93 | ✓ wasm (109 ms) 94 | ✓ asmJs (271 ms) 95 | ✓ pureJs (659 ms) 96 | ``` 97 | 98 | More or less, the results for **boolean operations** over moderately big polygons are: 99 | 100 | - Pure JS port of the Clipper library: **~1.0s, baseline** 101 | - This library (_WebAssembly_): **~0.4s** 102 | - This library (_Asm.js_): **~1.0s** (mostly due to the emulation of 64-bit integer operations) 103 | 104 | and for small polygons are: 105 | 106 | - Pure JS port of the Clipper library: **~1.0s, baseline** 107 | - This library (_WebAssembly_): **~0.7s** (due to the overhead of copying structures to/from JS/C++) 108 | - This library (_Asm.js_): **~1.4s** (mostly due to the emulation of 64-bit integer operations + the overhead of copying structures to/from JS/C++) 109 | 110 | As for **offsetting**, the results for a moderately big polygon are: 111 | 112 | - Pure JS port of the Clipper library: **~1s, baseline** 113 | - This library (_WebAssembly_): **~0.2s** 114 | - This library (_Asm.js_): **~0.5s** 115 | 116 | and for small polygons are: 117 | 118 | - Pure JS port of the Clipper library: **~1s, baseline** 119 | - This library (_WebAssembly_): **~0.2s** 120 | - This library (_Asm.js_): **~0.5s** 121 | 122 | --- 123 | 124 | ### Getting started 125 | 126 | ```js 127 | // universal version 128 | // import it with 129 | import * as clipperLib from "js-angusj-clipper"; // es6 / typescript 130 | // or 131 | const clipperLib = require("js-angusj-clipper"); // nodejs style require 132 | 133 | // web-only version (for example for angular 6+) 134 | // import it with 135 | import * as clipperLib from "js-angusj-clipper/web"; // es6 / typescript 136 | // or 137 | const clipperLib = require("js-angusj-clipper/web"); // nodejs style require 138 | 139 | async function mainAsync() { 140 | // create an instance of the library (usually only do this once in your app) 141 | const clipper = await clipperLib.loadNativeClipperLibInstanceAsync( 142 | // let it autodetect which one to use, but also available WasmOnly and AsmJsOnly 143 | clipperLib.NativeClipperLibRequestedFormat.WasmWithAsmJsFallback 144 | ); 145 | 146 | // create some polygons (note that they MUST be integer coordinates) 147 | const poly1 = [{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }]; 148 | 149 | const poly2 = [{ x: 10, y: 0 }, { x: 20, y: 0 }, { x: 20, y: 10 }, { x: 10, y: 10 }]; 150 | 151 | // get their union 152 | const polyResult = clipper.clipToPaths({ 153 | clipType: clipperLib.ClipType.Union, 154 | 155 | subjectInputs: [{ data: poly1, closed: true }], 156 | 157 | clipInputs: [{ data: poly2 }], 158 | 159 | subjectFillType: clipperLib.PolyFillType.EvenOdd 160 | }); 161 | 162 | /* polyResult will be: 163 | [ 164 | [ 165 | { x: 0, y: 0 }, 166 | { x: 20, y: 0 }, 167 | { x: 20, y: 10 }, 168 | { x: 0, y: 10 } 169 | ] 170 | ] 171 | */ 172 | } 173 | 174 | mainAsync(); 175 | ``` 176 | 177 | --- 178 | 179 | For an in-depth description of the library see: 180 | 181 | - [Overview](./docs/overview/index.md) 182 | - [API Reference](./docs/apiReference/index.md) 183 | - [FAQ](./docs/faq/index.md) 184 | -------------------------------------------------------------------------------- /__tests__/pureJs.ts: -------------------------------------------------------------------------------- 1 | import * as clipperLib from "../src"; 2 | 3 | // another port of clipper in pure js 4 | export const pureJsClipperLib = require("./external-libs/clipper"); 5 | 6 | export interface PureJsPoint { 7 | X: number; 8 | Y: number; 9 | } 10 | 11 | export type PureJsPath = PureJsPoint[]; 12 | export type PureJsPaths = PureJsPath[]; 13 | 14 | function isPureJsPaths(path: PureJsPath | PureJsPaths): path is PureJsPaths { 15 | return Array.isArray(path) && Array.isArray(path[0]); 16 | } 17 | 18 | export function pureJsTestPolyOperation( 19 | clipType: clipperLib.ClipType, 20 | subjectFillType: clipperLib.PolyFillType, 21 | subjectInput: PureJsPath | PureJsPaths, 22 | clipInput: PureJsPath | PureJsPaths, 23 | subjectClosed = true 24 | ) { 25 | const cl = new pureJsClipperLib.Clipper(); 26 | if (isPureJsPaths(subjectInput)) { 27 | cl.AddPaths(subjectInput, pureJsClipperLib.PolyType.ptSubject, subjectClosed); 28 | } else { 29 | cl.AddPath(subjectInput, pureJsClipperLib.PolyType.ptSubject, subjectClosed); 30 | } 31 | if (isPureJsPaths(clipInput)) { 32 | cl.AddPaths(clipInput, pureJsClipperLib.PolyType.ptClip, true); 33 | } else { 34 | cl.AddPath(clipInput, pureJsClipperLib.PolyType.ptClip, true); 35 | } 36 | 37 | const solutionPaths = new pureJsClipperLib.Paths(); 38 | cl.Execute( 39 | clipTypeToPureJs(clipType), 40 | solutionPaths, 41 | polyFillTypeToPureJs(subjectFillType), 42 | polyFillTypeToPureJs(subjectFillType) 43 | ); 44 | 45 | return solutionPaths; 46 | } 47 | 48 | export function pureJsTestOffset( 49 | input: PureJsPath | PureJsPaths, 50 | joinType: clipperLib.JoinType, 51 | endType: clipperLib.EndType, 52 | delta: number 53 | ) { 54 | const co = new pureJsClipperLib.ClipperOffset(); 55 | if (isPureJsPaths(input)) { 56 | co.AddPaths(input, joinTypeToPureJs(joinType), endTypeToPureJs(endType)); 57 | } else { 58 | co.AddPath(input, joinTypeToPureJs(joinType), endTypeToPureJs(endType)); 59 | } 60 | 61 | const solutionPaths = new pureJsClipperLib.Paths(); 62 | co.Execute(solutionPaths, delta); 63 | 64 | return solutionPaths; 65 | } 66 | 67 | export function pathToPureJs(path: clipperLib.ReadonlyPath): PureJsPath { 68 | return path.map((p) => ({ 69 | X: p.x, 70 | Y: p.y, 71 | })); 72 | } 73 | 74 | export function pathsToPureJs(paths: clipperLib.ReadonlyPaths): PureJsPaths { 75 | return paths.map((p) => pathToPureJs(p)); 76 | } 77 | 78 | export function clipTypeToPureJs(clipType: clipperLib.ClipType): number { 79 | switch (clipType) { 80 | case clipperLib.ClipType.Difference: 81 | return pureJsClipperLib.ClipType.ctDifference; 82 | case clipperLib.ClipType.Intersection: 83 | return pureJsClipperLib.ClipType.ctIntersection; 84 | case clipperLib.ClipType.Union: 85 | return pureJsClipperLib.ClipType.ctUnion; 86 | case clipperLib.ClipType.Xor: 87 | return pureJsClipperLib.ClipType.ctXor; 88 | default: 89 | return -1; 90 | } 91 | } 92 | 93 | export function polyFillTypeToPureJs(polyFillType: clipperLib.PolyFillType): number { 94 | switch (polyFillType) { 95 | case clipperLib.PolyFillType.EvenOdd: 96 | return pureJsClipperLib.PolyFillType.pftEvenOdd; 97 | case clipperLib.PolyFillType.Negative: 98 | return pureJsClipperLib.PolyFillType.pftNegative; 99 | case clipperLib.PolyFillType.NonZero: 100 | return pureJsClipperLib.PolyFillType.pftNonZero; 101 | case clipperLib.PolyFillType.Positive: 102 | return pureJsClipperLib.PolyFillType.pftPositive; 103 | default: 104 | return -1; 105 | } 106 | } 107 | 108 | export function joinTypeToPureJs(joinType: clipperLib.JoinType): number { 109 | switch (joinType) { 110 | case clipperLib.JoinType.Miter: 111 | return pureJsClipperLib.JoinType.jtMiter; 112 | case clipperLib.JoinType.Round: 113 | return pureJsClipperLib.JoinType.jtRound; 114 | case clipperLib.JoinType.Square: 115 | return pureJsClipperLib.JoinType.jtSquare; 116 | default: 117 | return -1; 118 | } 119 | } 120 | 121 | export function endTypeToPureJs(endType: clipperLib.EndType): number { 122 | switch (endType) { 123 | case clipperLib.EndType.ClosedLine: 124 | return pureJsClipperLib.EndType.etClosedLine; 125 | case clipperLib.EndType.ClosedPolygon: 126 | return pureJsClipperLib.EndType.etClosedPolygon; 127 | case clipperLib.EndType.OpenButt: 128 | return pureJsClipperLib.EndType.etOpenButt; 129 | case clipperLib.EndType.OpenRound: 130 | return pureJsClipperLib.EndType.etOpenRound; 131 | case clipperLib.EndType.OpenSquare: 132 | return pureJsClipperLib.EndType.etOpenSquare; 133 | default: 134 | return -1; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /__tests__/tests.spec.ts: -------------------------------------------------------------------------------- 1 | import * as clipperLib from "../src"; 2 | import { hiRange } from "../src/constants"; 3 | import { 4 | pathsToPureJs, 5 | pathToPureJs, 6 | pureJsClipperLib, 7 | pureJsTestOffset, 8 | pureJsTestPolyOperation, 9 | } from "./pureJs"; 10 | import { circlePath } from "./utils"; 11 | 12 | // used by pureJs implementation 13 | globalThis.alert = (msg) => console.error("window alert: ", msg); 14 | 15 | let clipperWasm: clipperLib.ClipperLibWrapper; 16 | let clipperAsmJs: clipperLib.ClipperLibWrapper; 17 | 18 | beforeAll(async () => { 19 | clipperWasm = await clipperLib.loadNativeClipperLibInstanceAsync( 20 | clipperLib.NativeClipperLibRequestedFormat.WasmOnly 21 | ); 22 | clipperAsmJs = await clipperLib.loadNativeClipperLibInstanceAsync( 23 | clipperLib.NativeClipperLibRequestedFormat.AsmJsOnly 24 | ); 25 | }, 60000); 26 | 27 | describe("unit tests", () => { 28 | test("wasm instance must be loaded", () => { 29 | expect(clipperWasm).toBeDefined(); 30 | expect(clipperWasm.instance).toBeDefined(); 31 | expect(clipperWasm.format).toEqual(clipperLib.NativeClipperLibLoadedFormat.Wasm); 32 | }); 33 | 34 | test("asmjs instance must be loaded", () => { 35 | expect(clipperAsmJs).toBeDefined(); 36 | expect(clipperAsmJs.instance).toBeDefined(); 37 | expect(clipperAsmJs.format).toEqual(clipperLib.NativeClipperLibLoadedFormat.AsmJs); 38 | }); 39 | 40 | test("pureJs instance must be loaded", () => { 41 | expect(pureJsClipperLib).toBeDefined(); 42 | expect(new pureJsClipperLib.Clipper()).toBeDefined(); 43 | }); 44 | 45 | describe("simple polygons", () => { 46 | // create some polygons (note that they MUST be integer coordinates) 47 | const poly1 = [ 48 | { x: 0, y: 10 }, 49 | { x: Math.trunc(hiRange / 3), y: 10 }, 50 | { x: Math.trunc(hiRange / 3), y: 20 }, 51 | { x: 0, y: 20 }, 52 | ]; 53 | const pureJsPoly1 = pathToPureJs(poly1); 54 | 55 | const poly2 = [ 56 | { x: 10, y: 0 }, 57 | { x: Math.trunc(hiRange / 4), y: 0 }, 58 | { x: Math.trunc(hiRange / 4), y: 30 }, 59 | { x: 10, y: 30 }, 60 | ]; 61 | const pureJsPoly2 = pathToPureJs(poly2); 62 | 63 | describe("boolean operations", () => { 64 | for (const clipType of [ 65 | clipperLib.ClipType.Intersection, 66 | clipperLib.ClipType.Union, 67 | clipperLib.ClipType.Difference, 68 | clipperLib.ClipType.Xor, 69 | ]) { 70 | for (const polyFillType of [ 71 | clipperLib.PolyFillType.EvenOdd, 72 | clipperLib.PolyFillType.NonZero, 73 | clipperLib.PolyFillType.Negative, 74 | clipperLib.PolyFillType.Positive, 75 | ]) { 76 | test(`clipType: ${clipType}, fillType: ${polyFillType}`, () => { 77 | const res = testPolyOperation(clipType, polyFillType, poly1, poly2, { 78 | wasm: true, 79 | asm: true, 80 | }); 81 | 82 | const pureJsRes = pureJsTestPolyOperation( 83 | clipType, 84 | polyFillType, 85 | pureJsPoly1, 86 | pureJsPoly2 87 | ); 88 | 89 | expect(res.asmResult).toEqual(res.wasmResult); 90 | expect(pureJsRes).toEqual(pathsToPureJs(res.wasmResult!)); 91 | expect(res.wasmResult).toMatchSnapshot(); 92 | }); 93 | } 94 | } 95 | }); 96 | 97 | describe("offset", () => { 98 | for (const joinType of [ 99 | clipperLib.JoinType.Miter, 100 | clipperLib.JoinType.Round, 101 | clipperLib.JoinType.Square, 102 | ]) { 103 | for (const endType of [ 104 | clipperLib.EndType.ClosedPolygon, 105 | clipperLib.EndType.ClosedLine, 106 | clipperLib.EndType.OpenButt, 107 | clipperLib.EndType.OpenRound, 108 | clipperLib.EndType.OpenSquare, 109 | ]) { 110 | for (const delta of [5, 0, -5]) { 111 | test(`joinType: ${joinType}, endType: ${endType}, delta: ${delta}`, () => { 112 | const res = testOffset(poly1, joinType, endType, delta, { 113 | wasm: true, 114 | asm: true, 115 | }); 116 | 117 | const pureJsRes = pureJsTestOffset(pureJsPoly1, joinType, endType, delta); 118 | 119 | expect(res.asmResult).toEqual(res.wasmResult); 120 | expect(pureJsRes).toEqual(pathsToPureJs(res.wasmResult!)); 121 | expect(res.wasmResult).toMatchSnapshot(); 122 | }); 123 | } 124 | } 125 | } 126 | }); 127 | }); 128 | 129 | test("using clipToPaths with open paths should throw", () => { 130 | const clipType = clipperLib.ClipType.Intersection; 131 | const polyFillType = clipperLib.PolyFillType.Positive; 132 | 133 | const poly1 = [ 134 | { x: 10, y: 10 }, 135 | { x: 90, y: 10 }, 136 | { x: 90, y: 90 }, 137 | ]; 138 | const poly2 = [ 139 | { x: 0, y: 0 }, 140 | { x: 50, y: 0 }, 141 | { x: 50, y: 50 }, 142 | { x: 0, y: 50 }, 143 | ]; 144 | 145 | function testShouldThrow(wasm: boolean) { 146 | expect(() => { 147 | testPolyOperation( 148 | clipType, 149 | polyFillType, 150 | poly1, 151 | poly2, 152 | { wasm: wasm, asm: !wasm }, 153 | false, 154 | false 155 | ); 156 | }).toThrow("clip to a PolyTree (not to a Path) when using open paths"); 157 | } 158 | 159 | testShouldThrow(true); 160 | testShouldThrow(false); 161 | }); 162 | 163 | describe("issue #4", () => { 164 | for (const subjectClosed of [true, false]) { 165 | test(`subjectClosed: ${subjectClosed}`, () => { 166 | const clipType = clipperLib.ClipType.Intersection; 167 | const polyFillType = clipperLib.PolyFillType.Positive; 168 | 169 | const poly1 = [ 170 | { x: 10, y: 10 }, 171 | { x: 90, y: 10 }, 172 | { x: 90, y: 90 }, 173 | ]; 174 | const pureJsPoly1 = pathToPureJs(poly1); 175 | 176 | const poly2 = [ 177 | { x: 0, y: 0 }, 178 | { x: 50, y: 0 }, 179 | { x: 50, y: 50 }, 180 | { x: 0, y: 50 }, 181 | ]; 182 | const pureJsPoly2 = pathToPureJs(poly2); 183 | 184 | const pureJsRes = pureJsTestPolyOperation( 185 | clipType, 186 | polyFillType, 187 | pureJsPoly1, 188 | pureJsPoly2, 189 | subjectClosed 190 | ); 191 | 192 | const res = testPolyOperation( 193 | clipType, 194 | polyFillType, 195 | poly1, 196 | poly2, 197 | { wasm: true, asm: true }, 198 | subjectClosed, 199 | true 200 | ); 201 | 202 | expect(res.ptAsmResult).toEqual(res.ptWasmResult); 203 | const open = clipperWasm.openPathsFromPolyTree(res.ptWasmResult!); 204 | const closed = clipperWasm.closedPathsFromPolyTree(res.ptWasmResult!); 205 | if (subjectClosed) { 206 | expect(pureJsRes).toEqual(pathsToPureJs(closed)); 207 | expect(open.length).toBe(0); 208 | } else { 209 | expect(pureJsRes).toEqual(pathsToPureJs(open)); 210 | expect(closed.length).toBe(0); 211 | } 212 | expect(res.ptWasmResult).toMatchSnapshot(); 213 | }); 214 | } 215 | }); 216 | 217 | test("issue #9", () => { 218 | const clipper = clipperWasm; 219 | const request = { 220 | clipType: clipperLib.ClipType.Union, 221 | subjectInputs: [ 222 | { 223 | data: [ 224 | { x: 50, y: 50 }, 225 | { x: -50, y: 50 }, 226 | { x: -50, y: -50 }, 227 | { x: 50, y: -50 }, 228 | ], 229 | closed: true, 230 | }, 231 | { 232 | data: [ 233 | { x: -5, y: -5 }, 234 | { x: -5, y: 5 }, 235 | { x: 5, y: 5 }, 236 | { x: 5, y: -5 }, 237 | ], 238 | closed: true, 239 | }, 240 | ], 241 | subjectFillType: clipperLib.PolyFillType.NonZero, 242 | strictlySimple: true, 243 | }; 244 | const result = clipper.clipToPolyTree(request); 245 | expect(result).toMatchSnapshot(); 246 | }); 247 | }); 248 | 249 | describe("benchmarks", () => { 250 | const oldNodeEnv = process.env.NODE_ENV; 251 | beforeAll(() => { 252 | process.env.NODE_ENV = "production"; 253 | }); 254 | afterAll(() => { 255 | process.env.NODE_ENV = oldNodeEnv; 256 | }); 257 | 258 | for (const benchmark of [ 259 | { ops: 500, points: 5000 }, 260 | { ops: 10000, points: 100 }, 261 | ]) { 262 | describe(`${benchmark.ops} boolean operations over two circles of ${benchmark.points} points each`, () => { 263 | const poly1 = circlePath({ x: 1000, y: 1000 }, 1000, benchmark.points); 264 | const poly2 = circlePath({ x: 2500, y: 1000 }, 1000, benchmark.points); 265 | const pureJsPoly1 = pathToPureJs(poly1); 266 | const pureJsPoly2 = pathToPureJs(poly2); 267 | const scale = 100; 268 | pureJsClipperLib.JS.ScaleUpPaths(pureJsPoly1, scale); 269 | pureJsClipperLib.JS.ScaleUpPaths(pureJsPoly2, scale); 270 | 271 | for (const clipType of [ 272 | clipperLib.ClipType.Intersection, 273 | clipperLib.ClipType.Union, 274 | clipperLib.ClipType.Difference, 275 | clipperLib.ClipType.Xor, 276 | ]) { 277 | for (const polyFillType of [ 278 | clipperLib.PolyFillType.EvenOdd, 279 | // clipperLib.PolyFillType.NonZero, 280 | // clipperLib.PolyFillType.Negative, 281 | // clipperLib.PolyFillType.Positive, 282 | ]) { 283 | describe(`clipType: ${clipType}, subjectFillType: ${polyFillType}`, () => { 284 | for (const mode of ["wasm", "asmJs", "pureJs"]) { 285 | test(`${mode}`, () => { 286 | for (let i = 0; i < benchmark.ops; i++) { 287 | if (mode === "wasm" || mode === "asmJs") { 288 | testPolyOperation(clipType, polyFillType, poly1, poly2, { 289 | wasm: mode === "wasm", 290 | asm: mode === "asmJs", 291 | }); 292 | } else if (mode === "pureJs") { 293 | pureJsTestPolyOperation(clipType, polyFillType, pureJsPoly1, pureJsPoly2); 294 | } 295 | } 296 | }); 297 | } 298 | }); 299 | } 300 | } 301 | }); 302 | } 303 | 304 | for (const benchmark of [ 305 | { ops: 100, points: 5000 }, 306 | { ops: 5000, points: 100 }, 307 | ]) { 308 | describe(`${benchmark.ops} offset operations over a circle of ${benchmark.points} points`, () => { 309 | const poly1 = circlePath({ x: 1000, y: 1000 }, 1000, benchmark.points); 310 | const pureJsPoly1 = pathToPureJs(poly1); 311 | const scale = 100; 312 | pureJsClipperLib.JS.ScaleUpPaths(pureJsPoly1, scale); 313 | 314 | for (const joinType of [ 315 | clipperLib.JoinType.Miter, 316 | // clipperLib.JoinType.Round, 317 | // clipperLib.JoinType.Square 318 | ]) { 319 | for (const endType of [ 320 | clipperLib.EndType.ClosedPolygon, 321 | // clipperLib.EndType.ClosedLine, 322 | // clipperLib.EndType.OpenButt, 323 | // clipperLib.EndType.OpenRound, 324 | // clipperLib.EndType.OpenSquare, 325 | ]) { 326 | for (const delta of [5, 0, -5]) { 327 | describe(`joinType: ${joinType}, endType: ${endType}, delta: ${delta}`, () => { 328 | for (const mode of ["wasm", "asmJs", "pureJs"]) { 329 | test(`${mode}`, () => { 330 | for (let i = 0; i < benchmark.ops; i++) { 331 | if (mode === "wasm" || mode === "asmJs") { 332 | testOffset(poly1, joinType, endType, delta, { 333 | wasm: mode === "wasm", 334 | asm: mode === "asmJs", 335 | }); 336 | } else if (mode === "pureJs") { 337 | pureJsTestOffset(pureJsPoly1, joinType, endType, delta); 338 | } 339 | } 340 | }); 341 | } 342 | }); 343 | } 344 | } 345 | } 346 | }); 347 | } 348 | }); 349 | 350 | function testPolyOperation( 351 | clipType: clipperLib.ClipType, 352 | subjectFillType: clipperLib.PolyFillType, 353 | subjectInput: clipperLib.Path | clipperLib.Paths, 354 | clipInput: clipperLib.Path | clipperLib.Paths, 355 | format: { wasm: boolean; asm: boolean }, 356 | subjectInputClosed = true, 357 | clipToPolyTrees = false 358 | ) { 359 | const data = { 360 | clipType: clipType, 361 | 362 | subjectInputs: [{ data: subjectInput, closed: subjectInputClosed }], 363 | 364 | clipInputs: [{ data: clipInput }], 365 | 366 | subjectFillType: subjectFillType, 367 | }; 368 | 369 | const pathResults = !clipToPolyTrees 370 | ? { 371 | asmResult: format.asm ? clipperAsmJs.clipToPaths(data) : undefined, 372 | wasmResult: format.wasm ? clipperWasm.clipToPaths(data) : undefined, 373 | } 374 | : {}; 375 | 376 | const polyTreeResults = clipToPolyTrees 377 | ? { 378 | ptAsmResult: format.asm ? clipperAsmJs.clipToPolyTree(data) : undefined, 379 | ptWasmResult: format.wasm ? clipperWasm.clipToPolyTree(data) : undefined, 380 | } 381 | : {}; 382 | 383 | return { 384 | ...pathResults, 385 | ...polyTreeResults, 386 | }; 387 | } 388 | 389 | function testOffset( 390 | input: clipperLib.Path | clipperLib.Paths, 391 | joinType: clipperLib.JoinType, 392 | endType: clipperLib.EndType, 393 | delta: number, 394 | format: { wasm: boolean; asm: boolean } 395 | ) { 396 | const data: clipperLib.OffsetParams = { 397 | delta: delta, 398 | offsetInputs: [ 399 | { 400 | joinType: joinType, 401 | endType: endType, 402 | data: input, 403 | }, 404 | ], 405 | }; 406 | 407 | return { 408 | wasmResult: format.wasm ? clipperWasm.offsetToPaths(data) : undefined, 409 | asmResult: format.asm ? clipperAsmJs.offsetToPaths(data) : undefined, 410 | }; 411 | } 412 | -------------------------------------------------------------------------------- /__tests__/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["../src/**/*", "./**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | import * as clipperLib from "../src"; 2 | 3 | export function circlePath( 4 | center: clipperLib.IntPoint, 5 | radius: number, 6 | points: number 7 | ): clipperLib.Path { 8 | const path = []; 9 | 10 | for (let i = 0; i < points; i++) { 11 | const radAngle = (i / points) * (Math.PI * 2); 12 | const p = { 13 | x: Math.round(center.x + Math.cos(radAngle) * radius), 14 | y: Math.round(center.y + Math.sin(radAngle) * radius), 15 | }; 16 | path.push(p); 17 | } 18 | 19 | return path; 20 | } 21 | -------------------------------------------------------------------------------- /build/compile-asm.ts: -------------------------------------------------------------------------------- 1 | import * as shelljs from "shelljs"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import * as commandLineArgs from "command-line-args"; 5 | 6 | const cmdLineOptions = commandLineArgs([{ name: "env", type: String, defaultValue: "universal" }]); 7 | 8 | const wasmDir = path.join(__dirname, "..", "src", "wasm"); 9 | console.log(`using "${wasmDir}" as wasm dir`); 10 | 11 | function build(wasmMode: boolean, environment: string) { 12 | const debug = false; 13 | 14 | const options = [ 15 | "--bind", 16 | "--no-entry", 17 | "-s STRICT=1", 18 | "-s ALLOW_MEMORY_GROWTH=1", 19 | "-s EXIT_RUNTIME=0", 20 | "-s SINGLE_FILE=1", 21 | "-s INVOKE_RUN=0", 22 | "-s NODEJS_CATCH_EXIT=0", 23 | "-s NO_FILESYSTEM=1", 24 | "-s MODULARIZE=1", 25 | "-s STRICT_JS=0", // adds "use strict" and breaks module compilation 26 | "-s EXPORTED_FUNCTIONS=\"['_malloc', '_free']\"", // without this these functions are not exported 27 | // no speed difference 28 | // "-s WASM_BIGINT=1", 29 | // wasm with asmjs fallback, but does not work with SINGLE_FILE 30 | // "-s WASM=2", 31 | ...(debug 32 | ? ["-s DISABLE_EXCEPTION_CATCHING=0", "-O0"] 33 | : [ 34 | // "-s ASSERTIONS=0", 35 | // "-s PRECISE_I64_MATH=0", 36 | // "-s ALIASING_FUNCTION_POINTERS=1", 37 | "-O3", 38 | ]), 39 | ]; 40 | if (environment !== "universal") { 41 | options.push(`-s ENVIRONMENT=${environment}`); 42 | } 43 | 44 | if (wasmMode) { 45 | options.push("-s WASM=1"); 46 | } else { 47 | options.push("-s WASM=0"); 48 | } 49 | 50 | const output = wasmMode ? `clipper-wasm.js` : `clipper.js`; 51 | 52 | const cmd = `docker run --rm -v ${wasmDir}:/src emscripten/emsdk em++ ${options.join( 53 | " " 54 | )} clipper.cpp -o ${output}`; 55 | const returnData = shelljs.exec(cmd); 56 | if (returnData.code !== 0) { 57 | console.error(`build failed with error code ${returnData.code}`); 58 | process.exit(returnData.code); 59 | } 60 | 61 | shelljs.mkdir("dist", "dist/wasm"); 62 | shelljs.cp(`src/wasm/${output}`, `dist/wasm/${output}`); 63 | } 64 | 65 | console.log("building asmjs version for env " + cmdLineOptions.env); 66 | build(false, cmdLineOptions.env); 67 | console.log("building wasm version for env " + cmdLineOptions.env); 68 | build(true, cmdLineOptions.env); 69 | -------------------------------------------------------------------------------- /build/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["./**/*.ts"], 4 | "compilerOptions": { 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docs/apiReference/clipping/ClipInput.md: -------------------------------------------------------------------------------- 1 | #### interface ClipInput 2 | 3 | A single clip input (of multiple possible inputs) for the clipToPaths / clipToPolyTree operations. 4 | 5 | Clipping paths must always be closed. Clipper allows polygons to clip both lines and other polygons, but doesn't allow lines to clip either lines or polygons. 6 | With closed paths, orientation should conform with the filling rule that will be passed via Clipper's execute method. 7 | 8 | ###### Required properties 9 | 10 | * **data: [Path](../shared/Path.md) | [Paths](../shared/Paths.md)** 11 | 12 | Path / Paths data. 13 | 14 | Path Coordinate range: 15 | 16 | Path coordinates must be between ± 9007199254740991 (Number.MAX_SAFE_INTEGER), otherwise a range error will be thrown when attempting to add the path to the Clipper object. 17 | If coordinates can be kept between ± 0x3FFFFFFF (± 1.0e+9), a modest increase in performance (approx. 15-20%) over the larger range can be achieved by 18 | avoiding large integer math. 19 | 20 | The function operation will throw an error if the path is invalid for clipping. A path is invalid for clipping when: 21 | - it has less than 2 vertices 22 | - it has 2 vertices but is not an open path 23 | - the vertices are all co-linear and it is not an open path 24 | -------------------------------------------------------------------------------- /docs/apiReference/clipping/ClipParams.md: -------------------------------------------------------------------------------- 1 | #### interface ClipParams 2 | 3 | Params for the clipToPaths / clipToPolyTree operations. 4 | 5 | Any number of subject and clip paths can be added to a clipping task. 6 | 7 | Boolean (clipping) operations are mostly applied to two sets of Polygons, represented in this library as subject and clip polygons. Whenever Polygons 8 | are added to the Clipper object, they must be assigned to either subject or clip polygons. 9 | 10 | UNION operations can be performed on one set or both sets of polygons, but all other boolean operations require both sets of polygons to derive 11 | meaningful solutions. 12 | 13 | ###### Required properties 14 | 15 | * **clipType: [ClipType](./ClipType.md)** 16 | 17 | Clipping operation type (Intersection, Union, Difference or Xor). 18 | 19 | * **subjectFillType: [PolyFillType](../shared/PolyFillType.md)** 20 | 21 | Winding (fill) rule for subject polygons. 22 | 23 | * **subjectInputs: [SubjectInput](./SubjectInput.md)[]** 24 | 25 | Subject inputs. 26 | 27 | ###### Optional properties 28 | 29 | * **clipFillType?: [PolyFillType](../shared/PolyFillType.md) = subjectFillType** 30 | 31 | Winding (fill) rule for clipping polygons. If missing it will use the same one as subjectFillType. 32 | 33 | * **clipInputs?: [ClipInput](./ClipInput.md)[] = []** 34 | 35 | Clipping inputs. Not required for union operations, required for others. 36 | 37 | * **reverseSolution?: boolean = false** 38 | 39 | When this property is set to true, polygons returned in the solution parameter of the clip method will have orientations opposite to their normal 40 | orientations. 41 | 42 | * **strictlySimple?: boolean = false** 43 | 44 | Terminology: 45 | 46 | * A simple polygon is one that does not self-intersect. 47 | * A weakly simple polygon is a simple polygon that contains 'touching' vertices, or 'touching' edges. 48 | * A strictly simple polygon is a simple polygon that does not contain 'touching' vertices, or 'touching' edges. 49 | 50 | Vertices 'touch' if they share the same coordinates (and are not adjacent). An edge touches another if one of its end vertices touches another edge excluding its adjacent edges, or if they are co-linear and overlapping (including adjacent edges). 51 | 52 | Polygons returned by clipping operations should always be simple polygons. When the StrictlySimply property is enabled, polygons returned will be strictly simple, otherwise they may be weakly simple. It's computationally expensive ensuring polygons are strictly simple and so this property is disabled by default. 53 | 54 | *Note: There's currently no guarantee that polygons will be strictly simple since 'simplifying' is still a work in progress.* 55 | 56 | 57 | ![image](https://user-images.githubusercontent.com/6306796/28289784-4875cc82-6b44-11e7-9be7-20d5eb30f597.png) 58 | 59 | In the image above, the two examples show weakly simple polygons being broken into two strictly simple polygons. (The outlines with arrows are intended to aid visualizing vertex order.) 60 | 61 | * **preserveCollinear?: boolean = false** 62 | 63 | By default, when three or more vertices are collinear in input polygons (subject or clip), the Clipper object removes the 'inner' vertices before 64 | clipping. When enabled the preserveCollinear property prevents this default behavior to allow these inner vertices to appear in the solution. 65 | -------------------------------------------------------------------------------- /docs/apiReference/clipping/ClipType.md: -------------------------------------------------------------------------------- 1 | #### enum ClipType 2 | 3 | There are four boolean operations - Intersection, Union, Difference & Xor. 4 | 5 | Given that subject and clip polygon brush 'filling' is defined both by their vertices and their respective filling rules, the four boolean operations can be applied to polygons to define new filling regions: 6 | 7 | ###### Values 8 | * **Intersection** 9 | 10 | AND - create regions where both subject and clip polygons are filled 11 | 12 | * **Union** 13 | 14 | OR create regions where either subject or clip polygons (or both) are filled 15 | 16 | * **Difference** 17 | 18 | NOT - create regions where subject polygons are filled except where clip polygons are filled 19 | 20 | * **Xor** 21 | 22 | Exclusive or - create regions where either subject or clip polygons are filled but not where both are filled 23 | 24 | ![image](https://user-images.githubusercontent.com/6306796/28289822-692abb18-6b44-11e7-9fa0-24382a7079fc.png) 25 | 26 | ![image](https://user-images.githubusercontent.com/6306796/28289832-7436ff08-6b44-11e7-98cd-cd1ac5d9c12a.png) 27 | ![image](https://user-images.githubusercontent.com/6306796/28289843-851c0fac-6b44-11e7-8442-f1a03b6aa170.png) 28 | ![image](https://user-images.githubusercontent.com/6306796/28289847-87ba1380-6b44-11e7-9919-108187d270f0.png) 29 | ![image](https://user-images.githubusercontent.com/6306796/28289851-8a15126a-6b44-11e7-865f-844fa3f491dd.png) 30 | 31 | All polygon clipping is performed within the clip method with the specific boolean operation indicated by the ClipType parameter passed as an argument. 32 | 33 | With regard to open paths (polylines), clipping rules generally match those of closed paths (polygons). 34 | However, when there are both polyline and polygon subjects, the following clipping rules apply: 35 | 36 | union operations - polylines will be clipped by any overlapping polygons so that non-overlapped portions will be returned in the solution together with the union-ed polygons 37 | intersection, difference and xor operations - polylines will be clipped only by 'clip' polygons and there will be not interaction between polylines and subject polygons. 38 | 39 | Example of clipping behaviour when mixing polyline and polygon subjects: 40 | 41 | ![image](https://user-images.githubusercontent.com/6306796/28289916-c0ebd4c2-6b44-11e7-8e47-15b78f9dd8c9.png) 42 | -------------------------------------------------------------------------------- /docs/apiReference/clipping/SubjectInput.md: -------------------------------------------------------------------------------- 1 | #### interface SubjectInput 2 | 3 | A single subject input (of multiple possible inputs) for the clipToPaths / clipToPolyTree operations 4 | 5 | 'Subject' paths may be either open (lines) or closed (polygons) or even a mixture of both. 6 | With closed paths, orientation should conform with the filling rule that will be passed via Clipper's execute method. 7 | 8 | ###### Properties 9 | 10 | * **data: [Path](../shared/Path.md) | [Paths](../shared/Paths.md)** 11 | 12 | Path / Paths data. 13 | 14 | Path Coordinate range: 15 | 16 | Path coordinates must be between ± 9007199254740991 (Number.MAX_SAFE_INTEGER), otherwise a range error will be thrown when attempting to add the path to the Clipper object. 17 | If coordinates can be kept between ± 0x3FFFFFFF (± 1.0e+9), a modest increase in performance (approx. 15-20%) over the larger range can be achieved by 18 | avoiding large integer math. 19 | 20 | The function operation will throw an error if the path is invalid for clipping. A path is invalid for clipping when: 21 | - it has less than 2 vertices 22 | - it has 2 vertices but is not an open path 23 | - the vertices are all co-linear and it is not an open path 24 | 25 | * **closed: boolean** 26 | 27 | If the path/paths is closed or not. 28 | -------------------------------------------------------------------------------- /docs/apiReference/clipping/clipTo.md: -------------------------------------------------------------------------------- 1 | #### clipToPaths(params: [ClipParams](./ClipParams.md)): [Paths](../shared/Paths.md) | undefined 2 | #### clipToPolyTree(params: [ClipParams](./ClipParams.md)): [PolyTree](../shared/PolyTree.md) | undefined 3 | 4 | Performs a polygon clipping (boolean) operation, returning the resulting Paths / PolyTree or throwing an error if failed. 5 | 6 | The solution parameter in this case is a Paths or PolyTree structure. The Paths structure is simpler than the PolyTree structure. Because of this it is 7 | quicker to populate and hence clipping performance is a little better (it's roughly 10% faster). However, the PolyTree data structure provides more 8 | information about the returned paths which may be important to users. Firstly, the PolyTree structure preserves nested parent-child polygon relationships 9 | (ie outer polygons owning/containing holes and holes owning/containing other outer polygons etc). Also, only the PolyTree structure can differentiate 10 | between open and closed paths since each PolyNode has an IsOpen property. (The Path structure has no member indicating whether it's open or closed.) 11 | For this reason, when open paths are passed to a Clipper object, the user must use a PolyTree object as the solution parameter, otherwise an exception 12 | will be raised. 13 | 14 | When a PolyTree object is used in a clipping operation on open paths, two ancilliary functions have been provided to quickly separate out open and 15 | closed paths from the solution - OpenPathsFromPolyTree and ClosedPathsFromPolyTree. PolyTreeToPaths is also available to convert path data to a Paths 16 | structure (irrespective of whether they're open or closed). 17 | 18 | There are several things to note about the solution paths returned: 19 | - they aren't in any specific order 20 | - they should never overlap or be self-intersecting (but see notes on rounding) 21 | - holes will be oriented opposite outer polygons 22 | - the solution fill type can be considered either EvenOdd or NonZero since it will comply with either filling rule 23 | - polygons may rarely share a common edge (though this is now very rare as of version 6) 24 | 25 | ![image](https://user-images.githubusercontent.com/6306796/28289968-efa9dfac-6b44-11e7-85b4-826a29c6015f.png) 26 | -------------------------------------------------------------------------------- /docs/apiReference/index.md: -------------------------------------------------------------------------------- 1 | ### API Reference 2 | 3 | 1. Start by creating a library wrapper (for either the WebAssembly or Asm.js version) by using [loadNativeClipperLibInstanceAsync](./libInit/loadNativeClipperLibInstanceAsync.md) 4 | 2. Explore and use all the functions available inside the returned [ClipperLibWrapper](./shared/ClipperLibWrapper.md) class instance. 5 | -------------------------------------------------------------------------------- /docs/apiReference/libInit/NativeClipperLibLoadedFormat.md: -------------------------------------------------------------------------------- 1 | #### enum NativeClipperLibLoadedFormat 2 | 3 | The format the native library instance being used is in. 4 | 5 | ###### Values 6 | * **Wasm = 'wasm'** 7 | 8 | WebAssembly. 9 | 10 | * **AsmJs = 'asmJs'** 11 | 12 | Asm.js. 13 | -------------------------------------------------------------------------------- /docs/apiReference/libInit/NativeClipperLibRequestedFormat.md: -------------------------------------------------------------------------------- 1 | #### enum NativeClipperLibRequestedFormat 2 | 3 | Format to use when loading the native library instance. 4 | 5 | ###### Values 6 | * **WasmWithAsmJsFallback = 'wasmWithAsmJsFallback'** 7 | 8 | Try to load the WebAssembly version, if it fails try to load the Asm.js version. 9 | 10 | * **WasmOnly = 'wasmOnly'** 11 | 12 | Load the WebAssembly version exclusively. 13 | 14 | * **AsmJsOnly = 'asmJsOnly'** 15 | 16 | Load the Asm.js version exclusively. 17 | -------------------------------------------------------------------------------- /docs/apiReference/libInit/loadNativeClipperLibInstanceAsync.md: -------------------------------------------------------------------------------- 1 | #### async loadNativeClipperLibInstanceAsync(format: [NativeClipperLibRequestedFormat](./NativeClipperLibRequestedFormat.md)): Promise<[ClipperLibWrapper](../shared/ClipperLibWrapper.md)> 2 | 3 | Asynchronously tries to load a new native instance of the clipper library to be shared across all method invocations. 4 | 5 | ###### Parameters 6 | 7 | - **format: [NativeClipperLibRequestedFormat](./NativeClipperLibRequestedFormat.md)** 8 | 9 | Format to load, either WasmThenAsmJs, WasmOnly or AsmJsOnly. 10 | 11 | ###### Returns 12 | 13 | - **Promise<[ClipperLibWrapper](../shared/ClipperLibWrapper.md)>** 14 | 15 | Promise that resolves with the wrapper instance. 16 | -------------------------------------------------------------------------------- /docs/apiReference/offsetting/EndType.md: -------------------------------------------------------------------------------- 1 | #### enum EndType 2 | 3 | The EndType enumerator has 5 values: 4 | 5 | ###### Values 6 | * **ClosedPolygon** 7 | 8 | Ends are joined using the JoinType value and the path filled as a polygon 9 | 10 | * **ClosedLine** 11 | 12 | Ends are joined using the JoinType value and the path filled as a polyline 13 | 14 | * **EvenOdd** 15 | 16 | Ends are squared off and extended delta units 17 | 18 | * **OpenSquare** 19 | 20 | Ends are rounded off and extended delta units 21 | 22 | * **OpenButt** 23 | 24 | Ends are squared off with no extension. 25 | 26 | * **OpenSingle (future)** 27 | 28 | Offsets an open path in a single direction. Planned for a future update. 29 | 30 | *Note:* With ClosedPolygon and ClosedLine types, the path closure will be the same regardless of whether or not the first and last vertices in the path match. 31 | 32 | ![image](https://user-images.githubusercontent.com/6306796/28289996-07f1520c-6b45-11e7-8ed8-7c0227915306.png) 33 | ![image](https://user-images.githubusercontent.com/6306796/28290016-1149915c-6b45-11e7-962a-57e5d0ffacf0.png) 34 | 35 | -------------------------------------------------------------------------------- /docs/apiReference/offsetting/JoinType.md: -------------------------------------------------------------------------------- 1 | #### enum JoinType 2 | 3 | When adding paths to a offset operation, the joinType parameter may be one of three types - Miter, Square or Round. 4 | 5 | ![image](https://user-images.githubusercontent.com/6306796/28290053-31d7b0b6-6b45-11e7-8b81-d47241617f90.png) 6 | 7 | ###### Values 8 | * **Miter** 9 | 10 | There's a necessary limit to mitered joins since offsetting edges that join at very acute angles will produce excessively long and narrow 'spikes'. To contain these potential spikes, the ClippOffset object's MiterLimit property specifies a maximum distance that vertices will be offset (in multiples of delta). For any given edge join, when miter offsetting would exceed that maximum distance, 'square' joining is applied. 11 | 12 | * **Round** 13 | 14 | While flattened paths can never perfectly trace an arc, they are approximated by a series of arc chords (see ClipperObject's ArcTolerance property). 15 | 16 | * **Square** 17 | 18 | Squaring is applied uniformally at all convex edge joins at 1 × delta. 19 | 20 | -------------------------------------------------------------------------------- /docs/apiReference/offsetting/OffsetInput.md: -------------------------------------------------------------------------------- 1 | #### interface OffsetInput 2 | 3 | A single input (of multiple possible inputs) for the offsetToPaths / offsetToPolyTree operation. 4 | 5 | ###### Required properties 6 | 7 | * **data: [Path](../shared/Path.md) | [Paths](../shared/Paths.md)** 8 | 9 | Data of one of the Path or Paths to be used in preparation for offsetting. 10 | 11 | All 'outer' Paths must have the same orientation, and any 'hole' paths must have reverse orientation. Closed paths must have at least 3 vertices. 12 | Open paths may have as few as one vertex. Open paths can only be offset with positive deltas. 13 | 14 | * **joinType: [JoinType](./JoinType.md)** 15 | 16 | Join type. 17 | 18 | * **endType: [EndType](./EndType.md)** 19 | 20 | End type. 21 | -------------------------------------------------------------------------------- /docs/apiReference/offsetting/OffsetParams.md: -------------------------------------------------------------------------------- 1 | #### interface OffsetParams 2 | 3 | Params for the polygon offset operation. 4 | 5 | ###### Required properties 6 | 7 | * **delta: number** 8 | 9 | Negative delta values shrink polygons and positive delta expand them. 10 | 11 | * **offsetInputs: [OffsetInput](./OffsetInput.md)[]** 12 | 13 | One or more inputs to use for the offset operation. 14 | 15 | ###### Optional properties 16 | 17 | * **arcTolerance?: number = 0.25** 18 | 19 | Firstly, this field/property is only relevant when JoinType = Round and/or EndType = Round. 20 | 21 | Since flattened paths can never perfectly represent arcs, this field/property specifies a maximum acceptable imprecision ('tolerance') when arcs are 22 | approximated in an offsetting operation. Smaller values will increase 'smoothness' up to a point though at a cost of performance and in creating more 23 | vertices to construct the arc. 24 | 25 | The default ArcTolerance is 0.25 units. This means that the maximum distance the flattened path will deviate from the 'true' arc will be no more 26 | than 0.25 units (before rounding). 27 | 28 | Reducing tolerances below 0.25 will not improve smoothness since vertex coordinates will still be rounded to integer values. The only way to achieve 29 | sub-integer precision is through coordinate scaling before and after offsetting (see example below). 30 | 31 | It's important to make ArcTolerance a sensible fraction of the offset delta (arc radius). Large tolerances relative to the offset delta will produce 32 | poor arc approximations but, just as importantly, very small tolerances will substantially slow offsetting performance while providing unnecessary 33 | degrees of precision. This is most likely to be an issue when offsetting polygons whose coordinates have been scaled to preserve floating point precision. 34 | 35 | Example: Imagine a set of polygons (defined in floating point coordinates) that is to be offset by 10 units using round joins, and the solution is to 36 | retain floating point precision up to at least 6 decimal places. 37 | To preserve this degree of floating point precision, and given that Clipper and ClipperOffset both operate on integer coordinates, the polygon 38 | coordinates will be scaled up by 108 (and rounded to integers) prior to offsetting. Both offset delta and ArcTolerance will also need to be scaled 39 | by this same factor. If ArcTolerance was left unscaled at the default 0.25 units, every arc in the solution would contain a fraction of 44 THOUSAND 40 | vertices while the final arc imprecision would be 0.25 × 10-8 units (ie once scaling was reversed). However, if 0.1 units was an acceptable imprecision 41 | in the final unscaled solution, then ArcTolerance should be set to 0.1 × scaling_factor (0.1 × 108 ). Now if scaling is applied equally to both 42 | ArcTolerance and to Delta Offset, then in this example the number of vertices (steps) defining each arc would be a fraction of 23. 43 | 44 | The formula for the number of steps in a full circular arc is ... ```Pi / acos(1 - arc_tolerance / abs(delta))``` 45 | 46 | * **miterLimit?: number = 2** 47 | 48 | This property sets the maximum distance in multiples of delta that vertices can be offset from their original positions before squaring is applied. 49 | (Squaring truncates a miter by 'cutting it off' at 1 × delta distance from the original vertex.) 50 | 51 | The default value for MiterLimit is 2 (ie twice delta). This is also the smallest MiterLimit that's allowed. If mitering was unrestricted (ie without 52 | any squaring), then offsets at very acute angles would generate unacceptably long 'spikes'. 53 | 54 | An example of an offsetting 'spike' at a narrow angle that's a consequence of using a large MiterLimit (25) ... 55 | 56 | ![image](https://user-images.githubusercontent.com/6306796/28290111-5b2f0afe-6b45-11e7-8607-af35ffc01cbf.png) 57 | -------------------------------------------------------------------------------- /docs/apiReference/offsetting/offsetTo.md: -------------------------------------------------------------------------------- 1 | #### offsetToPaths(params: [OffsetParams](./OffsetParams.md)): [Paths](../shared/Paths.md) | undefined 2 | #### offsetToPolyTree(params: [OffsetParams](./OffsetParams.md)): [PolyTree](../shared/PolyTree.md) | undefined 3 | 4 | Performs a polygon offset operation, returning the resulting PolyTree or undefined if failed. 5 | 6 | This method encapsulates the process of offsetting (inflating/deflating) both open and closed paths using a number of different join types 7 | and end types. 8 | 9 | ###### Preconditions for offsetting 10 | 11 | 1. The orientations of closed paths must be consistent such that outer polygons share the same orientation, and any holes have the opposite orientation 12 | (ie non-zero filling). Open paths must be oriented with closed outer polygons. 13 | 2. Polygons must not self-intersect. 14 | 15 | ###### Limitations 16 | 17 | When offsetting, small artefacts may appear where polygons overlap. To avoid these artefacts, offset overlapping polygons separately. 18 | 19 | ![image](https://user-images.githubusercontent.com/6306796/28290136-77a0f8a0-6b45-11e7-8fdd-a5a5570b96a7.png) 20 | -------------------------------------------------------------------------------- /docs/apiReference/shared/ClipperLibWrapper.md: -------------------------------------------------------------------------------- 1 | #### class ClipperLibWrapper 2 | 3 | A wrapper for the Native Clipper Library instance with all the operations available. 4 | 5 | ###### Properties 6 | 7 | * **static readonly hiRange = 9007199254740991 (Number.MAX_SAFE_INTEGER)** 8 | 9 | Max coordinate value (both positive and negative). 10 | 11 | * **readonly instance: NativeClipperLibInstance** 12 | 13 | Native library instance. 14 | 15 | * **readonly format: [NativeClipperLibLoadedFormat](../libInit/NativeClipperLibLoadedFormat.md)** 16 | 17 | Native library format. 18 | 19 | * **[clipToPaths](../clipping/clipTo.md)(params: [ClipParams](../clipping/ClipParams.md)): [Paths](../shared/Paths.md) | undefined** 20 | * **[clipToPolyTree](../clipping/clipTo.md)(params: [ClipParams](../clipping/ClipParams.md)): [PolyTree](../shared/PolyTree.md) | undefined** 21 | 22 | Performs a polygon clipping (boolean) operation, returning the resulting Paths/PolyTree or throwing an error if failed. 23 | See the method detailed [documentation](../clipping/clipTo.md) for more details. 24 | 25 | * **[offsetToPaths](../offsetting/offsetTo.md)(params: [OffsetParams](../offsetting/OffsetParams.md)): [Paths](../shared/Paths.md) | undefined** 26 | * **[offsetToPolyTree](../offsetting/offsetTo.md)(params: [OffsetParams](../offsetting/OffsetParams.md)): [PolyTree](../shared/PolyTree.md) | undefined** 27 | 28 | Performs a polygon offset operation, returning the resulting Paths/PolyTree or undefined if failed. 29 | See the method detailed [documentation](../offsetting/offsetTo.md) for more details. 30 | 31 | * **area(path: [Path](../shared/Path.md)): number** 32 | 33 | This function returns the area of the supplied polygon. It's assumed that the path is closed and does not self-intersect. Depending on orientation, 34 | * this value may be positive or negative. If Orientation is true, then the area will be positive and conversely, if Orientation is false, then the 35 | * area will be negative. 36 | 37 | * **cleanPolygon(path: [Path](../shared/Path.md), distance = 1.1415): [Path](../shared/Path.md)** 38 | * **cleanPolygons(paths: [Paths](../shared/Paths.md), distance = 1.1415): [Paths](../shared/Paths.md)** 39 | 40 | Removes vertices: 41 | 42 | * that join co-linear edges, or join edges that are almost co-linear (such that if the vertex was moved no more than the specified distance the edges would be co-linear) 43 | * that are within the specified distance of an adjacent vertex 44 | * that are within the specified distance of a semi-adjacent vertex together with their out-lying vertices 45 | 46 | Vertices are semi-adjacent when they are separated by a single (out-lying) vertex. 47 | 48 | The distance parameter's default value is approximately √2 so that a vertex will be removed when adjacent or semi-adjacent vertices having their corresponding X and Y coordinates differing by no more than 1 unit. (If the egdes are semi-adjacent the out-lying vertex will be removed too.) 49 | 50 | ![image](https://user-images.githubusercontent.com/6306796/28290343-5dc75e46-6b46-11e7-83aa-4a387823d1a9.png) 51 | ![image](https://user-images.githubusercontent.com/6306796/28290361-69632726-6b46-11e7-880a-09b965dde079.png) 52 | 53 | * **closedPathsFromPolyTree(polyTree: [PolyTree](../shared/PolyTree.md)): [Paths](../shared/Paths.md)** 54 | 55 | This function filters out open paths from the PolyTree structure and returns only closed paths in a Paths structure. 56 | 57 | * **minkowskiDiff(poly1: [Path](../shared/Path.md), poly2: [Path](../shared/Path.md)): [Paths](../shared/Paths.md)** 58 | 59 | Minkowski Difference is performed by subtracting each point in a polygon from the set of points in an open or closed path. A key feature of Minkowski Difference is that when it's applied to two polygons, the resulting polygon will contain the coordinate space origin whenever the two polygons touch or overlap. (This function is often used to determine when polygons collide.) 60 | 61 | ![image](https://user-images.githubusercontent.com/6306796/28290392-8cd0af94-6b46-11e7-9bad-fd2e245cddc8.png) 62 | 63 | In the image above left the blue polygon is the 'minkowski difference' of the two red boxes. The black dot represents the coordinate space origin 64 | 65 | * **minkowskiSumPath(pattern: [Path](../shared/Path.md), path: [Path](../shared/Path.md), pathIsClosed: boolean): [Paths](../shared/Paths.md)** 66 | * **minkowskiSumPaths(pattern: [Path](../shared/Path.md), paths: [Paths](../shared/Paths.md), pathIsClosed: boolean): [Paths](../shared/Paths.md)** 67 | 68 | Minkowski Addition is performed by adding each point in a polygon 'pattern' to the set of points in an open or closed path. The resulting polygon (or polygons) defines the region that the 'pattern' would pass over in moving from the beginning to the end of the 'path'. 69 | 70 | ![image](https://user-images.githubusercontent.com/6306796/28290405-9636f11a-6b46-11e7-95e6-2e5cddb20789.png) 71 | 72 | * **openPathsFromPolyTree(polyTree: [PolyTree](../shared/PolyTree.md)): [Paths](../shared/Paths.md)** 73 | 74 | This function filters out closed paths from the PolyTree structure and returns only open paths in a Paths structure. 75 | 76 | * **orientation(path: [Path](../shared/Path.md)): boolean** 77 | Orientation is only important to closed paths. Given that vertices are declared in a specific order, orientation refers to the direction (clockwise or counter-clockwise) that these vertices progress around a closed path. 78 | 79 | Orientation is also dependent on axis direction: 80 | * On Y-axis positive upward displays, orientation will return true if the polygon's orientation is counter-clockwise. 81 | * On Y-axis positive downward displays, orientation will return true if the polygon's orientation is clockwise. 82 | 83 | ![image](https://user-images.githubusercontent.com/6306796/28290420-a1877378-6b46-11e7-9d29-37f6eebd7755.png) 84 | 85 | Notes: 86 | 87 | * Self-intersecting polygons have indeterminate orientations in which case this function won't return a meaningful value. 88 | * The majority of 2D graphic display libraries (eg GDI, GDI+, XLib, Cairo, AGG, Graphics32) and even the SVG file format have their coordinate origins at the top-left corner of their respective viewports with their Y axes increasing downward. However, some display libraries (eg Quartz, OpenGL) have their coordinate origins undefined or in the classic bottom-left position with their Y axes increasing upward. 89 | * For Non-Zero filled polygons, the orientation of holes must be opposite that of outer polygons. 90 | * For closed paths (polygons) in the solution returned by the clip method, their orientations will always be true for outer polygons and false for hole polygons (unless the reverseSolution property has been enabled). 91 | 92 | * **pointInPolygon(point: [IntPoint](../shared/IntPoint.md), path: [Path](../shared/Path.md)): [PointInPolygonResult](../shared/PointInPolygonResult.md)** 93 | 94 | Returns *PointInPolygonResult.Outside* when false, *PointInPolygonResult.OnBoundary* when point is on poly and *PointInPolygonResult.Inside* when point is in poly. 95 | 96 | It's assumed that 'poly' is closed and does not self-intersect. 97 | 98 | * **polyTreeToPaths(polyTree: [PolyTree](../shared/PolyTree.md)): [Paths](../shared/Paths.md)** 99 | 100 | This function converts a PolyTree structure into a Paths structure. 101 | 102 | * **reversePath(path: [Path](../shared/Path.md)): void** 103 | 104 | Reverses the vertex order (and hence orientation) in the specified path. 105 | 106 | * **reversePaths(paths: [Paths](../shared/Paths.md)): void** 107 | 108 | Reverses the vertex order (and hence orientation) in each contained path. 109 | 110 | * **simplifyPolygon(path: [Path](../shared/Path.md), fillType: [PolyFillType](../shared/PolyFillType.md) = PolyFillType.EvenOdd): [Paths](../shared/Paths.md)** 111 | * **simplifyPolygons(paths: [Paths](../shared/Paths.md), fillType: [PolyFillType](../shared/PolyFillType.md) = PolyFillType.EvenOdd): [Paths](../shared/Paths.md)** 112 | 113 | Removes self-intersections from the supplied polygon (by performing a boolean union operation using the nominated PolyFillType). 114 | Polygons with non-contiguous duplicate vertices (ie 'touching') will be split into two polygons. 115 | 116 | Note: There's currently no guarantee that polygons will be strictly simple since 'simplifying' is still a work in progress. 117 | 118 | ![image](https://user-images.githubusercontent.com/6306796/28290432-b2402dfe-6b46-11e7-8d1f-d9e7ad7c1770.png) 119 | 120 | ![image](https://user-images.githubusercontent.com/6306796/28290441-c55f97b2-6b46-11e7-8c14-ab667fb4003f.png) 121 | ![image](https://user-images.githubusercontent.com/6306796/28290451-cca90fee-6b46-11e7-90e8-a7a0b98e7ae5.png) 122 | 123 | * **scalePath(path: [Path](../shared/Path.md), scale: number): [Path](../shared/Path.md)** 124 | 125 | Scales a path by multiplying all its points by a number and then rounding them. 126 | 127 | * **scalePaths(paths: [Paths](../shared/Paths.md), scale: number): [Paths](../shared/Paths.md)** 128 | 129 | Scales all inner paths by multiplying all its points by a number and then rounding them. 130 | -------------------------------------------------------------------------------- /docs/apiReference/shared/IntPoint.md: -------------------------------------------------------------------------------- 1 | #### interface IntPoint 2 | ```{ x: number; y: number; }``` 3 | 4 | The IntPoint structure is used to represent all vertices in the Clipper Library. An integer storage type has been deliberately chosen to preserve numerical robustness. (Early versions of the library used floating point coordinates, but it became apparent that floating point imprecision would always cause occasional errors.) 5 | 6 | A sequence of IntPoints are contained within a Path structure to represent a single contour. 7 | 8 | Users wishing to clip or offset polygons containing floating point coordinates need to use appropriate scaling when converting these values to and from IntPoints, since they will be auto-converted to integers. 9 | 10 | See also the notes on rounding. 11 | -------------------------------------------------------------------------------- /docs/apiReference/shared/Path.md: -------------------------------------------------------------------------------- 1 | #### type Path = [IntPoint](./IntPoint.md)[] 2 | 3 | This structure contains a sequence of IntPoint vertices defining a single contour (see also terminology). Paths may be open and represent a series of line segments bounded by 2 or more vertices, or they may be closed and represent polygons. Whether or not a path is open depends on context. Closed paths may be 'outer' contours or 'hole' contours. Which they are depends on orientation. 4 | 5 | Multiple paths can be grouped into a Paths structure. 6 | -------------------------------------------------------------------------------- /docs/apiReference/shared/Paths.md: -------------------------------------------------------------------------------- 1 | #### type Paths = [Path](./Path.md)[] 2 | 3 | This structure is fundamental to the Clipper Library. It's a list or array of one or more Path structures. (The Path structure contains an ordered list of vertices that make a single contour.) 4 | 5 | Paths may open (a series of line segments), or they may closed (polygons). Whether or not a path is open depends on context. Closed paths may be 'outer' contours or 'hole' contours. Which they are depends on orientation. 6 | -------------------------------------------------------------------------------- /docs/apiReference/shared/PointInPolygonResult.md: -------------------------------------------------------------------------------- 1 | #### enum PointInPolygonResult 2 | 3 | The PointInPolygonResult enumerator has 3 values: 4 | 5 | ###### Values 6 | * **Outside** 7 | 8 | Point is outside the polygon 9 | 10 | * **Inside** 11 | 12 | Point is inside the polygon 13 | 14 | * **OnBoundary** 15 | 16 | Point is on the polygon boundary 17 | -------------------------------------------------------------------------------- /docs/apiReference/shared/PolyFillType.md: -------------------------------------------------------------------------------- 1 | #### enum PolyFillType 2 | 3 | Filling indicates those regions that are inside a closed path (ie 'filled' with a brush color or pattern in a graphical display) and those regions that are outside. The Clipper Library supports 4 filling rules: Even-Odd, Non-Zero, Positive and Negative. 4 | 5 | The simplest filling rule is Even-Odd filling (sometimes called alternate filling). Given a group of closed paths start from a point outside the paths and progress along an imaginary line through the paths. When the first path is crossed the encountered region is filled. When the next path is crossed the encountered region is not filled. Likewise, each time a path is crossed, filling starts if it had stopped and stops if it had started. 6 | 7 | With the exception of Even-Odd filling, all other filling rules rely on edge direction and winding numbers to determine filling. Edge direction is determined by the order in which vertices are declared when constructing a path. Edge direction is used to determine the winding number of each polygon subregion. 8 | 9 | The winding number for each polygon sub-region can be derived by: 10 | 11 | 1. starting with a winding number of zero and 12 | 2. from a point (P1) that's outside all polygons, draw an imaginary line to a point that's inside a given sub-region (P2) 13 | 3. while traversing the line from P1 to P2, for each path that crosses the imaginary line from right to left increment the winding number, and for each path that crosses the line from left to right decrement the winding number. 14 | 4. Once you arrive at the given sub-region you have its winding number. 15 | 16 | ![image](https://user-images.githubusercontent.com/6306796/28290194-ba109ac4-6b45-11e7-963e-fc80681cfa00.png) 17 | 18 | ###### Values 19 | * **EvenOdd** 20 | 21 | Alternate: Odd numbered sub-regions are filled, while even numbered sub-regions are not. 22 | 23 | * **NonZero** 24 | 25 | Winding: All non-zero sub-regions are filled. 26 | 27 | * **Positive** 28 | 29 | All sub-regions with winding counts > 0 are filled. 30 | 31 | * **Negative** 32 | 33 | All sub-regions with winding counts < 0 are filled. 34 | 35 | Polygon regions are defined by one or more closed paths which may or may not intersect. A single polygon region can be defined by a single non-intersecting path or by multiple non-intersecting paths where there's typically an 'outer' path and one or more inner 'hole' paths. Looking at the three shapes in the image above, the middle shape consists of two concentric rectangles sharing the same clockwise orientation. With even-odd filling, where orientation can be disregarded, the inner rectangle would create a hole in the outer rectangular polygon. There would be no hole with non-zero filling. In the concentric rectangles on the right, where the inner rectangle is orientated opposite to the outer, a hole will be rendered with either even-odd or non-zero filling. A single path can also define multiple subregions if it self-intersects as in the example of the 5 pointed star shape below. 36 | 37 | ![image](https://user-images.githubusercontent.com/6306796/28290200-c49146ec-6b45-11e7-947a-59248b510388.png) 38 | ![image](https://user-images.githubusercontent.com/6306796/28290209-d1cefc82-6b45-11e7-8ca0-6ce51a24e884.png) 39 | ![image](https://user-images.githubusercontent.com/6306796/28290211-d34cca12-6b45-11e7-80e6-5ea5f1d7ccc6.png) 40 | ![image](https://user-images.githubusercontent.com/6306796/28290213-d51a5602-6b45-11e7-8c26-0925e2fb1f42.png) 41 | ![image](https://user-images.githubusercontent.com/6306796/28290216-d6966084-6b45-11e7-82ff-aeb032de7c0c.png) 42 | 43 | By far the most widely used fill rules are Even-Odd (aka Alternate) and Non-Zero (aka Winding). Most graphics rendering libraries (AGG, Android Graphics, Cairo, GDI+, OpenGL, Quartz 2D etc) and vector graphics storage formats (SVG, Postscript, Photoshop etc) support both these rules. However some libraries (eg Java's Graphics2D) only support one fill rule. Android Graphics and OpenGL are the only libraries (that I'm aware of) that support multiple filling rules. 44 | 45 | It's useful to note that edge direction has no affect on a winding number's odd-ness or even-ness. (This is why orientation is ignored when the Even-Odd rule is employed.) 46 | 47 | The direction of the Y-axis does affect polygon orientation and edge direction. However, changing Y-axis orientation will only change the sign of winding numbers, not their magnitudes, and has no effect on either Even-Odd or Non-Zero filling. 48 | -------------------------------------------------------------------------------- /docs/apiReference/shared/PolyNode.md: -------------------------------------------------------------------------------- 1 | #### class PolyNode 2 | _extended by [PolyTree](./PolyTree.md)_ 3 | 4 | PolyNodes are encapsulated within a PolyTree container, and together provide a data structure representing the parent-child relationships of polygon 5 | contours returned by clipping/ofsetting methods. 6 | 7 | A PolyNode object represents a single polygon. It's isHole property indicates whether it's an outer or a hole. PolyNodes may own any number of PolyNode 8 | children (childs), where children of outer polygons are holes, and children of holes are (nested) outer polygons. 9 | 10 | ###### Properties 11 | 12 | * **get parent(): [PolyNode](./PolyNode.md) | undefined** 13 | 14 | Returns the parent PolyNode. 15 | 16 | The PolyTree object (which is also a PolyNode) does not have a parent and will return undefined. 17 | 18 | * **get childs(): [PolyNode](./PolyNode.md)[]** 19 | 20 | A read-only list of PolyNode. 21 | 22 | Outer PolyNode children contain hole PolyNodes, and hole PolyNode children contain nested outer PolyNodes. 23 | 24 | * **get contour(): [Path](./Path.md)** 25 | 26 | Returns a path list which contains any number of vertices. 27 | 28 | * **get isOpen(): boolean** 29 | 30 | Returns true when the PolyNode's Contour results from a clipping operation on an open contour (path). Only top-level PolyNodes can contain open contours. 31 | 32 | * **get index(): number** 33 | 34 | Index in the parent's child list, or 0 if no parent. 35 | 36 | * **get isHole(): boolean** 37 | 38 | Returns true when the PolyNode's polygon (Contour) is a hole. 39 | 40 | Children of outer polygons are always holes, and children of holes are always (nested) outer polygons. 41 | The isHole property of a PolyTree object is undefined but its children are always top-level outer polygons. 42 | 43 | * **getNext(): [PolyNode](./PolyNode.md) | undefined** 44 | 45 | The returned PolyNode will be the first child if any, otherwise the next sibling, otherwise the next sibling of the Parent etc. 46 | 47 | A PolyTree can be traversed very easily by calling GetFirst() followed by GetNext() in a loop until the returned object is undefined. 48 | -------------------------------------------------------------------------------- /docs/apiReference/shared/PolyTree.md: -------------------------------------------------------------------------------- 1 | #### class PolyTree 2 | _extends [PolyNode](./PolyNode.md)_ 3 | 4 | PolyTree is intended as a read-only data structure that should only be used to receive solutions from clipping and offsetting operations. It's an 5 | alternative to the Paths data structure which also receives these solutions. PolyTree's two major advantages over the Paths structure are: it properly 6 | represents the parent-child relationships of the returned polygons; it differentiates between open and closed paths. However, since PolyTree is a more 7 | complex structure than the Paths structure, and since it's more computationally expensive to process (the Execute method being roughly 5-10% slower), it 8 | should used only be when parent-child polygon relationships are needed, or when open paths are being 'clipped'. 9 | 10 | A PolyTree object is a container for any number of PolyNode children, with each contained PolyNode representing a single polygon contour (either an outer 11 | or hole polygon). PolyTree itself is a specialized PolyNode whose immediate children represent the top-level outer polygons of the solution. (It's own 12 | Contour property is always empty.) The contained top-level PolyNodes may contain their own PolyNode children representing hole polygons that may also 13 | contain children representing nested outer polygons etc. Children of outers will always be holes, and children of holes will always be outers. 14 | 15 | PolyTrees can also contain open paths. Open paths will always be represented by top level PolyNodes. Two functions are provided to quickly separate out 16 | open and closed paths from a polytree - openPathsFromPolyTree and closedPathsFromPolyTree. 17 | 18 | ![polytree](https://user-images.githubusercontent.com/6306796/28290312-41613a88-6b46-11e7-8098-e6f1585af71f.png) 19 | 20 | ###### Properties 21 | 22 | * **get total(): number** 23 | 24 | Returns the total number of PolyNodes (polygons) contained within the PolyTree. This value is not to be confused with childs.length which returns the 25 | number of immediate children only (Childs) contained by PolyTree. 26 | 27 | * **getFirst(): [PolyNode](./PolyNode.md) | undefined** 28 | 29 | This method returns the first outer polygon contour if any, otherwise undefined. 30 | 31 | This function is equivalent to calling childs[0]. 32 | 33 | **Plus all properties from [PolyNode](./PolyNode.md)** 34 | -------------------------------------------------------------------------------- /docs/faq/index.md: -------------------------------------------------------------------------------- 1 | ### FAQ 2 | 3 | #### Why does Clipper use integer coordinates, not floats? 4 | 5 | This has been done to preserve numerical robustness. Early versions of the library did use floating point coordinates, but it became apparent that floating point imprecision was always going to cause occasional errors. 6 | 7 | #### How do I use floating point coordinates with Clipper? 8 | 9 | It's a simple task to multiply your floating point coordinates by a scaling factor (that's typically a power of 10 depending on the desired precision). Then with the solution paths, divide the returned coordinates by this same scaling factor. Clipper accepts integer coordinates as large as ±9007199254740991 (Number.MAX_SAFE_INTEGER), so it can accommodate very large scaling. 10 | 11 | #### Does Clipper handle polygons with holes? 12 | 13 | 'Holes' are defined by the specified polygon filling rule. 14 | 15 | #### Some polygons in the solution share a common edge. Is this a bug? 16 | 17 | No, though this should happen rarely as of version 6. 18 | 19 | #### I have lots of polygons that I want to 'union'. Can I do this in one operation? 20 | 21 | Yes. Just add all the polygons as subject polygons to the Clipper object. (You don't have to assign both subject and clip polygons.) 22 | 23 | #### The polygons produced by ClipperOffset have tiny artefacts? Could this be a bug? 24 | 25 | Make sure the input polygons don't self-intersect. Tiny self-intersections can sometimes be produced by previous clipping operations. These can be cleaned up using the CleanPolygon and CleanPolygons functions. Also, make sure the supplied polygons don't overlap. If they do, offset these separately. Finally, the precision of the input coordinates may be a problem. Because the Clipper Library only operates on integer coordinates, you may need to scale your coordinates (eg by a factor of 10) to improve precision. 26 | 27 | #### Is there an easy way to reverse polygon orientations? 28 | 29 | Yes, see reversePaths. 30 | 31 | #### My drawings contain lots of beziers, ellipses and arcs. How can I perform clipping operations on these? 32 | 33 | You'll have to convert them to 'flattened' paths. For an example of how this can be done (and even reconstructed back into beziers, arcs etc), see the CurvesDemo application included in this library. 34 | -------------------------------------------------------------------------------- /docs/overview/index.md: -------------------------------------------------------------------------------- 1 | ### Overview 2 | 3 | The Clipper Library performs clipping, and offsetting of both lines and polygons. 4 | 5 | A number of features set Clipper apart from other clipping libraries: 6 | 7 | - it accepts all types of polygons including self-intersecting ones 8 | - it supports multiple polygon filling rules (EvenOdd, NonZero, Positive, Negative) 9 | - it's very fast relative to other libraries 10 | - it's numerically robust 11 | - it also performs line and polygon offsetting 12 | - it's free to use in both freeware and commercial applications 13 | 14 | #### Terminology 15 | 16 | - **Clipping:** commonly refers to the process of cutting away from a set of 2-dimensional geometric shapes those parts that are outside a rectangular 'clipping' window. This can be achieved by intersecting subject paths (lines and polygons) with a clipping rectangle. In a more general sense, the clipping window need not be rectangular but can be any type of polygon, even multiple polygons. Also, while clipping typically refers to an intersection operation, in this documentation it will refer to any one of the four boolean operations (intersection, union, difference and exclusive-or). 17 | - **Path:** is an ordered sequence of vertices defining a single geometric contour that's either a line (an open path) or a polygon (a closed path). 18 | - **Line:** or polyline is an open path containing 2 or more vertices. 19 | - **Polygon:** commonly refers to a two-dimensional region bounded by an outer non-intersecting closed contour. That region may also contain a number of 'holes'. In this documentation however, polygon will simply refer to a closed path. 20 | - **Contour:** synonymous with path. 21 | - **Hole:** is a closed region within a polygon that's not part of the polygon. A 'hole polygon' is a closed path that forms the outer boundaries of a hole. 22 | - **Polygon Filling Rule:** the filling rule, together with a list of closed paths, defines those regions (bounded by paths) that are inside (ie regions 'brush filled' in a graphical display) and those which are outside (ie 'holes'). 23 | 24 | #### Rounding 25 | 26 | By using an integer type for polygon coordinates, the Clipper Library has been able to avoid problems of numerical robustness that can cause havoc with geometric computations. Problems associated with integer rounding and their possible solutions are discussed below. 27 | 28 | ![image](https://user-images.githubusercontent.com/6306796/28289644-b46044f0-6b43-11e7-84f6-90c6b22a5bfe.png) 29 | 30 | It's important to stress at the outset that rounding causes vertices to move fractions of a unit away from their 'true' positions. Nevertheless, the resulting imprecision can be very effectively managed by appropriate scaling. 31 | 32 | The Clipper Library supports scaling to very high degrees of precision by accepting integer coordinate values in the range ±9007199254740991 (Number.MAX_SAFE_INTEGER). 33 | 34 | Another complication of using a discrete numbers (as opposed to real numbers) is that very occasionally tiny self-intersection artefacts arise. In the unscaled image on the left (where one unit equals one pixel), the area of intersection of two polygons has been highlighted in bright green. 35 | 36 | ![image](https://user-images.githubusercontent.com/6306796/28289657-c2ba74d0-6b43-11e7-83cf-47ab4b82de3a.png) 37 | 38 | A 30X 'close up' of the lower points of intersection of these same two polygons shows the presence of a tiny self-intersecting artefact. The three 'black dots' highlight the actual points of intersection (with their fractional coordinates displayed). The 'red dots' show where these points of intersection are located once rounding is applied. With a little care you can see that rounding reverses the orientation of these vertices and causes a tiny self-intersecting artefact. 39 | 40 | Although these tiny self-intersections are uncommon, if it's deemed necessary, they are best removed with CleanPolygons. (Setting Clipper's StrictlySimple property to true would also address this self-intersection but the tiny (sub-unit) polygon 'artefact' with incorrect orientation would still appear in the solution.) 41 | 42 | ![image](https://user-images.githubusercontent.com/6306796/28289665-ca9deb64-6b43-11e7-823b-1bc02c3d5a58.png) 43 | 44 | In this final example, the single polygon above also has a tiny self-intersection. However, the clipping algorithm sees this vertex (88,50) as simply 'touching' rather than intersecting the right edge of the polygon (though only by a fraction of a unit). Since this intersection won't normally be detected, the clipping solution (eg following a union operation) will still contain this tiny self-intersection. Setting Clipper's StrictlySimple property to true avoids this uncommon problem. 45 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | transform: { 4 | ".+\\.tsx?$": "ts-jest", 5 | }, 6 | transformIgnorePatterns: ["/node_modules/", "/dist/"], 7 | testRegex: "/__tests__/.*\\.spec\\.tsx?$", 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-angusj-clipper", 3 | "version": "1.3.1", 4 | "description": "Polygon and line clipping and offsetting library for Javascript / Typescript - a port of Angus Johnson's clipper to WebAssembly / Asm.JS", 5 | "main": "universal/index.js", 6 | "typings": "universal/index.d.ts", 7 | "scripts": { 8 | "_pull-docker-image": "docker pull emscripten/emsdk", 9 | "travis": "yarn lint && yarn build && yarn test", 10 | "test": "jest -t unit", 11 | "benchmark": "jest -t benchmark", 12 | "lint": "eslint src", 13 | "build": "yarn build:web && yarn build:universal", 14 | "build:universal": "rimraf ./dist && rimraf ./universal && yarn build:ts && yarn build:asm && shx mv dist universal", 15 | "build:web": "rimraf ./dist && rimraf ./web && yarn build:ts && yarn build:asm --env web && shx mv dist web", 16 | "build:ts": "tsc -p .", 17 | "build:asm": "yarn _pull-docker-image && ts-node build/compile-asm", 18 | "prettier-all": "prettier --write ." 19 | }, 20 | "files": [ 21 | "universal", 22 | "web" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/xaviergonz/js-angusj-clipper.git" 27 | }, 28 | "keywords": [ 29 | "polygon", 30 | "clipping", 31 | "offseting", 32 | "boolean", 33 | "geometry" 34 | ], 35 | "author": "Javier Gonzalez Garces ", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/xaviergonz/js-angusj-clipper/issues" 39 | }, 40 | "homepage": "https://github.com/xaviergonz/js-angusj-clipper#readme", 41 | "devDependencies": { 42 | "@types/command-line-args": "^5.2.0", 43 | "@types/jest": "^29.5.0", 44 | "@types/node": "^18.15.10", 45 | "@types/shelljs": "^0.8.11", 46 | "@typescript-eslint/eslint-plugin": "^5.57.0", 47 | "@typescript-eslint/parser": "^5.57.0", 48 | "command-line-args": "^5.2.1", 49 | "eslint": "^8.36.0", 50 | "eslint-config-prettier": "^8.8.0", 51 | "eslint-plugin-import": "^2.27.5", 52 | "eslint-plugin-prettier": "^4.2.1", 53 | "jest": "^29.5.0", 54 | "prettier": "^2.8.7", 55 | "rimraf": "^4.4.1", 56 | "shelljs": "^0.8.5", 57 | "shx": "^0.3.4", 58 | "ts-jest": "^29.0.5", 59 | "ts-node": "^10.9.1", 60 | "typescript": "^5.0.2" 61 | }, 62 | "prettier": { 63 | "printWidth": 100, 64 | "arrowParens": "always", 65 | "endOfLine": "lf" 66 | }, 67 | "packageManager": "yarn@3.4.1" 68 | } 69 | -------------------------------------------------------------------------------- /src/Clipper.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | import { ClipType, PolyFillType, PolyType } from "./enums"; 3 | import { IntRect } from "./IntRect"; 4 | import { NativeClipper } from "./native/NativeClipper"; 5 | import { NativeClipperLibInstance } from "./native/NativeClipperLibInstance"; 6 | import { 7 | clipTypeToNative, 8 | polyFillTypeToNative, 9 | polyTypeToNative, 10 | } from "./native/nativeEnumConversion"; 11 | import { nativePathsToPaths, pathsToNativePaths } from "./native/PathsToNativePaths"; 12 | import { pathToNativePath } from "./native/PathToNativePath"; 13 | import { ReadonlyPath } from "./Path"; 14 | import { Paths, ReadonlyPaths } from "./Paths"; 15 | import { PolyTree } from "./PolyTree"; 16 | import { nativeFinalizationRegistry } from "./nativeFinalizationRegistry"; 17 | 18 | export interface ClipperInitOptions { 19 | /** 20 | * When this property is set to true, polygons returned in the solution parameter of the execute() method will have orientations opposite to their normal 21 | * orientations. 22 | */ 23 | reverseSolution?: boolean; 24 | 25 | /** 26 | * When this property is set to true, polygons returned in the solution parameter of the execute() method will have orientations opposite to their normal 27 | * orientations. 28 | */ 29 | strictlySimple?: boolean; 30 | 31 | /** 32 | * By default, when three or more vertices are collinear in input polygons (subject or clip), the Clipper object removes the 'inner' vertices before 33 | * clipping. When enabled the preserveCollinear property prevents this default behavior to allow these inner vertices to appear in the solution. 34 | */ 35 | preserveCollinear?: boolean; 36 | } 37 | 38 | export class Clipper { 39 | private _clipper?: NativeClipper; 40 | 41 | /** 42 | * By default, when three or more vertices are collinear in input polygons (subject or clip), the Clipper object removes the 'inner' vertices before 43 | * clipping. When enabled the preserveCollinear property prevents this default behavior to allow these inner vertices to appear in the solution. 44 | * 45 | * @return {boolean} - true if set, false otherwise 46 | */ 47 | get preserveCollinear(): boolean { 48 | return this._clipper!.preserveCollinear; 49 | } 50 | 51 | /** 52 | * By default, when three or more vertices are collinear in input polygons (subject or clip), the Clipper object removes the 'inner' vertices before 53 | * clipping. When enabled the preserveCollinear property prevents this default behavior to allow these inner vertices to appear in the solution. 54 | * 55 | * @param value - value to set 56 | */ 57 | set preserveCollinear(value: boolean) { 58 | this._clipper!.preserveCollinear = value; 59 | } 60 | 61 | /** 62 | * When this property is set to true, polygons returned in the solution parameter of the execute() method will have orientations opposite to their normal 63 | * orientations. 64 | * 65 | * @return {boolean} - true if set, false otherwise 66 | */ 67 | get reverseSolution(): boolean { 68 | return this._clipper!.reverseSolution; 69 | } 70 | 71 | /** 72 | * When this property is set to true, polygons returned in the solution parameter of the execute() method will have orientations opposite to their normal 73 | * orientations. 74 | * 75 | * @param value - value to set 76 | */ 77 | set reverseSolution(value: boolean) { 78 | this._clipper!.reverseSolution = value; 79 | } 80 | 81 | /** 82 | * Terminology: 83 | * - A simple polygon is one that does not self-intersect. 84 | * - A weakly simple polygon is a simple polygon that contains 'touching' vertices, or 'touching' edges. 85 | * - A strictly simple polygon is a simple polygon that does not contain 'touching' vertices, or 'touching' edges. 86 | * 87 | * Vertices 'touch' if they share the same coordinates (and are not adjacent). An edge touches another if one of its end vertices touches another edge 88 | * excluding its adjacent edges, or if they are co-linear and overlapping (including adjacent edges). 89 | * 90 | * Polygons returned by clipping operations (see Clipper.execute()) should always be simple polygons. When the StrictlySimply property is enabled, 91 | * polygons returned will be strictly simple, otherwise they may be weakly simple. It's computationally expensive ensuring polygons are strictly simple 92 | * and so this property is disabled by default. 93 | * 94 | * Note: There's currently no guarantee that polygons will be strictly simple since 'simplifying' is still a work in progress. 95 | * 96 | * @return {boolean} - true if set, false otherwise 97 | */ 98 | get strictlySimple(): boolean { 99 | return this._clipper!.strictlySimple; 100 | } 101 | 102 | /** 103 | * Terminology: 104 | * - A simple polygon is one that does not self-intersect. 105 | * - A weakly simple polygon is a simple polygon that contains 'touching' vertices, or 'touching' edges. 106 | * - A strictly simple polygon is a simple polygon that does not contain 'touching' vertices, or 'touching' edges. 107 | * 108 | * Vertices 'touch' if they share the same coordinates (and are not adjacent). An edge touches another if one of its end vertices touches another edge 109 | * excluding its adjacent edges, or if they are co-linear and overlapping (including adjacent edges). 110 | * 111 | * Polygons returned by clipping operations (see Clipper.execute()) should always be simple polygons. When the StrictlySimply property is enabled, 112 | * polygons returned will be strictly simple, otherwise they may be weakly simple. It's computationally expensive ensuring polygons are strictly simple 113 | * and so this property is disabled by default. 114 | * 115 | * Note: There's currently no guarantee that polygons will be strictly simple since 'simplifying' is still a work in progress. 116 | * 117 | * @param value - value to set 118 | */ 119 | set strictlySimple(value: boolean) { 120 | this._clipper!.strictlySimple = value; 121 | } 122 | 123 | /** 124 | * The Clipper constructor creates an instance of the Clipper class. One or more InitOptions may be passed as a parameter to set the corresponding properties. 125 | * (These properties can still be set or reset after construction.) 126 | * 127 | * @param _nativeLib 128 | * @param initOptions 129 | */ 130 | constructor( 131 | private readonly _nativeLib: NativeClipperLibInstance, 132 | initOptions: ClipperInitOptions = {} 133 | ) { 134 | const realInitOptions = { 135 | reverseSolutions: false, 136 | strictlySimple: false, 137 | preserveCollinear: false, 138 | ...initOptions, 139 | }; 140 | 141 | let nativeInitOptions = 0; 142 | if (realInitOptions.reverseSolutions) { 143 | nativeInitOptions += _nativeLib.InitOptions.ReverseSolution.value; 144 | } 145 | if (realInitOptions.strictlySimple) { 146 | nativeInitOptions += _nativeLib.InitOptions.StrictlySimple.value; 147 | } 148 | if (realInitOptions.preserveCollinear) { 149 | nativeInitOptions += _nativeLib.InitOptions.PreserveCollinear.value; 150 | } 151 | 152 | this._clipper = new _nativeLib.Clipper(nativeInitOptions); 153 | nativeFinalizationRegistry?.register(this, this._clipper, this); 154 | } 155 | 156 | /** 157 | * Any number of subject and clip paths can be added to a clipping task, either individually via the addPath() method, or as groups via the addPaths() 158 | * method, or even using both methods. 159 | * 160 | * 'Subject' paths may be either open (lines) or closed (polygons) or even a mixture of both, but 'clipping' paths must always be closed. Clipper allows 161 | * polygons to clip both lines and other polygons, but doesn't allow lines to clip either lines or polygons. 162 | * 163 | * With closed paths, orientation should conform with the filling rule that will be passed via Clipper's execute method. 164 | * 165 | * Path Coordinate range: 166 | * Path coordinates must be between ± 9007199254740991, otherwise a range error will be thrown when attempting to add the path to the Clipper object. 167 | * If coordinates can be kept between ± 0x3FFFFFFF (± 1.0e+9), a modest increase in performance (approx. 15-20%) over the larger range can be achieved by 168 | * avoiding large integer math. 169 | * 170 | * Return Value: 171 | * The function will return false if the path is invalid for clipping. A path is invalid for clipping when: 172 | * - it has less than 2 vertices 173 | * - it has 2 vertices but is not an open path 174 | * - the vertices are all co-linear and it is not an open path 175 | * 176 | * @param path - Path to add 177 | * @param polyType - Polygon type 178 | * @param closed - If the path is closed 179 | */ 180 | addPath(path: ReadonlyPath, polyType: PolyType, closed: boolean): boolean { 181 | const nativePath = pathToNativePath(this._nativeLib, path); 182 | try { 183 | return this._clipper!.addPath( 184 | nativePath, 185 | polyTypeToNative(this._nativeLib, polyType), 186 | closed 187 | ); 188 | } finally { 189 | nativePath.delete(); 190 | } 191 | } 192 | 193 | /** 194 | * Any number of subject and clip paths can be added to a clipping task, either individually via the addPath() method, or as groups via the addPaths() 195 | * method, or even using both methods. 196 | * 197 | * 'Subject' paths may be either open (lines) or closed (polygons) or even a mixture of both, but 'clipping' paths must always be closed. Clipper allows 198 | * polygons to clip both lines and other polygons, but doesn't allow lines to clip either lines or polygons. 199 | * 200 | * With closed paths, orientation should conform with the filling rule that will be passed via Clipper's execute method. 201 | * 202 | * Path Coordinate range: 203 | * Path coordinates must be between ± 9007199254740991, otherwise a range error will be thrown when attempting to add the path to the Clipper object. 204 | * If coordinates can be kept between ± 0x3FFFFFFF (± 1.0e+9), a modest increase in performance (approx. 15-20%) over the larger range can be achieved 205 | * by avoiding large integer math. 206 | * 207 | * Return Value: 208 | * The function will return false if the path is invalid for clipping. A path is invalid for clipping when: 209 | * - it has less than 2 vertices 210 | * - it has 2 vertices but is not an open path 211 | * - the vertices are all co-linear and it is not an open path 212 | * 213 | * @param paths - Paths to add 214 | * @param polyType - Paths polygon type 215 | * @param closed - If all the inner paths are closed 216 | */ 217 | addPaths(paths: ReadonlyPaths, polyType: PolyType, closed: boolean): boolean { 218 | const nativePaths = pathsToNativePaths(this._nativeLib, paths); 219 | try { 220 | return this._clipper!.addPaths( 221 | nativePaths, 222 | polyTypeToNative(this._nativeLib, polyType), 223 | closed 224 | ); 225 | } finally { 226 | nativePaths.delete(); 227 | } 228 | } 229 | 230 | /** 231 | * The Clear method removes any existing subject and clip polygons allowing the Clipper object to be reused for clipping operations on different polygon sets. 232 | */ 233 | clear(): void { 234 | this._clipper!.clear(); 235 | } 236 | 237 | /** 238 | * This method returns the axis-aligned bounding rectangle of all polygons that have been added to the Clipper object. 239 | * 240 | * @return {{left: number, right: number, top: number, bottom: number}} - Bounds 241 | */ 242 | getBounds(): IntRect { 243 | const nativeBounds = this._clipper!.getBounds(); 244 | const rect = { 245 | left: nativeBounds.left, 246 | right: nativeBounds.right, 247 | top: nativeBounds.top, 248 | bottom: nativeBounds.bottom, 249 | }; 250 | nativeBounds.delete(); 251 | return rect; 252 | } 253 | 254 | /** 255 | * Once subject and clip paths have been assigned (via addPath and/or addPaths), execute can then perform the clipping operation (intersection, union, 256 | * difference or XOR) specified by the clipType parameter. 257 | * 258 | * The solution parameter in this case is a Paths or PolyTree structure. The Paths structure is simpler than the PolyTree structure. Because of this it is 259 | * quicker to populate and hence clipping performance is a little better (it's roughly 10% faster). However, the PolyTree data structure provides more 260 | * information about the returned paths which may be important to users. Firstly, the PolyTree structure preserves nested parent-child polygon relationships 261 | * (ie outer polygons owning/containing holes and holes owning/containing other outer polygons etc). Also, only the PolyTree structure can differentiate 262 | * between open and closed paths since each PolyNode has an IsOpen property. (The Path structure has no member indicating whether it's open or closed.) 263 | * For this reason, when open paths are passed to a Clipper object, the user must use a PolyTree object as the solution parameter, otherwise an exception 264 | * will be raised. 265 | * 266 | * When a PolyTree object is used in a clipping operation on open paths, two ancilliary functions have been provided to quickly separate out open and 267 | * closed paths from the solution - OpenPathsFromPolyTree and ClosedPathsFromPolyTree. PolyTreeToPaths is also available to convert path data to a Paths 268 | * structure (irrespective of whether they're open or closed). 269 | * 270 | * There are several things to note about the solution paths returned: 271 | * - they aren't in any specific order 272 | * - they should never overlap or be self-intersecting (but see notes on rounding) 273 | * - holes will be oriented opposite outer polygons 274 | * - the solution fill type can be considered either EvenOdd or NonZero since it will comply with either filling rule 275 | * - polygons may rarely share a common edge (though this is now very rare as of version 6) 276 | * 277 | * The subjFillType and clipFillType parameters define the polygon fill rule to be applied to the polygons (ie closed paths) in the subject and clip 278 | * paths respectively. (It's usual though obviously not essential that both sets of polygons use the same fill rule.) 279 | * 280 | * execute can be called multiple times without reassigning subject and clip polygons (ie when different clipping operations are required on the 281 | * same polygon sets). 282 | * 283 | * @param clipType - Clip operation type 284 | * @param subjFillType - Fill type of the subject polygons 285 | * @param clipFillType - Fill type of the clip polygons 286 | * @param cleanDistance - Clean distance over the output, or undefined for no cleaning. 287 | * @return {Paths | undefined} - The solution or undefined if there was an error 288 | */ 289 | executeToPaths( 290 | clipType: ClipType, 291 | subjFillType: PolyFillType, 292 | clipFillType: PolyFillType, 293 | cleanDistance: number | undefined 294 | ): Paths | undefined { 295 | const outNativePaths = new this._nativeLib.Paths(); 296 | try { 297 | const success = this._clipper!.executePathsWithFillTypes( 298 | clipTypeToNative(this._nativeLib, clipType), 299 | outNativePaths, 300 | polyFillTypeToNative(this._nativeLib, subjFillType), 301 | polyFillTypeToNative(this._nativeLib, clipFillType) 302 | ); 303 | if (!success) { 304 | return undefined; 305 | } else { 306 | if (cleanDistance !== undefined) { 307 | this._nativeLib.cleanPolygons(outNativePaths, cleanDistance); 308 | } 309 | return nativePathsToPaths(this._nativeLib, outNativePaths, true); // frees outNativePaths 310 | } 311 | } finally { 312 | if (!outNativePaths.isDeleted()) { 313 | outNativePaths.delete(); 314 | } 315 | } 316 | } 317 | 318 | /** 319 | * Once subject and clip paths have been assigned (via addPath and/or addPaths), execute can then perform the clipping operation (intersection, union, 320 | * difference or XOR) specified by the clipType parameter. 321 | * 322 | * The solution parameter can be either a Paths or PolyTree structure. The Paths structure is simpler than the PolyTree structure. Because of this it is 323 | * quicker to populate and hence clipping performance is a little better (it's roughly 10% faster). However, the PolyTree data structure provides more 324 | * information about the returned paths which may be important to users. Firstly, the PolyTree structure preserves nested parent-child polygon relationships 325 | * (ie outer polygons owning/containing holes and holes owning/containing other outer polygons etc). Also, only the PolyTree structure can differentiate 326 | * between open and closed paths since each PolyNode has an IsOpen property. (The Path structure has no member indicating whether it's open or closed.) 327 | * For this reason, when open paths are passed to a Clipper object, the user must use a PolyTree object as the solution parameter, otherwise an exception 328 | * will be raised. 329 | * 330 | * When a PolyTree object is used in a clipping operation on open paths, two ancilliary functions have been provided to quickly separate out open and 331 | * closed paths from the solution - OpenPathsFromPolyTree and ClosedPathsFromPolyTree. PolyTreeToPaths is also available to convert path data to a Paths 332 | * structure (irrespective of whether they're open or closed). 333 | * 334 | * There are several things to note about the solution paths returned: 335 | * - they aren't in any specific order 336 | * - they should never overlap or be self-intersecting (but see notes on rounding) 337 | * - holes will be oriented opposite outer polygons 338 | * - the solution fill type can be considered either EvenOdd or NonZero since it will comply with either filling rule 339 | * - polygons may rarely share a common edge (though this is now very rare as of version 6) 340 | * 341 | * The subjFillType and clipFillType parameters define the polygon fill rule to be applied to the polygons (ie closed paths) in the subject and clip 342 | * paths respectively. (It's usual though obviously not essential that both sets of polygons use the same fill rule.) 343 | * 344 | * execute can be called multiple times without reassigning subject and clip polygons (ie when different clipping operations are required on the 345 | * same polygon sets). 346 | * 347 | * @param clipType - Clip operation type 348 | * @param subjFillType - Fill type of the subject polygons 349 | * @param clipFillType - Fill type of the clip polygons 350 | * @return {PolyTree | undefined} - The solution or undefined if there was an error 351 | */ 352 | executeToPolyTee( 353 | clipType: ClipType, 354 | subjFillType: PolyFillType, 355 | clipFillType: PolyFillType 356 | ): PolyTree | undefined { 357 | const outNativePolyTree = new this._nativeLib.PolyTree(); 358 | try { 359 | const success = this._clipper!.executePolyTreeWithFillTypes( 360 | clipTypeToNative(this._nativeLib, clipType), 361 | outNativePolyTree, 362 | polyFillTypeToNative(this._nativeLib, subjFillType), 363 | polyFillTypeToNative(this._nativeLib, clipFillType) 364 | ); 365 | if (!success) { 366 | return undefined; 367 | } else { 368 | return PolyTree.fromNativePolyTree(this._nativeLib, outNativePolyTree, true); // frees outNativePolyTree 369 | } 370 | } finally { 371 | if (!outNativePolyTree.isDeleted()) { 372 | outNativePolyTree.delete(); 373 | } 374 | } 375 | } 376 | 377 | /** 378 | * Checks if the object has been disposed. 379 | * 380 | * @return {boolean} - true if disposed, false if not 381 | */ 382 | isDisposed(): boolean { 383 | return this._clipper === undefined || this._clipper.isDeleted(); 384 | } 385 | 386 | /** 387 | * Since this library uses WASM/ASM.JS internally for speed this means that you must dispose objects after you are done using them or mem leaks will occur. 388 | * (If the runtime supports FinalizationRegistry then this becomes non-mandatory, but still recommended). 389 | */ 390 | dispose(): void { 391 | if (this._clipper) { 392 | this._clipper.delete(); 393 | nativeFinalizationRegistry?.unregister(this); 394 | this._clipper = undefined; 395 | } 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/ClipperError.ts: -------------------------------------------------------------------------------- 1 | export class ClipperError extends Error { 2 | constructor(public message: string) { 3 | super(message); 4 | Object.setPrototypeOf(this, ClipperError.prototype); 5 | this.name = this.constructor.name; 6 | this.stack = new Error().stack; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/ClipperOffset.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | import { EndType, JoinType } from "./enums"; 3 | import { NativeClipperLibInstance } from "./native/NativeClipperLibInstance"; 4 | import { NativeClipperOffset } from "./native/NativeClipperOffset"; 5 | import { endTypeToNative, joinTypeToNative } from "./native/nativeEnumConversion"; 6 | import { nativePathsToPaths, pathsToNativePaths } from "./native/PathsToNativePaths"; 7 | import { pathToNativePath } from "./native/PathToNativePath"; 8 | import { ReadonlyPath } from "./Path"; 9 | import { Paths, ReadonlyPaths } from "./Paths"; 10 | import { PolyTree } from "./PolyTree"; 11 | import { nativeFinalizationRegistry } from "./nativeFinalizationRegistry"; 12 | 13 | /** 14 | * The ClipperOffset class encapsulates the process of offsetting (inflating/deflating) both open and closed paths using a number of different join types 15 | * and end types. 16 | * 17 | * Preconditions for offsetting: 18 | * 1. The orientations of closed paths must be consistent such that outer polygons share the same orientation, and any holes have the opposite orientation 19 | * (ie non-zero filling). Open paths must be oriented with closed outer polygons. 20 | * 2. Polygons must not self-intersect. 21 | * 22 | * Limitations: 23 | * When offsetting, small artefacts may appear where polygons overlap. To avoid these artefacts, offset overlapping polygons separately. 24 | */ 25 | export class ClipperOffset { 26 | private _clipperOffset?: NativeClipperOffset; 27 | 28 | /** 29 | * Firstly, this field/property is only relevant when JoinType = Round and/or EndType = Round. 30 | * 31 | * Since flattened paths can never perfectly represent arcs, this field/property specifies a maximum acceptable imprecision ('tolerance') when arcs are 32 | * approximated in an offsetting operation. Smaller values will increase 'smoothness' up to a point though at a cost of performance and in creating more 33 | * vertices to construct the arc. 34 | * 35 | * The default ArcTolerance is 0.25 units. This means that the maximum distance the flattened path will deviate from the 'true' arc will be no more 36 | * than 0.25 units (before rounding). 37 | * 38 | * Reducing tolerances below 0.25 will not improve smoothness since vertex coordinates will still be rounded to integer values. The only way to achieve 39 | * sub-integer precision is through coordinate scaling before and after offsetting (see example below). 40 | * 41 | * It's important to make ArcTolerance a sensible fraction of the offset delta (arc radius). Large tolerances relative to the offset delta will produce 42 | * poor arc approximations but, just as importantly, very small tolerances will substantially slow offsetting performance while providing unnecessary 43 | * degrees of precision. This is most likely to be an issue when offsetting polygons whose coordinates have been scaled to preserve floating point precision. 44 | * 45 | * Example: Imagine a set of polygons (defined in floating point coordinates) that is to be offset by 10 units using round joins, and the solution is to 46 | * retain floating point precision up to at least 6 decimal places. 47 | * To preserve this degree of floating point precision, and given that Clipper and ClipperOffset both operate on integer coordinates, the polygon 48 | * coordinates will be scaled up by 108 (and rounded to integers) prior to offsetting. Both offset delta and ArcTolerance will also need to be scaled 49 | * by this same factor. If ArcTolerance was left unscaled at the default 0.25 units, every arc in the solution would contain a fraction of 44 THOUSAND 50 | * vertices while the final arc imprecision would be 0.25 × 10-8 units (ie once scaling was reversed). However, if 0.1 units was an acceptable imprecision 51 | * in the final unscaled solution, then ArcTolerance should be set to 0.1 × scaling_factor (0.1 × 108 ). Now if scaling is applied equally to both 52 | * ArcTolerance and to Delta Offset, then in this example the number of vertices (steps) defining each arc would be a fraction of 23. 53 | * 54 | * The formula for the number of steps in a full circular arc is ... Pi / acos(1 - arc_tolerance / abs(delta)) 55 | * 56 | * @return {number} - Current arc tolerance 57 | */ 58 | get arcTolerance(): number { 59 | return this._clipperOffset!.arcTolerance; 60 | } 61 | 62 | /** 63 | * Firstly, this field/property is only relevant when JoinType = Round and/or EndType = Round. 64 | * 65 | * Since flattened paths can never perfectly represent arcs, this field/property specifies a maximum acceptable imprecision ('tolerance') when arcs are 66 | * approximated in an offsetting operation. Smaller values will increase 'smoothness' up to a point though at a cost of performance and in creating more 67 | * vertices to construct the arc. 68 | * 69 | * The default ArcTolerance is 0.25 units. This means that the maximum distance the flattened path will deviate from the 'true' arc will be no more 70 | * than 0.25 units (before rounding). 71 | * 72 | * Reducing tolerances below 0.25 will not improve smoothness since vertex coordinates will still be rounded to integer values. The only way to achieve 73 | * sub-integer precision is through coordinate scaling before and after offsetting (see example below). 74 | * 75 | * It's important to make ArcTolerance a sensible fraction of the offset delta (arc radius). Large tolerances relative to the offset delta will produce 76 | * poor arc approximations but, just as importantly, very small tolerances will substantially slow offsetting performance while providing unnecessary 77 | * degrees of precision. This is most likely to be an issue when offsetting polygons whose coordinates have been scaled to preserve floating point precision. 78 | * 79 | * Example: Imagine a set of polygons (defined in floating point coordinates) that is to be offset by 10 units using round joins, and the solution is to 80 | * retain floating point precision up to at least 6 decimal places. 81 | * To preserve this degree of floating point precision, and given that Clipper and ClipperOffset both operate on integer coordinates, the polygon 82 | * coordinates will be scaled up by 108 (and rounded to integers) prior to offsetting. Both offset delta and ArcTolerance will also need to be scaled 83 | * by this same factor. If ArcTolerance was left unscaled at the default 0.25 units, every arc in the solution would contain a fraction of 44 THOUSAND 84 | * vertices while the final arc imprecision would be 0.25 × 10-8 units (ie once scaling was reversed). However, if 0.1 units was an acceptable imprecision 85 | * in the final unscaled solution, then ArcTolerance should be set to 0.1 × scaling_factor (0.1 × 108 ). Now if scaling is applied equally to both 86 | * ArcTolerance and to Delta Offset, then in this example the number of vertices (steps) defining each arc would be a fraction of 23. 87 | * 88 | * The formula for the number of steps in a full circular arc is ... Pi / acos(1 - arc_tolerance / abs(delta)) 89 | * 90 | * @param value - Arc tolerance to set. 91 | */ 92 | set arcTolerance(value: number) { 93 | this._clipperOffset!.arcTolerance = value; 94 | } 95 | 96 | /** 97 | * This property sets the maximum distance in multiples of delta that vertices can be offset from their original positions before squaring is applied. 98 | * (Squaring truncates a miter by 'cutting it off' at 1 × delta distance from the original vertex.) 99 | * 100 | * The default value for MiterLimit is 2 (ie twice delta). This is also the smallest MiterLimit that's allowed. If mitering was unrestricted (ie without 101 | * any squaring), then offsets at very acute angles would generate unacceptably long 'spikes'. 102 | * 103 | * @return {number} - Current miter limit 104 | */ 105 | get miterLimit(): number { 106 | return this._clipperOffset!.miterLimit; 107 | } 108 | 109 | /** 110 | * Sets the current miter limit (see getter docs for more info). 111 | * 112 | * @param value - Mit limit to set. 113 | */ 114 | set miterLimit(value: number) { 115 | this._clipperOffset!.miterLimit = value; 116 | } 117 | 118 | /** 119 | * The ClipperOffset constructor takes 2 optional parameters: MiterLimit and ArcTolerance. The two parameters corresponds to properties of the same name. 120 | * MiterLimit is only relevant when JoinType is Miter, and ArcTolerance is only relevant when JoinType is Round or when EndType is OpenRound. 121 | * 122 | * @param _nativeLib - Native clipper lib instance to use 123 | * @param miterLimit - Miter limit 124 | * @param arcTolerance - ArcTolerance (round precision) 125 | */ 126 | constructor( 127 | private readonly _nativeLib: NativeClipperLibInstance, 128 | miterLimit = 2, 129 | arcTolerance = 0.25 130 | ) { 131 | this._clipperOffset = new _nativeLib.ClipperOffset(miterLimit, arcTolerance); 132 | nativeFinalizationRegistry?.register(this, this._clipperOffset, this); 133 | } 134 | 135 | /** 136 | * Adds a Path to a ClipperOffset object in preparation for offsetting. 137 | * 138 | * Any number of paths can be added, and each has its own JoinType and EndType. All 'outer' Paths must have the same orientation, and any 'hole' paths must 139 | * have reverse orientation. Closed paths must have at least 3 vertices. Open paths may have as few as one vertex. Open paths can only be offset 140 | * with positive deltas. 141 | * 142 | * @param path - Path to add 143 | * @param joinType - Join type 144 | * @param endType - End type 145 | */ 146 | addPath(path: ReadonlyPath, joinType: JoinType, endType: EndType): void { 147 | const nativePath = pathToNativePath(this._nativeLib, path); 148 | try { 149 | this._clipperOffset!.addPath( 150 | nativePath, 151 | joinTypeToNative(this._nativeLib, joinType), 152 | endTypeToNative(this._nativeLib, endType) 153 | ); 154 | } finally { 155 | nativePath.delete(); 156 | } 157 | } 158 | 159 | /** 160 | * Adds Paths to a ClipperOffset object in preparation for offsetting. 161 | * 162 | * Any number of paths can be added, and each path has its own JoinType and EndType. All 'outer' Paths must have the same orientation, and any 'hole' 163 | * paths must have reverse orientation. Closed paths must have at least 3 vertices. Open paths may have as few as one vertex. Open paths can only be 164 | * offset with positive deltas. 165 | * 166 | * @param paths - Paths to add 167 | * @param joinType - Join type 168 | * @param endType - End type 169 | */ 170 | addPaths(paths: ReadonlyPaths, joinType: JoinType, endType: EndType): void { 171 | const nativePaths = pathsToNativePaths(this._nativeLib, paths); 172 | try { 173 | this._clipperOffset!.addPaths( 174 | nativePaths, 175 | joinTypeToNative(this._nativeLib, joinType), 176 | endTypeToNative(this._nativeLib, endType) 177 | ); 178 | } finally { 179 | nativePaths.delete(); 180 | } 181 | } 182 | 183 | /** 184 | * Negative delta values shrink polygons and positive delta expand them. 185 | * 186 | * This method can be called multiple times, offsetting the same paths by different amounts (ie using different deltas). 187 | * 188 | * @param delta - Delta 189 | * @param cleanDistance - Clean distance over the output, or undefined for no cleaning. 190 | * @return {Paths} - Solution paths 191 | */ 192 | executeToPaths(delta: number, cleanDistance: number | undefined): Paths { 193 | const outNativePaths = new this._nativeLib.Paths(); 194 | try { 195 | this._clipperOffset!.executePaths(outNativePaths, delta); 196 | if (cleanDistance !== undefined) { 197 | this._nativeLib.cleanPolygons(outNativePaths, cleanDistance); 198 | } 199 | return nativePathsToPaths(this._nativeLib, outNativePaths, true); // frees outNativePaths 200 | } finally { 201 | if (!outNativePaths.isDeleted()) { 202 | outNativePaths.delete(); 203 | } 204 | } 205 | } 206 | 207 | /** 208 | * This method takes two parameters. The first is the structure that receives the result of the offset operation (a PolyTree structure). The second parameter 209 | * is the amount to which the supplied paths will be offset. Negative delta values shrink polygons and positive delta expand them. 210 | * 211 | * This method can be called multiple times, offsetting the same paths by different amounts (ie using different deltas). 212 | * 213 | * @param delta - Delta 214 | * @return {Paths} - Solution paths 215 | */ 216 | executeToPolyTree(delta: number): PolyTree { 217 | const outNativePolyTree = new this._nativeLib.PolyTree(); 218 | try { 219 | this._clipperOffset!.executePolyTree(outNativePolyTree, delta); 220 | return PolyTree.fromNativePolyTree(this._nativeLib, outNativePolyTree, true); // frees outNativePolyTree 221 | } finally { 222 | if (!outNativePolyTree.isDeleted()) { 223 | outNativePolyTree.delete(); 224 | } 225 | } 226 | } 227 | 228 | /** 229 | * This method clears all paths from the ClipperOffset object, allowing new paths to be assigned. 230 | */ 231 | clear(): void { 232 | this._clipperOffset!.clear(); 233 | } 234 | 235 | /** 236 | * Checks if the object has been disposed. 237 | * 238 | * @return {boolean} - true if disposed, false if not 239 | */ 240 | isDisposed(): boolean { 241 | return this._clipperOffset === undefined || this._clipperOffset.isDeleted(); 242 | } 243 | 244 | /** 245 | * Since this library uses WASM/ASM.JS internally for speed this means that you must dispose objects after you are done using them or mem leaks will occur. 246 | * (If the runtime supports FinalizationRegistry then this becomes non-mandatory, but still recommended). 247 | */ 248 | dispose(): void { 249 | if (this._clipperOffset) { 250 | this._clipperOffset.delete(); 251 | nativeFinalizationRegistry?.unregister(this); 252 | this._clipperOffset = undefined; 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/IntPoint.ts: -------------------------------------------------------------------------------- 1 | export interface IntPoint { 2 | x: number; 3 | y: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/IntRect.ts: -------------------------------------------------------------------------------- 1 | export interface IntRect { 2 | left: number; 3 | top: number; 4 | right: number; 5 | bottom: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/Path.ts: -------------------------------------------------------------------------------- 1 | import { IntPoint } from "./IntPoint"; 2 | 3 | export type Path = IntPoint[]; 4 | export type ReadonlyPath = ReadonlyArray>; 5 | -------------------------------------------------------------------------------- /src/Paths.ts: -------------------------------------------------------------------------------- 1 | import { Path, ReadonlyPath } from "./Path"; 2 | 3 | export type Paths = Path[]; 4 | export type ReadonlyPaths = ReadonlyArray; 5 | -------------------------------------------------------------------------------- /src/PolyNode.ts: -------------------------------------------------------------------------------- 1 | import { NativeClipperLibInstance } from "./native/NativeClipperLibInstance"; 2 | import { NativePolyNode } from "./native/NativePolyNode"; 3 | import { nativePathToPath } from "./native/PathToNativePath"; 4 | import { ReadonlyPath } from "./Path"; 5 | 6 | /** 7 | * PolyNodes are encapsulated within a PolyTree container, and together provide a data structure representing the parent-child relationships of polygon 8 | * contours returned by clipping/ofsetting methods. 9 | * 10 | * A PolyNode object represents a single polygon. It's isHole property indicates whether it's an outer or a hole. PolyNodes may own any number of PolyNode 11 | * children (childs), where children of outer polygons are holes, and children of holes are (nested) outer polygons. 12 | */ 13 | export class PolyNode { 14 | protected _parent?: PolyNode; 15 | 16 | /** 17 | * Returns the parent PolyNode. 18 | * 19 | * The PolyTree object (which is also a PolyNode) does not have a parent and will return undefined. 20 | */ 21 | get parent(): PolyNode | undefined { 22 | return this._parent; 23 | } 24 | 25 | protected _childs: PolyNode[] = []; 26 | /** 27 | * A read-only list of PolyNode. 28 | * Outer PolyNode childs contain hole PolyNodes, and hole PolyNode childs contain nested outer PolyNodes. 29 | */ 30 | get childs(): PolyNode[] { 31 | return this._childs; 32 | } 33 | 34 | protected _contour: ReadonlyPath = []; 35 | /** 36 | * Returns a path list which contains any number of vertices. 37 | */ 38 | get contour(): ReadonlyPath { 39 | return this._contour; 40 | } 41 | 42 | protected _isOpen = false; 43 | /** 44 | * Returns true when the PolyNode's Contour results from a clipping operation on an open contour (path). Only top-level PolyNodes can contain open contours. 45 | */ 46 | get isOpen(): boolean { 47 | return this._isOpen; 48 | } 49 | 50 | protected _index = 0; 51 | /** 52 | * Index in the parent's child list, or 0 if no parent. 53 | */ 54 | get index(): number { 55 | return this._index; 56 | } 57 | 58 | protected _isHole?: boolean; 59 | /** 60 | * Returns true when the PolyNode's polygon (Contour) is a hole. 61 | * 62 | * Children of outer polygons are always holes, and children of holes are always (nested) outer polygons. 63 | * The isHole property of a PolyTree object is undefined but its children are always top-level outer polygons. 64 | * 65 | * @return {boolean} 66 | */ 67 | get isHole(): boolean { 68 | if (this._isHole === undefined) { 69 | let result = true; 70 | let node: PolyNode | undefined = this._parent; 71 | while (node !== undefined) { 72 | result = !result; 73 | node = node._parent; 74 | } 75 | this._isHole = result; 76 | } 77 | 78 | return this._isHole; 79 | } 80 | 81 | /** 82 | * The returned PolyNode will be the first child if any, otherwise the next sibling, otherwise the next sibling of the Parent etc. 83 | * 84 | * A PolyTree can be traversed very easily by calling GetFirst() followed by GetNext() in a loop until the returned object is undefined. 85 | * 86 | * @return {PolyNode | undefined} 87 | */ 88 | getNext(): PolyNode | undefined { 89 | if (this._childs.length > 0) { 90 | return this._childs[0]; 91 | } else { 92 | return this.getNextSiblingUp(); 93 | } 94 | } 95 | 96 | protected getNextSiblingUp(): PolyNode | undefined { 97 | if (this._parent === undefined) { 98 | return undefined; 99 | } else if (this._index === this._parent._childs.length - 1) { 100 | //noinspection TailRecursionJS 101 | return this._parent.getNextSiblingUp(); 102 | } else { 103 | return this._parent._childs[this._index + 1]; 104 | } 105 | } 106 | 107 | protected static fillFromNativePolyNode( 108 | pn: PolyNode, 109 | nativeLib: NativeClipperLibInstance, 110 | nativePolyNode: NativePolyNode, 111 | parent: PolyNode | undefined, 112 | childIndex: number, 113 | freeNativePolyNode: boolean 114 | ): void { 115 | pn._parent = parent; 116 | 117 | const childs = nativePolyNode.childs; 118 | const max = childs.size(); 119 | pn._childs.length = max; 120 | 121 | for (let i = 0; i < max; i++) { 122 | const newChild = PolyNode.fromNativePolyNode( 123 | nativeLib, 124 | childs.get(i), 125 | pn, 126 | i, 127 | freeNativePolyNode 128 | ); 129 | pn._childs[i] = newChild; 130 | } 131 | 132 | // do we need to clear the object ourselves? for now let's assume so (seems to work) 133 | pn._contour = nativePathToPath(nativeLib, nativePolyNode.contour, true); 134 | pn._isOpen = nativePolyNode.isOpen(); 135 | pn._index = childIndex; 136 | 137 | if (freeNativePolyNode) { 138 | nativePolyNode.delete(); 139 | } 140 | } 141 | 142 | protected static fromNativePolyNode( 143 | nativeLib: NativeClipperLibInstance, 144 | nativePolyNode: NativePolyNode, 145 | parent: PolyNode | undefined, 146 | childIndex: number, 147 | freeNativePolyNode: boolean 148 | ): PolyNode { 149 | const pn = new PolyNode(); 150 | PolyNode.fillFromNativePolyNode( 151 | pn, 152 | nativeLib, 153 | nativePolyNode, 154 | parent, 155 | childIndex, 156 | freeNativePolyNode 157 | ); 158 | return pn; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/PolyTree.ts: -------------------------------------------------------------------------------- 1 | import { NativeClipperLibInstance } from "./native/NativeClipperLibInstance"; 2 | import { NativePolyTree } from "./native/NativePolyTree"; 3 | import { PolyNode } from "./PolyNode"; 4 | 5 | /** 6 | * PolyTree is intended as a read-only data structure that should only be used to receive solutions from clipping and offsetting operations. It's an 7 | * alternative to the Paths data structure which also receives these solutions. PolyTree's two major advantages over the Paths structure are: it properly 8 | * represents the parent-child relationships of the returned polygons; it differentiates between open and closed paths. However, since PolyTree is a more 9 | * complex structure than the Paths structure, and since it's more computationally expensive to process (the Execute method being roughly 5-10% slower), it 10 | * should used only be when parent-child polygon relationships are needed, or when open paths are being 'clipped'. 11 | * 12 | * A PolyTree object is a container for any number of PolyNode children, with each contained PolyNode representing a single polygon contour (either an outer 13 | * or hole polygon). PolyTree itself is a specialized PolyNode whose immediate children represent the top-level outer polygons of the solution. (It's own 14 | * Contour property is always empty.) The contained top-level PolyNodes may contain their own PolyNode children representing hole polygons that may also 15 | * contain children representing nested outer polygons etc. Children of outers will always be holes, and children of holes will always be outers. 16 | * 17 | * PolyTrees can also contain open paths. Open paths will always be represented by top level PolyNodes. Two functions are provided to quickly separate out 18 | * open and closed paths from a polytree - openPathsFromPolyTree and closedPathsFromPolyTree. 19 | */ 20 | export class PolyTree extends PolyNode { 21 | protected _total = 0; 22 | 23 | /** 24 | * Returns the total number of PolyNodes (polygons) contained within the PolyTree. This value is not to be confused with childs.length which returns the 25 | * number of immediate children only (Childs) contained by PolyTree. 26 | */ 27 | get total(): number { 28 | return this._total; 29 | } 30 | 31 | /** 32 | * This method returns the first outer polygon contour if any, otherwise undefined. 33 | * 34 | * This function is equivalent to calling childs[0]. 35 | */ 36 | getFirst(): PolyNode | undefined { 37 | if (this.childs.length > 0) { 38 | return this.childs[0]; 39 | } else { 40 | return undefined; 41 | } 42 | } 43 | 44 | protected constructor() { 45 | super(); 46 | } 47 | 48 | /** 49 | * Internal use. 50 | * Constructs a PolyTree from a native PolyTree. 51 | */ 52 | static fromNativePolyTree( 53 | nativeLib: NativeClipperLibInstance, 54 | nativePolyTree: NativePolyTree, 55 | freeNativePolyTree: boolean 56 | ): PolyTree { 57 | const pt = new PolyTree(); 58 | PolyNode.fillFromNativePolyNode(pt, nativeLib, nativePolyTree, undefined, 0, false); // do NOT free them, they are freed on destruction of the polytree 59 | 60 | pt._total = nativePolyTree.total(); 61 | 62 | if (freeNativePolyTree) { 63 | nativePolyTree.delete(); // this deletes all inner paths, contours etc 64 | } 65 | 66 | return pt; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/clipFunctions.ts: -------------------------------------------------------------------------------- 1 | import { Clipper } from "./Clipper"; 2 | import { ClipperError } from "./ClipperError"; 3 | import { ClipType, PolyFillType, PolyType } from "./enums"; 4 | import { NativeClipperLibInstance } from "./native/NativeClipperLibInstance"; 5 | import { Path, ReadonlyPath } from "./Path"; 6 | import { Paths, ReadonlyPaths } from "./Paths"; 7 | import { PolyTree } from "./PolyTree"; 8 | 9 | const devMode = 10 | typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production"; 11 | 12 | /** 13 | * A single subject input (of multiple possible inputs) for the clipToPaths / clipToPolyTree operations 14 | * 15 | * 'Subject' paths may be either open (lines) or closed (polygons) or even a mixture of both. 16 | * With closed paths, orientation should conform with the filling rule that will be passed via Clipper's execute method. 17 | */ 18 | export interface SubjectInput { 19 | /** 20 | * Path / Paths data. 21 | * 22 | * Path Coordinate range: 23 | * Path coordinates must be between ± 9007199254740991, otherwise a range error will be thrown when attempting to add the path to the Clipper object. 24 | * If coordinates can be kept between ± 0x3FFFFFFF (± 1.0e+9), a modest increase in performance (approx. 15-20%) over the larger range can be achieved by 25 | * avoiding large integer math. 26 | * 27 | * The function operation will throw an error if the path is invalid for clipping. A path is invalid for clipping when: 28 | * - it has less than 2 vertices 29 | * - it has 2 vertices but is not an open path 30 | * - the vertices are all co-linear and it is not an open path 31 | */ 32 | data: ReadonlyPath | ReadonlyPaths; 33 | 34 | /** 35 | * If the path/paths is closed or not. 36 | */ 37 | closed: boolean; 38 | } 39 | 40 | /** 41 | * A single clip input (of multiple possible inputs) for the clipToPaths / clipToPolyTree operations. 42 | * 43 | * Clipping paths must always be closed. Clipper allows polygons to clip both lines and other polygons, but doesn't allow lines to clip either lines or polygons. 44 | * With closed paths, orientation should conform with the filling rule that will be passed via Clipper's execute method. 45 | */ 46 | export interface ClipInput { 47 | /** 48 | * Path / Paths data. 49 | * 50 | * Path Coordinate range: 51 | * Path coordinates must be between ± 9007199254740991, otherwise a range error will be thrown when attempting to add the path to the Clipper object. 52 | * If coordinates can be kept between ± 0x3FFFFFFF (± 1.0e+9), a modest increase in performance (approx. 15-20%) over the larger range can be achieved by 53 | * avoiding large integer math. 54 | * 55 | * The function operation will throw an error if the path is invalid for clipping. A path is invalid for clipping when: 56 | * - it has less than 2 vertices 57 | * - it has 2 vertices but is not an open path 58 | * - the vertices are all co-linear and it is not an open path 59 | */ 60 | data: ReadonlyPath | ReadonlyPaths; 61 | } 62 | 63 | /** 64 | * Params for the clipToPaths / clipToPolyTree operations. 65 | * 66 | * Any number of subject and clip paths can be added to a clipping task. 67 | * 68 | * Boolean (clipping) operations are mostly applied to two sets of Polygons, represented in this library as subject and clip polygons. Whenever Polygons 69 | * are added to the Clipper object, they must be assigned to either subject or clip polygons. 70 | * 71 | * UNION operations can be performed on one set or both sets of polygons, but all other boolean operations require both sets of polygons to derive 72 | * meaningful solutions. 73 | */ 74 | export interface ClipParams { 75 | /** 76 | * Clipping operation type (Intersection, Union, Difference or Xor). 77 | */ 78 | clipType: ClipType; 79 | 80 | /** 81 | * Winding (fill) rule for subject polygons. 82 | */ 83 | subjectFillType: PolyFillType; 84 | 85 | /** 86 | * Subject inputs. 87 | */ 88 | subjectInputs: SubjectInput[]; 89 | 90 | /** 91 | * Winding (fill) rule for clipping polygons. If missing it will use the same one as subjectFillType. 92 | */ 93 | clipFillType?: PolyFillType; 94 | 95 | /** 96 | * Clipping inputs. Not required for union operations, required for others. 97 | */ 98 | clipInputs?: ClipInput[]; 99 | 100 | /** 101 | * When this property is set to true, polygons returned in the solution parameter of the clip method will have orientations opposite to their normal 102 | * orientations. 103 | */ 104 | reverseSolution?: boolean; 105 | 106 | /** 107 | * Terminology: 108 | * - A simple polygon is one that does not self-intersect. 109 | * - A weakly simple polygon is a simple polygon that contains 'touching' vertices, or 'touching' edges. 110 | * - A strictly simple polygon is a simple polygon that does not contain 'touching' vertices, or 'touching' edges. 111 | * 112 | * Vertices 'touch' if they share the same coordinates (and are not adjacent). An edge touches another if one of its end vertices touches another edge 113 | * excluding its adjacent edges, or if they are co-linear and overlapping (including adjacent edges). 114 | * 115 | * Polygons returned by clipping operations (see Clipper.execute()) should always be simple polygons. When the StrictlySimply property is enabled, 116 | * polygons returned will be strictly simple, otherwise they may be weakly simple. It's computationally expensive ensuring polygons are strictly simple 117 | * and so this property is disabled by default. 118 | * 119 | * Note: There's currently no guarantee that polygons will be strictly simple since 'simplifying' is still a work in progress. 120 | */ 121 | strictlySimple?: boolean; 122 | 123 | /** 124 | * By default, when three or more vertices are collinear in input polygons (subject or clip), the Clipper object removes the 'inner' vertices before 125 | * clipping. When enabled the preserveCollinear property prevents this default behavior to allow these inner vertices to appear in the solution. 126 | */ 127 | preserveCollinear?: boolean; 128 | 129 | /** 130 | * If this is not undefined then cleaning of the result polygon will be performed. 131 | * This operation is only available when the output format is not a poly tree. 132 | */ 133 | cleanDistance?: number; 134 | } 135 | 136 | const addPathOrPaths = ( 137 | clipper: Clipper, 138 | inputDatas: (SubjectInput | ClipInput)[] | undefined, 139 | polyType: PolyType 140 | ) => { 141 | if (inputDatas === undefined) { 142 | return; 143 | } 144 | 145 | // add each input 146 | for (let i = 0, maxi = inputDatas.length; i < maxi; i++) { 147 | const inputData = inputDatas[i]; 148 | 149 | // add the path/paths 150 | const pathOrPaths = inputData.data; 151 | if (!pathOrPaths || pathOrPaths.length <= 0) { 152 | continue; 153 | } 154 | 155 | const closed = 156 | (inputData as SubjectInput).closed === undefined ? true : (inputData as SubjectInput).closed; 157 | 158 | // is it a path or paths? 159 | if (Array.isArray(pathOrPaths[0])) { 160 | // paths 161 | if (!clipper.addPaths(pathOrPaths as Paths, polyType, closed)) { 162 | throw new ClipperError("invalid paths"); 163 | } 164 | } else { 165 | // path 166 | if (!clipper.addPath(pathOrPaths as Path, polyType, closed)) { 167 | throw new ClipperError("invalid path"); 168 | } 169 | } 170 | } 171 | }; 172 | 173 | export function clipToPathsOrPolyTree( 174 | polyTreeMode: boolean, 175 | nativeClipperLib: NativeClipperLibInstance, 176 | params: ClipParams 177 | ): Paths | PolyTree { 178 | if (devMode) { 179 | if (!polyTreeMode && params.subjectInputs && params.subjectInputs.some((si) => !si.closed)) { 180 | throw new Error("clip to a PolyTree (not to a Path) when using open paths"); 181 | } 182 | } 183 | 184 | const clipper = new Clipper(nativeClipperLib, params); 185 | 186 | //noinspection UnusedCatchParameterJS 187 | try { 188 | addPathOrPaths(clipper, params.subjectInputs, PolyType.Subject); 189 | addPathOrPaths(clipper, params.clipInputs, PolyType.Clip); 190 | let result; 191 | const clipFillType = 192 | params.clipFillType === undefined ? params.subjectFillType : params.clipFillType; 193 | if (!polyTreeMode) { 194 | result = clipper.executeToPaths( 195 | params.clipType, 196 | params.subjectFillType, 197 | clipFillType, 198 | params.cleanDistance 199 | ); 200 | } else { 201 | if (params.cleanDistance !== undefined) { 202 | throw new ClipperError("cleaning is not available for poly tree results"); 203 | } 204 | result = clipper.executeToPolyTee(params.clipType, params.subjectFillType, clipFillType); 205 | } 206 | if (result === undefined) { 207 | throw new ClipperError("error while performing clipping task"); 208 | } 209 | return result; 210 | } finally { 211 | clipper.dispose(); 212 | } 213 | } 214 | 215 | export function clipToPaths(nativeClipperLib: NativeClipperLibInstance, params: ClipParams): Paths { 216 | return clipToPathsOrPolyTree(false, nativeClipperLib, params) as Paths; 217 | } 218 | 219 | export function clipToPolyTree( 220 | nativeClipperLib: NativeClipperLibInstance, 221 | params: ClipParams 222 | ): PolyTree { 223 | return clipToPathsOrPolyTree(true, nativeClipperLib, params) as PolyTree; 224 | } 225 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Max coordinate value (both positive and negative) 3 | */ 4 | export const hiRange = 9007199254740991; 5 | -------------------------------------------------------------------------------- /src/enums.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * By far the most widely used winding rules for polygon filling are EvenOdd & NonZero (GDI, GDI+, XLib, OpenGL, Cairo, AGG, Quartz, SVG, Gr32) 3 | * Others rules include Positive, Negative and ABS_GTR_EQ_TWO (only in OpenGL) 4 | * see http://glprogramming.com/red/chapter11.html 5 | */ 6 | export enum PolyFillType { 7 | EvenOdd = "evenOdd", 8 | NonZero = "nonZero", 9 | Positive = "positive", 10 | Negative = "negative", 11 | } 12 | 13 | export enum ClipType { 14 | Intersection = "intersection", 15 | Union = "union", 16 | Difference = "difference", 17 | Xor = "xor", 18 | } 19 | export enum PolyType { 20 | Subject = "subject", 21 | Clip = "clip", 22 | } 23 | 24 | export enum JoinType { 25 | Square = "square", 26 | Round = "round", 27 | Miter = "miter", 28 | } 29 | export enum EndType { 30 | ClosedPolygon = "closedPolygon", 31 | ClosedLine = "closedLine", 32 | OpenButt = "openButt", 33 | OpenSquare = "openSquare", 34 | OpenRound = "openRound", 35 | } 36 | 37 | export enum PointInPolygonResult { 38 | Outside = 0, 39 | Inside = 1, 40 | OnBoundary = -1, 41 | } 42 | 43 | /** 44 | * Format to use when loading the native library instance. 45 | */ 46 | export enum NativeClipperLibRequestedFormat { 47 | /** 48 | * Try to load the WebAssembly version, if it fails try to load the Asm.js version. 49 | */ 50 | WasmWithAsmJsFallback = "wasmWithAsmJsFallback", 51 | /** 52 | * Load the WebAssembly version exclusively. 53 | */ 54 | WasmOnly = "wasmOnly", 55 | /** 56 | * Load the Asm.js version exclusively. 57 | */ 58 | AsmJsOnly = "asmJsOnly", 59 | } 60 | 61 | /** 62 | * The format the native library being used is in. 63 | */ 64 | export enum NativeClipperLibLoadedFormat { 65 | /** 66 | * WebAssembly. 67 | */ 68 | Wasm = "wasm", 69 | /** 70 | * Asm.js. 71 | */ 72 | AsmJs = "asmJs", 73 | } 74 | -------------------------------------------------------------------------------- /src/functions.ts: -------------------------------------------------------------------------------- 1 | import { PointInPolygonResult, PolyFillType } from "./enums"; 2 | import { IntPoint } from "./IntPoint"; 3 | import { NativeClipperLibInstance } from "./native/NativeClipperLibInstance"; 4 | import { NativeDeletable } from "./native/NativeDeletable"; 5 | import { polyFillTypeToNative } from "./native/nativeEnumConversion"; 6 | import { nativePathsToPaths, pathsToNativePaths } from "./native/PathsToNativePaths"; 7 | import { nativePathToPath, pathToNativePath } from "./native/PathToNativePath"; 8 | import { Path, ReadonlyPath } from "./Path"; 9 | import { Paths, ReadonlyPaths } from "./Paths"; 10 | import { PolyNode } from "./PolyNode"; 11 | import { PolyTree } from "./PolyTree"; 12 | 13 | function tryDelete(...objs: NativeDeletable[]) { 14 | for (const obj of objs) { 15 | if (!obj.isDeleted()) { 16 | obj.delete(); 17 | } 18 | } 19 | } 20 | 21 | export function area(path: ReadonlyPath): number { 22 | // we use JS since copying structures is slower than actually doing it 23 | const cnt = path.length; 24 | if (cnt < 3) { 25 | return 0; 26 | } 27 | let a = 0; 28 | for (let i = 0, j = cnt - 1; i < cnt; ++i) { 29 | a += (path[j].x + path[i].x) * (path[j].y - path[i].y); 30 | j = i; 31 | } 32 | return -a * 0.5; 33 | } 34 | 35 | export function cleanPolygon( 36 | nativeLib: NativeClipperLibInstance, 37 | path: ReadonlyPath, 38 | distance = 1.1415 39 | ): Path { 40 | const nativePath = pathToNativePath(nativeLib, path); 41 | try { 42 | nativeLib.cleanPolygon(nativePath, distance); 43 | return nativePathToPath(nativeLib, nativePath, true); // frees nativePath 44 | } finally { 45 | tryDelete(nativePath); 46 | } 47 | } 48 | 49 | export function cleanPolygons( 50 | nativeLib: NativeClipperLibInstance, 51 | paths: ReadonlyPaths, 52 | distance = 1.1415 53 | ): Paths { 54 | const nativePaths = pathsToNativePaths(nativeLib, paths); 55 | try { 56 | nativeLib.cleanPolygons(nativePaths, distance); 57 | return nativePathsToPaths(nativeLib, nativePaths, true); // frees nativePath 58 | } finally { 59 | tryDelete(nativePaths); 60 | } 61 | } 62 | 63 | const enum NodeType { 64 | Any, 65 | Open, 66 | Closed, 67 | } 68 | 69 | function addPolyNodeToPaths(polynode: PolyNode, nt: NodeType, paths: ReadonlyPath[]): void { 70 | let match = true; 71 | switch (nt) { 72 | case NodeType.Open: 73 | return; 74 | case NodeType.Closed: 75 | match = !polynode.isOpen; 76 | break; 77 | default: 78 | break; 79 | } 80 | 81 | if (polynode.contour.length > 0 && match) { 82 | paths.push(polynode.contour); 83 | } 84 | for (let ii = 0, max = polynode.childs.length; ii < max; ii++) { 85 | const pn = polynode.childs[ii]; 86 | addPolyNodeToPaths(pn, nt, paths); 87 | } 88 | } 89 | 90 | export function closedPathsFromPolyTree(polyTree: PolyTree): Paths { 91 | // we do this in JS since copying path is more expensive than just doing it 92 | 93 | const result: Paths = []; 94 | // result.Capacity = polytree.Total; 95 | addPolyNodeToPaths(polyTree, NodeType.Closed, result); 96 | return result; 97 | } 98 | 99 | export function minkowskiDiff( 100 | nativeLib: NativeClipperLibInstance, 101 | poly1: ReadonlyPath, 102 | poly2: ReadonlyPath 103 | ): Paths { 104 | const nativePath1 = pathToNativePath(nativeLib, poly1); 105 | const nativePath2 = pathToNativePath(nativeLib, poly2); 106 | const outNativePaths = new nativeLib.Paths(); 107 | 108 | try { 109 | nativeLib.minkowskiDiff(nativePath1, nativePath2, outNativePaths); 110 | tryDelete(nativePath1, nativePath2); 111 | return nativePathsToPaths(nativeLib, outNativePaths, true); // frees outNativePaths 112 | } finally { 113 | tryDelete(nativePath1, nativePath2, outNativePaths); 114 | } 115 | } 116 | 117 | export function minkowskiSumPath( 118 | nativeLib: NativeClipperLibInstance, 119 | pattern: ReadonlyPath, 120 | path: ReadonlyPath, 121 | pathIsClosed: boolean 122 | ): Paths { 123 | const patternNativePath = pathToNativePath(nativeLib, pattern); 124 | const nativePath = pathToNativePath(nativeLib, path); 125 | const outNativePaths = new nativeLib.Paths(); 126 | 127 | try { 128 | nativeLib.minkowskiSumPath(patternNativePath, nativePath, outNativePaths, pathIsClosed); 129 | tryDelete(patternNativePath, nativePath); 130 | return nativePathsToPaths(nativeLib, outNativePaths, true); // frees outNativePaths 131 | } finally { 132 | tryDelete(patternNativePath, nativePath, outNativePaths); 133 | } 134 | } 135 | 136 | export function minkowskiSumPaths( 137 | nativeLib: NativeClipperLibInstance, 138 | pattern: ReadonlyPath, 139 | paths: ReadonlyPaths, 140 | pathIsClosed: boolean 141 | ): Paths { 142 | // TODO: im not sure if for this method we can reuse the input/output path 143 | 144 | const patternNativePath = pathToNativePath(nativeLib, pattern); 145 | const nativePaths = pathsToNativePaths(nativeLib, paths); 146 | 147 | try { 148 | nativeLib.minkowskiSumPaths(patternNativePath, nativePaths, nativePaths, pathIsClosed); 149 | tryDelete(patternNativePath); 150 | return nativePathsToPaths(nativeLib, nativePaths, true); // frees nativePaths 151 | } finally { 152 | tryDelete(patternNativePath, nativePaths); 153 | } 154 | } 155 | 156 | export function openPathsFromPolyTree(polyTree: PolyTree): ReadonlyPath[] { 157 | // we do this in JS since copying path is more expensive than just doing it 158 | 159 | const result = []; 160 | const len = polyTree.childs.length; 161 | result.length = len; 162 | let resultLength = 0; 163 | for (let i = 0; i < len; i++) { 164 | if (polyTree.childs[i].isOpen) { 165 | result[resultLength++] = polyTree.childs[i].contour; 166 | } 167 | } 168 | result.length = resultLength; 169 | return result; 170 | } 171 | 172 | export function orientation(path: ReadonlyPath): boolean { 173 | return area(path) >= 0; 174 | } 175 | 176 | export function pointInPolygon( 177 | point: Readonly, 178 | path: ReadonlyPath 179 | ): PointInPolygonResult { 180 | // we do this in JS since copying path is more expensive than just doing it 181 | 182 | // returns 0 if false, +1 if true, -1 if pt ON polygon boundary 183 | // See "The Point in Polygon Problem for Arbitrary Polygons" by Hormann & Agathos 184 | // http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf 185 | let result = 0; 186 | const cnt = path.length; 187 | if (cnt < 3) { 188 | return 0; 189 | } 190 | let ip = path[0]; 191 | for (let i = 1; i <= cnt; ++i) { 192 | const ipNext = i === cnt ? path[0] : path[i]; 193 | if (ipNext.y === point.y) { 194 | if (ipNext.x === point.x || (ip.y === point.y && ipNext.x > point.x === ip.x < point.x)) { 195 | return -1; 196 | } 197 | } 198 | if (ip.y < point.y !== ipNext.y < point.y) { 199 | if (ip.x >= point.x) { 200 | if (ipNext.x > point.x) { 201 | result = 1 - result; 202 | } else { 203 | const d = 204 | (ip.x - point.x) * (ipNext.y - point.y) - (ipNext.x - point.x) * (ip.y - point.y); 205 | if (d === 0) { 206 | return -1; 207 | } else if (d > 0 === ipNext.y > ip.y) { 208 | result = 1 - result; 209 | } 210 | } 211 | } else { 212 | if (ipNext.x > point.x) { 213 | const d = 214 | (ip.x - point.x) * (ipNext.y - point.y) - (ipNext.x - point.x) * (ip.y - point.y); 215 | if (d === 0) { 216 | return -1; 217 | } else if (d > 0 === ipNext.y > ip.y) { 218 | result = 1 - result; 219 | } 220 | } 221 | } 222 | } 223 | ip = ipNext; 224 | } 225 | return result; 226 | } 227 | 228 | export function polyTreeToPaths(polyTree: PolyTree): Paths { 229 | // we do this in JS since copying path is more expensive than just doing it 230 | 231 | const result: Paths = []; 232 | // result.Capacity = polytree.total; 233 | addPolyNodeToPaths(polyTree, NodeType.Any, result); 234 | return result; 235 | } 236 | 237 | export function reversePath(path: Path): void { 238 | // we use JS since copying structures is slower than actually doing it 239 | path.reverse(); 240 | } 241 | 242 | export function reversePaths(paths: Paths): void { 243 | // we use JS since copying structures is slower than actually doing it 244 | for (let i = 0, max = paths.length; i < max; i++) { 245 | reversePath(paths[i]); 246 | } 247 | } 248 | 249 | export function simplifyPolygon( 250 | nativeLib: NativeClipperLibInstance, 251 | path: ReadonlyPath, 252 | fillType: PolyFillType = PolyFillType.EvenOdd 253 | ): Paths { 254 | const nativePath = pathToNativePath(nativeLib, path); 255 | const outNativePaths = new nativeLib.Paths(); 256 | try { 257 | nativeLib.simplifyPolygon( 258 | nativePath, 259 | outNativePaths, 260 | polyFillTypeToNative(nativeLib, fillType) 261 | ); 262 | tryDelete(nativePath); 263 | return nativePathsToPaths(nativeLib, outNativePaths, true); // frees outNativePaths 264 | } finally { 265 | tryDelete(nativePath, outNativePaths); 266 | } 267 | } 268 | 269 | export function simplifyPolygons( 270 | nativeLib: NativeClipperLibInstance, 271 | paths: ReadonlyPaths, 272 | fillType: PolyFillType = PolyFillType.EvenOdd 273 | ): Paths { 274 | const nativePaths = pathsToNativePaths(nativeLib, paths); 275 | try { 276 | nativeLib.simplifyPolygonsOverwrite(nativePaths, polyFillTypeToNative(nativeLib, fillType)); 277 | return nativePathsToPaths(nativeLib, nativePaths, true); // frees nativePaths 278 | } finally { 279 | tryDelete(nativePaths); 280 | } 281 | } 282 | 283 | export function scalePath(path: ReadonlyPath, scale: number): Path { 284 | const len = path.length; 285 | 286 | const sol: Path = []; 287 | sol.length = path.length; 288 | 289 | for (let i = 0; i < len; i++) { 290 | const p = path[i]; 291 | sol[i] = { 292 | x: Math.round(p.x * scale), 293 | y: Math.round(p.y * scale), 294 | }; 295 | } 296 | 297 | return sol; 298 | } 299 | 300 | /** 301 | * Scales all inner paths by multiplying all its coordinates by a number and then rounding them. 302 | * 303 | * @param paths - Paths to scale 304 | * @param scale - Scale multiplier 305 | * @return {Paths} - The scaled paths 306 | */ 307 | export function scalePaths(paths: ReadonlyPaths, scale: number): Paths { 308 | if (scale === 0) { 309 | return []; 310 | } 311 | 312 | const len = paths.length; 313 | 314 | const sol: Paths = []; 315 | sol.length = len; 316 | 317 | for (let i = 0; i < len; i++) { 318 | sol[i] = scalePath(paths[i], scale); 319 | } 320 | 321 | return sol; 322 | } 323 | -------------------------------------------------------------------------------- /src/native/NativeClipper.ts: -------------------------------------------------------------------------------- 1 | import { NativeClipperBase } from "./NativeClipperBase"; 2 | import { NativeClipType, NativePolyFillType } from "./nativeEnums"; 3 | import { NativePaths } from "./NativePaths"; 4 | import { NativePolyTree } from "./NativePolyTree"; 5 | 6 | export interface NativeClipper extends NativeClipperBase { 7 | executePaths( 8 | clipType: NativeClipType, 9 | outPaths: NativePaths, 10 | polyFillType: NativePolyFillType 11 | ): boolean; 12 | executePathsWithFillTypes( 13 | clipType: NativeClipType, 14 | outPaths: NativePaths, 15 | subjPolyFillType: NativePolyFillType, 16 | clipPolyFillType: NativePolyFillType 17 | ): boolean; 18 | executePolyTree( 19 | clipType: NativeClipType, 20 | outPolyTree: NativePolyTree, 21 | polyFillType: NativePolyFillType 22 | ): boolean; 23 | executePolyTreeWithFillTypes( 24 | clipType: NativeClipType, 25 | outPolyTree: NativePolyTree, 26 | subjPolyFillType: NativePolyFillType, 27 | clipPolyFillType: NativePolyFillType 28 | ): boolean; 29 | reverseSolution: boolean; 30 | strictlySimple: boolean; 31 | } 32 | -------------------------------------------------------------------------------- /src/native/NativeClipperBase.ts: -------------------------------------------------------------------------------- 1 | import { NativeDeletable } from "./NativeDeletable"; 2 | import { NativePolyType } from "./nativeEnums"; 3 | import { NativeIntRect } from "./NativeIntRect"; 4 | import { NativePath } from "./NativePath"; 5 | import { NativePaths } from "./NativePaths"; 6 | 7 | export interface NativeClipperBase extends NativeDeletable { 8 | addPath(path: NativePath, polyType: NativePolyType, closed: boolean): boolean; 9 | addPaths(paths: NativePaths, polyType: NativePolyType, closed: boolean): boolean; 10 | clear(): void; 11 | getBounds(): NativeIntRect; 12 | preserveCollinear: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /src/native/NativeClipperLibInstance.ts: -------------------------------------------------------------------------------- 1 | import { NativeClipper } from "./NativeClipper"; 2 | import { NativeClipperOffset } from "./NativeClipperOffset"; 3 | import { 4 | NativeClipType, 5 | NativeEndType, 6 | NativeInitOptions, 7 | NativeJoinType, 8 | NativePolyFillType, 9 | NativePolyType, 10 | } from "./nativeEnums"; 11 | import { NativeIntPoint } from "./NativeIntPoint"; 12 | import { NativePath } from "./NativePath"; 13 | import { NativePaths } from "./NativePaths"; 14 | import { NativePolyTree } from "./NativePolyTree"; 15 | 16 | export interface NativeClipperLibInstance { 17 | // custom conversion functions 18 | toPath(dest: NativePath, coordsPtr: number): void; 19 | toPaths(dest: NativePaths, pathsPtr: number): void; 20 | fromPath(path: NativePath): Float64Array; 21 | fromPaths(paths: NativePaths): Float64Array; 22 | 23 | // memory 24 | _malloc(nofBytes: number): number; 25 | _free(ptr: number): void; 26 | HEAPF64: { 27 | buffer: ArrayBuffer; 28 | }; 29 | 30 | // types 31 | Path: new () => NativePath; 32 | Paths: new () => NativePaths; 33 | PolyTree: new () => NativePolyTree; 34 | Clipper: new (initOptions: number) => NativeClipper; 35 | ClipperOffset: new (miterLimit: number, arcTolerance: number) => NativeClipperOffset; 36 | 37 | // functions 38 | newIntPoint(x: number, y: number): NativeIntPoint; 39 | 40 | orientation(path: NativePath): boolean; 41 | area(path: NativePath): number; 42 | pointInPolygon(pt: NativeIntPoint, path: NativePath): number; 43 | 44 | simplifyPolygon(path: NativePath, outPaths: NativePaths, fillType: NativePolyFillType): void; 45 | simplifyPolygonsInOut( 46 | paths: NativePaths, 47 | outPaths: NativePaths, 48 | fillType: NativePolyFillType 49 | ): void; 50 | simplifyPolygonsOverwrite(paths: NativePaths, fillType: NativePolyFillType): void; 51 | 52 | cleanPolygon(path: NativePath, outPath: NativePath, distance: number): void; 53 | cleanPolygon(inOutPath: NativePath, distance: number): void; 54 | cleanPolygons(paths: NativePaths, outPaths: NativePaths, distance: number): void; 55 | cleanPolygons(inOutPaths: NativePaths, distance: number): void; 56 | 57 | minkowskiSumPath( 58 | pattern: NativePath, 59 | path: NativePath, 60 | outPaths: NativePaths, 61 | pathIsClosed: boolean 62 | ): void; 63 | minkowskiSumPaths( 64 | pattern: NativePath, 65 | paths: NativePaths, 66 | outPaths: NativePaths, 67 | pathIsClosed: boolean 68 | ): void; 69 | minkowskiDiff(path1: NativePath, path2: NativePath, outPaths: NativePaths): void; 70 | 71 | polyTreeToPaths(polyTree: NativePolyTree, outPaths: NativePaths): void; 72 | closedPathsFromPolyTree(polyTree: NativePolyTree, outPaths: NativePaths): void; 73 | openPathsFromPolyTree(polyTree: NativePolyTree, outPaths: NativePaths): void; 74 | 75 | reversePath(inOutPath: NativePath): void; 76 | reversePaths(inOutPaths: NativePaths): void; 77 | 78 | ClipType: NativeClipType; 79 | PolyType: NativePolyType; 80 | PolyFillType: NativePolyFillType; 81 | InitOptions: NativeInitOptions; 82 | JoinType: NativeJoinType; 83 | EndType: NativeEndType; 84 | } 85 | -------------------------------------------------------------------------------- /src/native/NativeClipperOffset.ts: -------------------------------------------------------------------------------- 1 | import { NativeDeletable } from "./NativeDeletable"; 2 | import { NativeEndType, NativeJoinType } from "./nativeEnums"; 3 | import { NativePath } from "./NativePath"; 4 | import { NativePaths } from "./NativePaths"; 5 | import { NativePolyTree } from "./NativePolyTree"; 6 | 7 | export interface NativeClipperOffset extends NativeDeletable { 8 | addPath(outPath: NativePath, joinType: NativeJoinType, endType: NativeEndType): void; 9 | addPaths(outPaths: NativePaths, joinType: NativeJoinType, endType: NativeEndType): void; 10 | executePaths(outPaths: NativePaths, delta: number): void; 11 | executePolyTree(outPolyTree: NativePolyTree, delta: number): void; 12 | clear(): void; 13 | miterLimit: number; 14 | arcTolerance: number; 15 | } 16 | -------------------------------------------------------------------------------- /src/native/NativeDeletable.ts: -------------------------------------------------------------------------------- 1 | export interface NativeDeletable { 2 | isDeleted(): boolean; 3 | delete(): void; 4 | } 5 | -------------------------------------------------------------------------------- /src/native/NativeIntPoint.ts: -------------------------------------------------------------------------------- 1 | import { NativeDeletable } from "./NativeDeletable"; 2 | 3 | export interface NativeIntPoint extends NativeDeletable { 4 | x: number; 5 | y: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/native/NativeIntRect.ts: -------------------------------------------------------------------------------- 1 | import { NativeDeletable } from "./NativeDeletable"; 2 | 3 | export interface NativeIntRect extends NativeDeletable { 4 | left: number; 5 | top: number; 6 | right: number; 7 | bottom: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/native/NativePath.ts: -------------------------------------------------------------------------------- 1 | import { NativeIntPoint } from "./NativeIntPoint"; 2 | import { NativeVector } from "./NativeVector"; 3 | 4 | export type NativePath = NativeVector; 5 | -------------------------------------------------------------------------------- /src/native/NativePaths.ts: -------------------------------------------------------------------------------- 1 | import { NativePath } from "./NativePath"; 2 | import { NativeVector } from "./NativeVector"; 3 | 4 | export type NativePaths = NativeVector; 5 | -------------------------------------------------------------------------------- /src/native/NativePolyNode.ts: -------------------------------------------------------------------------------- 1 | import { NativeDeletable } from "./NativeDeletable"; 2 | import { NativePath } from "./NativePath"; 3 | import { NativeVector } from "./NativeVector"; 4 | 5 | export interface NativePolyNode extends NativeDeletable { 6 | contour: NativePath; 7 | childs: NativeVector; 8 | getParent(): NativePolyNode | null; 9 | getNext(): NativePolyNode | null; 10 | isHole(): boolean; 11 | isOpen(): boolean; 12 | childCount(): number; 13 | index: number; 14 | } 15 | -------------------------------------------------------------------------------- /src/native/NativePolyTree.ts: -------------------------------------------------------------------------------- 1 | import { NativePolyNode } from "./NativePolyNode"; 2 | 3 | export interface NativePolyTree extends NativePolyNode { 4 | clear(): void; 5 | getFirst(): NativePolyNode; 6 | total(): number; 7 | } 8 | -------------------------------------------------------------------------------- /src/native/NativeVector.ts: -------------------------------------------------------------------------------- 1 | import { NativeDeletable } from "./NativeDeletable"; 2 | 3 | export interface NativeVector extends NativeDeletable { 4 | size(): number; 5 | get(index: number): T; 6 | set(index: number, value: T): void; 7 | } 8 | -------------------------------------------------------------------------------- /src/native/PathToNativePath.ts: -------------------------------------------------------------------------------- 1 | import { Path, ReadonlyPath } from "../Path"; 2 | import { freeTypedArray, mallocDoubleArray } from "./mem"; 3 | import { NativeClipperLibInstance } from "./NativeClipperLibInstance"; 4 | import { NativePath } from "./NativePath"; 5 | 6 | const coordsPerPoint = 2; 7 | 8 | export function getNofItemsForPath(path: ReadonlyPath): number { 9 | return 1 + path.length * coordsPerPoint; 10 | } 11 | 12 | // js to c++ 13 | 14 | export function writePathToDoubleArray( 15 | path: ReadonlyPath, 16 | heapBytes: Float64Array, 17 | startPtr: number 18 | ): number { 19 | const len = path.length; 20 | 21 | heapBytes[startPtr] = len; 22 | 23 | let arrayI = 1 + startPtr; 24 | for (let i = 0; i < len; i++) { 25 | heapBytes[arrayI++] = path[i].x; 26 | heapBytes[arrayI++] = path[i].y; 27 | } 28 | 29 | return arrayI; 30 | } 31 | 32 | export function pathToDoubleArray( 33 | nativeClipperLib: NativeClipperLibInstance, 34 | path: ReadonlyPath 35 | ): Float64Array { 36 | const nofItems = getNofItemsForPath(path); 37 | const heapBytes = mallocDoubleArray(nativeClipperLib, nofItems); 38 | writePathToDoubleArray(path, heapBytes, 0); 39 | return heapBytes; 40 | } 41 | 42 | export function doubleArrayToNativePath( 43 | nativeClipperLib: NativeClipperLibInstance, 44 | array: Float64Array, 45 | freeArray: boolean 46 | ): NativePath { 47 | const p = new nativeClipperLib.Path(); 48 | nativeClipperLib.toPath(p, array.byteOffset); 49 | if (freeArray) { 50 | freeTypedArray(nativeClipperLib, array); 51 | } 52 | return p; 53 | } 54 | 55 | export function pathToNativePath( 56 | nativeClipperLib: NativeClipperLibInstance, 57 | path: ReadonlyPath 58 | ): NativePath { 59 | const array = pathToDoubleArray(nativeClipperLib, path); 60 | return doubleArrayToNativePath(nativeClipperLib, array, true); 61 | } 62 | 63 | // c++ to js 64 | 65 | export function nativePathToDoubleArray( 66 | nativeClipperLib: NativeClipperLibInstance, 67 | nativePath: NativePath, 68 | freeNativePath: boolean 69 | ): Float64Array { 70 | const array = nativeClipperLib.fromPath(nativePath); 71 | if (freeNativePath) { 72 | nativePath.delete(); 73 | } 74 | return array; 75 | } 76 | 77 | export function doubleArrayToPath( 78 | nativeClipperLib: NativeClipperLibInstance, 79 | array: Float64Array, 80 | _freeDoubleArray: boolean, 81 | startPtr: number 82 | ): { path: Path; ptrEnd: number } { 83 | const len = array[startPtr]; 84 | const path = []; 85 | path.length = len; 86 | 87 | let arrayI = 1 + startPtr; 88 | for (let i = 0; i < len; i++) { 89 | path[i] = { 90 | x: array[arrayI++], 91 | y: array[arrayI++], 92 | }; 93 | } 94 | 95 | if (_freeDoubleArray) { 96 | freeTypedArray(nativeClipperLib, array); 97 | } 98 | 99 | return { 100 | path: path, 101 | ptrEnd: arrayI, 102 | }; 103 | } 104 | 105 | export function nativePathToPath( 106 | nativeClipperLib: NativeClipperLibInstance, 107 | nativePath: NativePath, 108 | freeNativePath: boolean 109 | ): Path { 110 | const array = nativePathToDoubleArray(nativeClipperLib, nativePath, freeNativePath); 111 | return doubleArrayToPath(nativeClipperLib, array, true, 0).path; 112 | } 113 | -------------------------------------------------------------------------------- /src/native/PathsToNativePaths.ts: -------------------------------------------------------------------------------- 1 | import { Path } from "../Path"; 2 | import { Paths, ReadonlyPaths } from "../Paths"; 3 | import { freeTypedArray, mallocDoubleArray } from "./mem"; 4 | import { NativeClipperLibInstance } from "./NativeClipperLibInstance"; 5 | import { NativePaths } from "./NativePaths"; 6 | import { doubleArrayToPath, getNofItemsForPath, writePathToDoubleArray } from "./PathToNativePath"; 7 | 8 | // js to c++ 9 | 10 | export function pathsToDoubleArray( 11 | nativeClipperLib: NativeClipperLibInstance, 12 | myPaths: ReadonlyPaths 13 | ): Float64Array { 14 | const nofPaths = myPaths.length; 15 | 16 | // first calculate nof items required 17 | let nofItems = 1; // for path count 18 | for (let i = 0; i < nofPaths; i++) { 19 | nofItems += getNofItemsForPath(myPaths[i]); 20 | } 21 | const heapBytes = mallocDoubleArray(nativeClipperLib, nofItems); 22 | heapBytes[0] = nofPaths; 23 | 24 | let ptr = 1; 25 | for (let i = 0; i < nofPaths; i++) { 26 | const path = myPaths[i]; 27 | ptr = writePathToDoubleArray(path, heapBytes, ptr); 28 | } 29 | 30 | return heapBytes; 31 | } 32 | 33 | export function doubleArrayToNativePaths( 34 | nativeClipperLib: NativeClipperLibInstance, 35 | array: Float64Array, 36 | freeArray: boolean 37 | ): NativePaths { 38 | const p = new nativeClipperLib.Paths(); 39 | nativeClipperLib.toPaths(p, array.byteOffset); 40 | if (freeArray) { 41 | freeTypedArray(nativeClipperLib, array); 42 | } 43 | return p; 44 | } 45 | 46 | export function pathsToNativePaths( 47 | nativeClipperLib: NativeClipperLibInstance, 48 | paths: ReadonlyPaths 49 | ): NativePaths { 50 | const array = pathsToDoubleArray(nativeClipperLib, paths); 51 | return doubleArrayToNativePaths(nativeClipperLib, array, true); 52 | } 53 | 54 | // c++ to js 55 | 56 | export function nativePathsToDoubleArray( 57 | nativeClipperLib: NativeClipperLibInstance, 58 | nativePaths: NativePaths, 59 | freeNativePaths: boolean 60 | ): Float64Array { 61 | const array = nativeClipperLib.fromPaths(nativePaths); 62 | if (freeNativePaths) { 63 | nativePaths.delete(); 64 | } 65 | return array; 66 | } 67 | 68 | export function doubleArrayToPaths( 69 | nativeClipperLib: NativeClipperLibInstance, 70 | array: Float64Array, 71 | _freeDoubleArray: boolean 72 | ): Paths { 73 | const len = array[0]; 74 | const paths: Path[] = []; 75 | paths.length = len; 76 | 77 | let arrayI = 1; 78 | for (let i = 0; i < len; i++) { 79 | const result = doubleArrayToPath(nativeClipperLib, array, false, arrayI); 80 | paths[i] = result.path; 81 | arrayI = result.ptrEnd; 82 | } 83 | 84 | if (_freeDoubleArray) { 85 | freeTypedArray(nativeClipperLib, array); 86 | } 87 | 88 | return paths; 89 | } 90 | 91 | export function nativePathsToPaths( 92 | nativeClipperLib: NativeClipperLibInstance, 93 | nativePaths: NativePaths, 94 | freeNativePaths: boolean 95 | ): Paths { 96 | const array = nativePathsToDoubleArray(nativeClipperLib, nativePaths, freeNativePaths); 97 | return doubleArrayToPaths(nativeClipperLib, array, true); 98 | } 99 | -------------------------------------------------------------------------------- /src/native/mem.ts: -------------------------------------------------------------------------------- 1 | import { NativeClipperLibInstance } from "./NativeClipperLibInstance"; 2 | 3 | export function mallocDoubleArray( 4 | nativeClipperLib: NativeClipperLibInstance, 5 | len: number 6 | ): Float64Array { 7 | const nofBytes = len * Float64Array.BYTES_PER_ELEMENT; 8 | const ptr = nativeClipperLib._malloc(nofBytes); 9 | return new Float64Array(nativeClipperLib.HEAPF64.buffer, ptr, len); 10 | } 11 | 12 | export function freeTypedArray( 13 | nativeClipperLib: NativeClipperLibInstance, 14 | array: Float64Array | Uint32Array 15 | ): void { 16 | nativeClipperLib._free(array.byteOffset); 17 | } 18 | -------------------------------------------------------------------------------- /src/native/nativeEnumConversion.ts: -------------------------------------------------------------------------------- 1 | import { ClipType, EndType, JoinType, PolyFillType, PolyType } from "../enums"; 2 | import { NativeClipperLibInstance } from "./NativeClipperLibInstance"; 3 | import { 4 | NativeClipType, 5 | NativeEndType, 6 | NativeJoinType, 7 | NativePolyFillType, 8 | NativePolyType, 9 | } from "./nativeEnums"; 10 | 11 | export function polyFillTypeToNative( 12 | nativeLib: NativeClipperLibInstance, 13 | polyFillType: PolyFillType 14 | ): NativePolyFillType { 15 | switch (polyFillType) { 16 | case PolyFillType.EvenOdd: 17 | return nativeLib.PolyFillType.EvenOdd; 18 | case PolyFillType.NonZero: 19 | return nativeLib.PolyFillType.NonZero; 20 | case PolyFillType.Positive: 21 | return nativeLib.PolyFillType.Positive; 22 | case PolyFillType.Negative: 23 | return nativeLib.PolyFillType.Negative; 24 | default: 25 | throw new Error("unknown poly fill type"); 26 | } 27 | } 28 | 29 | export function clipTypeToNative( 30 | nativeLib: NativeClipperLibInstance, 31 | clipType: ClipType 32 | ): NativeClipType { 33 | switch (clipType) { 34 | case ClipType.Intersection: 35 | return nativeLib.ClipType.Intersection; 36 | case ClipType.Union: 37 | return nativeLib.ClipType.Union; 38 | case ClipType.Difference: 39 | return nativeLib.ClipType.Difference; 40 | case ClipType.Xor: 41 | return nativeLib.ClipType.Xor; 42 | default: 43 | throw new Error("unknown clip type"); 44 | } 45 | } 46 | 47 | export function polyTypeToNative( 48 | nativeLib: NativeClipperLibInstance, 49 | polyType: PolyType 50 | ): NativePolyType { 51 | switch (polyType) { 52 | case PolyType.Subject: 53 | return nativeLib.PolyType.Subject; 54 | case PolyType.Clip: 55 | return nativeLib.PolyType.Clip; 56 | default: 57 | throw new Error("unknown poly type"); 58 | } 59 | } 60 | 61 | export function joinTypeToNative( 62 | nativeLib: NativeClipperLibInstance, 63 | joinType: JoinType 64 | ): NativeJoinType { 65 | switch (joinType) { 66 | case JoinType.Square: 67 | return nativeLib.JoinType.Square; 68 | case JoinType.Round: 69 | return nativeLib.JoinType.Round; 70 | case JoinType.Miter: 71 | return nativeLib.JoinType.Miter; 72 | default: 73 | throw new Error("unknown join type"); 74 | } 75 | } 76 | 77 | export function endTypeToNative( 78 | nativeLib: NativeClipperLibInstance, 79 | endType: EndType 80 | ): NativeEndType { 81 | switch (endType) { 82 | case EndType.ClosedPolygon: 83 | return nativeLib.EndType.ClosedPolygon; 84 | case EndType.ClosedLine: 85 | return nativeLib.EndType.ClosedLine; 86 | case EndType.OpenButt: 87 | return nativeLib.EndType.OpenButt; 88 | case EndType.OpenSquare: 89 | return nativeLib.EndType.OpenSquare; 90 | case EndType.OpenRound: 91 | return nativeLib.EndType.OpenRound; 92 | default: 93 | throw new Error("unknown end type"); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/native/nativeEnums.ts: -------------------------------------------------------------------------------- 1 | export interface NativeEnum { 2 | value: number; 3 | } 4 | 5 | // native enum 6 | export interface NativeClipType { 7 | Intersection: NativeEnum & NativeClipType; 8 | Union: NativeEnum & NativeClipType; 9 | Difference: NativeEnum & NativeClipType; 10 | Xor: NativeEnum & NativeClipType; 11 | } 12 | 13 | // native enum 14 | export interface NativePolyType { 15 | Subject: NativeEnum & NativePolyType; 16 | Clip: NativeEnum & NativePolyType; 17 | } 18 | 19 | // native enum 20 | export interface NativePolyFillType { 21 | EvenOdd: NativeEnum & NativePolyFillType; 22 | NonZero: NativeEnum & NativePolyFillType; 23 | Positive: NativeEnum & NativePolyFillType; 24 | Negative: NativeEnum & NativePolyFillType; 25 | } 26 | 27 | // native enum 28 | export interface NativeInitOptions { 29 | ReverseSolution: NativeEnum & NativeInitOptions; 30 | StrictlySimple: NativeEnum & NativeInitOptions; 31 | PreserveCollinear: NativeEnum & NativeInitOptions; 32 | } 33 | 34 | // native enum 35 | export interface NativeJoinType { 36 | Square: NativeEnum & NativeJoinType; 37 | Round: NativeEnum & NativeJoinType; 38 | Miter: NativeEnum & NativeJoinType; 39 | } 40 | 41 | // native enum 42 | export interface NativeEndType { 43 | ClosedPolygon: NativeEnum & NativeEndType; 44 | ClosedLine: NativeEnum & NativeEndType; 45 | OpenButt: NativeEnum & NativeEndType; 46 | OpenSquare: NativeEnum & NativeEndType; 47 | OpenRound: NativeEnum & NativeEndType; 48 | } 49 | -------------------------------------------------------------------------------- /src/nativeFinalizationRegistry.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { NativeDeletable } from "./native/NativeDeletable"; 3 | 4 | interface FinalizationRegistry { 5 | readonly [Symbol.toStringTag]: "FinalizationRegistry"; 6 | 7 | /** 8 | * Registers an object with the registry. 9 | * @param target The target object to register. 10 | * @param heldValue The value to pass to the finalizer for this object. This cannot be the 11 | * target object. 12 | * @param unregisterToken The token to pass to the unregister method to unregister the target 13 | * object. If provided (and not undefined), this must be an object. If not provided, the target 14 | * cannot be unregistered. 15 | */ 16 | register(target: any, heldValue: any, unregisterToken?: any): void; 17 | 18 | /** 19 | * Unregisters an object from the registry. 20 | * @param unregisterToken The token that was used as the unregisterToken argument when calling 21 | * register to register the target object. 22 | */ 23 | unregister(unregisterToken: any): void; 24 | } 25 | 26 | interface FinalizationRegistryConstructor { 27 | readonly prototype: FinalizationRegistry; 28 | 29 | /** 30 | * Creates a finalization registry with an associated cleanup callback 31 | * @param cleanupCallback The callback to call after an object in the registry has been reclaimed. 32 | */ 33 | new (cleanupCallback: (heldValue: any) => void): FinalizationRegistry; 34 | } 35 | 36 | declare let FinalizationRegistry: FinalizationRegistryConstructor; 37 | 38 | export const nativeFinalizationRegistry = 39 | typeof FinalizationRegistry === "undefined" 40 | ? undefined 41 | : new FinalizationRegistry((nativeObj: NativeDeletable) => { 42 | if (!nativeObj.isDeleted()) { 43 | nativeObj.delete(); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /src/offsetFunctions.ts: -------------------------------------------------------------------------------- 1 | import { ClipperOffset } from "./ClipperOffset"; 2 | import { EndType, JoinType } from "./enums"; 3 | import { NativeClipperLibInstance } from "./native/NativeClipperLibInstance"; 4 | import { ReadonlyPath } from "./Path"; 5 | import { Paths, ReadonlyPaths } from "./Paths"; 6 | import { PolyTree } from "./PolyTree"; 7 | 8 | /** 9 | * A single input (of multiple possible inputs) for the offsetToPaths / offsetToPolyTree operation. 10 | */ 11 | export interface OffsetInput { 12 | /** 13 | * Join type. 14 | */ 15 | joinType: JoinType; 16 | 17 | /** 18 | * End type. 19 | */ 20 | endType: EndType; 21 | 22 | /** 23 | * Data of one of the Path or Paths to be used in preparation for offsetting. 24 | * 25 | * All 'outer' Paths must have the same orientation, and any 'hole' paths must have reverse orientation. Closed paths must have at least 3 vertices. 26 | * Open paths may have as few as one vertex. Open paths can only be offset with positive deltas. 27 | */ 28 | data: ReadonlyPath | ReadonlyPaths; 29 | } 30 | 31 | /** 32 | * Params for the polygon offset operation. 33 | */ 34 | export interface OffsetParams { 35 | /** 36 | * Firstly, this field/property is only relevant when JoinType = Round and/or EndType = Round. 37 | * 38 | * Since flattened paths can never perfectly represent arcs, this field/property specifies a maximum acceptable imprecision ('tolerance') when arcs are 39 | * approximated in an offsetting operation. Smaller values will increase 'smoothness' up to a point though at a cost of performance and in creating more 40 | * vertices to construct the arc. 41 | * 42 | * The default ArcTolerance is 0.25 units. This means that the maximum distance the flattened path will deviate from the 'true' arc will be no more 43 | * than 0.25 units (before rounding). 44 | * 45 | * Reducing tolerances below 0.25 will not improve smoothness since vertex coordinates will still be rounded to integer values. The only way to achieve 46 | * sub-integer precision is through coordinate scaling before and after offsetting (see example below). 47 | * 48 | * It's important to make ArcTolerance a sensible fraction of the offset delta (arc radius). Large tolerances relative to the offset delta will produce 49 | * poor arc approximations but, just as importantly, very small tolerances will substantially slow offsetting performance while providing unnecessary 50 | * degrees of precision. This is most likely to be an issue when offsetting polygons whose coordinates have been scaled to preserve floating point precision. 51 | * 52 | * Example: Imagine a set of polygons (defined in floating point coordinates) that is to be offset by 10 units using round joins, and the solution is to 53 | * retain floating point precision up to at least 6 decimal places. 54 | * To preserve this degree of floating point precision, and given that Clipper and ClipperOffset both operate on integer coordinates, the polygon 55 | * coordinates will be scaled up by 108 (and rounded to integers) prior to offsetting. Both offset delta and ArcTolerance will also need to be scaled 56 | * by this same factor. If ArcTolerance was left unscaled at the default 0.25 units, every arc in the solution would contain a fraction of 44 THOUSAND 57 | * vertices while the final arc imprecision would be 0.25 × 10-8 units (ie once scaling was reversed). However, if 0.1 units was an acceptable imprecision 58 | * in the final unscaled solution, then ArcTolerance should be set to 0.1 × scaling_factor (0.1 × 108 ). Now if scaling is applied equally to both 59 | * ArcTolerance and to Delta Offset, then in this example the number of vertices (steps) defining each arc would be a fraction of 23. 60 | * 61 | * The formula for the number of steps in a full circular arc is ... Pi / acos(1 - arc_tolerance / abs(delta)) 62 | */ 63 | arcTolerance?: number; // defaults to 0.25 64 | 65 | /** 66 | * This property sets the maximum distance in multiples of delta that vertices can be offset from their original positions before squaring is applied. 67 | * (Squaring truncates a miter by 'cutting it off' at 1 × delta distance from the original vertex.) 68 | * 69 | * The default value for MiterLimit is 2 (ie twice delta). This is also the smallest MiterLimit that's allowed. If mitering was unrestricted (ie without 70 | * any squaring), then offsets at very acute angles would generate unacceptably long 'spikes'. 71 | */ 72 | miterLimit?: number; // defaults to 2 (twice delta) 73 | 74 | /** 75 | * Negative delta values shrink polygons and positive delta expand them. 76 | */ 77 | delta: number; 78 | 79 | /** 80 | * One or more inputs to use for the offset operation. 81 | */ 82 | offsetInputs: OffsetInput[]; 83 | 84 | /** 85 | * If this is not undefined then cleaning of the result polygon will be performed. 86 | * This operation is only available when the output format is not a poly tree. 87 | */ 88 | cleanDistance?: number; 89 | } 90 | 91 | const addPathOrPaths = (offset: ClipperOffset, inputDatas: OffsetInput[] | undefined) => { 92 | if (inputDatas === undefined) { 93 | return; 94 | } 95 | 96 | // add each input 97 | for (let i = 0, maxi = inputDatas.length; i < maxi; i++) { 98 | const inputData = inputDatas[i]; 99 | 100 | // add the path/paths 101 | const pathOrPaths = inputData.data; 102 | if (!pathOrPaths || pathOrPaths.length <= 0) { 103 | continue; 104 | } 105 | 106 | // is it a path or paths? 107 | if (Array.isArray(pathOrPaths[0])) { 108 | // paths 109 | offset.addPaths(pathOrPaths as ReadonlyPaths, inputData.joinType, inputData.endType); 110 | } else { 111 | // path 112 | offset.addPath(pathOrPaths as ReadonlyPath, inputData.joinType, inputData.endType); 113 | } 114 | } 115 | }; 116 | 117 | function offsetToPathsOrPolyTree( 118 | polyTreeMode: boolean, 119 | nativeClipperLib: NativeClipperLibInstance, 120 | params: OffsetParams 121 | ): Paths | PolyTree | undefined { 122 | const filledData = { 123 | arcTolerance: 0.25, 124 | miterLimit: 2, 125 | ...params, 126 | }; 127 | 128 | const offset = new ClipperOffset( 129 | nativeClipperLib, 130 | filledData.miterLimit, 131 | filledData.arcTolerance 132 | ); 133 | 134 | //noinspection UnusedCatchParameterJS 135 | try { 136 | addPathOrPaths(offset, params.offsetInputs); 137 | if (!polyTreeMode) { 138 | return offset.executeToPaths(params.delta, params.cleanDistance); 139 | } else { 140 | if (params.cleanDistance !== undefined) { 141 | return undefined; // cleaning is not available for poly tree results 142 | } 143 | return offset.executeToPolyTree(params.delta); 144 | } 145 | } catch (err) { 146 | return undefined; 147 | } finally { 148 | offset.dispose(); 149 | } 150 | } 151 | 152 | export function offsetToPaths( 153 | nativeClipperLib: NativeClipperLibInstance, 154 | params: OffsetParams 155 | ): Paths | undefined { 156 | return offsetToPathsOrPolyTree(false, nativeClipperLib, params) as Paths | undefined; 157 | } 158 | 159 | export function offsetToPolyTree( 160 | nativeClipperLib: NativeClipperLibInstance, 161 | params: OffsetParams 162 | ): PolyTree | undefined { 163 | return offsetToPathsOrPolyTree(true, nativeClipperLib, params) as PolyTree | undefined; 164 | } 165 | -------------------------------------------------------------------------------- /src/wasm/binding.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace emscripten; 5 | using namespace ClipperLib; 6 | 7 | #ifdef use_xyz 8 | int coordsPerPoint = 3; 9 | #else 10 | int coordsPerPoint = 2; 11 | #endif 12 | 13 | typedef unsigned int intPtr; 14 | 15 | size_t JS_DoublesForPath(Path &path) { 16 | return 1 + (path.size() * coordsPerPoint); 17 | } 18 | 19 | size_t JS_DoublesForPaths(Paths &paths) { 20 | size_t nofPaths = paths.size(); 21 | int items = 1; // for path count 22 | for (size_t i = 0; i < nofPaths; i++) { 23 | items += JS_DoublesForPath(paths[i]); 24 | } 25 | return items; 26 | } 27 | 28 | double* JS_ToPathHelper(Path &dest, double* coords) { 29 | // first double in array is nof coords 30 | 31 | double* pointer = coords; 32 | size_t nofCoords = *pointer; pointer++; 33 | dest.clear(); 34 | dest.resize(nofCoords); 35 | IntPoint p; 36 | for (size_t i = 0; i < nofCoords; ++i) { 37 | p.X = *pointer; pointer++; 38 | p.Y = *pointer; pointer++; 39 | #ifdef use_xyz 40 | p.Z = *pointer; pointer++; 41 | #endif 42 | dest[i] = p; 43 | } 44 | 45 | return pointer; 46 | } 47 | 48 | void JS_ToPath(Path &dest, intPtr coordsPtr) { 49 | JS_ToPathHelper(dest, reinterpret_cast(coordsPtr)); 50 | } 51 | 52 | void JS_ToPathsHelper(Paths &dest, double* p) { 53 | // first double in array has nof paths 54 | // then each path 55 | 56 | size_t nofPaths = *p; ++p; 57 | dest.clear(); 58 | dest.reserve(nofPaths); 59 | for (size_t i = 0; i < nofPaths; ++i) { 60 | Path path; 61 | p = JS_ToPathHelper(path, p); 62 | dest.push_back(path); 63 | } 64 | } 65 | 66 | void JS_ToPaths(Paths &dest, intPtr pathsPtr) { 67 | JS_ToPathsHelper(dest, reinterpret_cast(pathsPtr)); 68 | } 69 | 70 | double* JS_WriteFromPath(Path &path, double* p) { 71 | // first double in array is nof coords 72 | 73 | size_t size = path.size(); 74 | double* p2 = p; 75 | 76 | *p2 = size; p2++; 77 | for (size_t i = 0; i < size; ++i) { 78 | IntPoint *point = &path[i]; 79 | *p2 = point->X; p2++; 80 | *p2 = point->Y; p2++; 81 | #ifdef use_xyz 82 | *p2 = point->Z; p2++; 83 | #endif 84 | } 85 | 86 | return p2; 87 | } 88 | 89 | double* JS_FromPathHelper(Path &path) { 90 | // first double in array is nof coords 91 | 92 | size_t size = path.size(); 93 | size_t nofBytes = JS_DoublesForPath(path) * sizeof(double); 94 | double* p = (double*)malloc(nofBytes); 95 | JS_WriteFromPath(path, p); 96 | return p; 97 | } 98 | 99 | val JS_FromPath(Path &path) { 100 | double* p = JS_FromPathHelper(path); 101 | return val(typed_memory_view(JS_DoublesForPath(path), p)); 102 | } 103 | 104 | double* JS_FromPathsHelper(Paths &paths) { 105 | // first double in array is nof paths 106 | 107 | size_t size = paths.size(); 108 | size_t nofBytes = JS_DoublesForPaths(paths) * sizeof(double); 109 | double* p = (double*)malloc(nofBytes); 110 | double* p2 = p; 111 | 112 | *p2 = size; p2++; 113 | 114 | for (size_t i = 0; i < size; ++i) { 115 | p2 = JS_WriteFromPath(paths[i], p2); 116 | } 117 | 118 | return p; 119 | } 120 | 121 | val JS_FromPaths(Paths &paths) { 122 | return val(typed_memory_view(JS_DoublesForPaths(paths), JS_FromPathsHelper(paths))); 123 | } 124 | 125 | 126 | 127 | EMSCRIPTEN_BINDINGS(ClipperLib) { 128 | function("toPath", &JS_ToPath); 129 | function("toPaths", &JS_ToPaths); 130 | function("fromPath", &JS_FromPath); 131 | function("fromPaths", &JS_FromPaths); 132 | 133 | enum_("ClipType") 134 | .value("Intersection", ctIntersection) 135 | .value("Union", ctUnion) 136 | .value("Difference", ctDifference) 137 | .value("Xor", ctXor) 138 | ; 139 | 140 | enum_("PolyType") 141 | .value("Subject", ptSubject) 142 | .value("Clip", ptClip) 143 | ; 144 | 145 | enum_("PolyFillType") 146 | .value("EvenOdd", pftEvenOdd) 147 | .value("NonZero", pftNonZero) 148 | .value("Positive", pftPositive) 149 | .value("Negative", pftNegative) 150 | ; 151 | 152 | class_("IntPoint") 153 | .property("x", &IntPoint::JS_GetX, &IntPoint::JS_SetX) 154 | .property("y", &IntPoint::JS_GetY, &IntPoint::JS_SetY) 155 | #ifdef use_xyz 156 | .property("z", &IntPoint::JS_GetZ, &IntPoint::JS_SetZ) 157 | #endif 158 | ; 159 | 160 | function("newIntPoint", &NewIntPoint); 161 | 162 | register_vector("Path"); 163 | register_vector("Paths"); 164 | 165 | #ifdef use_xyz 166 | // TODO: ZFillCallback? 167 | #endif 168 | 169 | enum_("InitOptions") 170 | .value("ReverseSolution", ioReverseSolution) 171 | .value("StrictlySimple", ioStrictlySimple) 172 | .value("PreserveCollinear", ioPreserveCollinear) 173 | ; 174 | 175 | enum_("JoinType") 176 | .value("Square", jtSquare) 177 | .value("Round", jtRound) 178 | .value("Miter", jtMiter) 179 | ; 180 | 181 | enum_("EndType") 182 | .value("ClosedPolygon", etClosedPolygon) 183 | .value("ClosedLine", etClosedLine) 184 | .value("OpenButt", etOpenButt) 185 | .value("OpenSquare", etOpenSquare) 186 | .value("OpenRound", etOpenRound) 187 | ; 188 | 189 | class_("PolyNode") 190 | .constructor<>() 191 | .property("contour", &PolyNode::Contour) 192 | .property("childs", &PolyNode::Childs) 193 | //.property("parent", &PolyNode::Parent) 194 | .function("getParent", &PolyNode::JS_GetParent, allow_raw_pointers()) 195 | .function("getNext", &PolyNode::GetNext, allow_raw_pointers()) 196 | .function("isHole", &PolyNode::IsHole) 197 | .function("isOpen", &PolyNode::IsOpen) 198 | .function("childCount", &PolyNode::ChildCount) 199 | ; 200 | 201 | register_vector("PolyNodes"); 202 | 203 | class_>("PolyTree") 204 | .constructor<>() 205 | .function("getFirst", &PolyTree::GetFirst, allow_raw_pointers()) 206 | .function("clear", &PolyTree::Clear) 207 | .function("total", &PolyTree::Total) 208 | ; 209 | 210 | function("orientation", &Orientation); 211 | function("area", select_overload(&Area)); 212 | function("pointInPolygon", select_overload(&PointInPolygon)); 213 | 214 | function("simplifyPolygon", &SimplifyPolygon); 215 | function("simplifyPolygonsInOut", select_overload(&SimplifyPolygons)); 216 | function("simplifyPolygonsOverwrite", select_overload(&SimplifyPolygons)); 217 | 218 | function("cleanPolygon", select_overload(&CleanPolygon)); 219 | function("cleanPolygon", select_overload(&CleanPolygon)); 220 | function("cleanPolygons", select_overload(&CleanPolygons)); 221 | function("cleanPolygons", select_overload(&CleanPolygons)); 222 | 223 | function("minkowskiSumPath", select_overload(&MinkowskiSum)); 224 | function("minkowskiSumPaths", select_overload(&MinkowskiSum)); 225 | function("minkowskiDiff", &MinkowskiDiff); 226 | 227 | function("polyTreeToPaths", &PolyTreeToPaths); 228 | function("closedPathsFromPolyTree", &ClosedPathsFromPolyTree); 229 | function("openPathsFromPolyTree", &OpenPathsFromPolyTree); 230 | 231 | function("reversePath", &ReversePath); 232 | function("reversePaths", &ReversePaths); 233 | 234 | class_("IntRect") 235 | .property("left", &IntRect::JS_GetLeft, &IntRect::JS_SetLeft) 236 | .property("top", &IntRect::JS_GetTop, &IntRect::JS_SetTop) 237 | .property("right", &IntRect::JS_GetRight, &IntRect::JS_SetRight) 238 | .property("bottom", &IntRect::JS_GetBottom, &IntRect::JS_SetBottom) 239 | ; 240 | 241 | class_("ClipperBase") 242 | //.constructor<>() 243 | .function("addPath", &ClipperBase::AddPath) 244 | .function("addPaths", &ClipperBase::AddPaths) 245 | .function("clear", &ClipperBase::Clear) 246 | .function("getBounds", &ClipperBase::GetBounds) 247 | .property("preserveCollinear", 248 | &ClipperBase::JS_GetPreserveCollinear, 249 | &ClipperBase::JS_SetPreserveCollinear 250 | ) 251 | ; 252 | 253 | class_>("Clipper") 254 | .constructor() 255 | .function("executePaths", select_overload(&Clipper::Execute)) 256 | .function("executePathsWithFillTypes", select_overload(&Clipper::Execute)) 257 | .function("executePolyTree", select_overload(&Clipper::Execute)) 258 | .function("executePolyTreeWithFillTypes", select_overload(&Clipper::Execute)) 259 | .property("reverseSolution", 260 | &Clipper::JS_GetReverseSolution, 261 | &Clipper::JS_SetReverseSolution 262 | ) 263 | .property("strictlySimple", 264 | &Clipper::JS_GetStrictlySimple, 265 | &Clipper::JS_SetStrictlySimple 266 | ) 267 | #ifdef use_xyz 268 | .function("zFillFunction", &Clipper::ZFillFunction) 269 | #endif 270 | ; 271 | 272 | class_("ClipperOffset") 273 | .constructor() 274 | .function("addPath", &ClipperOffset::AddPath) 275 | .function("addPaths", &ClipperOffset::AddPaths) 276 | .function("executePaths", select_overload(&ClipperOffset::Execute)) 277 | .function("executePolyTree", select_overload(&ClipperOffset::Execute)) 278 | .function("clear", &ClipperOffset::Clear) 279 | .property("miterLimit", &ClipperOffset::MiterLimit) 280 | .property("arcTolerance", &ClipperOffset::ArcTolerance) 281 | ; 282 | } 283 | -------------------------------------------------------------------------------- /src/wasm/clipper-wasm.js: -------------------------------------------------------------------------------- 1 | // dummy file to keep travis happy 2 | -------------------------------------------------------------------------------- /src/wasm/clipper.hpp: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * * 3 | * Author : Angus Johnson * 4 | * Version : 6.4.2 * 5 | * Date : 27 February 2017 * 6 | * Website : http://www.angusj.com * 7 | * Copyright : Angus Johnson 2010-2017 * 8 | * * 9 | * License: * 10 | * Use, modification & distribution is subject to Boost Software License Ver 1. * 11 | * http://www.boost.org/LICENSE_1_0.txt * 12 | * * 13 | * Attributions: * 14 | * The code in this library is an extension of Bala Vatti's clipping algorithm: * 15 | * "A generic solution to polygon clipping" * 16 | * Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * 17 | * http://portal.acm.org/citation.cfm?id=129906 * 18 | * * 19 | * Computer graphics and geometric modeling: implementation and algorithms * 20 | * By Max K. Agoston * 21 | * Springer; 1 edition (January 4, 2005) * 22 | * http://books.google.com/books?q=vatti+clipping+agoston * 23 | * * 24 | * See also: * 25 | * "Polygon Offsetting by Computing Winding Numbers" * 26 | * Paper no. DETC2005-85513 pp. 565-575 * 27 | * ASME 2005 International Design Engineering Technical Conferences * 28 | * and Computers and Information in Engineering Conference (IDETC/CIE2005) * 29 | * September 24-28, 2005 , Long Beach, California, USA * 30 | * http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * 31 | * * 32 | *******************************************************************************/ 33 | 34 | #ifndef clipper_hpp 35 | #define clipper_hpp 36 | 37 | #define CLIPPER_VERSION "6.4.2" 38 | 39 | //use_int32: When enabled 32bit ints are used instead of 64bit ints. This 40 | //improve performance but coordinate values are limited to the range +/- 46340 41 | //#define use_int32 42 | 43 | //use_xyz: adds a Z member to IntPoint. Adds a minor cost to perfomance. 44 | //#define use_xyz 45 | 46 | //use_lines: Enables line clipping. Adds a very minor cost to performance. 47 | #define use_lines 48 | 49 | //use_deprecated: Enables temporary support for the obsolete functions 50 | //#define use_deprecated 51 | 52 | #include 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include 60 | #include 61 | 62 | namespace ClipperLib { 63 | 64 | enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor }; 65 | enum PolyType { ptSubject, ptClip }; 66 | //By far the most widely used winding rules for polygon filling are 67 | //EvenOdd & NonZero (GDI, GDI+, XLib, OpenGL, Cairo, AGG, Quartz, SVG, Gr32) 68 | //Others rules include Positive, Negative and ABS_GTR_EQ_TWO (only in OpenGL) 69 | //see http://glprogramming.com/red/chapter11.html 70 | enum PolyFillType { pftEvenOdd, pftNonZero, pftPositive, pftNegative }; 71 | 72 | #ifdef use_int32 73 | typedef int cInt; 74 | static cInt const loRange = 0x7FFF; 75 | static cInt const hiRange = 0x7FFF; 76 | #else 77 | typedef signed long long cInt; 78 | 79 | static cInt const loRange = 0x3FFFFFFF; 80 | //static cInt const hiRange = 0x3FFFFFFFFFFFFFFFLL; 81 | 82 | // JS - loRange can be kept the same since internally we use 64 bits 83 | // however hiRange has to change since double do not support 64 bits ints 84 | //static cInt const loRange = 47453132; // sqrt(2^53 -1)/2 85 | //static cInt const hiRange = 4503599627370495LL; // sqrt(2^106 -1)/2 86 | static cInt const hiRange = 9007199254740991LL; 87 | 88 | typedef signed long long long64; //used by Int128 class 89 | typedef unsigned long long ulong64; 90 | 91 | #endif 92 | 93 | struct IntPoint { 94 | cInt X; 95 | 96 | void JS_SetX(double x) { this->X = (cInt)x; }; 97 | double JS_GetX() const { return (double)this->X; }; 98 | 99 | cInt Y; 100 | 101 | void JS_SetY(double y) { this->Y = (cInt)y; }; 102 | double JS_GetY() const { return (double)this->Y; }; 103 | 104 | #ifdef use_xyz 105 | cInt Z; 106 | 107 | void JS_SetZ(double z) { this->Z = (cInt)z; }; 108 | double JS_GetZ() const { return (double)this->Z; }; 109 | 110 | IntPoint(cInt x = 0, cInt y = 0, cInt z = 0): X(x), Y(y), Z(z) {}; 111 | #else 112 | IntPoint(cInt x = 0, cInt y = 0): X(x), Y(y) {}; 113 | #endif 114 | 115 | friend inline bool operator== (const IntPoint& a, const IntPoint& b) 116 | { 117 | return a.X == b.X && a.Y == b.Y; 118 | } 119 | friend inline bool operator!= (const IntPoint& a, const IntPoint& b) 120 | { 121 | return a.X != b.X || a.Y != b.Y; 122 | } 123 | }; 124 | 125 | #ifdef use_xyz 126 | IntPoint NewIntPoint(double x, double y, double z) { // JS 127 | return IntPoint(x, y, z); 128 | } 129 | #else 130 | IntPoint NewIntPoint(double x, double y) { // JS 131 | return IntPoint(x, y); 132 | } 133 | #endif 134 | //------------------------------------------------------------------------------ 135 | 136 | typedef std::vector< IntPoint > Path; 137 | typedef std::vector< Path > Paths; 138 | 139 | inline Path& operator <<(Path& poly, const IntPoint& p) {poly.push_back(p); return poly;} 140 | inline Paths& operator <<(Paths& polys, const Path& p) {polys.push_back(p); return polys;} 141 | 142 | std::ostream& operator <<(std::ostream &s, const IntPoint &p); 143 | std::ostream& operator <<(std::ostream &s, const Path &p); 144 | std::ostream& operator <<(std::ostream &s, const Paths &p); 145 | 146 | struct DoublePoint 147 | { 148 | double X; 149 | double Y; 150 | DoublePoint(double x = 0, double y = 0) : X(x), Y(y) {} 151 | DoublePoint(IntPoint ip) : X((double)ip.X), Y((double)ip.Y) {} 152 | }; 153 | //------------------------------------------------------------------------------ 154 | 155 | #ifdef use_xyz 156 | typedef void (*ZFillCallback)(IntPoint& e1bot, IntPoint& e1top, IntPoint& e2bot, IntPoint& e2top, IntPoint& pt); 157 | #endif 158 | 159 | enum InitOptions {ioReverseSolution = 1, ioStrictlySimple = 2, ioPreserveCollinear = 4}; 160 | enum JoinType {jtSquare, jtRound, jtMiter}; 161 | enum EndType {etClosedPolygon, etClosedLine, etOpenButt, etOpenSquare, etOpenRound}; 162 | 163 | class PolyNode; 164 | typedef std::vector< PolyNode* > PolyNodes; 165 | 166 | class PolyNode 167 | { 168 | public: 169 | PolyNode(); 170 | virtual ~PolyNode(){}; 171 | Path Contour; 172 | PolyNodes Childs; 173 | PolyNode* Parent; 174 | PolyNode* GetNext() const; 175 | bool IsHole() const; 176 | bool IsOpen() const; 177 | int ChildCount() const; 178 | 179 | PolyNode *JS_GetParent() const { return this->Parent; }; // JS 180 | 181 | private: 182 | //PolyNode& operator =(PolyNode& other); 183 | unsigned Index; //node index in Parent.Childs 184 | bool m_IsOpen; 185 | JoinType m_jointype; 186 | EndType m_endtype; 187 | PolyNode* GetNextSiblingUp() const; 188 | void AddChild(PolyNode& child); 189 | friend class Clipper; //to access Index 190 | friend class ClipperOffset; 191 | }; 192 | 193 | class PolyTree: public PolyNode 194 | { 195 | public: 196 | ~PolyTree(){ Clear(); }; 197 | PolyNode* GetFirst() const; 198 | void Clear(); 199 | int Total() const; 200 | private: 201 | //PolyTree& operator =(PolyTree& other); 202 | PolyNodes AllNodes; 203 | friend class Clipper; //to access AllNodes 204 | }; 205 | 206 | bool Orientation(const Path &poly); 207 | double Area(const Path &poly); 208 | int PointInPolygon(const IntPoint &pt, const Path &path); 209 | 210 | void SimplifyPolygon(const Path &in_poly, Paths &out_polys, PolyFillType fillType = pftEvenOdd); 211 | void SimplifyPolygons(const Paths &in_polys, Paths &out_polys, PolyFillType fillType = pftEvenOdd); 212 | void SimplifyPolygons(Paths &polys, PolyFillType fillType = pftEvenOdd); 213 | 214 | void CleanPolygon(const Path& in_poly, Path& out_poly, double distance = 1.415); 215 | void CleanPolygon(Path& poly, double distance = 1.415); 216 | void CleanPolygons(const Paths& in_polys, Paths& out_polys, double distance = 1.415); 217 | void CleanPolygons(Paths& polys, double distance = 1.415); 218 | 219 | void MinkowskiSum(const Path& pattern, const Path& path, Paths& solution, bool pathIsClosed); 220 | void MinkowskiSum(const Path& pattern, const Paths& paths, Paths& solution, bool pathIsClosed); 221 | void MinkowskiDiff(const Path& poly1, const Path& poly2, Paths& solution); 222 | 223 | void PolyTreeToPaths(const PolyTree& polytree, Paths& paths); 224 | void ClosedPathsFromPolyTree(const PolyTree& polytree, Paths& paths); 225 | void OpenPathsFromPolyTree(PolyTree& polytree, Paths& paths); 226 | 227 | void ReversePath(Path& p); 228 | void ReversePaths(Paths& p); 229 | 230 | struct IntRect { 231 | cInt left; 232 | 233 | void JS_SetLeft(double left) { this->left = (cInt)left; }; 234 | double JS_GetLeft() const { return (double)this->left; }; 235 | 236 | cInt top; 237 | 238 | void JS_SetTop(double top) { this->top = (cInt)top; }; 239 | double JS_GetTop() const { return (double)this->top; }; 240 | 241 | cInt right; 242 | 243 | void JS_SetRight(double right) { this->right = (cInt)right; }; 244 | double JS_GetRight() const { return (double)this->right; }; 245 | 246 | cInt bottom; 247 | 248 | void JS_SetBottom(double x) { this->bottom = (cInt)bottom; }; 249 | double JS_GetBottom() const { return (double)this->bottom; }; 250 | }; 251 | 252 | //enums that are used internally ... 253 | enum EdgeSide { esLeft = 1, esRight = 2}; 254 | 255 | //forward declarations (for stuff used internally) ... 256 | struct TEdge; 257 | struct IntersectNode; 258 | struct LocalMinimum; 259 | struct OutPt; 260 | struct OutRec; 261 | struct Join; 262 | 263 | typedef std::vector < OutRec* > PolyOutList; 264 | typedef std::vector < TEdge* > EdgeList; 265 | typedef std::vector < Join* > JoinList; 266 | typedef std::vector < IntersectNode* > IntersectList; 267 | 268 | //------------------------------------------------------------------------------ 269 | 270 | //ClipperBase is the ancestor to the Clipper class. It should not be 271 | //instantiated directly. This class simply abstracts the conversion of sets of 272 | //polygon coordinates into edge objects that are stored in a LocalMinima list. 273 | class ClipperBase 274 | { 275 | public: 276 | ClipperBase(); 277 | virtual ~ClipperBase(); 278 | virtual bool AddPath(const Path &pg, PolyType PolyTyp, bool Closed); 279 | bool AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed); 280 | virtual void Clear(); 281 | IntRect GetBounds(); 282 | bool PreserveCollinear() {return m_PreserveCollinear;}; 283 | void PreserveCollinear(bool value) {m_PreserveCollinear = value;}; 284 | 285 | bool JS_GetPreserveCollinear() const {return m_PreserveCollinear;}; // JS 286 | void JS_SetPreserveCollinear(bool value) {m_PreserveCollinear = value;}; // JS 287 | 288 | protected: 289 | void DisposeLocalMinimaList(); 290 | TEdge* AddBoundsToLML(TEdge *e, bool IsClosed); 291 | virtual void Reset(); 292 | TEdge* ProcessBound(TEdge* E, bool IsClockwise); 293 | void InsertScanbeam(const cInt Y); 294 | bool PopScanbeam(cInt &Y); 295 | bool LocalMinimaPending(); 296 | bool PopLocalMinima(cInt Y, const LocalMinimum *&locMin); 297 | OutRec* CreateOutRec(); 298 | void DisposeAllOutRecs(); 299 | void DisposeOutRec(PolyOutList::size_type index); 300 | void SwapPositionsInAEL(TEdge *edge1, TEdge *edge2); 301 | void DeleteFromAEL(TEdge *e); 302 | void UpdateEdgeIntoAEL(TEdge *&e); 303 | 304 | typedef std::vector MinimaList; 305 | MinimaList::iterator m_CurrentLM; 306 | MinimaList m_MinimaList; 307 | 308 | bool m_UseFullRange; 309 | EdgeList m_edges; 310 | bool m_PreserveCollinear; 311 | bool m_HasOpenPaths; 312 | PolyOutList m_PolyOuts; 313 | TEdge *m_ActiveEdges; 314 | 315 | typedef std::priority_queue ScanbeamList; 316 | ScanbeamList m_Scanbeam; 317 | }; 318 | //------------------------------------------------------------------------------ 319 | 320 | class Clipper : public /*virtual*/ ClipperBase 321 | { 322 | public: 323 | Clipper(int initOptions = 0); 324 | bool Execute(ClipType clipType, 325 | Paths &solution, 326 | PolyFillType fillType = pftEvenOdd); 327 | bool Execute(ClipType clipType, 328 | Paths &solution, 329 | PolyFillType subjFillType, 330 | PolyFillType clipFillType); 331 | bool Execute(ClipType clipType, 332 | PolyTree &polytree, 333 | PolyFillType fillType = pftEvenOdd); 334 | bool Execute(ClipType clipType, 335 | PolyTree &polytree, 336 | PolyFillType subjFillType, 337 | PolyFillType clipFillType); 338 | bool ReverseSolution() { return m_ReverseOutput; }; 339 | void ReverseSolution(bool value) {m_ReverseOutput = value;}; 340 | bool StrictlySimple() {return m_StrictSimple;}; 341 | void StrictlySimple(bool value) {m_StrictSimple = value;}; 342 | 343 | bool JS_GetReverseSolution() const { return m_ReverseOutput; }; 344 | void JS_SetReverseSolution(bool value) {m_ReverseOutput = value;}; 345 | bool JS_GetStrictlySimple() const {return m_StrictSimple;}; 346 | void JS_SetStrictlySimple(bool value) {m_StrictSimple = value;}; 347 | 348 | //set the callback function for z value filling on intersections (otherwise Z is 0) 349 | #ifdef use_xyz 350 | void ZFillFunction(ZFillCallback zFillFunc); 351 | #endif 352 | protected: 353 | virtual bool ExecuteInternal(); 354 | private: 355 | JoinList m_Joins; 356 | JoinList m_GhostJoins; 357 | IntersectList m_IntersectList; 358 | ClipType m_ClipType; 359 | typedef std::list MaximaList; 360 | MaximaList m_Maxima; 361 | TEdge *m_SortedEdges; 362 | bool m_ExecuteLocked; 363 | PolyFillType m_ClipFillType; 364 | PolyFillType m_SubjFillType; 365 | bool m_ReverseOutput; 366 | bool m_UsingPolyTree; 367 | bool m_StrictSimple; 368 | #ifdef use_xyz 369 | ZFillCallback m_ZFill; //custom callback 370 | #endif 371 | void SetWindingCount(TEdge& edge); 372 | bool IsEvenOddFillType(const TEdge& edge) const; 373 | bool IsEvenOddAltFillType(const TEdge& edge) const; 374 | void InsertLocalMinimaIntoAEL(const cInt botY); 375 | void InsertEdgeIntoAEL(TEdge *edge, TEdge* startEdge); 376 | void AddEdgeToSEL(TEdge *edge); 377 | bool PopEdgeFromSEL(TEdge *&edge); 378 | void CopyAELToSEL(); 379 | void DeleteFromSEL(TEdge *e); 380 | void SwapPositionsInSEL(TEdge *edge1, TEdge *edge2); 381 | bool IsContributing(const TEdge& edge) const; 382 | bool IsTopHorz(const cInt XPos); 383 | void DoMaxima(TEdge *e); 384 | void ProcessHorizontals(); 385 | void ProcessHorizontal(TEdge *horzEdge); 386 | void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); 387 | OutPt* AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); 388 | OutRec* GetOutRec(int idx); 389 | void AppendPolygon(TEdge *e1, TEdge *e2); 390 | void IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &pt); 391 | OutPt* AddOutPt(TEdge *e, const IntPoint &pt); 392 | OutPt* GetLastOutPt(TEdge *e); 393 | bool ProcessIntersections(const cInt topY); 394 | void BuildIntersectList(const cInt topY); 395 | void ProcessIntersectList(); 396 | void ProcessEdgesAtTopOfScanbeam(const cInt topY); 397 | void BuildResult(Paths& polys); 398 | void BuildResult2(PolyTree& polytree); 399 | void SetHoleState(TEdge *e, OutRec *outrec); 400 | void DisposeIntersectNodes(); 401 | bool FixupIntersectionOrder(); 402 | void FixupOutPolygon(OutRec &outrec); 403 | void FixupOutPolyline(OutRec &outrec); 404 | bool IsHole(TEdge *e); 405 | bool FindOwnerFromSplitRecs(OutRec &outRec, OutRec *&currOrfl); 406 | void FixHoleLinkage(OutRec &outrec); 407 | void AddJoin(OutPt *op1, OutPt *op2, const IntPoint offPt); 408 | void ClearJoins(); 409 | void ClearGhostJoins(); 410 | void AddGhostJoin(OutPt *op, const IntPoint offPt); 411 | bool JoinPoints(Join *j, OutRec* outRec1, OutRec* outRec2); 412 | void JoinCommonEdges(); 413 | void DoSimplePolygons(); 414 | void FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec); 415 | void FixupFirstLefts2(OutRec* InnerOutRec, OutRec* OuterOutRec); 416 | void FixupFirstLefts3(OutRec* OldOutRec, OutRec* NewOutRec); 417 | #ifdef use_xyz 418 | void SetZ(IntPoint& pt, TEdge& e1, TEdge& e2); 419 | #endif 420 | }; 421 | //------------------------------------------------------------------------------ 422 | 423 | class ClipperOffset 424 | { 425 | public: 426 | ClipperOffset(double miterLimit = 2.0, double roundPrecision = 0.25); 427 | ~ClipperOffset(); 428 | void AddPath(const Path& path, JoinType joinType, EndType endType); 429 | void AddPaths(const Paths& paths, JoinType joinType, EndType endType); 430 | void Execute(Paths& solution, double delta); 431 | void Execute(PolyTree& solution, double delta); 432 | void Clear(); 433 | double MiterLimit; 434 | double ArcTolerance; 435 | private: 436 | Paths m_destPolys; 437 | Path m_srcPoly; 438 | Path m_destPoly; 439 | std::vector m_normals; 440 | double m_delta, m_sinA, m_sin, m_cos; 441 | double m_miterLim, m_StepsPerRad; 442 | IntPoint m_lowest; 443 | PolyNode m_polyNodes; 444 | 445 | void FixOrientations(); 446 | void DoOffset(double delta); 447 | void OffsetPoint(int j, int& k, JoinType jointype); 448 | void DoSquare(int j, int k); 449 | void DoMiter(int j, int k, double r); 450 | void DoRound(int j, int k); 451 | }; 452 | //------------------------------------------------------------------------------ 453 | 454 | class clipperException : public std::exception 455 | { 456 | public: 457 | clipperException(const char* description): m_descr(description) {} 458 | virtual ~clipperException() throw() {} 459 | virtual const char* what() const throw() {return m_descr.c_str();} 460 | private: 461 | std::string m_descr; 462 | }; 463 | //------------------------------------------------------------------------------ 464 | 465 | } //ClipperLib namespace 466 | 467 | // JS additions 468 | 469 | 470 | #endif //clipper_hpp 471 | 472 | 473 | -------------------------------------------------------------------------------- /src/wasm/clipper.js: -------------------------------------------------------------------------------- 1 | // dummy file to keep travis happy 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 6 | "target": "es5", 7 | /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 8 | "module": "commonjs", 9 | /* Specify library files to be included in the compilation: */ 10 | "lib": ["es2016", "dom", "dom.iterable", "es2017.object"], 11 | // "allowJs": true, /* Allow javascript files to be compiled. */ 12 | // "checkJs": true, /* Report errors in .js files. */ 13 | /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 14 | "jsx": "react", 15 | "declaration": true /* Generates corresponding '.d.ts' file. */, 16 | "sourceMap": false /* Generates corresponding '.map' file. */, 17 | // "outFile": "./", /* Concatenate and emit output to single file. */ 18 | "outDir": "./dist" /* Redirect output structure to the directory. */, 19 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | "downlevelIteration": true, 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | /* Enable all strict type-checking options. */ 29 | "strict": true, 30 | // "strictFunctionTypes": true, 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | // "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | /* Report error when not all code paths in function return a value. */ 40 | "noImplicitReturns": true, 41 | /* Report errors for fallthrough cases in switch statement. */ 42 | "noFallthroughCasesInSwitch": true, 43 | 44 | /* Module Resolution Options */ 45 | /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | "moduleResolution": "node", 47 | /* Base directory to resolve non-absolute module names. */ 48 | // "baseUrl": "", 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | /* Emit a single file with source maps instead of having a separate file. */ 60 | "inlineSourceMap": true, 61 | /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | "inlineSources": true, 63 | 64 | /* Experimental Options */ 65 | /* Enables experimental support for ES7 decorators. */ 66 | "experimentalDecorators": true, 67 | /* Enables experimental support for emitting type metadata for decorators. */ 68 | "emitDecoratorMetadata": true, 69 | 70 | /* Extras */ 71 | "suppressImplicitAnyIndexErrors": false, 72 | "forceConsistentCasingInFileNames": true, 73 | "noEmitOnError": false 74 | } 75 | } 76 | --------------------------------------------------------------------------------