├── .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 |
16 |
17 |
18 | Export SVG
19 |
20 |
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 |
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 |
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 |
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 |
15 |
16 |
17 | Export SVG
18 |
19 |
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 |
--------------------------------------------------------------------------------