├── .gitignore ├── LICENSE ├── README.md ├── dist └── three-plotter-renderer.js ├── examples ├── box.html ├── box.js ├── example01.html ├── example01.js ├── example02.html ├── example02.js ├── example03.html ├── example03.js ├── models │ ├── example01.stl │ ├── example02.stl │ └── example03.stl ├── output │ ├── example01.svg │ ├── example02.png │ ├── example02.svg │ ├── example03.svg │ └── preview.png └── styles.css ├── externals.js ├── package-lock.json ├── package.json ├── src ├── analyzer.js ├── expander.js ├── geom │ ├── booleanshape.js │ ├── geom.js │ └── shapes.js ├── hatcher.js ├── optimize.js ├── plotter-renderer.js ├── projector.js └── three-plotter-renderer.js └── tests ├── test.box.html └── test.box.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Geoff Gaudreault 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 | # three-plotter-renderer 2 | An SVG renderer with occlusion for plotters and SVG editors 3 | 4 | **This package is a WIP: Try loading examples with a live development server.** 5 | 6 | 7 | 8 | ## Usage 9 | 10 | This _should_ be as simple as including `../dist/three-plotter-renderer.js` as a script in the head of your HTML document _after_ your *three.js* script include. 11 | 12 | NOTE: This is a WIP and this will eventually be packaged for easy install in your projects. 13 | 14 | ## Examples 15 | 16 | The examples demonstrate usage of the packaged renderer in the `./dist` directory. Please note it must be added _after_ including `three.min.js` 17 | Please check the `./examples` folder and make sure they work for you. 18 | 19 | Examples use an OrbitController and start unoccluded. More complex models take time and you may see a blank screen for a few seconds. 20 | 21 | ## Usage as module 22 | 23 | The `./tests` directory demonstrates usage as a module. Importing `./src/three-plotter-renderer.js` should work as long as you've also installed and imported `three` and `js-angusjs-clipper` as modules. 24 | 25 | ## Model types 26 | 27 | The renderer works best with CSG models. It _does not_ support intersecting faces (touching is fine). Also, avoid stretched faces, and use multiple height/width/depth segments to get your faces as square as possible to prevent depth-fighting. 28 | 29 | ## Adjusting hatching 30 | 31 | If you follow the examples you can adjust hatching after rendering by using the <,> keys to switch groups, the \[,\] keys to adjust rotation, and the -,= keys to adjust spacing 32 | 33 | ## Layers 34 | 35 | The renderer will export an _edges_, _outline_, and _shading_ layer, as well as a hidden _polygons_ layer for use in Inkscape. These layers are only Inkscape-compatible and will come in as groups in other programs. 36 | 37 | --- 38 | 39 | ## How it works 40 | 41 | This renderer leverages the _projector.js_ module from _SVGRenderer_ to get a projected scene as an array of faces sorted by depth. The renderer then takes the faces and does the following: 42 | 43 | 1. Put each face in a group of faces with the same normal and depth (distance from 0,0,0 world position) 44 | 2. For each face, in order of depth, from back to front, union the projected polygon to the accumulated faces in that normal group. 45 | 3. For that face, _subtract_ the projected polygon from all other normal groups. 46 | 4. Finally, _union_ that face to the _outline_ group. 47 | 5. Proceed to the next-closest face and repeat from step 2. 48 | 49 | You will end up with a set of polygons, each matching a planar section of your model. They will all fit together exactly, since they all were assembled from the same set of faces, just with different logic. 50 | 51 | --- 52 | 53 | ## Contributions welcome! 54 | 55 | There are a lot of developers out there who are smarter than me. I hope you can help me make this faster and more versatile! 56 | -------------------------------------------------------------------------------- /examples/box.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SVG Plotter Renderer: Simple Box 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 17 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/box.js: -------------------------------------------------------------------------------- 1 | import { OrbitControls } from "../node_modules/three/examples/jsm/controls/OrbitControls.js"; 2 | 3 | /** @typedef {import('../node_modules/three/build/three.module.js')} THREE */ 4 | 5 | var camera, scene, renderer; 6 | var cameraControls; 7 | var canvasWidth = window.innerWidth; 8 | var canvasHeight = window.innerHeight; 9 | var focused = false; 10 | 11 | window.onload = () => { 12 | init(); 13 | render(); 14 | }; 15 | 16 | window.onblur = () => { 17 | focused = false; 18 | }; 19 | 20 | window.onfocus = window.onclick = () => { 21 | focused = true; 22 | }; 23 | 24 | window.onkeypress = (e) => { 25 | console.log(e.keyCode); 26 | switch (e.keyCode) { 27 | case 61: 28 | renderer.increaseSpacing(); 29 | break; 30 | case 45: 31 | renderer.decreaseSpacing(); 32 | break; 33 | case 93: 34 | renderer.increaseRotation(); 35 | break; 36 | case 91: 37 | renderer.decreaseRotation(); 38 | break; 39 | case 46: 40 | renderer.nextHatchGroup(); 41 | break; 42 | case 44: 43 | renderer.previousHatchGroup(); 44 | break; 45 | } 46 | }; 47 | 48 | function init() { 49 | 50 | var view = document.getElementById("view"); 51 | var container = document.getElementById("plot"); 52 | var overla= document.getElementById("plot"); 53 | 54 | // CAMERA 55 | camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 1, 8000); 56 | camera.position.set(300, 300, 300); 57 | 58 | // RENDERER 59 | renderer = new THREE.PlotterRenderer(); 60 | 61 | renderer.setSize(canvasWidth, canvasHeight); 62 | container.appendChild(renderer.domElement); 63 | 64 | // EVENTS 65 | window.addEventListener("resize", onWindowResize, false); 66 | 67 | // CONTROLS 68 | // @ts-ignore 69 | cameraControls = new OrbitControls(camera, view); 70 | cameraControls.zoomSpeed = 2; 71 | 72 | // scene itself 73 | scene = new THREE.Scene(); 74 | scene.background = new THREE.Color(0xaaaaaa); 75 | 76 | const dirLight = new THREE.DirectionalLight(0xffffff, 0.75); 77 | dirLight.position.set(300, 300, 300); 78 | 79 | scene.add(dirLight); 80 | 81 | const dirLight2 = new THREE.DirectionalLight(0x333333, 0.75); 82 | dirLight2.position.set(-100, 300, -500); 83 | 84 | scene.add(dirLight2); 85 | 86 | const light = new THREE.PointLight(0xffffff, 1.0, 5000); 87 | light.position.x = 300; 88 | light.position.z = 600; 89 | light.position.y = 1000; 90 | 91 | camera.add(light); 92 | 93 | scene.add(camera); 94 | 95 | // GUI 96 | setupGui(); 97 | 98 | var geom = new THREE.BoxGeometry(100, 100, 100, 3, 3, 3); 99 | var mesh = new THREE.Mesh(geom, new THREE.MeshPhongMaterial({ opacity: 1, color: 0xffffff })); 100 | scene.add(mesh); 101 | 102 | var tick = function () { 103 | if (focused) { 104 | renderer.render(scene, camera, 0.2, 0.3); 105 | } 106 | requestAnimationFrame(tick); 107 | }; 108 | 109 | var optimizeTimeout = null; 110 | 111 | var setOptimize = function () { 112 | clearTimeout(optimizeTimeout); 113 | optimizeTimeout = setTimeout(() => { 114 | renderer.doOptimize = true; 115 | }, 500); 116 | }; 117 | 118 | cameraControls.addEventListener("start", function () { 119 | renderer.doOptimize = false; 120 | clearTimeout(optimizeTimeout); 121 | }); 122 | 123 | cameraControls.addEventListener("end", function () { 124 | setOptimize(); 125 | }); 126 | 127 | cameraControls.addEventListener("change", function () { 128 | renderer.doOptimize = false; 129 | clearTimeout(optimizeTimeout); 130 | setOptimize(); 131 | }); 132 | 133 | tick(); 134 | //setOptimize(); 135 | } 136 | 137 | function onWindowResize() { 138 | renderer.setSize(window.innerWidth, window.innerHeight); 139 | 140 | camera.aspect = canvasWidth / canvasHeight; 141 | camera.updateProjectionMatrix(); 142 | 143 | render(); 144 | } 145 | 146 | function setupGui() { 147 | var exportButton = document.getElementById("exportsvg"); 148 | exportButton.addEventListener("click", exportSVG); 149 | } 150 | 151 | function render() { 152 | renderer.render(scene, camera); 153 | } 154 | 155 | function exportSVG() { 156 | saveString(document.getElementById("plot").innerHTML, "plot.svg"); 157 | } 158 | 159 | function save(blob, filename) { 160 | link.href = URL.createObjectURL(blob); 161 | link.download = filename; 162 | link.click(); 163 | } 164 | 165 | function saveString(text, filename) { 166 | save(new Blob([text], { type: "text/plain" }), filename); 167 | } 168 | 169 | var link = document.createElement("a"); 170 | link.style.display = "none"; 171 | document.body.appendChild(link); 172 | -------------------------------------------------------------------------------- /examples/example01.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SVG Plotter Renderer Example 01 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/example01.js: -------------------------------------------------------------------------------- 1 | import { OrbitControls } from "../node_modules/three/examples/jsm/controls/OrbitControls.js"; 2 | import { STLLoader } from "../node_modules/three/examples/jsm/loaders/STLLoader.js"; 3 | 4 | /** @typedef {import('../node_modules/three/build/three.module.js')} THREE */ 5 | 6 | var camera, scene, renderer; 7 | var cameraControls; 8 | var canvasWidth = window.innerWidth; 9 | var canvasHeight = window.innerHeight; 10 | var focused = false; 11 | 12 | window.onload = () => { 13 | init(); 14 | render(); 15 | }; 16 | 17 | window.onblur = () => { 18 | focused = false; 19 | }; 20 | 21 | window.onfocus = window.onclick = () => { 22 | focused = true; 23 | }; 24 | 25 | window.onkeypress = (e) => { 26 | console.log(e.keyCode); 27 | switch (e.keyCode) { 28 | case 61: 29 | renderer.increaseSpacing(); 30 | break; 31 | case 45: 32 | renderer.decreaseSpacing(); 33 | break; 34 | case 93: 35 | renderer.increaseRotation(); 36 | break; 37 | case 91: 38 | renderer.decreaseRotation(); 39 | break; 40 | case 46: 41 | renderer.nextHatchGroup(); 42 | break; 43 | case 44: 44 | renderer.previousHatchGroup(); 45 | break; 46 | } 47 | }; 48 | 49 | function init() { 50 | 51 | var view = document.getElementById("view"); 52 | var container = document.getElementById("plot"); 53 | 54 | // CAMERA 55 | camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 8000); 56 | camera.position.set(900, 900, 900); 57 | 58 | // RENDERER 59 | renderer = new THREE.PlotterRenderer(); 60 | 61 | renderer.setSize(canvasWidth, canvasHeight); 62 | container.appendChild(renderer.domElement); 63 | 64 | // EVENTS 65 | window.addEventListener("resize", onWindowResize, false); 66 | 67 | // CONTROLS 68 | // @ts-ignore 69 | cameraControls = new OrbitControls(camera, view); 70 | cameraControls.zoomSpeed = 2; 71 | 72 | // scene itself 73 | scene = new THREE.Scene(); 74 | scene.background = new THREE.Color(0xaaaaaa); 75 | 76 | const dirLight = new THREE.DirectionalLight(0xffffff, 0.75); 77 | dirLight.position.set(300, 300, 300); 78 | 79 | scene.add(dirLight); 80 | 81 | const dirLight2 = new THREE.DirectionalLight(0x333333, 0.75); 82 | dirLight2.position.set(-100, 300, -500); 83 | 84 | scene.add(dirLight2); 85 | 86 | const light = new THREE.PointLight(0xffffff, 1.0, 5000); 87 | light.position.x = 300; 88 | light.position.z = 600; 89 | light.position.y = 1000; 90 | 91 | camera.add(light); 92 | 93 | scene.add(camera); 94 | 95 | // GUI 96 | setupGui(); 97 | 98 | var loader = new STLLoader() 99 | loader.load("./models/example01.stl", function (bg) { 100 | let geom = new THREE.Geometry().fromBufferGeometry(bg); 101 | let obj = new THREE.Mesh(geom, new THREE.MeshPhongMaterial()) 102 | scene.add(obj); 103 | renderer.render(scene, camera); 104 | }); 105 | 106 | var tick = function () { 107 | if (focused) { 108 | renderer.render(scene, camera); 109 | } 110 | requestAnimationFrame(tick); 111 | }; 112 | 113 | var optimizeTimeout = null; 114 | 115 | var setOptimize = function () { 116 | clearTimeout(optimizeTimeout); 117 | optimizeTimeout = setTimeout(() => { 118 | renderer.doOptimize = true; 119 | }, 500); 120 | }; 121 | 122 | cameraControls.addEventListener("start", function () { 123 | renderer.doOptimize = false; 124 | clearTimeout(optimizeTimeout); 125 | }); 126 | 127 | cameraControls.addEventListener("end", function () { 128 | setOptimize(); 129 | }); 130 | 131 | cameraControls.addEventListener("change", function () { 132 | renderer.doOptimize = false; 133 | clearTimeout(optimizeTimeout); 134 | setOptimize(); 135 | }); 136 | 137 | tick(); 138 | //setOptimize(); 139 | } 140 | 141 | function onWindowResize() { 142 | renderer.setSize(window.innerWidth, window.innerHeight); 143 | 144 | camera.aspect = canvasWidth / canvasHeight; 145 | camera.updateProjectionMatrix(); 146 | 147 | render(); 148 | } 149 | 150 | function setupGui() { 151 | var exportButton = document.getElementById("exportsvg"); 152 | exportButton.addEventListener("click", exportSVG); 153 | } 154 | 155 | function render() { 156 | renderer.render(scene, camera); 157 | } 158 | 159 | function exportSVG() { 160 | saveString(document.getElementById("plot").innerHTML, "plot.svg"); 161 | } 162 | 163 | function save(blob, filename) { 164 | link.href = URL.createObjectURL(blob); 165 | link.download = filename; 166 | link.click(); 167 | } 168 | 169 | function saveString(text, filename) { 170 | save(new Blob([text], { type: "text/plain" }), filename); 171 | } 172 | 173 | var link = document.createElement("a"); 174 | link.style.display = "none"; 175 | document.body.appendChild(link); 176 | -------------------------------------------------------------------------------- /examples/example02.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SVG Plotter Renderer Example 01 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/example02.js: -------------------------------------------------------------------------------- 1 | import { OrbitControls } from "../node_modules/three/examples/jsm/controls/OrbitControls.js"; 2 | import { STLLoader } from "../node_modules/three/examples/jsm/loaders/STLLoader.js"; 3 | 4 | /** @typedef {import('../node_modules/three/build/three.module.js')} THREE */ 5 | 6 | var camera, scene, renderer; 7 | var cameraControls; 8 | var canvasWidth = window.innerWidth; 9 | var canvasHeight = window.innerHeight; 10 | var focused = false; 11 | 12 | window.onload = () => { 13 | init(); 14 | render(); 15 | }; 16 | 17 | window.onblur = () => { 18 | focused = false; 19 | }; 20 | 21 | window.onfocus = window.onclick = () => { 22 | focused = true; 23 | }; 24 | 25 | window.onkeypress = (e) => { 26 | console.log(e.keyCode); 27 | switch (e.keyCode) { 28 | case 61: 29 | renderer.increaseSpacing(); 30 | break; 31 | case 45: 32 | renderer.decreaseSpacing(); 33 | break; 34 | case 93: 35 | renderer.increaseRotation(); 36 | break; 37 | case 91: 38 | renderer.decreaseRotation(); 39 | break; 40 | case 46: 41 | renderer.nextHatchGroup(); 42 | break; 43 | case 44: 44 | renderer.previousHatchGroup(); 45 | break; 46 | } 47 | }; 48 | 49 | function init() { 50 | 51 | var view = document.getElementById("view"); 52 | var container = document.getElementById("plot"); 53 | 54 | // CAMERA 55 | camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 8000); 56 | camera.position.set(900, 900, 900); 57 | 58 | // RENDERER 59 | renderer = new THREE.PlotterRenderer(); 60 | 61 | renderer.setSize(canvasWidth, canvasHeight); 62 | container.appendChild(renderer.domElement); 63 | 64 | // EVENTS 65 | window.addEventListener("resize", onWindowResize, false); 66 | 67 | // CONTROLS 68 | // @ts-ignore 69 | cameraControls = new OrbitControls(camera, view); 70 | cameraControls.zoomSpeed = 2; 71 | 72 | // scene itself 73 | scene = new THREE.Scene(); 74 | scene.background = new THREE.Color(0xaaaaaa); 75 | 76 | const dirLight = new THREE.DirectionalLight(0xffffff, 0.75); 77 | dirLight.position.set(300, 300, 300); 78 | 79 | scene.add(dirLight); 80 | 81 | const dirLight2 = new THREE.DirectionalLight(0x333333, 0.75); 82 | dirLight2.position.set(-100, 300, -500); 83 | 84 | scene.add(dirLight2); 85 | 86 | const light = new THREE.PointLight(0xffffff, 1.0, 5000); 87 | light.position.x = 300; 88 | light.position.z = 600; 89 | light.position.y = 1000; 90 | 91 | camera.add(light); 92 | 93 | scene.add(camera); 94 | 95 | // GUI 96 | setupGui(); 97 | 98 | var loader = new STLLoader() 99 | loader.load("./models/example02.stl", function (bg) { 100 | let geom = new THREE.Geometry().fromBufferGeometry(bg); 101 | let obj = new THREE.Mesh(geom, new THREE.MeshPhongMaterial()) 102 | scene.add(obj); 103 | renderer.render(scene, camera); 104 | }); 105 | 106 | var tick = function () { 107 | if (focused) { 108 | renderer.render(scene, camera); 109 | } 110 | requestAnimationFrame(tick); 111 | }; 112 | 113 | var optimizeTimeout = null; 114 | 115 | var setOptimize = function () { 116 | clearTimeout(optimizeTimeout); 117 | optimizeTimeout = setTimeout(() => { 118 | renderer.doOptimize = true; 119 | }, 500); 120 | }; 121 | 122 | cameraControls.addEventListener("start", function () { 123 | renderer.doOptimize = false; 124 | clearTimeout(optimizeTimeout); 125 | }); 126 | 127 | cameraControls.addEventListener("end", function () { 128 | setOptimize(); 129 | }); 130 | 131 | cameraControls.addEventListener("change", function () { 132 | renderer.doOptimize = false; 133 | clearTimeout(optimizeTimeout); 134 | setOptimize(); 135 | }); 136 | 137 | tick(); 138 | //setOptimize(); 139 | } 140 | 141 | function onWindowResize() { 142 | renderer.setSize(window.innerWidth, window.innerHeight); 143 | 144 | camera.aspect = canvasWidth / canvasHeight; 145 | camera.updateProjectionMatrix(); 146 | 147 | render(); 148 | } 149 | 150 | function setupGui() { 151 | var exportButton = document.getElementById("exportsvg"); 152 | exportButton.addEventListener("click", exportSVG); 153 | } 154 | 155 | function render() { 156 | renderer.render(scene, camera); 157 | } 158 | 159 | function exportSVG() { 160 | saveString(document.getElementById("plot").innerHTML, "plot.svg"); 161 | } 162 | 163 | function save(blob, filename) { 164 | link.href = URL.createObjectURL(blob); 165 | link.download = filename; 166 | link.click(); 167 | } 168 | 169 | function saveString(text, filename) { 170 | save(new Blob([text], { type: "text/plain" }), filename); 171 | } 172 | 173 | var link = document.createElement("a"); 174 | link.style.display = "none"; 175 | document.body.appendChild(link); 176 | -------------------------------------------------------------------------------- /examples/example03.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SVG Plotter Renderer Example 01 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/example03.js: -------------------------------------------------------------------------------- 1 | import { OrbitControls } from "../node_modules/three/examples/jsm/controls/OrbitControls.js"; 2 | import { STLLoader } from "../node_modules/three/examples/jsm/loaders/STLLoader.js"; 3 | 4 | /** @typedef {import('../node_modules/three/build/three.module.js')} THREE */ 5 | 6 | var camera, scene, renderer; 7 | var cameraControls; 8 | var canvasWidth = window.innerWidth; 9 | var canvasHeight = window.innerHeight; 10 | var focused = false; 11 | 12 | window.onload = () => { 13 | init(); 14 | render(); 15 | }; 16 | 17 | window.onblur = () => { 18 | focused = false; 19 | }; 20 | 21 | window.onfocus = window.onclick = () => { 22 | focused = true; 23 | }; 24 | 25 | window.onkeypress = (e) => { 26 | console.log(e.keyCode); 27 | switch (e.keyCode) { 28 | case 61: 29 | renderer.increaseSpacing(); 30 | break; 31 | case 45: 32 | renderer.decreaseSpacing(); 33 | break; 34 | case 93: 35 | renderer.increaseRotation(); 36 | break; 37 | case 91: 38 | renderer.decreaseRotation(); 39 | break; 40 | case 46: 41 | renderer.nextHatchGroup(); 42 | break; 43 | case 44: 44 | renderer.previousHatchGroup(); 45 | break; 46 | } 47 | }; 48 | 49 | function init() { 50 | 51 | var view = document.getElementById("view"); 52 | var container = document.getElementById("plot"); 53 | 54 | // CAMERA 55 | camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 8000); 56 | camera.position.set(300, 300, 300); 57 | 58 | // RENDERER 59 | //renderer = new THREE.WebGLRenderer(); 60 | renderer = new THREE.PlotterRenderer(); 61 | 62 | renderer.setSize(canvasWidth, canvasHeight); 63 | container.appendChild(renderer.domElement); 64 | 65 | // EVENTS 66 | window.addEventListener("resize", onWindowResize, false); 67 | 68 | // CONTROLS 69 | // @ts-ignore 70 | cameraControls = new OrbitControls(camera, view); 71 | cameraControls.zoomSpeed = 2; 72 | 73 | // scene itself 74 | scene = new THREE.Scene(); 75 | scene.background = new THREE.Color(0xaaaaaa); 76 | 77 | const dirLight = new THREE.DirectionalLight(0xffffff, 2); 78 | dirLight.position.set(100, 600, 100); 79 | 80 | scene.add(dirLight); 81 | 82 | const dirLight2 = new THREE.DirectionalLight(0x333333, 0.75); 83 | dirLight2.position.set(-100, 300, -500); 84 | 85 | scene.add(dirLight2); 86 | 87 | const light = new THREE.PointLight(0xffffff, 1.0, 5000); 88 | light.position.x = 300; 89 | light.position.z = 600; 90 | light.position.y = 1000; 91 | 92 | camera.add(light); 93 | 94 | scene.add(camera); 95 | 96 | // GUI 97 | setupGui(); 98 | 99 | var loader = new STLLoader() 100 | loader.load("./models/example03.stl", function (bg) { 101 | let geom = new THREE.Geometry().fromBufferGeometry(bg); 102 | let obj = new THREE.Mesh(geom, new THREE.MeshPhongMaterial()) 103 | scene.add(obj); 104 | renderer.render(scene, camera); 105 | }); 106 | 107 | var tick = function () { 108 | if (focused) { 109 | renderer.render(scene, camera); 110 | } 111 | requestAnimationFrame(tick); 112 | }; 113 | 114 | var optimizeTimeout = null; 115 | 116 | var setOptimize = function () { 117 | clearTimeout(optimizeTimeout); 118 | optimizeTimeout = setTimeout(() => { 119 | renderer.doOptimize = true; 120 | }, 500); 121 | }; 122 | 123 | cameraControls.addEventListener("start", function () { 124 | renderer.doOptimize = false; 125 | clearTimeout(optimizeTimeout); 126 | }); 127 | 128 | cameraControls.addEventListener("end", function () { 129 | setOptimize(); 130 | }); 131 | 132 | cameraControls.addEventListener("change", function () { 133 | renderer.doOptimize = false; 134 | clearTimeout(optimizeTimeout); 135 | setOptimize(); 136 | }); 137 | 138 | tick(); 139 | //setOptimize(); 140 | } 141 | 142 | function onWindowResize() { 143 | renderer.setSize(window.innerWidth, window.innerHeight); 144 | 145 | camera.aspect = canvasWidth / canvasHeight; 146 | camera.updateProjectionMatrix(); 147 | 148 | render(); 149 | } 150 | 151 | function setupGui() { 152 | var exportButton = document.getElementById("exportsvg"); 153 | exportButton.addEventListener("click", exportSVG); 154 | } 155 | 156 | function render() { 157 | renderer.render(scene, camera); 158 | } 159 | 160 | function exportSVG() { 161 | saveString(document.getElementById("plot").innerHTML, "plot.svg"); 162 | } 163 | 164 | function save(blob, filename) { 165 | link.href = URL.createObjectURL(blob); 166 | link.download = filename; 167 | link.click(); 168 | } 169 | 170 | function saveString(text, filename) { 171 | save(new Blob([text], { type: "text/plain" }), filename); 172 | } 173 | 174 | var link = document.createElement("a"); 175 | link.style.display = "none"; 176 | document.body.appendChild(link); 177 | -------------------------------------------------------------------------------- /examples/output/example01.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/output/example02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurofuzzy/three-plotter-renderer/144a4d027698c727a61645e34ba726a2c12f2a78/examples/output/example02.png -------------------------------------------------------------------------------- /examples/output/example03.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/output/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurofuzzy/three-plotter-renderer/144a4d027698c727a61645e34ba726a2c12f2a78/examples/output/preview.png -------------------------------------------------------------------------------- /examples/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background-color: #000; 4 | color: #fff; 5 | font-family: Monospace; 6 | font-size: 13px; 7 | line-height: 24px; 8 | } 9 | 10 | a { 11 | color: #ff0; 12 | text-decoration: none; 13 | } 14 | 15 | a:hover { 16 | text-decoration: underline; 17 | } 18 | 19 | button { 20 | cursor: pointer; 21 | text-transform: uppercase; 22 | } 23 | 24 | canvas { 25 | display: block; 26 | } 27 | 28 | footer { 29 | position: fixed; 30 | bottom: 0; 31 | width: 100%; 32 | height: 90px; 33 | padding: 10px; 34 | box-sizing: border-box; 35 | text-align: center; 36 | -moz-user-select: none; 37 | -webkit-user-select: none; 38 | -ms-user-select: none; 39 | user-select: none; 40 | z-index: 1000; /* TODO Solve this in HTML */ 41 | } 42 | 43 | .dg.ac { 44 | -moz-user-select: none; 45 | -webkit-user-select: none; 46 | -ms-user-select: none; 47 | user-select: none; 48 | z-index: 2 !important; /* TODO Solve this in HTML */ 49 | } 50 | 51 | #overlay { 52 | position: absolute; 53 | width: 100%; 54 | height: 100%; 55 | top: 0; 56 | left: 0; 57 | z-index: 100000; 58 | } -------------------------------------------------------------------------------- /externals.js: -------------------------------------------------------------------------------- 1 | module.exports = function(path) { 2 | 3 | if (path.indexOf("three.module.js") !== -1 && process.env.NODE_ENV === "npm") { 4 | return `three => THREE`; 5 | } 6 | 7 | return undefined; 8 | 9 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-plotter-renderer", 3 | "version": "0.0.1", 4 | "description": "An SVG renderer with occlusion for plotters and SVG editors", 5 | "main": "dist/three-plotter-renderer.js", 6 | "module": "src/plotter-renderer.js", 7 | "scripts": { 8 | "build": "NODE_ENV=npm parcel build ./src/three-plotter-renderer.js --no-source-maps" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/neurofuzzy/three-plotter-renderer.git" 13 | }, 14 | "keywords": [ 15 | "svg", 16 | "plotter", 17 | "renderer", 18 | "three.js", 19 | "clipper" 20 | ], 21 | "author": "Geoff Gaudreault", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/neurofuzzy/three-plotter-renderer/issues" 25 | }, 26 | "homepage": "https://github.com/neurofuzzy/three-plotter-renderer#readme", 27 | "externals": "./externals.js", 28 | "devDependencies": { 29 | "parcel-bundler": "^1.12.4", 30 | "parcel-plugin-externals": "^0.5.1" 31 | }, 32 | "dependencies": { 33 | "js-angusj-clipper": "^1.1.0", 34 | "three": "^0.120.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/analyzer.js: -------------------------------------------------------------------------------- 1 | import { Segment, Point, GeomUtil } from "./geom/geom.js"; 2 | import { PolygonShape } from "./geom/shapes.js"; 3 | import { BooleanShape } from "./geom/booleanshape.js"; 4 | 5 | export class Analyzer { 6 | 7 | /** 8 | * @property {Segment[]} segs 9 | * @property {boolean} splitTeeIntersections 10 | * @returns {{ originalPts: Object., pts: string[], cxs: Object. }} 11 | */ 12 | static getSegsAndConnections (segs, splitTeeIntersections = false, splitCrossIntersections = false) { 13 | 14 | /** @type {Object.} */ 15 | let cxs = {}; 16 | /** @type {string[]} */ 17 | let pts = []; 18 | /** @type {Object.} */ 19 | let originalPts = {}; 20 | 21 | let token = pt => { 22 | let t = `${Math.round(pt.x * 1)}|${Math.round(pt.y * 1)}`; 23 | originalPts[t] = pt; 24 | return t; 25 | } 26 | 27 | if (splitTeeIntersections) { 28 | 29 | // step 0, split segments that cross a point (T intersections); 30 | 31 | let allPts = segs.reduce((arr, seg) => arr.concat(seg.a, seg.b), []); 32 | let j = allPts.length; 33 | 34 | while(j--) { 35 | let ptA = allPts[j]; 36 | let i = j; 37 | while (i--) { 38 | let ptB = allPts[i]; 39 | if (GeomUtil.pointsEqual(ptA, ptB)) { 40 | allPts.splice(j, 1); 41 | break; 42 | } 43 | } 44 | } 45 | 46 | let i = segs.length; 47 | 48 | while (i--) { 49 | 50 | let seg = segs[i]; 51 | 52 | let crossPts = []; 53 | 54 | allPts.forEach(pt => { 55 | if (GeomUtil.distancePointSegment(pt, seg) < 0.1) { 56 | if (!GeomUtil.pointsEqual(pt, seg.a) && !GeomUtil.pointsEqual(pt, seg.b)) { 57 | crossPts.push(pt); 58 | } 59 | } 60 | }); 61 | 62 | if (crossPts.length) { 63 | 64 | crossPts.sort((ptA, ptB) => { 65 | const da = GeomUtil.distanceBetweenSquared(ptA, seg.a); 66 | const db = GeomUtil.distanceBetweenSquared(ptB, seg.a); 67 | if (da < db) { 68 | return -1; 69 | } else if (da > db) { 70 | return 1; 71 | } 72 | return 0; 73 | }); 74 | 75 | const newSegs = []; 76 | 77 | let ptA = seg.a; 78 | for (let k = 0; k < crossPts.length; k++) { 79 | let ptB = crossPts[k]; 80 | newSegs.push(new Segment(ptA, ptB)); 81 | ptA = ptB; 82 | } 83 | newSegs.push(new Segment(ptA, seg.b)); 84 | 85 | segs.splice(i, 1, ...newSegs); 86 | 87 | } 88 | 89 | } 90 | 91 | } 92 | 93 | if (splitCrossIntersections) { 94 | 95 | let j = segs.length; 96 | while (j--) { 97 | let i = j; 98 | let found = false 99 | while (i--) { 100 | let segA = segs[j]; 101 | let segB = segs[i]; 102 | let intPt = GeomUtil.segmentSegmentIntersect(segA, segB, true); 103 | if (intPt) { 104 | found = true; 105 | segs.splice(j, 1, new Segment(Point.clone(segA.a), Point.clone(intPt)), new Segment(Point.clone(intPt), Point.clone(segA.b))); 106 | segs.splice(i, 1, new Segment(Point.clone(segB.a), Point.clone(intPt)), new Segment(Point.clone(intPt), Point.clone(segB.b))); 107 | } 108 | } 109 | if (found) { 110 | j = segs.length; 111 | } 112 | } 113 | 114 | } 115 | 116 | // step 1, collect endpoints 117 | // step 2, filter out dupes 118 | // step 3, collect connected endpoints for each endpoint 119 | 120 | segs.forEach(seg => { 121 | let ta = token(seg.a); 122 | let tb = token(seg.b); 123 | if (!cxs[ta]) cxs[ta] = []; 124 | if (!cxs[tb]) cxs[tb] = []; 125 | if (cxs[ta].indexOf(tb) === -1) { 126 | cxs[ta].push(tb); 127 | } 128 | if (cxs[tb].indexOf(ta) === -1) { 129 | cxs[tb].push(ta); 130 | } 131 | if (pts.indexOf(ta) === -1) { 132 | pts.push(ta); 133 | } 134 | if (pts.indexOf(tb) === -1) { 135 | pts.push(tb); 136 | } 137 | }); 138 | 139 | return { 140 | originalPts, 141 | pts, 142 | cxs 143 | }; 144 | 145 | } 146 | 147 | /** 148 | * @property {Segment[]} segs 149 | * @property {boolean} splitTeeIntersections 150 | * @returns {Segment[]} 151 | */ 152 | static pathOrder (segs, splitTeeIntersections = false, splitCrossIntersections = false) { 153 | 154 | let res = []; 155 | let { originalPts, pts, cxs } = Analyzer.getSegsAndConnections(segs, splitTeeIntersections, splitCrossIntersections); 156 | 157 | let nekot = str => { 158 | return originalPts[str]; 159 | }; 160 | 161 | let byNumConnections = (ta, tb) => { 162 | if (cxs[ta].length > cxs[tb].length) { 163 | return 1; 164 | } else if (cxs[ta].length < cxs[tb].length) { 165 | return -1; 166 | } 167 | return 0; 168 | } 169 | 170 | // step 1, sort by number of connections, desc 171 | // step 2, choose first endpoint 172 | // step 3, pick the connected one with the lowest index that isn't in the stack, remove from connections list, push onto stack 173 | // step 4, resort by number of connections, desc 174 | // step 5, repeat step 6 until no more connections 175 | 176 | pts.sort(byNumConnections); 177 | 178 | while (pts.length) { 179 | 180 | pts.sort(byNumConnections); 181 | let ptA = pts.shift(); 182 | 183 | while (ptA) { 184 | 185 | if (cxs[ptA].length) { 186 | 187 | cxs[ptA].sort(byNumConnections); 188 | let ptB = cxs[ptA].shift(); 189 | 190 | let oppIdx = cxs[ptB].indexOf(ptA); 191 | if (oppIdx !== -1) cxs[ptB].splice(oppIdx, 1); 192 | 193 | res.push(new Segment(nekot(ptA), nekot(ptB))); 194 | 195 | if (cxs[ptA].length) { 196 | pts.unshift(ptA); 197 | } 198 | 199 | ptA = ptB; 200 | 201 | } else { 202 | 203 | ptA = null; 204 | 205 | } 206 | 207 | } 208 | 209 | } 210 | 211 | return res; 212 | 213 | } 214 | 215 | /** 216 | * @property {Segment[]} segs 217 | * @property {number} offset 218 | * @returns {Point[]} 219 | */ 220 | static getEndingSegmentPoints (segs, offset = 0) { 221 | 222 | segs = segs.concat(); 223 | segs = Analyzer.pathOrder(segs, true, true); 224 | 225 | let { originalPts, pts, cxs } = Analyzer.getSegsAndConnections(segs, true); 226 | 227 | let nekot = str => { 228 | return originalPts[str]; 229 | }; 230 | 231 | // return all points with one connection 232 | 233 | const endTokens = pts.filter(ta => cxs[ta].length === 1); 234 | 235 | const out = []; 236 | endTokens.forEach(tb => { 237 | const ptB = Point.clone(nekot(tb) ); 238 | if (offset === 0) { 239 | out.push(ptB); 240 | return; 241 | } 242 | const ptA = nekot(cxs[tb]); 243 | const ang = GeomUtil.angleBetween(ptA, ptB); 244 | const pt = new Point(0, offset); 245 | GeomUtil.rotatePoint(pt, Math.PI * 0.5 - ang); 246 | GeomUtil.addToPoint(ptB, pt); 247 | out.push(ptB); 248 | }); 249 | 250 | return out; 251 | 252 | } 253 | 254 | /** 255 | * @property {Segment[]} segs 256 | * @property {number} searchMultiplier multiple of typical segmentation distance to search for flood-fill points 257 | * @returns {Point[][]} 258 | */ 259 | static getFills (segs, searchMultiplier = 5) { 260 | 261 | segs = segs.concat(); 262 | 263 | let { originalPts, pts, cxs } = Analyzer.getSegsAndConnections(segs, true, true); 264 | 265 | let token = pt => { 266 | let t = `${Math.round(pt.x * 1)}|${Math.round(pt.y * 1)}`; 267 | originalPts[t] = pt; 268 | return t; 269 | } 270 | 271 | let cenTokens = []; 272 | let pointGroups = []; 273 | 274 | // 1. iterate through all points 275 | // 2. for each point pick a each connection 276 | // 3. for each pair, proceed to find a winding polygon 277 | 278 | let minX = 100000; 279 | let minY = 100000; 280 | let maxX = -100000; 281 | let maxY = -100000; 282 | let minDx = 100000; 283 | let minDy = 100000; 284 | 285 | let ptArray = []; 286 | 287 | // get extents 288 | 289 | for (let token in originalPts) { 290 | let pt = originalPts[token]; 291 | ptArray.push(pt); 292 | minX = Math.min(minX, pt.x); 293 | minY = Math.min(minY, pt.y); 294 | maxX = Math.max(maxX, pt.x); 295 | maxY = Math.max(maxY, pt.y); 296 | } 297 | 298 | // get minimum spacing 299 | 300 | ptArray.sort((a, b) => { 301 | if (a.x < b.x) { 302 | return -1; 303 | } else if (a.x > b.x) { 304 | return 1; 305 | } 306 | return 0; 307 | }); 308 | 309 | ptArray.forEach((ptA, idx) => { 310 | if (idx > 0) { 311 | let ptB = ptArray[idx - 1]; 312 | let dx = Math.round(Math.abs(ptA.x - ptB.x)); 313 | if (dx > 1) { 314 | minDx = Math.min(minDx, dx); 315 | } 316 | } 317 | }); 318 | 319 | ptArray.sort((a, b) => { 320 | if (a.y < b.y) { 321 | return -1; 322 | } else if (a.y > b.y) { 323 | return 1; 324 | } 325 | return 0; 326 | }); 327 | 328 | ptArray.forEach((ptA, idx) => { 329 | if (idx > 0) { 330 | let ptB = ptArray[idx - 1]; 331 | let dy = Math.round(Math.abs(ptA.y - ptB.y)); 332 | if (dy > 1) { 333 | minDy = Math.min(minDy, dy); 334 | } 335 | } 336 | }); 337 | 338 | let hDx = minDx * 0.5; 339 | let hDy = minDy * 0.5; 340 | 341 | let rayPts = []; 342 | 343 | for (let j = minY; j < maxY; j += minDy) { 344 | for (let i = minX; i < maxX; i += minDx) { 345 | rayPts.push(new Point(i + hDx, j + hDy)); 346 | } 347 | } 348 | 349 | rayPts.forEach(rayPt => { 350 | let nearPts = []; 351 | ptArray.forEach(pt => { 352 | let dist = GeomUtil.distanceBetween(pt, rayPt); 353 | if (dist < Math.max(minDx, minDy) * searchMultiplier) { 354 | let ang = GeomUtil.angleBetween(pt, rayPt); 355 | nearPts.push({ 356 | pt, 357 | dist, 358 | ang 359 | }); 360 | } 361 | }); 362 | if (nearPts.length < 4) { 363 | return; 364 | } 365 | let i = nearPts.length; 366 | while (i--) { 367 | let nPt = nearPts[i].pt; 368 | let seg = new Segment(rayPt, nPt); 369 | let hits = GeomUtil.segmentSegmentsIntersections(seg, segs, true); 370 | if (hits.length > 0) { 371 | nearPts.splice(i, 1); 372 | } 373 | } 374 | nearPts.sort((a, b) => { 375 | if (a.ang < b.ang) { 376 | return -1; 377 | } else if (a.ang > b.ang) { 378 | return 1; 379 | } 380 | return 0; 381 | }); 382 | i = nearPts.length; 383 | while (i--) { 384 | let nPtA = nearPts[i].pt; 385 | let tokenA = token(nPtA); 386 | let j = nearPts.length; 387 | let ok = false; 388 | while (j--) { 389 | if (i === j) { 390 | continue; 391 | } 392 | let nPtB = nearPts[j].pt; 393 | let tokenB = token(nPtB); 394 | if (cxs[tokenA].indexOf(tokenB) === -1) { 395 | ok = true; 396 | break; 397 | } 398 | } 399 | if (!ok) { 400 | nearPts.splice(i, 1); 401 | } 402 | } 403 | let ok = true; 404 | nearPts.forEach((npA, idx) => { 405 | let npB = nearPts[(idx + 1) % nearPts.length]; 406 | let tokenA = token(npA.pt); 407 | let tokenB = token(npB.pt); 408 | if (cxs[tokenA].indexOf(tokenB) === -1) { 409 | ok = false; 410 | } 411 | }); 412 | if (ok) { 413 | let polyPts = nearPts.map(nPt => nPt.pt); 414 | let cen = GeomUtil.averagePoints(...polyPts); 415 | let cenToken = token(cen); 416 | if (cenTokens.indexOf(cenToken) === -1) { 417 | cenTokens.push(cenToken); 418 | pointGroups.push(polyPts); 419 | } 420 | } 421 | }); 422 | 423 | return pointGroups; 424 | 425 | } 426 | 427 | /** 428 | * @param {Point[][]} fills 429 | * @returns {Segment[]} 430 | */ 431 | static getFillsOutline (fills) { 432 | 433 | let outlineShape = new BooleanShape(); 434 | 435 | fills.forEach(fillPts => { 436 | let fill = new PolygonShape(fillPts); 437 | outlineShape.add(fill); 438 | }); 439 | 440 | return outlineShape.toSegments(); 441 | 442 | } 443 | 444 | } 445 | 446 | -------------------------------------------------------------------------------- /src/expander.js: -------------------------------------------------------------------------------- 1 | 2 | import { GeomUtil, Point, Segment } from "./geom/geom.js"; 3 | import { PolygonShape, CompoundShape } from "./geom/shapes.js"; 4 | import * as clipperLib from "js-angusj-clipper"; 5 | 6 | /** @type {clipperLib.ClipperLibWrapper} */ 7 | let clipper = null; 8 | 9 | clipperLib 10 | .loadNativeClipperLibInstanceAsync( 11 | // let it autodetect which one to use, but also available WasmOnly and AsmJsOnly 12 | clipperLib.NativeClipperLibRequestedFormat.WasmWithAsmJsFallback 13 | ) 14 | .then((res) => { 15 | clipper = res; 16 | }); 17 | 18 | export class Expander { 19 | 20 | /** 21 | * 22 | * @param {Point} a 23 | * @param {Point} b 24 | * @param {Point} c 25 | * @param {number} amount 26 | */ 27 | static expandFace(a, b, c, amount) { 28 | const polyResult = clipper.offsetToPaths({ 29 | offsetInputs: [{ 30 | joinType: clipperLib.JoinType.Square, 31 | endType: clipperLib.EndType.ClosedPolygon, 32 | // @ts-ignore 33 | data: [a, b, c], 34 | }], 35 | delta: amount 36 | }); 37 | return polyResult; 38 | } 39 | 40 | /** 41 | * 42 | * @param {number} amount 43 | * @param {Segment[]} segs 44 | * @param {number} scale 45 | */ 46 | static expandSegs(amount, segs, scale = 1) { 47 | 48 | let ppts = []; 49 | let pts = []; 50 | 51 | segs.forEach((seg, idx) => { 52 | if (idx > 0) { 53 | let pseg = segs[idx - 1]; 54 | if (!GeomUtil.pointsEqual(pseg.b, seg.a, scale)) { 55 | ppts.push(pts); 56 | pts = []; 57 | } 58 | } 59 | pts.push(Point.clone(seg.a)); 60 | pts.push(Point.clone(seg.b)); 61 | }); 62 | 63 | ppts.push(pts); 64 | 65 | const polyResult = clipper.offsetToPaths({ 66 | offsetInputs: [{ 67 | joinType: clipperLib.JoinType.Square, 68 | endType: clipperLib.EndType.ClosedPolygon, 69 | data: ppts, 70 | }], 71 | delta: amount * scale 72 | }); 73 | 74 | if (!polyResult) { 75 | console.log("no offset result from expand segs") 76 | return []; 77 | } 78 | 79 | let cs = new CompoundShape(); 80 | 81 | for (let i = 0; i < polyResult.length; i++) { 82 | cs.shapes.push(new PolygonShape(polyResult[i])); 83 | } 84 | 85 | return cs.toSegments(); 86 | 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /src/geom/booleanshape.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { Segment } from "./geom.js"; 4 | import { Shape, PolygonShape, CompoundShape } from "./shapes.js"; 5 | import * as clipperLib from "js-angusj-clipper"; 6 | 7 | /** @type {clipperLib.ClipperLibWrapper} */ 8 | let clipper = null; 9 | 10 | clipperLib 11 | .loadNativeClipperLibInstanceAsync( 12 | // let it autodetect which one to use, but also available WasmOnly and AsmJsOnly 13 | clipperLib.NativeClipperLibRequestedFormat.WasmWithAsmJsFallback 14 | ) 15 | .then((res) => { 16 | clipper = res; 17 | }); 18 | 19 | export class BooleanShape extends CompoundShape { 20 | 21 | /** 22 | * 23 | * @param {Shape[]} [shapes] 24 | */ 25 | constructor(shapes = [], cleanDistance = 0.1) { 26 | super(shapes); 27 | this.additiveShapes = 0; 28 | this.subtractiveShapes = 0; 29 | this.cleanDistance = cleanDistance; 30 | } 31 | /** 32 | * 33 | * @param {Shape} shape 34 | * @returns {BooleanShape} 35 | */ 36 | add(shape) { 37 | if (shape.isInverted) { 38 | shape.invert(); 39 | } 40 | this.shapes.push(shape); 41 | this.additiveShapes++; 42 | return this; 43 | } 44 | 45 | /** 46 | * 47 | * @param {Shape} shape 48 | * @returns {BooleanShape} 49 | */ 50 | sub(shape) { 51 | if (!shape.isInverted) { 52 | shape.invert(); 53 | } 54 | this.shapes.push(shape); 55 | this.subtractiveShapes++; 56 | return this; 57 | } 58 | 59 | /** 60 | * @returns {Segment[]}; 61 | */ 62 | toSegments() { 63 | let computedShapes = []; 64 | 65 | for (let i = 0; i < this.shapes.length; i++) { 66 | computedShapes = BooleanShape.combine(this.shapes[i], computedShapes, this.cleanDistance); 67 | } 68 | 69 | if (!computedShapes) { 70 | return []; 71 | } 72 | 73 | return computedShapes.map((shape) => shape.toSegments()).reduce((acc, segs, _, []) => acc.concat(segs), []); 74 | } 75 | 76 | /** 77 | * 78 | * @param {Shape} shape 79 | * @param {Shape[]} shapes 80 | * @returns {Shape[]} 81 | */ 82 | static combine(shape, shapes, cleanDistance = 0.01) { 83 | if (!shapes) { 84 | return []; 85 | } 86 | 87 | let geoms = shapes.map((shape) => shape.toPoints()); 88 | let gshape = shape.toPoints(); 89 | 90 | let res; 91 | try { 92 | if (!shape.isInverted) { 93 | res = clipper.clipToPaths({ 94 | clipType: clipperLib.ClipType.Union, 95 | subjectInputs: [{ data: gshape, closed: true }], 96 | clipInputs: geoms.map((geom) => { 97 | return { data: geom, closed: true }; 98 | }), 99 | subjectFillType: clipperLib.PolyFillType.EvenOdd, 100 | cleanDistance: cleanDistance, 101 | }); 102 | } else { 103 | res = clipper.clipToPaths({ 104 | clipType: clipperLib.ClipType.Difference, 105 | clipInputs: [{ data: gshape }], 106 | subjectInputs: geoms.map((geom) => { 107 | return { data: geom, closed: true }; 108 | }), 109 | subjectFillType: clipperLib.PolyFillType.EvenOdd, 110 | cleanDistance: cleanDistance, 111 | }); 112 | } 113 | } catch (err) { 114 | console.log(err.toString()); 115 | console.log(err); 116 | return; 117 | } 118 | 119 | let computedShapes = []; 120 | 121 | res.forEach((mpoly) => { 122 | //console.log(mpoly.length) 123 | computedShapes.push(PolygonShape.fromPoints(mpoly)); 124 | }); 125 | 126 | //console.log("computed shapes: " + computedShapes.length + " " + geoms.length + " " + gshape.length); 127 | 128 | return computedShapes; 129 | } 130 | } 131 | 132 | BooleanShape.UNION = "union"; 133 | BooleanShape.SUBTRACT = "subtract"; 134 | -------------------------------------------------------------------------------- /src/geom/geom.js: -------------------------------------------------------------------------------- 1 | const EPSILON = 0.001; 2 | 3 | export class Point { 4 | /** 5 | * @param {number} x 6 | * @param {number} y 7 | */ 8 | constructor(x, y) { 9 | this.x = x; 10 | this.y = y; 11 | } 12 | /** 13 | * @param {Point} pt 14 | */ 15 | static clone(pt) { 16 | return new Point(pt.x, pt.y); 17 | } 18 | } 19 | 20 | export class BoundingBox { 21 | /** 22 | * @param {number} minX 23 | * @param {number} minY 24 | * @param {number} maxX 25 | * @param {number} maxY 26 | */ 27 | constructor(minX, minY, maxX, maxY) { 28 | this.minX = minX; 29 | this.minY = minY; 30 | this.maxX = maxX; 31 | this.maxY = maxY; 32 | } 33 | width() { 34 | return Math.abs(this.maxX - this.minX); 35 | } 36 | height() { 37 | return Math.abs(this.maxY - this.minY); 38 | } 39 | } 40 | 41 | export class BoundingCircle { 42 | /** 43 | * 44 | * @param {number} r radius 45 | */ 46 | constructor(r = 0) { 47 | this.r = r; 48 | } 49 | } 50 | 51 | export class Segment { 52 | /** 53 | * 54 | * @param {Point} a start point 55 | * @param {Point} b end point 56 | */ 57 | constructor(a, b) { 58 | this.a = a; 59 | this.b = b; 60 | this.tags = {}; 61 | } 62 | 63 | /** 64 | * 65 | * @param {Segment} segA 66 | * @param {Segment} segB 67 | */ 68 | static isEqual(segA, segB) { 69 | return ( 70 | (GeomUtil.pointsEqual(segA.a, segB.a) && GeomUtil.pointsEqual(segA.b, segB.b)) || 71 | (GeomUtil.pointsEqual(segA.b, segB.a) && GeomUtil.pointsEqual(segA.a, segB.b)) 72 | ); 73 | } 74 | 75 | /** 76 | * @param {Segment} seg 77 | */ 78 | static clone(seg) { 79 | return new Segment(new Point(seg.a.x, seg.a.y), new Point(seg.b.x, seg.b.y)); 80 | } 81 | } 82 | 83 | export class SegmentCollection { 84 | constructor() { 85 | this.pivot = { x: 0, y: 0 }; 86 | this.rotation = 0; 87 | this.isOpen = true; 88 | this.isGroup = false; 89 | this.isStrong = false; 90 | /** 91 | * 92 | * @param {Point[]} pts 93 | */ 94 | this._makeAbsolute = (pts) => { 95 | let rot = (this.rotation * Math.PI) / 180; 96 | pts.forEach((pt, idx) => { 97 | const ptA = { x: pt.x, y: pt.y }; 98 | GeomUtil.rotatePoint(ptA, rot); 99 | ptA.x += this.pivot.x; 100 | ptA.y += this.pivot.y; 101 | pts[idx] = ptA; 102 | }); 103 | }; 104 | /** 105 | * 106 | * @param {Segment[]} segs 107 | */ 108 | this._makeSegsAbsolute = (segs) => { 109 | let rot = (this.rotation * Math.PI) / 180; 110 | segs.forEach((seg) => { 111 | const ptA = { x: seg.a.x, y: seg.a.y }; 112 | const ptB = { x: seg.b.x, y: seg.b.y }; 113 | GeomUtil.rotatePoint(ptA, rot); 114 | GeomUtil.rotatePoint(ptB, rot); 115 | GeomUtil.addToPoint(ptA, this.pivot); 116 | GeomUtil.addToPoint(ptB, this.pivot); 117 | seg.a = ptA; 118 | seg.b = ptB; 119 | }); 120 | }; 121 | } 122 | 123 | /** 124 | * @param {boolean} local 125 | * @returns {Point[]} 126 | */ 127 | toPoints(local = false) { 128 | throw "not implemented"; 129 | } 130 | 131 | /** 132 | * 133 | * @param {boolean} local 134 | * @returns {Segment[]}; 135 | */ 136 | toSegments(local = false) { 137 | throw "not implemented"; 138 | } 139 | 140 | /** 141 | * 142 | * @param {boolean} local 143 | * @returns {BoundingBox} 144 | */ 145 | getBoundingBox(local = false) { 146 | const bb = new BoundingBox(1000000, 1000000, -1000000, -1000000); 147 | const pts = this.toPoints(local); 148 | pts.forEach((pt) => { 149 | bb.minX = Math.min(bb.minX, pt.x); 150 | bb.minY = Math.min(bb.minY, pt.y); 151 | bb.maxX = Math.max(bb.maxX, pt.x); 152 | bb.maxY = Math.max(bb.maxY, pt.y); 153 | }); 154 | 155 | return bb; 156 | } 157 | 158 | /** 159 | * @returns {BoundingCircle} 160 | */ 161 | getBoundingCircle() { 162 | const bc = new BoundingCircle(); 163 | const pts = this.toPoints(true); 164 | pts.forEach((pt) => { 165 | bc.r = Math.max(bc.r, Math.sqrt(pt.x * pt.x + pt.y * pt.y)); 166 | }); 167 | return bc; 168 | } 169 | } 170 | 171 | export class Segments extends SegmentCollection { 172 | /** 173 | * 174 | * @param {Segment[]} segments 175 | */ 176 | constructor(segments) { 177 | super(); 178 | /** @type {Segment[]} */ 179 | this._segments = segments; 180 | } 181 | 182 | /** 183 | * @param {Segment[]} segs 184 | */ 185 | add(...segs) { 186 | this._segments = this._segments.concat(segs); 187 | } 188 | 189 | /** 190 | * @param {boolean} local 191 | * @returns {Point[]} 192 | */ 193 | toPoints(local = false) { 194 | return this.toSegments(local).reduce((arr, seg) => (seg ? arr.concat([seg.a, seg.b]) : arr), []); 195 | } 196 | /** 197 | * 198 | * @param {boolean} local 199 | * @returns {Segment[]}; 200 | */ 201 | toSegments(local = false) { 202 | let segs = this._segments.reduce((arr, seg) => (seg ? arr.concat(Segment.clone(seg)) : arr), []); 203 | if (!local) { 204 | this._makeSegsAbsolute(segs); 205 | } 206 | return segs; 207 | } 208 | 209 | bake() { 210 | // noOp 211 | } 212 | 213 | result() { 214 | return Segments.clone(this); 215 | } 216 | 217 | /** 218 | * 219 | * @param {Segments} segs 220 | */ 221 | static clone(segs) { 222 | let sA = segs._segments; 223 | let sB = []; 224 | let i = sA.length; 225 | while (i--) { 226 | sB.unshift(Segment.clone(sA[i])); 227 | } 228 | let s = new Segments(sB); 229 | s.pivot.x = segs.pivot.x; 230 | s.pivot.y = segs.pivot.y; 231 | s.rotation = segs.rotation; 232 | return s; 233 | } 234 | } 235 | 236 | export class GeomUtil { 237 | /** 238 | * 239 | * @param {number} a 240 | * @param {number} b 241 | * @param {number} d 242 | * @returns {number} 243 | */ 244 | static lerp(a, b, d) { 245 | return (1 - d) * a + d * b; 246 | } 247 | 248 | /** 249 | * 250 | * @param {Point} ptA 251 | * @param {Point} ptB 252 | */ 253 | static angleBetween(ptA, ptB) { 254 | return Math.atan2(ptB.y - ptA.y, ptB.x - ptA.x); 255 | } 256 | 257 | /** 258 | * 259 | * @param {Segment} segA 260 | * @param {Segment} segB 261 | */ 262 | static sameAngle(segA, segB) { 263 | let aA = GeomUtil.angleBetween(segA.a, segA.b); 264 | let aB = GeomUtil.angleBetween(segB.a, segB.b); 265 | 266 | return Math.abs(aA - aB) < EPSILON; 267 | } 268 | 269 | /** 270 | * 271 | * @param {Segment} segA 272 | * @param {Segment} segB 273 | */ 274 | static sameAngleRev(segA, segB) { 275 | let aA = GeomUtil.angleBetween(segA.a, segA.b); 276 | let aB = GeomUtil.angleBetween(segB.b, segB.a); 277 | 278 | return Math.abs(aA - aB) < EPSILON; 279 | } 280 | 281 | /** 282 | * 283 | * @param {Point} ptA 284 | * @param {Point} ptB 285 | * @param {number} d 286 | * @returns {Point} 287 | */ 288 | static lerpPoints(ptA, ptB, d) { 289 | return { 290 | x: GeomUtil.lerp(ptA.x, ptB.x, d), 291 | y: GeomUtil.lerp(ptA.y, ptB.y, d), 292 | }; 293 | } 294 | 295 | /** 296 | * 297 | * @param {Point} pt the point to rotate in place 298 | * @param {number} deg angle in degrees 299 | */ 300 | static rotatePointDeg(pt, deg) { 301 | GeomUtil.rotatePoint(pt, (deg * Math.PI) / 180); 302 | } 303 | 304 | /** 305 | * 306 | * @param {Point} pt 307 | * @param {*} rad 308 | */ 309 | static rotatePoint(pt, rad) { 310 | const cos = Math.cos(rad); 311 | const sin = Math.sin(rad); 312 | 313 | const oldY = pt.y; 314 | const oldX = pt.x; 315 | 316 | pt.y = cos * oldY - sin * oldX; 317 | pt.x = sin * oldY + cos * oldX; 318 | } 319 | 320 | /** 321 | * 322 | * @param {number} rad 323 | * @param {...Point} points 324 | */ 325 | static rotatePoints(rad, ...points) { 326 | points.forEach((pt) => { 327 | GeomUtil.rotatePoint(pt, rad); 328 | }); 329 | } 330 | 331 | /** 332 | * 333 | * @param {number} deg 334 | * @param {...Point} points 335 | */ 336 | static rotatePointsDeg(deg, ...points) { 337 | let rad = (deg * Math.PI) / 180; 338 | points.forEach((pt) => { 339 | GeomUtil.rotatePoint(pt, rad); 340 | }); 341 | } 342 | 343 | // Based on http://stackoverflow.com/a/12037737 344 | 345 | static outerTangents(ptA, rA, ptB, rB) { 346 | var dx = ptB.x - ptA.x; 347 | var dy = ptB.y - ptA.y; 348 | var dist = Math.sqrt(dx * dx + dy * dy); 349 | 350 | if (dist <= Math.abs(rB - rA)) return []; // no valid tangents 351 | 352 | // Rotation from x-axis 353 | var angle1 = Math.atan2(dy, dx); 354 | var angle2 = Math.acos((rA - rB) / dist); 355 | 356 | return [ 357 | new Segment( 358 | { 359 | x: ptA.x + rA * Math.cos(angle1 + angle2), 360 | y: ptA.y + rA * Math.sin(angle1 + angle2), 361 | }, 362 | { 363 | x: ptB.x + rB * Math.cos(angle1 + angle2), 364 | y: ptB.y + rB * Math.sin(angle1 + angle2), 365 | } 366 | ), 367 | new Segment( 368 | { 369 | x: ptA.x + rA * Math.cos(angle1 - angle2), 370 | y: ptA.y + rA * Math.sin(angle1 - angle2), 371 | }, 372 | { 373 | x: ptB.x + rB * Math.cos(angle1 - angle2), 374 | y: ptB.y + rB * Math.sin(angle1 - angle2), 375 | } 376 | ), 377 | ]; 378 | } 379 | 380 | /** 381 | * 382 | * @param {Point} pt 383 | */ 384 | static cartesian2Polar(pt) { 385 | const d = Math.sqrt(pt.x * pt.x + pt.y * pt.y); 386 | const r = Math.atan2(pt.y, pt.x); 387 | pt.x = d; 388 | pt.y = r; 389 | } 390 | 391 | /** 392 | * 393 | * @param {Point} ptA 394 | * @param {Point} ptB 395 | * @param {number} [scale] 396 | */ 397 | static pointsEqual(ptA, ptB, scale = 1) { 398 | return ( 399 | Math.round(ptA.x * 10000 / scale) == Math.round(ptB.x * 10000 / scale) && Math.round(ptA.y * 10000 / scale) == Math.round(ptB.y * 10000 / scale) 400 | ); 401 | } 402 | 403 | /** 404 | * 405 | * @param {Point} ptA 406 | * @param {Point} ptB 407 | * @returns {number} 408 | */ 409 | static distanceBetween(ptA, ptB) { 410 | const dx = ptB.x - ptA.x; 411 | const dy = ptB.y - ptA.y; 412 | return Math.sqrt(dx * dx + dy * dy); 413 | } 414 | /** 415 | * 416 | * @param {Point} ptA 417 | * @param {Point} ptB 418 | * @returns {number} 419 | */ 420 | static distanceBetweenSquared(ptA, ptB) { 421 | const dx = ptB.x - ptA.x; 422 | const dy = ptB.y - ptA.y; 423 | return dx * dx + dy * dy; 424 | } 425 | 426 | /** 427 | * 428 | * @param {Point} ptA 429 | * @param {Point} ptB 430 | * @param {number} numSegs 431 | * @returns {Point[]} 432 | */ 433 | static interpolatePoints(ptA, ptB, numSegs) { 434 | let pts = [{ x: ptA.x, y: ptA.y }]; 435 | let perc = 1 / numSegs; 436 | let deltaX = (ptB.x - ptA.x) * perc; 437 | let deltaY = (ptB.y - ptA.y) * perc; 438 | for (var i = 1; i < numSegs; i++) { 439 | pts.push(new Point(ptA.x + deltaX * i, ptA.y + deltaY * i)); 440 | } 441 | pts.push({ x: ptB.x, y: ptB.y }); 442 | return pts; 443 | } 444 | 445 | /** 446 | * 447 | * @param {...Point} pts 448 | */ 449 | static averagePoints(...pts) { 450 | let a = new Point(0, 0); 451 | pts.forEach((pt) => { 452 | a.x += pt.x; 453 | a.y += pt.y; 454 | }); 455 | a.x /= pts.length; 456 | a.y /= pts.length; 457 | return a; 458 | } 459 | 460 | /** 461 | * 462 | * @param {Point} targetPt the point that will be added to 463 | * @param {Point} sourcePt the point to add to the target 464 | */ 465 | static addToPoint(targetPt, sourcePt) { 466 | targetPt.x += sourcePt.x; 467 | targetPt.y += sourcePt.y; 468 | } 469 | 470 | /** 471 | * 472 | * @param {Point} targetPt the point that will be subtracted from 473 | * @param {Point} sourcePt the point tosubtract from the target 474 | */ 475 | static subFromPoint(targetPt, sourcePt) { 476 | targetPt.x -= sourcePt.x; 477 | targetPt.y -= sourcePt.y; 478 | } 479 | 480 | /** 481 | * 482 | * @param {Point} ptA 483 | * @param {Point} ptB 484 | * @param {number} delta 485 | * @returns {Point[]} 486 | */ 487 | static subdivideByDistance(ptA, ptB, delta) { 488 | if (delta === 0) { 489 | return [ptA, ptB]; 490 | } 491 | let pts = [{ x: ptA.x, y: ptA.y }]; 492 | let dist = GeomUtil.distanceBetween(ptA, ptB); 493 | let perc = delta / dist; 494 | let numFit = Math.floor(1 / perc); 495 | let remain = dist % delta; 496 | delta += remain / numFit; 497 | perc = delta / dist; 498 | let travel = perc; 499 | let i = 1; 500 | let deltaX = (ptB.x - ptA.x) * perc; 501 | let deltaY = (ptB.y - ptA.y) * perc; 502 | while (travel < 1) { 503 | pts.push(new Point(ptA.x + deltaX * i, ptA.y + deltaY * i)); 504 | travel += perc; 505 | i++; 506 | } 507 | pts.push({ x: ptB.x, y: ptB.y }); 508 | return pts; 509 | } 510 | 511 | /** 512 | * 513 | * @param {Segment} segA 514 | * @param {Segment} segB 515 | * @param {number} [scale] 516 | */ 517 | static segmentsConnected(segA, segB, scale = 1) { 518 | return GeomUtil.pointsEqual(segA.b, segB.a, scale) || GeomUtil.pointsEqual(segA.a, segB.b, scale); 519 | } 520 | 521 | /** 522 | * 523 | * @param {Segment[]} segs 524 | * @returns {Point[]} 525 | */ 526 | static segmentsToPoints(segs) { 527 | let pts = segs.reduce((arr, seg) => { 528 | return arr.concat(seg.a, seg.b); 529 | }, []); 530 | let i = pts.length; 531 | while (i--) { 532 | let pt = pts[i]; 533 | if (i > 0 && GeomUtil.pointsEqual(pt, pts[i - 1])) { 534 | pts.splice(i, 1); 535 | } 536 | } 537 | return pts; 538 | } 539 | 540 | /** 541 | * 542 | * @param {Point[]} pts 543 | * @returns {number} 544 | */ 545 | static polygonArea(pts) { 546 | let area = 0; 547 | let j = pts.length - 1; 548 | for (var i = 0; i < pts.length; i++) { 549 | area += pts[i].x * pts[j].y; 550 | area -= pts[j].x * pts[i].y; 551 | j = i; 552 | } 553 | return area / 2; 554 | } 555 | 556 | /** 557 | * 558 | * @param {Point[]} pts 559 | * @returns {BoundingBox} 560 | */ 561 | static pointsBoundingBox(pts) { 562 | const b = new BoundingBox(1000000, 1000000, -1000000, -1000000); 563 | 564 | pts.forEach((pt) => { 565 | b.minX = Math.min(b.minX, pt.x); 566 | b.minY = Math.min(b.minY, pt.y); 567 | b.maxX = Math.max(b.maxX, pt.x); 568 | b.maxY = Math.max(b.maxY, pt.y); 569 | }); 570 | 571 | return b; 572 | } 573 | 574 | /** 575 | * 576 | * @param {BoundingBox[]} bbs 577 | * @returns {BoundingBox} 578 | */ 579 | static boundingBoxesBoundingBox(bbs) { 580 | const b = new BoundingBox(1000000, 1000000, -1000000, -1000000); 581 | 582 | bbs.forEach((bb) => { 583 | b.minX = Math.min(b.minX, bb.minX); 584 | b.minY = Math.min(b.minY, bb.minY); 585 | b.maxX = Math.max(b.maxX, bb.maxX); 586 | b.maxY = Math.max(b.maxY, bb.maxY); 587 | }); 588 | 589 | return b; 590 | } 591 | 592 | /** 593 | * 594 | * @param {Segment[]} segs 595 | * @returns {BoundingBox} 596 | */ 597 | static segmentsBoundingBox(segs) { 598 | const pts = []; 599 | segs.forEach((seg) => { 600 | pts.push(seg.a); 601 | pts.push(seg.b); 602 | }); 603 | return GeomUtil.pointsBoundingBox(pts); 604 | } 605 | 606 | /** 607 | * 608 | * @param {BoundingBox} ab 609 | * @param {BoundingBox} bb 610 | */ 611 | static boundingBoxesIntersect(ab, bb) { 612 | return ab.maxX >= bb.minX && ab.maxY >= bb.minY && ab.minX <= bb.maxX && ab.minY <= bb.maxY; 613 | } 614 | 615 | /** 616 | * 617 | * @param {Point[]} pts 618 | * @returns {boolean} 619 | */ 620 | static polygonIsClockwise(pts) { 621 | return GeomUtil.polygonArea(pts) > 0; 622 | } 623 | 624 | /** 625 | * 626 | * @param {Point} p1 627 | * @param {Point} p2 628 | * @param {Point} p3 629 | */ 630 | static ccw(p1, p2, p3) { 631 | return (p3.y - p1.y) * (p2.x - p1.x) > (p2.y - p1.y) * (p3.x - p1.x); 632 | } 633 | 634 | /** 635 | * 636 | * @param {Segment} segA 637 | * @param {Segment} segB 638 | * @returns {boolean} 639 | */ 640 | static segmentsIntersect(segA, segB) { 641 | const fn = GeomUtil.ccw; 642 | return ( 643 | fn(segA.a, segB.a, segB.b) != fn(segA.b, segB.a, segB.b) && 644 | fn(segA.a, segA.b, segB.a) != fn(segA.a, segA.b, segB.b) 645 | ); 646 | } 647 | 648 | /** 649 | * 650 | * @param {Segment} segA 651 | * @param {Segment} segB 652 | * @returns {Point} 653 | */ 654 | static segmentSegmentIntersect(segA, segB, ignoreTouching = false) { 655 | const x1 = segA.a.x; 656 | const y1 = segA.a.y; 657 | const x2 = segA.b.x; 658 | const y2 = segA.b.y; 659 | const x3 = segB.a.x; 660 | const y3 = segB.a.y; 661 | const x4 = segB.b.x; 662 | const y4 = segB.b.y; 663 | 664 | const s1_x = x2 - x1; 665 | const s1_y = y2 - y1; 666 | const s2_x = x4 - x3; 667 | const s2_y = y4 - y3; 668 | 669 | const s = (-s1_y * (x1 - x3) + s1_x * (y1 - y3)) / (-s2_x * s1_y + s1_x * s2_y); 670 | const t = (s2_x * (y1 - y3) - s2_y * (x1 - x3)) / (-s2_x * s1_y + s1_x * s2_y); 671 | 672 | if (s >= 0 && s <= 1 && t >= 0 && t <= 1) { 673 | const atX = x1 + t * s1_x; 674 | const atY = y1 + t * s1_y; 675 | let intPt = { x: atX, y: atY }; 676 | if (ignoreTouching) { 677 | if (GeomUtil.pointsEqual(intPt, segB.a) || GeomUtil.pointsEqual(intPt, segB.b)) { 678 | return; 679 | } 680 | if (GeomUtil.pointsEqual(intPt, segA.a) || GeomUtil.pointsEqual(intPt, segA.b)) { 681 | return; 682 | } 683 | } 684 | return intPt; 685 | } 686 | 687 | return null; 688 | } 689 | 690 | /** 691 | * 692 | * @param {Segment} segA 693 | * @param {Segment[]} segs 694 | * @returns {Point[]} 695 | */ 696 | static segmentSegmentsIntersections(segA, segs, ignoreTouching = false) { 697 | let pts = []; 698 | segs.forEach((seg) => { 699 | if (seg == segA) { 700 | return; 701 | } 702 | let intPt = GeomUtil.segmentSegmentIntersect(segA, seg, ignoreTouching); 703 | if (intPt) { 704 | pts.push(intPt); 705 | } 706 | }); 707 | return pts; 708 | } 709 | 710 | /** 711 | * 712 | * @param {Point} ptA 713 | * @param {Point} ptB 714 | */ 715 | static dot(ptA, ptB) { 716 | return ptA.x * ptB.x + ptA.y * ptB.y; 717 | } 718 | 719 | /** 720 | * 721 | * @param {Point} ptA 722 | * @param {Point} ptB 723 | */ 724 | static cross(ptA, ptB) { 725 | return ptA.x * ptB.y - ptA.y * ptB.x; 726 | } 727 | 728 | /** 729 | * 730 | * @param {Point} pt 731 | * @param {Point} ptA 732 | * @param {Point} ptB 733 | */ 734 | static lineSide (pt, ptA, ptB) { 735 | return Math.round(((ptB.x - ptA.x) * (pt.y - ptA.y) - (ptB.y - ptA.y) * (pt.x - ptA.x)) * 100) / 100; 736 | } 737 | 738 | /** 739 | * 740 | * @param {Point} ptA 741 | * @param {Point} ptB 742 | */ 743 | static sub(ptA, ptB) { 744 | return new Point(ptA.x - ptB.x, ptA.y - ptB.y); 745 | } 746 | 747 | /** 748 | * 749 | * @param {Point} ptA 750 | * @param {Point} ptB 751 | */ 752 | static add(ptA, ptB) { 753 | return new Point(ptA.x + ptB.x, ptA.y + ptB.y); 754 | } 755 | 756 | /** 757 | * 758 | * @param {Point} pt 759 | * @param {Segment} seg 760 | * @returns {Point} 761 | */ 762 | static closestPtPointSegment(pt, seg) { 763 | var ab = GeomUtil.sub(seg.b, seg.a); 764 | var ca = GeomUtil.sub(pt, seg.a); 765 | var t = GeomUtil.dot(ca, ab); 766 | 767 | if (t < 0) { 768 | pt = seg.a; 769 | } else { 770 | var denom = GeomUtil.dot(ab, ab); 771 | if (t >= denom) { 772 | pt = seg.b; 773 | } else { 774 | t /= denom; 775 | // reuse ca 776 | ca.x = seg.a.x + t * ab.x; 777 | ca.y = seg.a.y + t * ab.y; 778 | pt = ca; 779 | } 780 | } 781 | 782 | return Point.clone(pt); 783 | } 784 | 785 | /** 786 | * 787 | * @param {Point} pt 788 | * @param {Segment} seg 789 | */ 790 | static distancePointSegment(pt, seg) { 791 | return GeomUtil.distanceBetween(pt, GeomUtil.closestPtPointSegment(pt, seg)); 792 | } 793 | 794 | /** 795 | * 796 | * @param {*} pt 797 | * @param {*} boundingBox 798 | * @returns {boolean} 799 | */ 800 | static pointWithinBoundingBox(pt, boundingBox) { 801 | return pt.x >= boundingBox.minX && pt.y >= boundingBox.minY && pt.x <= boundingBox.maxX && pt.y <= boundingBox.maxY; 802 | } 803 | 804 | /** 805 | * 806 | * @param {Point} pt 807 | * @param {Segment[]} polySegs 808 | * @returns {boolean} 809 | */ 810 | static pointWithinPolygon(pt, polySegs, ignoreTouching) { 811 | const b = GeomUtil.segmentsBoundingBox(polySegs); 812 | // early out 813 | if (!this.pointWithinBoundingBox(pt, b)) { 814 | return false; 815 | } 816 | 817 | let startPt = new Point(100000, 100000); 818 | let seg = new Segment(startPt, pt); 819 | 820 | let pts = GeomUtil.segmentSegmentsIntersections(seg, polySegs); 821 | 822 | if (!(pts.length % 2 == 0)) { 823 | if (ignoreTouching && GeomUtil.pointsEqual(pt, pts[0])) { 824 | return false; 825 | } 826 | } 827 | return !(pts.length % 2 == 0); 828 | } 829 | 830 | /** 831 | * 832 | * @param {Segment} seg 833 | * @param {Segment[]} polySegs 834 | * @returns {boolean} 835 | */ 836 | static segmentWithinPolygon(seg, polySegs) { 837 | let aTouching = this.pointWithinPolygon(seg.a, polySegs, false); 838 | let bTouching = this.pointWithinPolygon(seg.b, polySegs, false); 839 | let aWithin = this.pointWithinPolygon(seg.a, polySegs, true); 840 | let bWithin = this.pointWithinPolygon(seg.b, polySegs, true); 841 | return (aWithin && bWithin) || (aWithin && bTouching) || (bWithin && aTouching); 842 | } 843 | 844 | static sign(p1, p2, p3) { 845 | return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); 846 | } 847 | 848 | /** 849 | * 850 | * @param {Point} pt 851 | * @param {Point} v1 852 | * @param {Point} v2 853 | * @param {Point} v3 854 | * @returns {boolean} 855 | */ 856 | static pointWithinTriangle(pt, v1, v2, v3, ignoreTouching) { 857 | const d1 = GeomUtil.sign(pt, v1, v2); 858 | const d2 = GeomUtil.sign(pt, v2, v3); 859 | const d3 = GeomUtil.sign(pt, v3, v1); 860 | 861 | const has_neg = d1 < 0 || d2 < 0 || d3 < 0; 862 | const has_pos = d1 > 0 || d2 > 0 || d3 > 0; 863 | 864 | if (!(has_neg && has_pos) && ignoreTouching) { 865 | let seg = { a: v1, b: v2, tags: null }; 866 | if (GeomUtil.distancePointSegment(pt, seg) < 1) return false; 867 | seg.a = v2; 868 | seg.b = v3; 869 | if (GeomUtil.distancePointSegment(pt, seg) < 1) return false; 870 | seg.a = v3; 871 | seg.b = v1; 872 | if (GeomUtil.distancePointSegment(pt, seg) < 1) return false; 873 | } 874 | 875 | return !(has_neg && has_pos); 876 | } 877 | 878 | /** 879 | * 880 | * @param {Segment} seg 881 | * @param {Point} v1 882 | * @param {Point} v2 883 | * @param {Point} v3 884 | * @returns {boolean} 885 | */ 886 | static segmentWithinTriangle(seg, v1, v2, v3) { 887 | let aTouching = this.pointWithinTriangle(seg.a, v1, v2, v3, false); 888 | let bTouching = this.pointWithinTriangle(seg.b, v1, v2, v3, false); 889 | let aWithin = this.pointWithinTriangle(seg.a, v1, v2, v3, true); 890 | let bWithin = this.pointWithinTriangle(seg.b, v1, v2, v3, true); 891 | let pt = GeomUtil.averagePoints(seg.a, seg.b); 892 | return (aWithin && bWithin) || (aWithin && bTouching) || (bWithin && aTouching) || (aTouching && bTouching); 893 | } 894 | 895 | /** 896 | * 897 | * @param {Point[]} pts 898 | * @returns {Segment[]} 899 | */ 900 | static pointsToClosedPolySegments(...pts) { 901 | let out = []; 902 | for (let i = 0; i < pts.length; i++) { 903 | out.push(new Segment(pts[i], i < pts.length - 1 ? pts[i + 1] : pts[0])); 904 | } 905 | return out; 906 | } 907 | 908 | /** 909 | * 910 | * @param {Segment[]} polySegsA 911 | * @param {Segment[]} polySegsB 912 | * @returns {boolean} 913 | */ 914 | static polygonWithinPolygon(polySegsA, polySegsB) { 915 | const ab = GeomUtil.segmentsBoundingBox(polySegsA); 916 | const bb = GeomUtil.segmentsBoundingBox(polySegsB); 917 | 918 | // early out 919 | if (!GeomUtil.boundingBoxesIntersect(ab, bb)) { 920 | return false; 921 | } 922 | 923 | const startPt = new Point(bb.minX - 100, bb.minY - 100); 924 | 925 | for (let i = 0; i < polySegsA.length; i++) { 926 | let seg = polySegsA[i]; 927 | let pts = GeomUtil.segmentSegmentsIntersections(seg, polySegsB); 928 | 929 | if (pts.length % 2 == 0) { 930 | return false; 931 | } 932 | } 933 | 934 | return true; 935 | } 936 | 937 | /** 938 | * 939 | * @param {Point} ptA 940 | * @param {Point} ptB 941 | * @param {Point} ptC 942 | * @param {number} iterations 943 | */ 944 | static splinePoints(ptA, ptB, ptC, iterations = 0) { 945 | let divide = (pts) => { 946 | let out = [pts[0]]; 947 | for (let i = 0; i < pts.length - 1; i++) { 948 | let pt = new Point(0, 0); 949 | if (i + 1 < pts.length * 0.4) { 950 | pt.x = (pts[i].x * 40 + pts[i + 1].x * 60) * 0.01; 951 | pt.y = (pts[i].y * 40 + pts[i + 1].y * 60) * 0.01; 952 | } else if (i + 1 > pts.length * 0.6) { 953 | pt.x = (pts[i].x * 60 + pts[i + 1].x * 40) * 0.01; 954 | pt.y = (pts[i].y * 60 + pts[i + 1].y * 40) * 0.01; 955 | } else { 956 | pt.x = (pts[i].x + pts[i + 1].x) * 0.5; 957 | pt.y = (pts[i].y + pts[i + 1].y) * 0.5; 958 | } 959 | out.push(pt); 960 | } 961 | out.push(pts[pts.length - 1]); 962 | return out; 963 | }; 964 | 965 | let spts = [ptA, ptB, ptC]; 966 | 967 | for (let i = 0; i < iterations; i++) { 968 | spts = divide(spts); 969 | } 970 | 971 | return spts; 972 | } 973 | } -------------------------------------------------------------------------------- /src/geom/shapes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { Point, BoundingBox, Segment, SegmentCollection, GeomUtil } from "./geom.js"; 4 | 5 | export class Shape extends SegmentCollection { 6 | constructor() { 7 | super(); 8 | this.isOpen = false; 9 | this.isInverted = false; 10 | } 11 | open() { 12 | this.isOpen = true; 13 | return this; 14 | } 15 | invert() { 16 | this.isInverted = !this.isInverted; 17 | return this; 18 | } 19 | /** 20 | * 21 | * @param {boolean} local 22 | * @returns {Point[]}; 23 | */ 24 | toPoints(local = false) { 25 | throw "not implemented"; 26 | } 27 | 28 | toGeomPoints() { 29 | return [this.toPoints(false).map((pt) => [pt.x, pt.y])]; 30 | } 31 | /** 32 | * 33 | * @param {boolean} local 34 | * @returns {Segment[]}; 35 | */ 36 | toSegments(local = false) { 37 | let pts = this.toPoints(local); 38 | const segs = []; 39 | let start = 0; 40 | let len = pts.length; 41 | let isClockwise = GeomUtil.polygonIsClockwise(pts); 42 | let doReverse = isClockwise == !this.isInverted; 43 | if (doReverse) { 44 | pts = pts.reverse(); 45 | } 46 | if (this.isOpen) { 47 | if (doReverse) { 48 | start++; 49 | } else { 50 | len--; 51 | } 52 | } 53 | for (let i = start; i < len; i++) { 54 | let a = pts[i]; 55 | let b = pts[i < pts.length - 1 ? i + 1 : 0]; 56 | segs.push(new Segment(a, b)); 57 | } 58 | return segs; 59 | } 60 | } 61 | 62 | export class PolygonShape extends Shape { 63 | /** 64 | * 65 | * @param {Point[]} points 66 | * @param {number} [divisionDistance] distance between subdivisions, 0 for no subdivisions 67 | */ 68 | constructor(points, divisionDistance = 0) { 69 | super(); 70 | this.points = points; 71 | this.divisionDistance = divisionDistance; 72 | } 73 | /** 74 | * @param {boolean} local 75 | * @returns {Point[]} 76 | */ 77 | toPoints(local = false) { 78 | let pts = this.points ? this.points.concat() : []; 79 | if (!local) { 80 | this._makeAbsolute(pts); 81 | } 82 | if (this.divisionDistance == 0) { 83 | return pts; 84 | } else { 85 | let dpts = []; 86 | this.points.forEach((ptA, idx) => { 87 | let ptB = this.points[(idx + 1) % pts.length]; 88 | dpts = dpts.concat(GeomUtil.subdivideByDistance(ptA, ptB, this.divisionDistance)); 89 | }); 90 | return dpts; 91 | } 92 | } 93 | /** 94 | * @param {boolean} local 95 | * @returns {Segment[]} 96 | */ 97 | toSegments(local = false) { 98 | let pts = this.toPoints(local); 99 | let segs = []; 100 | for (let i = 0; i < pts.length; i++) { 101 | let a = pts[i]; 102 | let b = pts[i < pts.length - 1 ? i + 1 : 0]; 103 | segs.push(new Segment(a, b)); 104 | } 105 | return segs; 106 | } 107 | /** 108 | * 109 | * @param {number[][]} geomPts 110 | */ 111 | static fromGeomPoints(geomPts) { 112 | const pts = geomPts.map((gpt) => { 113 | return { x: gpt[0], y: gpt[1] }; 114 | }); 115 | return new PolygonShape(pts); 116 | } 117 | /** 118 | * 119 | * @param {{x:number, y:number}[]} pts 120 | */ 121 | static fromPoints(pts) { 122 | return new PolygonShape(pts); 123 | } 124 | } 125 | 126 | export class CompoundShape extends Shape { 127 | /** 128 | * 129 | * @param {Shape[]} [shapes] 130 | */ 131 | constructor(shapes = []) { 132 | super(); 133 | this.shapes = shapes; 134 | } 135 | open() { 136 | return this; 137 | } 138 | invert() { 139 | return this; 140 | } 141 | /** 142 | * @param {boolean} local 143 | * @returns {Point[]} 144 | */ 145 | toPoints(local = false) { 146 | const pts = this.shapes.map( 147 | shape => shape.toPoints() 148 | ).reduce( 149 | (acc, pts) => acc.concat(pts), 150 | [], 151 | ); 152 | if (!local) { 153 | this._makeAbsolute(pts); 154 | } 155 | return pts; 156 | } 157 | /** 158 | * @param {boolean} local 159 | * @returns {Segment[]} 160 | */ 161 | toSegments(local = false) { 162 | const segs = this.shapes.map( 163 | shape => shape.toSegments() 164 | ).reduce( 165 | (acc, segs) => acc.concat(segs), 166 | [], 167 | ); 168 | if (!local) { 169 | this._makeSegsAbsolute(segs); 170 | } 171 | return segs; 172 | } 173 | /** 174 | * @returns {BoundingBox} 175 | */ 176 | getBoundingBox() { 177 | const bb = new BoundingBox(1000000, 1000000, -1000000, -1000000); 178 | const pts = this.toPoints(); 179 | pts.forEach(pt => { 180 | bb.minX = Math.min(bb.minX, pt.x); 181 | bb.minY = Math.min(bb.minY, pt.y); 182 | bb.maxX = Math.max(bb.maxX, pt.x); 183 | bb.maxY = Math.max(bb.maxY, pt.y); 184 | }); 185 | return bb; 186 | } 187 | } -------------------------------------------------------------------------------- /src/hatcher.js: -------------------------------------------------------------------------------- 1 | 2 | import { GeomUtil, Point, Segment } from "./geom/geom.js"; 3 | import { Expander } from "./expander.js"; 4 | 5 | export class Hatcher { 6 | 7 | /** 8 | * 9 | * @param {Segment[]} borderSegments 10 | * @param {number} spacing 11 | * @param {number} rotation 12 | * @param {number} padding 13 | * @param {number} scale 14 | */ 15 | static flatHatchSegments(borderSegments, spacing, rotation = 0, padding = 2, scale = 1) { 16 | 17 | console.log("adding hatching", spacing, rotation) 18 | 19 | let segs = []; 20 | 21 | borderSegments.forEach(seg => { 22 | seg = Segment.clone(seg); 23 | seg.a.x *= 100000; 24 | seg.a.y *= 100000; 25 | seg.b.x *= 100000; 26 | seg.b.y *= 100000; 27 | segs.push(seg); 28 | }) 29 | 30 | // create hatch rect 31 | 32 | let bb = GeomUtil.segmentsBoundingBox(segs); 33 | 34 | let hatchSegs = []; 35 | 36 | let bbCen = new Point(bb.minX + (bb.maxX - bb.minX) * 0.5, bb.minY + (bb.maxY - bb.minY) * 0.5); 37 | 38 | let hr = Math.max(Math.abs(bb.maxX - bb.minX), Math.abs(bb.maxY - bb.minY)); 39 | let hreo = hr; 40 | for (let hx = bbCen.x - hr; hx < bbCen.x + hr; hx += (spacing * scale)) { 41 | hatchSegs.push(new Segment({ x: hx, y: bbCen.y - hreo }, { x: hx, y: bbCen.y + hreo })); 42 | hreo = 0 - hreo; // alternate hatch direction for less pen travel 43 | } 44 | 45 | // rotate hatching around centroid of polygon to hatch 46 | hatchSegs.forEach((seg) => { 47 | GeomUtil.subFromPoint(seg.a, bbCen); 48 | GeomUtil.subFromPoint(seg.b, bbCen); 49 | GeomUtil.rotatePointDeg(seg.a, rotation); 50 | GeomUtil.rotatePointDeg(seg.b, rotation); 51 | GeomUtil.addToPoint(seg.a, bbCen); 52 | GeomUtil.addToPoint(seg.b, bbCen); 53 | }); 54 | 55 | // create a contracted (offset) polygon to hatch if padding is added 56 | if (padding) { 57 | segs = Expander.expandSegs(0 - padding, segs, 100000); 58 | } 59 | 60 | let d = ""; 61 | 62 | let out = []; 63 | 64 | // raycast each hatch line through the polygon 65 | hatchSegs.forEach((seg) => { 66 | 67 | const hitPts = GeomUtil.segmentSegmentsIntersections(seg, segs); 68 | 69 | hitPts.sort((a, b) => { 70 | const distA = GeomUtil.distanceBetweenSquared(seg.a, a); 71 | const distB = GeomUtil.distanceBetweenSquared(seg.a, b); 72 | if (distA > distB) { 73 | return 1; 74 | } else if (distA < distB) { 75 | return -1; 76 | } 77 | return 0; 78 | }); 79 | 80 | // draw the hatching 81 | 82 | let penDown = true; 83 | 84 | for (let h = 1; h < hitPts.length; h++) { 85 | if (penDown) { 86 | let a = hitPts[h - 1]; 87 | let b = hitPts[h]; 88 | out.push(new Segment( 89 | new Point(a.x / 100000, a.y / 100000), 90 | new Point(b.x / 100000, b.y / 100000), 91 | )); 92 | } 93 | penDown = !penDown; 94 | } 95 | 96 | }); 97 | 98 | return out; 99 | 100 | } 101 | 102 | /** 103 | * 104 | * @param {Segment[]} borderSegments 105 | * @param {number} spacing 106 | * @param {number} rotation 107 | * @param {number} padding 108 | * @param {number} scale 109 | */ 110 | static addFlatHatching(borderSegments, spacing, rotation = 0, padding = 2, scale = 1) { 111 | 112 | console.log("adding hatching", spacing, rotation, scale) 113 | 114 | // create hatch rect 115 | 116 | let bb = GeomUtil.segmentsBoundingBox(borderSegments); 117 | 118 | let hatchSegs = []; 119 | 120 | let bbCen = new Point(bb.minX + (bb.maxX - bb.minX) * 0.5, bb.minY + (bb.maxY - bb.minY) * 0.5); 121 | 122 | let hr = Math.max(Math.abs(bb.maxX - bb.minX), Math.abs(bb.maxY - bb.minY)); 123 | let hreo = hr; 124 | for (let hx = bbCen.x - hr; hx < bbCen.x + hr; hx += (spacing * scale)) { 125 | hatchSegs.push(new Segment({ x: hx, y: bbCen.y - hreo }, { x: hx, y: bbCen.y + hreo })); 126 | hreo = 0 - hreo; // alternate hatch direction for less pen travel 127 | } 128 | 129 | // rotate hatching around centroid of polygon to hatch 130 | hatchSegs.forEach((seg) => { 131 | GeomUtil.subFromPoint(seg.a, bbCen); 132 | GeomUtil.subFromPoint(seg.b, bbCen); 133 | GeomUtil.rotatePointDeg(seg.a, rotation); 134 | GeomUtil.rotatePointDeg(seg.b, rotation); 135 | GeomUtil.addToPoint(seg.a, bbCen); 136 | GeomUtil.addToPoint(seg.b, bbCen); 137 | }); 138 | 139 | // create a contracted (offset) polygon to hatch if padding is added 140 | if (padding) { 141 | borderSegments = Expander.expandSegs(0 - padding, borderSegments, scale); 142 | } 143 | 144 | let d = ""; 145 | 146 | // raycast each hatch line through the polygon 147 | hatchSegs.forEach((seg) => { 148 | 149 | const hitPts = GeomUtil.segmentSegmentsIntersections(seg, borderSegments); 150 | 151 | hitPts.sort((a, b) => { 152 | const distA = GeomUtil.distanceBetweenSquared(seg.a, a); 153 | const distB = GeomUtil.distanceBetweenSquared(seg.a, b); 154 | if (distA > distB) { 155 | return 1; 156 | } else if (distA < distB) { 157 | return -1; 158 | } 159 | return 0; 160 | }); 161 | 162 | // draw the hatching 163 | 164 | let penDown = true; 165 | 166 | for (let h = 1; h < hitPts.length; h++) { 167 | if (penDown) { 168 | let a = hitPts[h - 1]; 169 | let b = hitPts[h]; 170 | d += ` M${Math.round(a.x / 1000) / 100},${Math.round(a.y / 1000) / 100}L${Math.round(b.x / 1000) / 100},${ 171 | Math.round(b.y / 1000) / 100 172 | }`; 173 | } 174 | penDown = !penDown; 175 | } 176 | 177 | }); 178 | 179 | let path = document.createElementNS("http://www.w3.org/2000/svg", "path"); 180 | path.setAttribute("d", d); 181 | path.setAttribute("stroke", "black"); 182 | path.setAttribute("stroke-width", "0.5"); 183 | return path; 184 | 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /src/optimize.js: -------------------------------------------------------------------------------- 1 | import { Segment, Segments, SegmentCollection, Point, GeomUtil } from "./geom/geom.js"; 2 | import { Analyzer } from "./analyzer.js"; 3 | 4 | export class Optimize { 5 | /** 6 | * 7 | * @param {SegmentCollection[]} segCols 8 | * @param {boolean} [noSplit] 9 | * @param {boolean} [trimSmall] 10 | * @param {number} [smallDist] 11 | * @param {boolean} [optimizePathOrder] 12 | * @param {boolean} [splitTeeIntersections] 13 | * @returns {Segments} 14 | */ 15 | static segmentCollections(segCols, noSplit = false, trimSmall = true, smallDist = 1, optimizePathOrder = false, splitTeeIntersections = false, splitCrossIntersections = false) { 16 | let allsegs = segCols.reduce((arr, sc) => arr.concat(sc.toSegments()), []); 17 | return Optimize.segments(allsegs, noSplit, trimSmall, smallDist, optimizePathOrder, splitTeeIntersections, splitCrossIntersections); 18 | } 19 | /** 20 | * 21 | * @param {SegmentCollection[]} segCols 22 | * @param {boolean} [splitTeeIntersections] 23 | * @returns {Segments} 24 | */ 25 | static segmentCollectionsPathOrder(segCols, splitTeeIntersections = false, splitCrossIntersections = false) { 26 | let allsegs = segCols.reduce((arr, sc) => arr.concat(sc.toSegments()), []); 27 | return new Segments(Analyzer.pathOrder(allsegs, splitTeeIntersections, splitCrossIntersections)); 28 | } 29 | /** 30 | * 31 | * @param {Segment[]} segs 32 | * @param {boolean} [noSplitColinear] 33 | * @param {boolean} [trimSmall] 34 | * @param {number} [smallDist] 35 | * @param {boolean} [optimizePathOrder] 36 | * @param {boolean} [splitTeeIntersections] 37 | * @returns {Segments} 38 | */ 39 | static segments(segs, noSplitColinear = false, trimSmall = true, smallDist= 1, optimizePathOrder = false, splitTeeIntersections = false, splitCrossIntersections = false) { 40 | 41 | const sb = segs; 42 | segs = []; 43 | 44 | while (sb.length) { 45 | let s = sb.shift(); 46 | let n = segs.length 47 | let found = false; 48 | while (n--) { 49 | const sn = segs[n]; 50 | if (Segment.isEqual(s, sn)) { 51 | found = true; 52 | break; 53 | } 54 | } 55 | if (!found) { 56 | segs.push(s); 57 | } 58 | } 59 | 60 | if (!noSplitColinear) { 61 | 62 | for (let n = 0; n < 3; n++) { 63 | let i = segs.length; 64 | let overlaps = 0; 65 | 66 | while (i--) { 67 | let segA = segs[i]; 68 | let aa, ab, ba, bb, heading; 69 | for (let j = i - 1; j >= 0; j--) { 70 | let segB = segs[j]; 71 | let same = false; 72 | let isRev = false; 73 | if (GeomUtil.sameAngle(segA, segB)) { 74 | same = true; 75 | aa = Point.clone(segA.a); 76 | ab = Point.clone(segA.b); 77 | ba = Point.clone(segB.a); 78 | bb = Point.clone(segB.b); 79 | } else if (GeomUtil.sameAngleRev(segA, segB)) { 80 | same = isRev = true; 81 | aa = Point.clone(segA.b); 82 | ab = Point.clone(segA.a); 83 | ba = Point.clone(segB.a); 84 | bb = Point.clone(segB.b); 85 | } 86 | if (same) { 87 | heading = GeomUtil.angleBetween(aa, ab); 88 | GeomUtil.rotatePoints(heading, aa, ab, ba, bb); 89 | if (Math.abs(aa.y - ba.y) < 0.1 && ab.x >= ba.x - 0.0001 && aa.x <= bb.x + 0.0001) { 90 | overlaps++; 91 | if (aa.x < ba.x) { 92 | if (!isRev) { 93 | segB.a = segA.a; 94 | } else { 95 | segB.a = segA.b; 96 | } 97 | } 98 | if (ab.x > bb.x) { 99 | if (!isRev) { 100 | segB.b = segA.b; 101 | } else { 102 | segB.b = segA.a; 103 | } 104 | } 105 | segs.splice(i, 1); 106 | break; 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | } 114 | 115 | let i = segs.length; 116 | while (i--) { 117 | let seg = segs[i]; 118 | if (!seg) { 119 | segs.splice(i, 1); 120 | continue; 121 | } 122 | if (trimSmall && GeomUtil.distanceBetween(seg.a, seg.b) < smallDist) { 123 | segs.splice(i, 1); 124 | continue; 125 | } 126 | } 127 | 128 | if (optimizePathOrder) { 129 | segs = Analyzer.pathOrder(segs, splitTeeIntersections, splitCrossIntersections); 130 | } 131 | 132 | return new Segments(segs); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/plotter-renderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * based on SVGRenderer @author mrdoob / http://mrdoob.com/ 3 | */ 4 | 5 | import { Box2, Camera, Color, Matrix3, Matrix4, Object3D, Vector3 } from "three"; 6 | import { Projector, RenderableFace} from "./projector.js"; 7 | import { GeomUtil, Point, Segment } from "./geom/geom.js"; 8 | import { Optimize } from "./optimize.js"; 9 | import { BooleanShape } from "./geom/booleanshape.js"; 10 | import { PolygonShape } from "./geom/shapes.js"; 11 | import { Expander } from "./expander.js"; 12 | import { Hatcher } from "./hatcher.js"; 13 | 14 | var lop = (n) => { 15 | return Math.round(n * 100) / 100; 16 | }; 17 | 18 | var defaultSpacings = [ 19 | 4, 6, 8, 10, 12, 14, 20 | 16, 18, 20, 22, 24, 26, 21 | 28, 30, 32, 34, 36, 40, 22 | ]; 23 | var defaultRotations = [ 24 | 45, 135, 0, 30, 60, 120, 25 | 45, 135, 0, 30, 60, 120, 26 | 45, 135, 0, 30, 60, 120 27 | ]; 28 | 29 | var SVGObject = function (node) { 30 | Object3D.call(this); 31 | 32 | this.node = node; 33 | }; 34 | 35 | SVGObject.prototype = Object.create(Object3D.prototype); 36 | SVGObject.prototype.constructor = SVGObject; 37 | 38 | var PlotterRenderer = function () { 39 | var _this = this, 40 | _renderData, 41 | _elements, 42 | _lights, 43 | _projector = new Projector(), 44 | _svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"), 45 | _defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"), 46 | _outline = document.createElementNS("http://www.w3.org/2000/svg", "g"), 47 | _edges = document.createElementNS("http://www.w3.org/2000/svg", "g"), 48 | _polygons = document.createElementNS("http://www.w3.org/2000/svg", "g"), 49 | _shading = document.createElementNS("http://www.w3.org/2000/svg", "g"), 50 | _svgWidth, 51 | _svgHeight, 52 | _svgWidthHalf, 53 | _svgHeightHalf, 54 | _v1, 55 | _v2, 56 | _v3, 57 | _clipBox = new Box2(), 58 | _elemBox = new Box2(), 59 | _color = new Color(), 60 | _ambientLight = new Color(), 61 | _directionalLights = new Color(), 62 | _pointLights = new Color(), 63 | _clearColor = new Color(), 64 | _vector3 = new Vector3(), // Needed for PointLight 65 | _centroid = new Vector3(), 66 | _normalViewMatrix = new Matrix3(), 67 | _viewMatrix = new Matrix4(), 68 | _viewProjectionMatrix = new Matrix4(), 69 | _svgPathPool = [], 70 | _svgNode, 71 | _pathCount = 0, 72 | _currentPath, 73 | _currentStyle, 74 | _quality = 1, 75 | _precision = null; 76 | 77 | _defs.innerHTML = ``; 78 | _svg.appendChild(_defs); 79 | 80 | _outline.setAttribute("inkscape:label", "Outline"); 81 | _outline.setAttribute("inkscape:groupmode", "layer"); 82 | _outline.id = "outline_layer"; 83 | _svg.appendChild(_outline); 84 | 85 | _edges.setAttribute("inkscape:label", "Edges"); 86 | _edges.setAttribute("inkscape:groupmode", "layer"); 87 | _edges.id = "edges_layer"; 88 | _svg.appendChild(_edges); 89 | 90 | _polygons.setAttribute("inkscape:label", "Polygons"); 91 | _polygons.setAttribute("inkscape:groupmode", "layer"); 92 | //_polygons.setAttribute("style", "display:none"); 93 | _polygons.id = "polygons_layer"; 94 | _svg.appendChild(_polygons); 95 | 96 | _shading.setAttribute("inkscape:label", "Shading"); 97 | _shading.setAttribute("inkscape:groupmode", "layer"); 98 | _shading.id = "shading_layer"; 99 | _svg.appendChild(_shading); 100 | 101 | this.domElement = _svg; 102 | this.doOptimize = false; 103 | this._cachekey = ""; 104 | var _cache = (this._cache = { 105 | segs: [], 106 | faces: [], 107 | optimized: false, 108 | shaded: false, 109 | shadeGrouped: false, 110 | shadeCombined: false, 111 | normGroups: {}, 112 | normGroupSegs: {}, 113 | outlineGroup: null, 114 | combinedShadeGroups: [], 115 | rotations: defaultRotations.concat(), 116 | spacings: defaultSpacings.concat(), 117 | hatchGroups: {}, 118 | hatchGroupLums: {}, 119 | hatchGroupFaceTotals: {}, 120 | currentHatchGroup: 0, 121 | }); 122 | 123 | this.autoClear = true; 124 | this.sortObjects = true; 125 | this.sortElements = true; 126 | 127 | this.info = { 128 | render: { 129 | vertices: 0, 130 | faces: 0, 131 | }, 132 | }; 133 | 134 | this.setQuality = function (quality) { 135 | switch (quality) { 136 | case "high": 137 | _quality = 1; 138 | break; 139 | case "low": 140 | _quality = 0; 141 | break; 142 | } 143 | }; 144 | 145 | this.setClearColor = function (color) { 146 | _clearColor.set(color); 147 | }; 148 | 149 | this.setPixelRatio = function () {}; 150 | 151 | this.setSize = function (width, height) { 152 | _svgWidth = width; 153 | _svgHeight = height; 154 | _svgWidthHalf = _svgWidth / 2; 155 | _svgHeightHalf = _svgHeight / 2; 156 | 157 | _svg.setAttribute("viewBox", -_svgWidthHalf + " " + -_svgHeightHalf + " " + _svgWidth + " " + _svgHeight); 158 | _svg.setAttribute("width", _svgWidth); 159 | _svg.setAttribute("height", _svgHeight); 160 | 161 | _clipBox.min.set(-_svgWidthHalf, -_svgHeightHalf); 162 | _clipBox.max.set(_svgWidthHalf, _svgHeightHalf); 163 | }; 164 | 165 | this.getSize = function () { 166 | return { 167 | width: _svgWidth, 168 | height: _svgHeight, 169 | }; 170 | }; 171 | 172 | this.setPrecision = function (precision) { 173 | _precision = precision; 174 | }; 175 | 176 | function removeChildNodes() { 177 | _pathCount = 0; 178 | 179 | while (_outline.childNodes.length > 0) { 180 | _outline.removeChild(_outline.childNodes[0]); 181 | } 182 | while (_edges.childNodes.length > 0) { 183 | _edges.removeChild(_edges.childNodes[0]); 184 | } 185 | while (_polygons.childNodes.length > 0) { 186 | _polygons.removeChild(_polygons.childNodes[0]); 187 | } 188 | _polygons.setAttribute("style", "display:block"); 189 | while (_shading.childNodes.length > 0) { 190 | _shading.removeChild(_shading.childNodes[0]); 191 | } 192 | } 193 | 194 | function convert(c) { 195 | return _precision !== null ? c.toFixed(_precision) : c; 196 | } 197 | 198 | this.clear = function () { 199 | removeChildNodes(); 200 | _svg.style.backgroundColor = _clearColor.getStyle(); 201 | }; 202 | 203 | this.render = function (scene, camera) { 204 | 205 | if (camera instanceof Camera === false) { 206 | console.error("THREE.SVGRenderer.render: camera is not an instance of Camera."); 207 | return; 208 | } 209 | 210 | //console.log("PROJECTION:", camera instanceof OrthographicCamera ? "ortho": "persp"); 211 | 212 | let segs = []; 213 | let faces = []; 214 | let useCache = false; 215 | 216 | const cp = camera.position; 217 | 218 | // cache based on camera position; 219 | const cacheKey = `${Math.round(cp.x)}|${Math.round(cp.y)}|${Math.round(cp.z)}`; 220 | 221 | // use cache if camera not moved 222 | if (this._cachekey == cacheKey && this.doOptimize) { 223 | 224 | segs = this._cache.segs; 225 | faces = this._cache.faces; 226 | useCache = true; 227 | 228 | // start caching 229 | } else if (this.doOptimize) { 230 | 231 | this._cachekey = cacheKey; 232 | this._cache.segs = segs; 233 | this._cache.faces = faces; 234 | this._cache.optimized = false; 235 | this._cache.shaded = false; 236 | this._cache.shadeGrouped = false; 237 | this._cache.shadeCombined = false; 238 | this._cache.normGroups = {}; 239 | this._cache.normGroupSegs = {}; 240 | this._cache.outlineGroup = null; 241 | this._cache.combinedShadeGroups = []; 242 | this._cache.hatchGroups = {}; 243 | this._cache.hatchGroupLums = {}; 244 | this._cache.hatchGroupFaceTotals = {}; 245 | this._cache.currentHatchGroup = 0; 246 | 247 | // not in optimize mode (user is moving camera) 248 | } else { 249 | this._cachekey = ""; 250 | } 251 | 252 | var background = scene.background; 253 | 254 | if (!useCache) { 255 | if (background && background.isColor) { 256 | removeChildNodes(); 257 | _svg.style.backgroundColor = background.getStyle(); 258 | } else if (this.autoClear === true) { 259 | this.clear(); 260 | } 261 | } 262 | 263 | // render in wireframe mode 264 | if (!useCache) { 265 | _this.info.render.vertices = 0; 266 | _this.info.render.faces = 0; 267 | 268 | _viewMatrix.copy(camera.matrixWorldInverse); 269 | _viewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, _viewMatrix); 270 | 271 | let r = _projector.projectScene(scene, camera, false, false); 272 | _renderData = _projector.projectScene(scene, camera, this.sortObjects, this.sortElements); 273 | 274 | _elements = _renderData.elements; 275 | _lights = _renderData.lights; 276 | 277 | _normalViewMatrix.getNormalMatrix(camera.matrixWorldInverse); 278 | 279 | // reset accumulated path 280 | 281 | _currentPath = ""; 282 | _currentStyle = ""; 283 | 284 | for (var e = 0, el = _elements.length; e < el; e++) { 285 | var element = _elements[e]; 286 | var material = element.material; 287 | 288 | if (material === undefined || material.opacity === 0) continue; 289 | 290 | _elemBox.makeEmpty(); 291 | 292 | if (element instanceof RenderableFace) { 293 | _v1 = element.v1; 294 | _v2 = element.v2; 295 | _v3 = element.v3; 296 | 297 | if (_v1.positionScreen.z < -1 || _v1.positionScreen.z > 1) continue; 298 | if (_v2.positionScreen.z < -1 || _v2.positionScreen.z > 1) continue; 299 | if (_v3.positionScreen.z < -1 || _v3.positionScreen.z > 1) continue; 300 | 301 | _v1.positionScreen.x *= _svgWidthHalf; 302 | _v1.positionScreen.y *= -_svgHeightHalf; 303 | _v2.positionScreen.x *= _svgWidthHalf; 304 | _v2.positionScreen.y *= -_svgHeightHalf; 305 | _v3.positionScreen.x *= _svgWidthHalf; 306 | _v3.positionScreen.y *= -_svgHeightHalf; 307 | 308 | // @ts-ignore 309 | _elemBox.setFromPoints([_v1.positionScreen, _v2.positionScreen, _v3.positionScreen]); 310 | 311 | if (_clipBox.intersectsBox(_elemBox) === true) { 312 | renderFace3( 313 | _v1, 314 | _v2, 315 | _v3, 316 | element, 317 | material, 318 | segs, 319 | faces, 320 | ); 321 | } 322 | } 323 | } 324 | } 325 | 326 | // start shading 327 | if (useCache && !this._cache.shaded) { 328 | 329 | console.log("shading.."); 330 | 331 | let outlineGroup = this._cache.outlineGroup || new BooleanShape([], 500); 332 | let normGroups = this._cache.normGroups || {}; 333 | let normGroupSegs = this._cache.normGroupSegs || {}; 334 | let hatchGroups = this._cache.hatchGroups || {}; 335 | let hatchGroupLums = this._cache.hatchGroupLums || {}; 336 | let hatchGroupFaceTotals = this._cache.hatchGroupFaceTotals || {}; 337 | this._cache.outlineGroup = outlineGroup; 338 | this._cache.normGroups = normGroups; 339 | this._cache.normGroups = normGroups; 340 | this._cache.normGroupSegs = normGroupSegs; 341 | this._cache.hatchGroups = hatchGroups; 342 | this._cache.hatchGroupLums = hatchGroupLums; 343 | this._cache.hatchGroupFaceTotals = hatchGroupFaceTotals; 344 | 345 | // first prepare face groups for edge rendering 346 | if (!this._cache.shadeGrouped) { 347 | 348 | calculateLights(_lights); 349 | 350 | // group faces by normal 351 | // create a booleanshape for each normal group 352 | faces.forEach((f) => { 353 | let n2 = f.v1.positionWorld.clone().add(f.v2.positionWorld).add(f.v3.positionWorld).divideScalar(3).multiply(f.n); 354 | let nPlaneDepth = Math.round((n2.x + n2.y + n2.z) / 10); 355 | let hatchGroupName = `${Math.round(f.n.x * 10)}|${Math.round(f.n.y * 10)}|${Math.round(f.n.z * 10)}`; 356 | let normGroupName = hatchGroupName + `||${nPlaneDepth}`; 357 | if (!normGroups[normGroupName]) { 358 | normGroups[normGroupName] = new BooleanShape([], 5); 359 | } 360 | if (!hatchGroups[hatchGroupName]) { 361 | hatchGroups[hatchGroupName] = true; 362 | hatchGroupLums[hatchGroupName] = 0; 363 | hatchGroupFaceTotals[hatchGroupName] = 0; 364 | } 365 | f.normGroup = normGroupName; 366 | f.hatchGroup = hatchGroupName; 367 | }); 368 | 369 | // 1. take each face in order from back to front 370 | // 2. ADD to own normal group 371 | // 3. SUBTRACT from other normal groups 372 | faces.forEach((f, idx) => { 373 | for (let normGroup in normGroups) { 374 | let pts = [Point.clone(f.p1), Point.clone(f.p2), Point.clone(f.p3)]; 375 | 376 | let area = Math.abs(GeomUtil.polygonArea(pts)); 377 | if (area === 0) { 378 | return; 379 | } 380 | 381 | pts[0].x = Math.round(pts[0].x * 10000000) / 100; 382 | pts[0].y = Math.round(pts[0].y * 10000000) / 100; 383 | pts[1].x = Math.round(pts[1].x * 10000000) / 100; 384 | pts[1].y = Math.round(pts[1].y * 10000000) / 100; 385 | pts[2].x = Math.round(pts[2].x * 10000000) / 100; 386 | pts[2].y = Math.round(pts[2].y * 10000000) / 100; 387 | 388 | const offsetPtSets = Expander.expandFace(pts[0], pts[1], pts[2], 20); 389 | const shape = new PolygonShape(offsetPtSets[0]); 390 | 391 | let boolGroup = normGroups[normGroup]; 392 | 393 | if (normGroup !== f.normGroup) { 394 | if (idx > 0) { 395 | if (boolGroup.shapes.length) { 396 | boolGroup.sub(shape); 397 | } 398 | } 399 | } else { 400 | boolGroup.add(shape); 401 | hatchGroupFaceTotals[f.hatchGroup]++; 402 | } 403 | } 404 | 405 | _color.copy(_ambientLight); 406 | _centroid.copy(f.v1.positionWorld).add(f.v2.positionWorld).add(f.v3.positionWorld).divideScalar(3); 407 | calculateLight(_lights, _centroid, f.n, _color); 408 | hatchGroupLums[f.hatchGroup] += _color.g; 409 | 410 | const pts = [Point.clone(f.p1), Point.clone(f.p2), Point.clone(f.p3)]; 411 | 412 | pts[0].x = Math.round(pts[0].x * 100000); 413 | pts[0].y = Math.round(pts[0].y * 100000); 414 | pts[1].x = Math.round(pts[1].x * 100000); 415 | pts[1].y = Math.round(pts[1].y * 100000); 416 | pts[2].x = Math.round(pts[2].x * 100000); 417 | pts[2].y = Math.round(pts[2].y * 100000); 418 | 419 | const offsetPtSets = Expander.expandFace(pts[0], pts[1], pts[2], 200); 420 | const shape = new PolygonShape(offsetPtSets[0]); 421 | outlineGroup.add(shape); // outline group is ALWAYS additive as we are merging all the faces together like a silhouette 422 | }); 423 | 424 | normGroups["outline"] = outlineGroup; 425 | 426 | for (let hatchGroup in hatchGroupLums) { 427 | hatchGroupLums[hatchGroup] /= hatchGroupFaceTotals[hatchGroup]; 428 | } 429 | 430 | this._cache.shadeGrouped = true; 431 | } 432 | 433 | // after normal grouping... 434 | // booleanshape->combine 1 normal group at a time 435 | 436 | let combinedShadeGroups = this._cache.combinedShadeGroups; 437 | let normGroup = ""; 438 | 439 | let j = 0; 440 | 441 | // find a group that hasn't been combined yet 442 | for (let g in normGroups) { 443 | if (combinedShadeGroups.indexOf(g) === -1) { 444 | normGroup = g; 445 | combinedShadeGroups.push(g); 446 | break; 447 | } 448 | j++; 449 | } 450 | 451 | // if a group hasn't been combined, then combined it 452 | if (normGroup) { 453 | 454 | // no need to combine if it's only subtractive 455 | if (normGroups[normGroup].additiveShapes > 1) { 456 | 457 | console.log("combining normGroup", normGroup, " ,shapes", normGroups[normGroup].additiveShapes, normGroups[normGroup].subtractiveShapes); 458 | 459 | let path = document.createElementNS("http://www.w3.org/2000/svg", "path"); 460 | let segs = normGroupSegs[normGroup]; 461 | if (!segs) { 462 | segs = normGroups[normGroup].toSegments(); // the boolean combine happens when we ask for the segments 463 | segs = Optimize.segments(segs)._segments; 464 | normGroupSegs[normGroup] = segs; 465 | } 466 | 467 | if (normGroup !== "outline") { 468 | addHatching(segs, normGroup, camera); 469 | } 470 | 471 | // draw the polygons (hidden) and the outline 472 | 473 | let pointsStr = ""; 474 | 475 | if (segs && segs.length) { 476 | for (let i = 0; i < segs.length; i++) { 477 | let command = "M"; 478 | if (i > 0 && GeomUtil.segmentsConnected(segs[i - 1], segs[i], 100000)) { 479 | command = "L"; 480 | } 481 | if (i > 0 && command == "M") { 482 | pointsStr += "z"; 483 | } 484 | pointsStr += `${command} ${lop(segs[i].b.x / 100000)} ${lop(segs[i].b.y / 100000)} `; 485 | } 486 | pointsStr += "z"; 487 | } 488 | 489 | path.setAttribute("d", pointsStr); 490 | 491 | path.setAttribute("fill", "none"); 492 | path.setAttribute("stroke", "black"); 493 | path.setAttribute("id", normGroup); 494 | 495 | if (normGroup === "outline") { 496 | path.setAttribute("stroke-width", "0.65mm"); 497 | _outline.appendChild(path); 498 | } else { 499 | path.setAttribute("stroke-width", "0.35mm"); 500 | _polygons.appendChild(path); 501 | } 502 | 503 | } 504 | 505 | } else { 506 | 507 | // shade grouping complete 508 | 509 | this._cache.shaded = true; 510 | let totalGroups = 0; 511 | 512 | for (let g in this._cache.hatchGroupLums) { 513 | totalGroups++; 514 | } 515 | 516 | if (totalGroups < this._cache.rotations.length) { 517 | this._cache.rotations = this._cache.rotations.slice(0, totalGroups); 518 | this._cache.spacings = this._cache.spacings.slice(0, totalGroups); 519 | } 520 | 521 | // add optimized edges 522 | 523 | console.log("optimizing edges to draw..."); 524 | 525 | let segs = []; 526 | 527 | // put all the edges from all groups together into one array 528 | for (let n in normGroupSegs) { 529 | let set = normGroupSegs[n]; 530 | if (Array.isArray(set) && set.length > 1) { 531 | set.forEach(seg => { 532 | segs.push(Segment.clone(seg)); 533 | }); 534 | } 535 | } 536 | 537 | // scale back down from clipping 538 | segs.forEach(seg => { 539 | seg.a = Point.clone(seg.a); 540 | seg.b = Point.clone(seg.b); 541 | seg.a.x /= 100000; 542 | seg.a.y /= 100000; 543 | seg.b.x /= 100000; 544 | seg.b.y /= 100000; 545 | }) 546 | 547 | // optimize! 548 | // this removes all overlapping edges and optimizes drawing order 549 | console.log("segments before optimization:", segs.length); 550 | segs = Optimize.segments(segs, false, true, 1, true)._segments; 551 | console.log("segments after optimization:", segs.length); 552 | 553 | // draw the optimized edges to the edges layer 554 | 555 | let pointsStr = ""; 556 | 557 | if (segs && segs.length) { 558 | for (let i = 0; i < segs.length; i++) { 559 | if (i === 0 || !GeomUtil.segmentsConnected(segs[i - 1], segs[i], 1)) { 560 | pointsStr += `M ${lop(segs[i].a.x)} ${lop(segs[i].a.y)} `; 561 | } 562 | pointsStr += `L ${lop(segs[i].b.x)} ${lop(segs[i].b.y)} `; 563 | } 564 | } 565 | 566 | let path = document.createElementNS("http://www.w3.org/2000/svg", "path"); 567 | 568 | path.setAttribute("d", pointsStr); 569 | 570 | path.setAttribute("fill", "none"); 571 | path.setAttribute("stroke", "black"); 572 | 573 | path.setAttribute("stroke-width", "0.35mm"); 574 | _edges.appendChild(path); 575 | _polygons.setAttribute("style", "display:none"); 576 | 577 | // DONE! 578 | console.log("DONE"); 579 | } 580 | } 581 | 582 | // if not drawing final render, render wireframe 583 | if (!this.doOptimize) { 584 | segs.forEach(seg => renderSegment(seg)); 585 | flushPath(); // just to flush last svg:path 586 | } 587 | 588 | }; 589 | 590 | function addHatching(segs, normGroup, camera, padding = 2) { 591 | 592 | const hatchGroup = normGroup.split("||")[0]; 593 | const hatchGroupLums = _cache.hatchGroupLums; 594 | const hatchGroupKey = hatchGroupLums[hatchGroup] + "-" + hatchGroup 595 | 596 | // order luminosity groups by luminosity 597 | let lums = []; 598 | for (let k in hatchGroupLums) { 599 | lums.push(hatchGroupLums[k] + "-" + k); 600 | } 601 | lums.sort(); 602 | 603 | // find the luminosity index of this group 604 | let hatchIndex = lums.indexOf(hatchGroupKey); 605 | 606 | if (hatchIndex == -1) { 607 | return; 608 | } 609 | 610 | hatchIndex %= _cache.spacings.length; 611 | 612 | // get the spacing that matches the luminosity (needs work) 613 | let spacing = _cache.spacings[hatchIndex]; 614 | let rotation = _cache.rotations[hatchIndex]; 615 | 616 | //let path = Hatcher.addFlatHatching(segs, spacing, rotation, padding, 100000); 617 | 618 | let normalParts = normGroup.split("||")[0].split("|"); 619 | let normal = new Vector3( 620 | parseFloat(normalParts[0]) / 10, 621 | parseFloat(normalParts[1]) / 10, 622 | parseFloat(normalParts[2]) / 10 623 | ); 624 | 625 | let path = Hatcher.addFlatHatching(segs, spacing, rotation, padding, 100000); 626 | _shading.appendChild(path); 627 | 628 | } 629 | 630 | function calculateLights(lights) { 631 | _ambientLight.setRGB(0, 0, 0); 632 | _directionalLights.setRGB(0, 0, 0); 633 | _pointLights.setRGB(0, 0, 0); 634 | 635 | for (var l = 0, ll = lights.length; l < ll; l++) { 636 | var light = lights[l]; 637 | var lightColor = light.color; 638 | 639 | if (light.isAmbientLight) { 640 | _ambientLight.r += lightColor.r; 641 | _ambientLight.g += lightColor.g; 642 | _ambientLight.b += lightColor.b; 643 | } else if (light.isDirectionalLight) { 644 | _directionalLights.r += lightColor.r; 645 | _directionalLights.g += lightColor.g; 646 | _directionalLights.b += lightColor.b; 647 | } else if (light.isPointLight) { 648 | _pointLights.r += lightColor.r; 649 | _pointLights.g += lightColor.g; 650 | _pointLights.b += lightColor.b; 651 | } 652 | } 653 | } 654 | 655 | function calculateLight(lights, position, normal, color) { 656 | for (var l = 0, ll = lights.length; l < ll; l++) { 657 | var light = lights[l]; 658 | var lightColor = light.color; 659 | 660 | if (light.isDirectionalLight) { 661 | var lightPosition = _vector3.setFromMatrixPosition(light.matrixWorld).normalize(); 662 | 663 | var amount = normal.dot(lightPosition); 664 | 665 | if (amount <= 0) continue; 666 | 667 | amount *= light.intensity; 668 | 669 | color.r += lightColor.r * amount; 670 | color.g += lightColor.g * amount; 671 | color.b += lightColor.b * amount; 672 | } else if (light.isPointLight) { 673 | var lightPosition = _vector3.setFromMatrixPosition(light.matrixWorld); 674 | 675 | var amount = normal.dot(_vector3.subVectors(lightPosition, position).normalize()); 676 | 677 | if (amount <= 0) continue; 678 | 679 | amount *= light.distance == 0 ? 1 : 1 - Math.min(position.distanceTo(lightPosition) / light.distance, 1); 680 | 681 | if (amount == 0) continue; 682 | 683 | amount *= light.intensity; 684 | 685 | color.r += lightColor.r * amount; 686 | color.g += lightColor.g * amount; 687 | color.b += lightColor.b * amount; 688 | } 689 | } 690 | } 691 | 692 | function renderFace3( 693 | v1, 694 | v2, 695 | v3, 696 | element, 697 | material, 698 | segs, 699 | faces 700 | ) { 701 | _this.info.render.vertices += 3; 702 | _this.info.render.faces++; 703 | 704 | let f1 = { 705 | x: v1.positionScreen.x, 706 | y: v1.positionScreen.y, 707 | }; 708 | let f2 = { 709 | x: v2.positionScreen.x, 710 | y: v2.positionScreen.y, 711 | }; 712 | let f3 = { 713 | x: v3.positionScreen.x, 714 | y: v3.positionScreen.y, 715 | }; 716 | 717 | let p1 = { 718 | x: v1.positionScreen.x, 719 | y: v1.positionScreen.y, 720 | }; 721 | let p2 = { 722 | x: v2.positionScreen.x, 723 | y: v2.positionScreen.y, 724 | }; 725 | let p3 = { 726 | x: v3.positionScreen.x, 727 | y: v3.positionScreen.y, 728 | }; 729 | 730 | faces.push({ 731 | face: GeomUtil.pointsToClosedPolySegments(f1, f2, f3), 732 | element, 733 | n: element.normalModel.clone(), 734 | v1, 735 | v2, 736 | v3, 737 | p1, 738 | p2, 739 | p3, 740 | }); 741 | 742 | segs.push({ 743 | a: { 744 | x: convert(v1.positionScreen.x), 745 | y: convert(v1.positionScreen.y), 746 | v: v1, 747 | }, 748 | b: { 749 | x: convert(v2.positionScreen.x), 750 | y: convert(v2.positionScreen.y), 751 | v: v2, 752 | }, 753 | element, 754 | material, 755 | }); 756 | 757 | segs.push({ 758 | a: { 759 | x: convert(v2.positionScreen.x), 760 | y: convert(v2.positionScreen.y), 761 | v: v2, 762 | }, 763 | b: { 764 | x: convert(v3.positionScreen.x), 765 | y: convert(v3.positionScreen.y), 766 | v: v3, 767 | }, 768 | element, 769 | material, 770 | }); 771 | 772 | segs.push({ 773 | a: { 774 | x: convert(v3.positionScreen.x), 775 | y: convert(v3.positionScreen.y), 776 | v: v3, 777 | }, 778 | b: { 779 | x: convert(v1.positionScreen.x), 780 | y: convert(v1.positionScreen.y), 781 | v: v1, 782 | }, 783 | element, 784 | material, 785 | }); 786 | 787 | } 788 | 789 | // wireframe path rendering 790 | 791 | function renderSegment(seg) { 792 | const path = "M" + seg.a.x + "," + seg.a.y + "L" + seg.b.x + "," + seg.b.y; 793 | const style = "fill:none; stroke:#06c; opacity: 0.5; stroke-width:1.5;"; 794 | addPath(style, path); 795 | } 796 | 797 | function addPath(style, path) { 798 | if (_currentStyle === style) { 799 | _currentPath += path; 800 | } else { 801 | flushPath(); 802 | _currentStyle = style; 803 | _currentPath = path; 804 | } 805 | } 806 | 807 | function flushPath() { 808 | if (_currentPath) { 809 | _svgNode = getPathNode(_pathCount++); 810 | _svgNode.setAttribute("d", _currentPath); 811 | _svgNode.setAttribute("style", _currentStyle); 812 | _polygons.appendChild(_svgNode); 813 | } 814 | 815 | _currentPath = ""; 816 | _currentStyle = ""; 817 | } 818 | 819 | function getPathNode(id) { 820 | if (_svgPathPool[id] == null) { 821 | _svgPathPool[id] = document.createElementNS("http://www.w3.org/2000/svg", "path"); 822 | 823 | if (_quality == 0) { 824 | _svgPathPool[id].setAttribute("shape-rendering", "crispEdges"); //optimizeSpeed 825 | } 826 | 827 | return _svgPathPool[id]; 828 | } 829 | 830 | return _svgPathPool[id]; 831 | } 832 | 833 | // interactive hatch editing 834 | 835 | this.nextHatchGroup = function () { 836 | _cache.currentHatchGroup++; 837 | _cache.currentHatchGroup %= _cache.rotations.length; 838 | console.log("editing hatch group", _cache.currentHatchGroup, "..."); 839 | }; 840 | 841 | this.previousHatchGroup = function () { 842 | _cache.currentHatchGroup++; 843 | _cache.currentHatchGroup %= _cache.rotations.length; 844 | console.log("editing hatch group", _cache.currentHatchGroup, "..."); 845 | }; 846 | 847 | this.increaseSpacing = function () { 848 | _cache.spacings[_cache.currentHatchGroup] += 2; 849 | console.log("hatch group", _cache.currentHatchGroup, "spacing is now", _cache.spacings[_cache.currentHatchGroup]); 850 | this.redrawHatching(); 851 | }; 852 | 853 | this.decreaseSpacing = function () { 854 | _cache.spacings[_cache.currentHatchGroup] -= 2; 855 | _cache.spacings[_cache.currentHatchGroup] = Math.max(2, _cache.spacings[_cache.currentHatchGroup]); 856 | console.log("hatch group", _cache.currentHatchGroup, "spacing is now", _cache.spacings[_cache.currentHatchGroup]); 857 | this.redrawHatching(); 858 | }; 859 | 860 | this.increaseRotation = function () { 861 | _cache.rotations[_cache.currentHatchGroup] += 5; 862 | console.log("hatch group", _cache.currentHatchGroup, "rotation is now", _cache.rotations[_cache.currentHatchGroup]); 863 | this.redrawHatching(); 864 | }; 865 | 866 | this.decreaseRotation = function () { 867 | _cache.rotations[_cache.currentHatchGroup] -= 5; 868 | console.log("hatch group", _cache.currentHatchGroup, "rotation is now", _cache.rotations[_cache.currentHatchGroup]); 869 | this.redrawHatching(); 870 | }; 871 | 872 | this.redrawHatching = function () { 873 | while (_shading.childNodes.length > 0) { 874 | _shading.removeChild(_shading.childNodes[0]); 875 | } 876 | for (let normGroup in _cache.normGroupSegs) { 877 | addHatching(_cache.normGroupSegs[normGroup], normGroup); 878 | } 879 | }; 880 | 881 | }; 882 | 883 | export { PlotterRenderer }; 884 | -------------------------------------------------------------------------------- /src/projector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | * @author supereggbert / http://www.paulbrunt.co.uk/ 4 | * @author julianwa / https://github.com/julianwa 5 | */ 6 | 7 | import { 8 | BackSide, 9 | Box3, 10 | BufferGeometry, 11 | Color, 12 | DoubleSide, 13 | FrontSide, 14 | Frustum, 15 | Geometry, 16 | Light, 17 | Line, 18 | LineSegments, 19 | Matrix3, 20 | Matrix4, 21 | Mesh, 22 | Points, 23 | Sprite, 24 | Vector2, 25 | Vector3, 26 | Vector4 27 | } from "three"; 28 | 29 | var RenderableObject = function () { 30 | 31 | this.id = 0; 32 | 33 | this.object = null; 34 | this.z = 0; 35 | this.renderOrder = 0; 36 | 37 | }; 38 | 39 | // 40 | 41 | var RenderableFace = function () { 42 | 43 | this.id = 0; 44 | 45 | this.v1 = new RenderableVertex(); 46 | this.v2 = new RenderableVertex(); 47 | this.v3 = new RenderableVertex(); 48 | 49 | this.normalModel = new Vector3(); 50 | 51 | this.vertexNormalsModel = [ new Vector3(), new Vector3(), new Vector3() ]; 52 | this.vertexNormalsLength = 0; 53 | 54 | this.color = new Color(); 55 | this.material = null; 56 | this.uvs = [ new Vector2(), new Vector2(), new Vector2() ]; 57 | 58 | this.z = 0; 59 | this.renderOrder = 0; 60 | 61 | }; 62 | 63 | // 64 | 65 | var RenderableVertex = function () { 66 | 67 | this.position = new Vector3(); 68 | this.positionWorld = new Vector3(); 69 | this.positionScreen = new Vector4(); 70 | 71 | this.visible = true; 72 | 73 | }; 74 | 75 | RenderableVertex.prototype.copy = function ( vertex ) { 76 | 77 | this.positionWorld.copy( vertex.positionWorld ); 78 | this.positionScreen.copy( vertex.positionScreen ); 79 | 80 | }; 81 | 82 | // 83 | 84 | var RenderableLine = function () { 85 | 86 | this.id = 0; 87 | 88 | this.v1 = new RenderableVertex(); 89 | this.v2 = new RenderableVertex(); 90 | 91 | this.vertexColors = [ new Color(), new Color() ]; 92 | this.material = null; 93 | 94 | this.z = 0; 95 | this.renderOrder = 0; 96 | 97 | }; 98 | 99 | // 100 | 101 | var RenderableSprite = function () { 102 | 103 | this.id = 0; 104 | 105 | this.object = null; 106 | 107 | this.x = 0; 108 | this.y = 0; 109 | this.z = 0; 110 | 111 | this.rotation = 0; 112 | this.scale = new Vector2(); 113 | 114 | this.material = null; 115 | this.renderOrder = 0; 116 | 117 | }; 118 | 119 | // 120 | 121 | var Projector = function () { 122 | 123 | var _object, _objectCount, _objectPool = [], _objectPoolLength = 0, 124 | _vertex, _vertexCount, _vertexPool = [], _vertexPoolLength = 0, 125 | _face, _faceCount, _facePool = [], _facePoolLength = 0, 126 | _line, _lineCount, _linePool = [], _linePoolLength = 0, 127 | _sprite, _spriteCount, _spritePool = [], _spritePoolLength = 0, 128 | 129 | _renderData = { objects: [], lights: [], elements: [] }, 130 | 131 | _vector3 = new Vector3(), 132 | _vector4 = new Vector4(), 133 | 134 | _clipBox = new Box3( new Vector3( - 1, - 1, - 1 ), new Vector3( 1, 1, 1 ) ), 135 | _boundingBox = new Box3(), 136 | _points3 = new Array( 3 ), 137 | 138 | _viewMatrix = new Matrix4(), 139 | _viewProjectionMatrix = new Matrix4(), 140 | 141 | _modelMatrix, 142 | _modelViewProjectionMatrix = new Matrix4(), 143 | 144 | _normalMatrix = new Matrix3(), 145 | 146 | _frustum = new Frustum(), 147 | 148 | _clippedVertex1PositionScreen = new Vector4(), 149 | _clippedVertex2PositionScreen = new Vector4(); 150 | 151 | // 152 | 153 | this.projectVector = function ( vector, camera ) { 154 | 155 | console.warn( 'THREE.Projector: .projectVector() is now vector.project().' ); 156 | vector.project( camera ); 157 | 158 | }; 159 | 160 | this.unprojectVector = function ( vector, camera ) { 161 | 162 | console.warn( 'THREE.Projector: .unprojectVector() is now vector.unproject().' ); 163 | vector.unproject( camera ); 164 | 165 | }; 166 | 167 | this.pickingRay = function () { 168 | 169 | console.error( 'THREE.Projector: .pickingRay() is now raycaster.setFromCamera().' ); 170 | 171 | }; 172 | 173 | // 174 | 175 | var RenderList = function () { 176 | 177 | var normals = []; 178 | var colors = []; 179 | var uvs = []; 180 | 181 | var object = null; 182 | 183 | var normalMatrix = new Matrix3(); 184 | 185 | function setObject( value ) { 186 | 187 | object = value; 188 | 189 | normalMatrix.getNormalMatrix( object.matrixWorld ); 190 | 191 | normals.length = 0; 192 | colors.length = 0; 193 | uvs.length = 0; 194 | 195 | } 196 | 197 | function projectVertex( vertex ) { 198 | 199 | var position = vertex.position; 200 | var positionWorld = vertex.positionWorld; 201 | var positionScreen = vertex.positionScreen; 202 | 203 | positionWorld.copy( position ).applyMatrix4( _modelMatrix ); 204 | positionScreen.copy( positionWorld ).applyMatrix4( _viewProjectionMatrix ); 205 | 206 | var invW = 1 / positionScreen.w; 207 | 208 | positionScreen.x *= invW; 209 | positionScreen.y *= invW; 210 | positionScreen.z *= invW; 211 | 212 | vertex.visible = positionScreen.x >= - 1 && positionScreen.x <= 1 && 213 | positionScreen.y >= - 1 && positionScreen.y <= 1 && 214 | positionScreen.z >= - 1 && positionScreen.z <= 1; 215 | 216 | } 217 | 218 | function pushVertex( x, y, z ) { 219 | 220 | _vertex = getNextVertexInPool(); 221 | _vertex.position.set( x, y, z ); 222 | 223 | projectVertex( _vertex ); 224 | 225 | } 226 | 227 | function pushNormal( x, y, z ) { 228 | 229 | normals.push( x, y, z ); 230 | 231 | } 232 | 233 | function pushColor( r, g, b ) { 234 | 235 | colors.push( r, g, b ); 236 | 237 | } 238 | 239 | function pushUv( x, y ) { 240 | 241 | uvs.push( x, y ); 242 | 243 | } 244 | 245 | function checkTriangleVisibility( v1, v2, v3 ) { 246 | 247 | if ( v1.visible === true || v2.visible === true || v3.visible === true ) return true; 248 | 249 | _points3[ 0 ] = v1.positionScreen; 250 | _points3[ 1 ] = v2.positionScreen; 251 | _points3[ 2 ] = v3.positionScreen; 252 | 253 | return _clipBox.intersectsBox( _boundingBox.setFromPoints( _points3 ) ); 254 | 255 | } 256 | 257 | function checkBackfaceCulling( v1, v2, v3 ) { 258 | 259 | return ( ( v3.positionScreen.x - v1.positionScreen.x ) * 260 | ( v2.positionScreen.y - v1.positionScreen.y ) - 261 | ( v3.positionScreen.y - v1.positionScreen.y ) * 262 | ( v2.positionScreen.x - v1.positionScreen.x ) ) < 0; 263 | 264 | } 265 | 266 | function pushLine( a, b ) { 267 | 268 | var v1 = _vertexPool[ a ]; 269 | var v2 = _vertexPool[ b ]; 270 | 271 | // Clip 272 | 273 | v1.positionScreen.copy( v1.position ).applyMatrix4( _modelViewProjectionMatrix ); 274 | v2.positionScreen.copy( v2.position ).applyMatrix4( _modelViewProjectionMatrix ); 275 | 276 | if ( clipLine( v1.positionScreen, v2.positionScreen ) === true ) { 277 | 278 | // Perform the perspective divide 279 | v1.positionScreen.multiplyScalar( 1 / v1.positionScreen.w ); 280 | v2.positionScreen.multiplyScalar( 1 / v2.positionScreen.w ); 281 | 282 | _line = getNextLineInPool(); 283 | _line.id = object.id; 284 | _line.v1.copy( v1 ); 285 | _line.v2.copy( v2 ); 286 | _line.z = Math.max( v1.positionScreen.z, v2.positionScreen.z ); 287 | _line.renderOrder = object.renderOrder; 288 | 289 | _line.material = object.material; 290 | 291 | if ( object.material.vertexColors ) { 292 | 293 | _line.vertexColors[ 0 ].fromArray( colors, a * 3 ); 294 | _line.vertexColors[ 1 ].fromArray( colors, b * 3 ); 295 | 296 | } 297 | 298 | _renderData.elements.push( _line ); 299 | 300 | } 301 | 302 | } 303 | 304 | function pushTriangle( a, b, c, material ) { 305 | 306 | var v1 = _vertexPool[ a ]; 307 | var v2 = _vertexPool[ b ]; 308 | var v3 = _vertexPool[ c ]; 309 | 310 | if ( checkTriangleVisibility( v1, v2, v3 ) === false ) return; 311 | 312 | if ( material.side === DoubleSide || checkBackfaceCulling( v1, v2, v3 ) === true ) { 313 | 314 | _face = getNextFaceInPool(); 315 | 316 | _face.id = object.id; 317 | _face.v1.copy( v1 ); 318 | _face.v2.copy( v2 ); 319 | _face.v3.copy( v3 ); 320 | _face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3; 321 | _face.renderOrder = object.renderOrder; 322 | 323 | // face normal 324 | _vector3.subVectors( v3.position, v2.position ); 325 | _vector4.subVectors( v1.position, v2.position ); 326 | _vector3.cross( _vector4 ); 327 | _face.normalModel.copy( _vector3 ); 328 | _face.normalModel.applyMatrix3( normalMatrix ).normalize(); 329 | 330 | for ( var i = 0; i < 3; i ++ ) { 331 | 332 | var normal = _face.vertexNormalsModel[ i ]; 333 | normal.fromArray( normals, arguments[ i ] * 3 ); 334 | normal.applyMatrix3( normalMatrix ).normalize(); 335 | 336 | var uv = _face.uvs[ i ]; 337 | uv.fromArray( uvs, arguments[ i ] * 2 ); 338 | 339 | } 340 | 341 | _face.vertexNormalsLength = 3; 342 | 343 | _face.material = material; 344 | 345 | if ( material.vertexColors ) { 346 | 347 | _face.color.fromArray( colors, a * 3 ); 348 | 349 | } 350 | 351 | _renderData.elements.push( _face ); 352 | 353 | } 354 | 355 | } 356 | 357 | return { 358 | setObject: setObject, 359 | projectVertex: projectVertex, 360 | checkTriangleVisibility: checkTriangleVisibility, 361 | checkBackfaceCulling: checkBackfaceCulling, 362 | pushVertex: pushVertex, 363 | pushNormal: pushNormal, 364 | pushColor: pushColor, 365 | pushUv: pushUv, 366 | pushLine: pushLine, 367 | pushTriangle: pushTriangle 368 | }; 369 | 370 | }; 371 | 372 | var renderList = new RenderList(); 373 | 374 | function projectObject( object ) { 375 | 376 | if ( object.visible === false ) return; 377 | 378 | if ( object instanceof Light ) { 379 | 380 | _renderData.lights.push( object ); 381 | 382 | } else if ( object instanceof Mesh || object instanceof Line || object instanceof Points ) { 383 | 384 | if ( object.material.visible === false ) return; 385 | if ( object.frustumCulled === true && _frustum.intersectsObject( object ) === false ) return; 386 | 387 | addObject( object ); 388 | 389 | } else if ( object instanceof Sprite ) { 390 | 391 | if ( object.material.visible === false ) return; 392 | if ( object.frustumCulled === true && _frustum.intersectsSprite( object ) === false ) return; 393 | 394 | addObject( object ); 395 | 396 | } 397 | 398 | var children = object.children; 399 | 400 | for ( var i = 0, l = children.length; i < l; i ++ ) { 401 | 402 | projectObject( children[ i ] ); 403 | 404 | } 405 | 406 | } 407 | 408 | function addObject( object ) { 409 | 410 | _object = getNextObjectInPool(); 411 | _object.id = object.id; 412 | _object.object = object; 413 | 414 | _vector3.setFromMatrixPosition( object.matrixWorld ); 415 | _vector3.applyMatrix4( _viewProjectionMatrix ); 416 | _object.z = _vector3.z; 417 | _object.renderOrder = object.renderOrder; 418 | 419 | _renderData.objects.push( _object ); 420 | 421 | } 422 | 423 | this.projectScene = function ( scene, camera, sortObjects, sortElements ) { 424 | 425 | _faceCount = 0; 426 | _lineCount = 0; 427 | _spriteCount = 0; 428 | 429 | _renderData.elements.length = 0; 430 | 431 | if ( scene.autoUpdate === true ) scene.updateMatrixWorld(); 432 | if ( camera.parent === null ) camera.updateMatrixWorld(); 433 | 434 | _viewMatrix.copy( camera.matrixWorldInverse ); 435 | _viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, _viewMatrix ); 436 | 437 | _frustum.setFromProjectionMatrix( _viewProjectionMatrix ); 438 | 439 | // 440 | 441 | _objectCount = 0; 442 | 443 | _renderData.objects.length = 0; 444 | _renderData.lights.length = 0; 445 | 446 | projectObject( scene ); 447 | 448 | if ( sortObjects === true ) { 449 | 450 | _renderData.objects.sort( painterSort ); 451 | 452 | } 453 | 454 | // 455 | 456 | var objects = _renderData.objects; 457 | 458 | for ( var o = 0, ol = objects.length; o < ol; o ++ ) { 459 | 460 | var object = objects[ o ].object; 461 | var geometry = object.geometry; 462 | 463 | renderList.setObject( object ); 464 | 465 | _modelMatrix = object.matrixWorld; 466 | 467 | _vertexCount = 0; 468 | 469 | if ( object instanceof Mesh ) { 470 | 471 | if ( geometry instanceof BufferGeometry ) { 472 | 473 | var material = object.material; 474 | 475 | var isMultiMaterial = Array.isArray( material ); 476 | 477 | var attributes = geometry.attributes; 478 | var groups = geometry.groups; 479 | 480 | if ( attributes.position === undefined ) continue; 481 | 482 | var positions = attributes.position.array; 483 | 484 | for ( var i = 0, l = positions.length; i < l; i += 3 ) { 485 | 486 | var x = positions[ i ]; 487 | var y = positions[ i + 1 ]; 488 | var z = positions[ i + 2 ]; 489 | 490 | if ( material.morphTargets === true ) { 491 | 492 | var morphTargets = geometry.morphAttributes.position; 493 | var morphTargetsRelative = geometry.morphTargetsRelative; 494 | var morphInfluences = object.morphTargetInfluences; 495 | 496 | for ( var t = 0, tl = morphTargets.length; t < tl; t ++ ) { 497 | 498 | var influence = morphInfluences[ t ]; 499 | 500 | if ( influence === 0 ) continue; 501 | 502 | var target = morphTargets[ t ]; 503 | 504 | if ( morphTargetsRelative ) { 505 | 506 | x += target.getX( i / 3 ) * influence; 507 | y += target.getY( i / 3 ) * influence; 508 | z += target.getZ( i / 3 ) * influence; 509 | 510 | } else { 511 | 512 | x += ( target.getX( i / 3 ) - positions[ i ] ) * influence; 513 | y += ( target.getY( i / 3 ) - positions[ i + 1 ] ) * influence; 514 | z += ( target.getZ( i / 3 ) - positions[ i + 2 ] ) * influence; 515 | 516 | } 517 | 518 | } 519 | 520 | } 521 | 522 | renderList.pushVertex( x, y, z ); 523 | 524 | } 525 | 526 | if ( attributes.normal !== undefined ) { 527 | 528 | var normals = attributes.normal.array; 529 | 530 | for ( var i = 0, l = normals.length; i < l; i += 3 ) { 531 | 532 | renderList.pushNormal( normals[ i ], normals[ i + 1 ], normals[ i + 2 ] ); 533 | 534 | } 535 | 536 | } 537 | 538 | if ( attributes.color !== undefined ) { 539 | 540 | var colors = attributes.color.array; 541 | 542 | for ( var i = 0, l = colors.length; i < l; i += 3 ) { 543 | 544 | renderList.pushColor( colors[ i ], colors[ i + 1 ], colors[ i + 2 ] ); 545 | 546 | } 547 | 548 | } 549 | 550 | if ( attributes.uv !== undefined ) { 551 | 552 | var uvs = attributes.uv.array; 553 | 554 | for ( var i = 0, l = uvs.length; i < l; i += 2 ) { 555 | 556 | renderList.pushUv( uvs[ i ], uvs[ i + 1 ] ); 557 | 558 | } 559 | 560 | } 561 | 562 | if ( geometry.index !== null ) { 563 | 564 | var indices = geometry.index.array; 565 | 566 | if ( groups.length > 0 ) { 567 | 568 | for ( var g = 0; g < groups.length; g ++ ) { 569 | 570 | var group = groups[ g ]; 571 | 572 | material = isMultiMaterial === true 573 | ? object.material[ group.materialIndex ] 574 | : object.material; 575 | 576 | if ( material === undefined ) continue; 577 | 578 | for ( var i = group.start, l = group.start + group.count; i < l; i += 3 ) { 579 | 580 | renderList.pushTriangle( indices[ i ], indices[ i + 1 ], indices[ i + 2 ], material ); 581 | 582 | } 583 | 584 | } 585 | 586 | } else { 587 | 588 | for ( var i = 0, l = indices.length; i < l; i += 3 ) { 589 | 590 | renderList.pushTriangle( indices[ i ], indices[ i + 1 ], indices[ i + 2 ], material ); 591 | 592 | } 593 | 594 | } 595 | 596 | } else { 597 | 598 | if ( groups.length > 0 ) { 599 | 600 | for ( var g = 0; g < groups.length; g ++ ) { 601 | 602 | var group = groups[ g ]; 603 | 604 | material = isMultiMaterial === true 605 | ? object.material[ group.materialIndex ] 606 | : object.material; 607 | 608 | if ( material === undefined ) continue; 609 | 610 | for ( var i = group.start, l = group.start + group.count; i < l; i += 3 ) { 611 | 612 | renderList.pushTriangle( i, i + 1, i + 2, material ); 613 | 614 | } 615 | 616 | } 617 | 618 | } else { 619 | 620 | for ( var i = 0, l = positions.length / 3; i < l; i += 3 ) { 621 | 622 | renderList.pushTriangle( i, i + 1, i + 2, material ); 623 | 624 | } 625 | 626 | } 627 | 628 | } 629 | 630 | } else if ( geometry instanceof Geometry ) { 631 | 632 | var vertices = geometry.vertices; 633 | var faces = geometry.faces; 634 | var faceVertexUvs = geometry.faceVertexUvs[ 0 ]; 635 | 636 | _normalMatrix.getNormalMatrix( _modelMatrix ); 637 | 638 | var material = object.material; 639 | 640 | var isMultiMaterial = Array.isArray( material ); 641 | 642 | for ( var v = 0, vl = vertices.length; v < vl; v ++ ) { 643 | 644 | var vertex = vertices[ v ]; 645 | 646 | _vector3.copy( vertex ); 647 | 648 | if ( material.morphTargets === true ) { 649 | 650 | var morphTargets = geometry.morphTargets; 651 | var morphInfluences = object.morphTargetInfluences; 652 | 653 | for ( var t = 0, tl = morphTargets.length; t < tl; t ++ ) { 654 | 655 | var influence = morphInfluences[ t ]; 656 | 657 | if ( influence === 0 ) continue; 658 | 659 | var target = morphTargets[ t ]; 660 | var targetVertex = target.vertices[ v ]; 661 | 662 | _vector3.x += ( targetVertex.x - vertex.x ) * influence; 663 | _vector3.y += ( targetVertex.y - vertex.y ) * influence; 664 | _vector3.z += ( targetVertex.z - vertex.z ) * influence; 665 | 666 | } 667 | 668 | } 669 | 670 | renderList.pushVertex( _vector3.x, _vector3.y, _vector3.z ); 671 | 672 | } 673 | 674 | for ( var f = 0, fl = faces.length; f < fl; f ++ ) { 675 | 676 | var face = faces[ f ]; 677 | 678 | material = isMultiMaterial === true 679 | ? object.material[ face.materialIndex ] 680 | : object.material; 681 | 682 | if ( material === undefined ) continue; 683 | 684 | var side = material.side; 685 | 686 | var v1 = _vertexPool[ face.a ]; 687 | var v2 = _vertexPool[ face.b ]; 688 | var v3 = _vertexPool[ face.c ]; 689 | 690 | if ( renderList.checkTriangleVisibility( v1, v2, v3 ) === false ) continue; 691 | 692 | var visible = renderList.checkBackfaceCulling( v1, v2, v3 ); 693 | 694 | if ( side !== DoubleSide ) { 695 | 696 | if ( side === FrontSide && visible === false ) continue; 697 | if ( side === BackSide && visible === true ) continue; 698 | 699 | } 700 | 701 | _face = getNextFaceInPool(); 702 | 703 | _face.id = object.id; 704 | _face.v1.copy( v1 ); 705 | _face.v2.copy( v2 ); 706 | _face.v3.copy( v3 ); 707 | 708 | _face.normalModel.copy( face.normal ); 709 | 710 | if ( visible === false && ( side === BackSide || side === DoubleSide ) ) { 711 | 712 | _face.normalModel.negate(); 713 | 714 | } 715 | 716 | _face.normalModel.applyMatrix3( _normalMatrix ).normalize(); 717 | 718 | var faceVertexNormals = face.vertexNormals; 719 | 720 | for ( var n = 0, nl = Math.min( faceVertexNormals.length, 3 ); n < nl; n ++ ) { 721 | 722 | var normalModel = _face.vertexNormalsModel[ n ]; 723 | normalModel.copy( faceVertexNormals[ n ] ); 724 | 725 | if ( visible === false && ( side === BackSide || side === DoubleSide ) ) { 726 | 727 | normalModel.negate(); 728 | 729 | } 730 | 731 | normalModel.applyMatrix3( _normalMatrix ).normalize(); 732 | 733 | } 734 | 735 | _face.vertexNormalsLength = faceVertexNormals.length; 736 | 737 | var vertexUvs = faceVertexUvs[ f ]; 738 | 739 | if ( vertexUvs !== undefined ) { 740 | 741 | for ( var u = 0; u < 3; u ++ ) { 742 | 743 | _face.uvs[ u ].copy( vertexUvs[ u ] ); 744 | 745 | } 746 | 747 | } 748 | 749 | _face.color = face.color; 750 | _face.material = material; 751 | 752 | _face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3; 753 | _face.renderOrder = object.renderOrder; 754 | 755 | _renderData.elements.push( _face ); 756 | 757 | } 758 | 759 | } 760 | 761 | } else if ( object instanceof Line ) { 762 | 763 | _modelViewProjectionMatrix.multiplyMatrices( _viewProjectionMatrix, _modelMatrix ); 764 | 765 | if ( geometry instanceof BufferGeometry ) { 766 | 767 | var attributes = geometry.attributes; 768 | 769 | if ( attributes.position !== undefined ) { 770 | 771 | var positions = attributes.position.array; 772 | 773 | for ( var i = 0, l = positions.length; i < l; i += 3 ) { 774 | 775 | renderList.pushVertex( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] ); 776 | 777 | } 778 | 779 | if ( attributes.color !== undefined ) { 780 | 781 | var colors = attributes.color.array; 782 | 783 | for ( var i = 0, l = colors.length; i < l; i += 3 ) { 784 | 785 | renderList.pushColor( colors[ i ], colors[ i + 1 ], colors[ i + 2 ] ); 786 | 787 | } 788 | 789 | } 790 | 791 | if ( geometry.index !== null ) { 792 | 793 | var indices = geometry.index.array; 794 | 795 | for ( var i = 0, l = indices.length; i < l; i += 2 ) { 796 | 797 | renderList.pushLine( indices[ i ], indices[ i + 1 ] ); 798 | 799 | } 800 | 801 | } else { 802 | 803 | var step = object instanceof LineSegments ? 2 : 1; 804 | 805 | for ( var i = 0, l = ( positions.length / 3 ) - 1; i < l; i += step ) { 806 | 807 | renderList.pushLine( i, i + 1 ); 808 | 809 | } 810 | 811 | } 812 | 813 | } 814 | 815 | } else if ( geometry instanceof Geometry ) { 816 | 817 | var vertices = object.geometry.vertices; 818 | 819 | if ( vertices.length === 0 ) continue; 820 | 821 | v1 = getNextVertexInPool(); 822 | v1.positionScreen.copy( vertices[ 0 ] ).applyMatrix4( _modelViewProjectionMatrix ); 823 | 824 | var step = object instanceof LineSegments ? 2 : 1; 825 | 826 | for ( var v = 1, vl = vertices.length; v < vl; v ++ ) { 827 | 828 | v1 = getNextVertexInPool(); 829 | v1.positionScreen.copy( vertices[ v ] ).applyMatrix4( _modelViewProjectionMatrix ); 830 | 831 | if ( ( v + 1 ) % step > 0 ) continue; 832 | 833 | v2 = _vertexPool[ _vertexCount - 2 ]; 834 | 835 | _clippedVertex1PositionScreen.copy( v1.positionScreen ); 836 | _clippedVertex2PositionScreen.copy( v2.positionScreen ); 837 | 838 | if ( clipLine( _clippedVertex1PositionScreen, _clippedVertex2PositionScreen ) === true ) { 839 | 840 | // Perform the perspective divide 841 | _clippedVertex1PositionScreen.multiplyScalar( 1 / _clippedVertex1PositionScreen.w ); 842 | _clippedVertex2PositionScreen.multiplyScalar( 1 / _clippedVertex2PositionScreen.w ); 843 | 844 | _line = getNextLineInPool(); 845 | 846 | _line.id = object.id; 847 | _line.v1.positionScreen.copy( _clippedVertex1PositionScreen ); 848 | _line.v2.positionScreen.copy( _clippedVertex2PositionScreen ); 849 | 850 | _line.z = Math.max( _clippedVertex1PositionScreen.z, _clippedVertex2PositionScreen.z ); 851 | _line.renderOrder = object.renderOrder; 852 | 853 | _line.material = object.material; 854 | 855 | if ( object.material.vertexColors ) { 856 | 857 | _line.vertexColors[ 0 ].copy( object.geometry.colors[ v ] ); 858 | _line.vertexColors[ 1 ].copy( object.geometry.colors[ v - 1 ] ); 859 | 860 | } 861 | 862 | _renderData.elements.push( _line ); 863 | 864 | } 865 | 866 | } 867 | 868 | } 869 | 870 | } else if ( object instanceof Points ) { 871 | 872 | _modelViewProjectionMatrix.multiplyMatrices( _viewProjectionMatrix, _modelMatrix ); 873 | 874 | if ( geometry instanceof Geometry ) { 875 | 876 | var vertices = object.geometry.vertices; 877 | 878 | for ( var v = 0, vl = vertices.length; v < vl; v ++ ) { 879 | 880 | var vertex = vertices[ v ]; 881 | 882 | _vector4.set( vertex.x, vertex.y, vertex.z, 1 ); 883 | _vector4.applyMatrix4( _modelViewProjectionMatrix ); 884 | 885 | pushPoint( _vector4, object, camera ); 886 | 887 | } 888 | 889 | } else if ( geometry instanceof BufferGeometry ) { 890 | 891 | var attributes = geometry.attributes; 892 | 893 | if ( attributes.position !== undefined ) { 894 | 895 | var positions = attributes.position.array; 896 | 897 | for ( var i = 0, l = positions.length; i < l; i += 3 ) { 898 | 899 | _vector4.set( positions[ i ], positions[ i + 1 ], positions[ i + 2 ], 1 ); 900 | _vector4.applyMatrix4( _modelViewProjectionMatrix ); 901 | 902 | pushPoint( _vector4, object, camera ); 903 | 904 | } 905 | 906 | } 907 | 908 | } 909 | 910 | } else if ( object instanceof Sprite ) { 911 | 912 | object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld ); 913 | _vector4.set( _modelMatrix.elements[ 12 ], _modelMatrix.elements[ 13 ], _modelMatrix.elements[ 14 ], 1 ); 914 | _vector4.applyMatrix4( _viewProjectionMatrix ); 915 | 916 | pushPoint( _vector4, object, camera ); 917 | 918 | } 919 | 920 | } 921 | 922 | if ( sortElements === true ) { 923 | 924 | _renderData.elements.sort( painterSort ); 925 | 926 | } 927 | 928 | return _renderData; 929 | 930 | }; 931 | 932 | function pushPoint( _vector4, object, camera ) { 933 | 934 | var invW = 1 / _vector4.w; 935 | 936 | _vector4.z *= invW; 937 | 938 | if ( _vector4.z >= - 1 && _vector4.z <= 1 ) { 939 | 940 | _sprite = getNextSpriteInPool(); 941 | _sprite.id = object.id; 942 | _sprite.x = _vector4.x * invW; 943 | _sprite.y = _vector4.y * invW; 944 | _sprite.z = _vector4.z; 945 | _sprite.renderOrder = object.renderOrder; 946 | _sprite.object = object; 947 | 948 | _sprite.rotation = object.rotation; 949 | 950 | _sprite.scale.x = object.scale.x * Math.abs( _sprite.x - ( _vector4.x + camera.projectionMatrix.elements[ 0 ] ) / ( _vector4.w + camera.projectionMatrix.elements[ 12 ] ) ); 951 | _sprite.scale.y = object.scale.y * Math.abs( _sprite.y - ( _vector4.y + camera.projectionMatrix.elements[ 5 ] ) / ( _vector4.w + camera.projectionMatrix.elements[ 13 ] ) ); 952 | 953 | _sprite.material = object.material; 954 | 955 | _renderData.elements.push( _sprite ); 956 | 957 | } 958 | 959 | } 960 | 961 | // Pools 962 | 963 | function getNextObjectInPool() { 964 | 965 | if ( _objectCount === _objectPoolLength ) { 966 | 967 | var object = new RenderableObject(); 968 | _objectPool.push( object ); 969 | _objectPoolLength ++; 970 | _objectCount ++; 971 | return object; 972 | 973 | } 974 | 975 | return _objectPool[ _objectCount ++ ]; 976 | 977 | } 978 | 979 | function getNextVertexInPool() { 980 | 981 | if ( _vertexCount === _vertexPoolLength ) { 982 | 983 | var vertex = new RenderableVertex(); 984 | _vertexPool.push( vertex ); 985 | _vertexPoolLength ++; 986 | _vertexCount ++; 987 | return vertex; 988 | 989 | } 990 | 991 | return _vertexPool[ _vertexCount ++ ]; 992 | 993 | } 994 | 995 | function getNextFaceInPool() { 996 | 997 | if ( _faceCount === _facePoolLength ) { 998 | 999 | var face = new RenderableFace(); 1000 | _facePool.push( face ); 1001 | _facePoolLength ++; 1002 | _faceCount ++; 1003 | return face; 1004 | 1005 | } 1006 | 1007 | return _facePool[ _faceCount ++ ]; 1008 | 1009 | 1010 | } 1011 | 1012 | function getNextLineInPool() { 1013 | 1014 | if ( _lineCount === _linePoolLength ) { 1015 | 1016 | var line = new RenderableLine(); 1017 | _linePool.push( line ); 1018 | _linePoolLength ++; 1019 | _lineCount ++; 1020 | return line; 1021 | 1022 | } 1023 | 1024 | return _linePool[ _lineCount ++ ]; 1025 | 1026 | } 1027 | 1028 | function getNextSpriteInPool() { 1029 | 1030 | if ( _spriteCount === _spritePoolLength ) { 1031 | 1032 | var sprite = new RenderableSprite(); 1033 | _spritePool.push( sprite ); 1034 | _spritePoolLength ++; 1035 | _spriteCount ++; 1036 | return sprite; 1037 | 1038 | } 1039 | 1040 | return _spritePool[ _spriteCount ++ ]; 1041 | 1042 | } 1043 | 1044 | // 1045 | 1046 | function painterSort( a, b ) { 1047 | 1048 | if ( a.renderOrder !== b.renderOrder ) { 1049 | 1050 | return a.renderOrder - b.renderOrder; 1051 | 1052 | } else if ( a.z !== b.z ) { 1053 | 1054 | return b.z - a.z; 1055 | 1056 | } else if ( a.id !== b.id ) { 1057 | 1058 | return a.id - b.id; 1059 | 1060 | } else { 1061 | 1062 | return 0; 1063 | 1064 | } 1065 | 1066 | } 1067 | 1068 | function clipLine( s1, s2 ) { 1069 | 1070 | var alpha1 = 0, alpha2 = 1, 1071 | 1072 | // Calculate the boundary coordinate of each vertex for the near and far clip planes, 1073 | // Z = -1 and Z = +1, respectively. 1074 | 1075 | bc1near = s1.z + s1.w, 1076 | bc2near = s2.z + s2.w, 1077 | bc1far = - s1.z + s1.w, 1078 | bc2far = - s2.z + s2.w; 1079 | 1080 | if ( bc1near >= 0 && bc2near >= 0 && bc1far >= 0 && bc2far >= 0 ) { 1081 | 1082 | // Both vertices lie entirely within all clip planes. 1083 | return true; 1084 | 1085 | } else if ( ( bc1near < 0 && bc2near < 0 ) || ( bc1far < 0 && bc2far < 0 ) ) { 1086 | 1087 | // Both vertices lie entirely outside one of the clip planes. 1088 | return false; 1089 | 1090 | } else { 1091 | 1092 | // The line segment spans at least one clip plane. 1093 | 1094 | if ( bc1near < 0 ) { 1095 | 1096 | // v1 lies outside the near plane, v2 inside 1097 | alpha1 = Math.max( alpha1, bc1near / ( bc1near - bc2near ) ); 1098 | 1099 | } else if ( bc2near < 0 ) { 1100 | 1101 | // v2 lies outside the near plane, v1 inside 1102 | alpha2 = Math.min( alpha2, bc1near / ( bc1near - bc2near ) ); 1103 | 1104 | } 1105 | 1106 | if ( bc1far < 0 ) { 1107 | 1108 | // v1 lies outside the far plane, v2 inside 1109 | alpha1 = Math.max( alpha1, bc1far / ( bc1far - bc2far ) ); 1110 | 1111 | } else if ( bc2far < 0 ) { 1112 | 1113 | // v2 lies outside the far plane, v2 inside 1114 | alpha2 = Math.min( alpha2, bc1far / ( bc1far - bc2far ) ); 1115 | 1116 | } 1117 | 1118 | if ( alpha2 < alpha1 ) { 1119 | 1120 | // The line segment spans two boundaries, but is outside both of them. 1121 | // (This can't happen when we're only clipping against just near/far but good 1122 | // to leave the check here for future usage if other clip planes are added.) 1123 | return false; 1124 | 1125 | } else { 1126 | 1127 | // Update the s1 and s2 vertices to match the clipped line segment. 1128 | s1.lerp( s2, alpha1 ); 1129 | s2.lerp( s1, 1 - alpha2 ); 1130 | 1131 | return true; 1132 | 1133 | } 1134 | 1135 | } 1136 | 1137 | } 1138 | 1139 | }; 1140 | 1141 | export { RenderableObject, RenderableFace, RenderableVertex, RenderableLine, RenderableSprite, Projector }; 1142 | -------------------------------------------------------------------------------- /src/three-plotter-renderer.js: -------------------------------------------------------------------------------- 1 | import { PlotterRenderer } from './plotter-renderer.js'; 2 | 3 | let THREE = window['THREE'] = window['THREE'] || {}; 4 | THREE['PlotterRenderer'] = PlotterRenderer; 5 | -------------------------------------------------------------------------------- /tests/test.box.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SVG Plotter Renderer Example 01 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 | 16 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/test.box.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { PlotterRenderer } from "../src/plotter-renderer.js"; 3 | import { OrbitControls } from "../node_modules/three/examples/jsm/controls/OrbitControls.js"; 4 | 5 | var camera, scene, renderer; 6 | var cameraControls; 7 | var canvasWidth = window.innerWidth; 8 | var canvasHeight = window.innerHeight; 9 | var focused = false; 10 | 11 | window.onload = () => { 12 | init(); 13 | render(); 14 | }; 15 | 16 | window.onblur = () => { 17 | focused = false; 18 | }; 19 | 20 | window.onfocus = window.onclick = () => { 21 | focused = true; 22 | }; 23 | 24 | window.onkeypress = (e) => { 25 | console.log(e.keyCode); 26 | switch (e.keyCode) { 27 | case 61: 28 | renderer.increaseSpacing(); 29 | break; 30 | case 45: 31 | renderer.decreaseSpacing(); 32 | break; 33 | case 93: 34 | renderer.increaseRotation(); 35 | break; 36 | case 91: 37 | renderer.decreaseRotation(); 38 | break; 39 | case 46: 40 | renderer.nextHatchGroup(); 41 | break; 42 | case 44: 43 | renderer.previousHatchGroup(); 44 | break; 45 | } 46 | }; 47 | 48 | function init() { 49 | 50 | var view = document.getElementById("view"); 51 | var container = document.getElementById("plot"); 52 | var overla= document.getElementById("plot"); 53 | 54 | // CAMERA 55 | camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 1, 8000); 56 | camera.position.set(300, 300, 300); 57 | 58 | // RENDERER 59 | renderer = new PlotterRenderer(); 60 | 61 | renderer.setSize(canvasWidth, canvasHeight); 62 | container.appendChild(renderer.domElement); 63 | 64 | // EVENTS 65 | window.addEventListener("resize", onWindowResize, false); 66 | 67 | // CONTROLS 68 | // @ts-ignore 69 | cameraControls = new OrbitControls(camera, view); 70 | cameraControls.zoomSpeed = 2; 71 | 72 | // scene itself 73 | scene = new THREE.Scene(); 74 | scene.background = new THREE.Color(0xaaaaaa); 75 | 76 | const dirLight = new THREE.DirectionalLight(0xffffff, 0.75); 77 | dirLight.position.set(300, 300, 300); 78 | 79 | scene.add(dirLight); 80 | 81 | const dirLight2 = new THREE.DirectionalLight(0x333333, 0.75); 82 | dirLight2.position.set(-100, 300, -500); 83 | 84 | scene.add(dirLight2); 85 | 86 | const light = new THREE.PointLight(0xffffff, 1.0, 5000); 87 | light.position.x = 300; 88 | light.position.z = 600; 89 | light.position.y = 1000; 90 | 91 | camera.add(light); 92 | 93 | scene.add(camera); 94 | 95 | // GUI 96 | setupGui(); 97 | 98 | var geom = new THREE.BoxGeometry(100, 100, 100, 3, 3, 3); 99 | var mesh = new THREE.Mesh(geom, new THREE.MeshPhongMaterial({ opacity: 1, color: 0xffffff })); 100 | scene.add(mesh); 101 | 102 | var tick = function () { 103 | if (focused) { 104 | renderer.render(scene, camera, 0.2, 0.3); 105 | } 106 | requestAnimationFrame(tick); 107 | }; 108 | 109 | var optimizeTimeout = null; 110 | 111 | var setOptimize = function () { 112 | clearTimeout(optimizeTimeout); 113 | optimizeTimeout = setTimeout(() => { 114 | renderer.doOptimize = true; 115 | }, 500); 116 | }; 117 | 118 | cameraControls.addEventListener("start", function () { 119 | renderer.doOptimize = false; 120 | clearTimeout(optimizeTimeout); 121 | }); 122 | 123 | cameraControls.addEventListener("end", function () { 124 | setOptimize(); 125 | }); 126 | 127 | cameraControls.addEventListener("change", function () { 128 | renderer.doOptimize = false; 129 | clearTimeout(optimizeTimeout); 130 | setOptimize(); 131 | }); 132 | 133 | tick(); 134 | //setOptimize(); 135 | } 136 | 137 | function onWindowResize() { 138 | renderer.setSize(window.innerWidth, window.innerHeight); 139 | 140 | camera.aspect = canvasWidth / canvasHeight; 141 | camera.updateProjectionMatrix(); 142 | 143 | render(); 144 | } 145 | 146 | function setupGui() { 147 | var exportButton = document.getElementById("exportsvg"); 148 | exportButton.addEventListener("click", exportSVG); 149 | } 150 | 151 | function render() { 152 | renderer.render(scene, camera); 153 | } 154 | 155 | function exportSVG() { 156 | saveString(document.getElementById("plot").innerHTML, "plot.svg"); 157 | } 158 | 159 | function save(blob, filename) { 160 | link.href = URL.createObjectURL(blob); 161 | link.download = filename; 162 | link.click(); 163 | } 164 | 165 | function saveString(text, filename) { 166 | save(new Blob([text], { type: "text/plain" }), filename); 167 | } 168 | 169 | var link = document.createElement("a"); 170 | link.style.display = "none"; 171 | document.body.appendChild(link); 172 | --------------------------------------------------------------------------------