├── .gitignore ├── .npmrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs └── api.md ├── package.json └── src ├── clone.js ├── clone.test.js ├── color.js ├── color.test.js ├── debug.js ├── group.js ├── helpers.js ├── index.js ├── log.js ├── maths.js ├── maths.test.js ├── meta.js ├── ops-booleans.js ├── ops-booleans.test.js ├── ops-combined.test.js ├── ops-extrusions.js ├── ops-extrusions.test.js ├── ops-transformations.js ├── ops-transformations.test.js ├── primitives2d.js ├── primitives2d.test.js ├── primitives3d.js ├── primitives3d.test.js ├── test-helpers.js ├── text.js └── text.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | .nyc_output 4 | coverage 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | - "9" 6 | sudo: false 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [0.5.1](https://github.com/jscad/scad-api/compare/v0.5.0...v0.5.1) (2017-12-31) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * **cube:** fixed issues with rounded cube & added test ([#37](https://github.com/jscad/scad-api/issues/37)) ([0d4969b](https://github.com/jscad/scad-api/commit/0d4969b)) 8 | 9 | 10 | 11 | 12 | # [0.5.0](https://github.com/jscad/scad-api/compare/v0.4.2...v0.5.0) (2017-12-31) 13 | 14 | 15 | ### Features 16 | 17 | * **transform:** added transform function : implementation of matrix transform as an operation ([#36](https://github.com/jscad/scad-api/issues/36)) ([e392794](https://github.com/jscad/scad-api/commit/e392794)) 18 | 19 | 20 | 21 | 22 | ## [0.4.2](https://github.com/jscad/scad-api/compare/v0.4.1...v0.4.2) (2017-12-02) 23 | 24 | 25 | 26 | 27 | ## [0.4.1](https://github.com/jscad/scad-api/compare/v0.4.0...v0.4.1) (2017-11-24) 28 | 29 | 30 | 31 | 32 | # [0.4.0](https://github.com/jscad/scad-api/compare/v0.3.6...v0.4.0) (2017-11-24) 33 | 34 | 35 | ### Features 36 | 37 | * **rotate_extrude:** partial rotate extrude support and internal fixes ([6b23dd6](https://github.com/jscad/scad-api/commit/6b23dd6)), closes [#28](https://github.com/jscad/scad-api/issues/28) 38 | 39 | 40 | 41 | 42 | ## [0.3.6](https://github.com/jscad/scad-api/compare/v0.3.5...v0.3.6) (2017-11-03) 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## scad-api 2 | 3 | ## OpenSCAD like modeling API for OpenJSCAD - Contributing Guide 4 | 5 | This library is part of the JSCAD Organization, and is maintained by a group of volunteers. We welcome and encourage anyone to pitch in but please take a moment to read the following guidelines. 6 | 7 | * If you want to submit a bug report please make sure to follow the [Reporting Issues](https://github.com/jscad/scad-api/wiki/Reporting-Issues) guide. Bug reports are accepted as [Issues](https://github.com/jscad/scad-api/issues/) via GitHub. 8 | 9 | * If you want to submit a change or a patch, please read the contents below on how to make changes. New contributions are accepted as [Pull Requests](https://github.com/jscad/scad-api/pulls/) via GithHub. 10 | 11 | * We only accept bug reports and pull requests on **GitHub**. 12 | 13 | * If you have a question about how to use OpenJSCAD, then please start a conversation at the [OpenJSCAD.org User Group](https://plus.google.com/communities/114958480887231067224). You might find the answer in the [OpenJSCAD.org User Guide](https://github.com/Spiritdude/OpenJSCAD.org/wiki/User-Guide). 14 | 15 | * If you have a change or new feature in mind, please start a conversation with the [Core Developers](https://plus.google.com/communities/114958480887231067224) and start contributing changes. 16 | 17 | Thanks! 18 | 19 | The JSCAD Organization 20 | 21 | ## Making Changes 22 | 23 | First, we suggest that you fork this GIT repository. This will keep your changes separate from the fast lane. And make pull requests easier. 24 | 25 | Once forked, clone your copy of the OpenJSCAD library. 26 | ``` 27 | git clone https://github.com/myusername/scad-api.git 28 | cd scad-api 29 | ``` 30 | 31 | **We suggest downloading NPM. This will allow you to generate API documents, and run test suites.** 32 | 33 | Once you have NPM, install all the dependencies for development. 34 | ``` 35 | npm install 36 | ``` 37 | And, run the test cases to verify the library is functional. 38 | ``` 39 | npm test 40 | ``` 41 | 42 | The project is structured as: 43 | - src/*.js : library and test suite source code 44 | - dist/index.js : generated library for distribution 45 | 46 | You can now make changes to the library, as well as the test suites. 47 | 48 | If you intend to contribute changes back to JSCAD then please be sure to create test cases. 49 | 50 | Done? Great! Push the changes back to the fork. 51 | ``` 52 | git commit changed-file 53 | git add test/new-test-suite.js 54 | git commit test/new-test-suite.js 55 | git push 56 | ``` 57 | Finally, you can review your changes via GitHub, and create a pull request. 58 | 59 | TIPS for successful pull requests: 60 | - Commit often, and comment well 61 | - Follow the [JavaScript Standard Style](https://github.com/feross/standard) 62 | - Create test cases for all changes 63 | - Verify that all tests suites pass 64 | 65 | WOW! Thanks for the cool code. 66 | 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2017 Mark 'kaosat-dev' Moissette 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scad-api 2 | 3 | [](https://badge.fury.io/gh/jscad%2Fscad-api) 4 | [](http://github.com/badges/stability-badges) 5 | [](https://travis-ci.org/jscad/scad-api) 6 | [](https://david-dm.org/jscad/scad-api) 7 | [](https://david-dm.org/jscad/scad-api#info=devDependencies) 8 | 9 | 10 | > OpenSCAD like modeling API for OpenJSCAD 11 | 12 | This package provides [OpenSCAD](http://www.openscad.org/) functionality for [OpenJSCAD](openjscad.org) & Co. 13 | 14 | ## Table of Contents 15 | 16 | - [Background](#background) 17 | - [Installation](#installation) 18 | - [Usage](#usage) 19 | - [API](#api) 20 | - [Contribute](#contribute) 21 | - [License](#license) 22 | 23 | ## Background 24 | 25 | This package provides an opinionated API that tries to mimic (up to a point) that of [OpenSCAD](http://www.openscad.org/). Currently, this package uses the [CSG.js](https://github.com/jscad/csg.js) library to implement most functionality. 26 | 27 | This package was part of [OpenJSCAD.ORG](https://github.com/Spiritdude/OpenJSCAD.org) but is now an 'independent' module in the JSCAD organization. Hopefully, this makes usage and development easier. 28 | 29 | It gives you ONE variant of syntaxic sugar/ flavor to do solid modeling. 30 | 31 | It is using semantic versioning to signal minor and breaking changes. 32 | 33 | ## Installation 34 | 35 | ``` 36 | npm install @jscad/scad-api 37 | ``` 38 | 39 | ## Usage 40 | 41 | This package is included by default in [OpenJSCAD.org](http://openjscad.org/) but you can also use it 'standalone'. 42 | 43 | ```javascript 44 | const scadApi = require('@jscad/scad-api') 45 | 46 | const {cube, sphere} = scadApi.primitives3d 47 | const {union} = scadApi.booleanOps 48 | 49 | const base = cube({size: 1, center: true}) 50 | const top = sphere({r: 10, fn: 100, type: 'geodesic'}) 51 | 52 | const result = union(base, top) 53 | 54 | ``` 55 | 56 | ## API 57 | 58 | The API documentation (incomplete) can be found [here](./docs/api.md) 59 | For more information, see the [OpenJsCad User Guide](https://en.wikibooks.org/wiki/OpenJSCAD_User_Guide). 60 | 61 | For questions about the API, please contact the [User Group](https://plus.google.com/communities/114958480887231067224) 62 | 63 | >NOTE: at this time combined union() of 2d & 3d shapes is not **officially** supported 64 | but still possible via union({extrude2d: true}, op1, op2) this might change in the future 65 | 66 | ## Contribute 67 | 68 | This library is part of the JSCAD Organization, and is maintained by a group of volunteers. We welcome and encourage anyone to pitch in but please take a moment to read the following guidelines. 69 | 70 | * If you want to submit a bug report please make sure to follow the [Reporting Issues](https://github.com/jscad/scad-api/wiki/Reporting-Issues) guide. Bug reports are accepted as [Issues](https://github.com/jscad/scad-api/issues/) via GitHub. 71 | 72 | * If you want to submit a change or a patch, please see the [Contributing guidelines](https://github.com/jscad/scad-api/blob/master/CONTRIBUTING.md). New contributions are accepted as [Pull Requests](https://github.com/jscad/scad-api/pulls/) via GithHub. 73 | 74 | * We only accept bug reports and pull requests on **GitHub**. 75 | 76 | * If you have a question about how to use CSG.js, then please start a conversation at the [OpenJSCAD.org User Group](https://plus.google.com/communities/114958480887231067224). You might find the answer in the [OpenJSCAD.org User Guide](https://github.com/Spiritdude/OpenJSCAD.org/wiki/User-Guide). 77 | 78 | * If you have a change or new feature in mind, please start a conversation with the [Core Developers](https://plus.google.com/communities/114958480887231067224) and start contributing changes. 79 | 80 | Small Note: If editing the Readme, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. 81 | 82 | 83 | ## License 84 | 85 | [The MIT License (MIT)](https://github.com/jscad/scad-api/blob/master/LICENSE) 86 | (unless specified otherwise) 87 | 88 | NOTE: OpenSCAD and OpenSCAD API are released under the General Public License version 2. 89 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | ## Functions 2 | 3 |
CSG
clone the given object
6 |Converts an CSS color name to RGB color.
9 |CSG
apply the given color to the input object(s)
12 |Converts an RGB color value to HSL. Conversion formula 15 | adapted from http://en.wikipedia.org/wiki/HSL_color_space. 16 | Assumes r, g, and b are contained in the set [0, 1] and 17 | returns h, s, and l in the set [0, 1].
18 |Converts an HSL color value to RGB. Conversion formula 21 | adapted from http://en.wikipedia.org/wiki/HSL_color_space. 22 | Assumes h, s, and l are contained in the set [0, 1] and 23 | returns r, g, and b in the set [0, 1].
24 |Converts an RGB color value to HSV. Conversion formula 27 | adapted from http://en.wikipedia.org/wiki/HSV_color_space. 28 | Assumes r, g, and b are contained in the set [0, 1] and 29 | returns h, s, and v in the set [0, 1].
30 |Converts an HSV color value to RGB. Conversion formula 33 | adapted from http://en.wikipedia.org/wiki/HSV_color_space. 34 | Assumes h, s, and v are contained in the set [0, 1] and 35 | returns r, g, and b in the set [0, 1].
36 |Converts a HTML5 color value (string) to RGB values 39 | See the color input type of HTML5 forms 40 | Conversion formula:
41 |Converts RGB color value to HTML5 color value (string) 48 | Conversion forumla:
49 |CSG
union/ combine the given shapes
56 |CSG
difference/ subtraction of the given shapes ie: 59 | cut out C From B From A ie : a - b - c etc
60 |CSG
intersection of the given shapes: ie keep only the common parts between the given shapes
63 |CSG
linear extrusion of the input 2d shape
66 |CSG
rotate extrusion / revolve of the given 2d shape
69 |CSG
rectangular extrusion of the given array of points
72 |CSG
translate an object in 2D/3D space
75 |CSG
scale an object in 2D/3D space
78 |CSG
rotate an object in 2D/3D space
81 |CSG
apply the given matrix transform to the given objects
84 |CSG
center an object in 2D/3D space
87 |CSG
mirror an object in 2D/3D space
90 |CSG/CAG
expand an object in 2D/3D space
93 |CSG/CAG
contract an object(s) in 2D/3D space
96 |CSG
create a minkowski sum of the given shapes
99 |CSG
create a convex hull of the given shapes
102 |CSG
create a chain hull of the given shapes 105 | Originally "Whosa whatsis" suggested "Chain Hull" , 106 | as described at https://plus.google.com/u/0/105535247347788377245/posts/aZGXKFX1ACN 107 | essentially hull A+B, B+C, C+D and then union those
108 |CAG
Construct a square/rectangle
111 |CAG
Construct a circle
114 |CAG
Construct a polygon either from arrays of paths and points, or just arrays of points 117 | nested paths (multiple paths) and flat paths are supported
118 |CAG
Construct a triangle
121 |CSG
Construct a cuboid
124 |CSG
Construct a sphere
127 |CSG
Construct a cylinder
130 |CSG
Construct a torus
133 |CSG
Construct a polyhedron from the given triangles/ polygons/points
136 |Object
Construct a with, segments tupple from a character
139 |Array
Construct an array of with, segments tupple from a string
142 |CSG
148 | clone the given object
149 |
150 | **Kind**: global function
151 | **Returns**: CSG
- new CSG object , a copy of the input
152 |
153 | | Param | Type | Description |
154 | | --- | --- | --- |
155 | | obj | Object
| the object to clone by |
156 |
157 | **Example**
158 | ```js
159 | let copy = clone(sphere())
160 | ```
161 |
162 |
163 | ## css2rgb(String) ⇒
164 | Converts an CSS color name to RGB color.
165 |
166 | **Kind**: global function
167 | **Returns**: Array The RGB representation, or [0,0,0] default
168 |
169 | | Param | Description |
170 | | --- | --- |
171 | | String | s The CSS color name |
172 |
173 |
174 |
175 | ## color(color, objects) ⇒ CSG
176 | apply the given color to the input object(s)
177 |
178 | **Kind**: global function
179 | **Returns**: CSG
- new CSG object , with the given color
180 |
181 | | Param | Type | Description |
182 | | --- | --- | --- |
183 | | color | Object
| either an array or a hex string of color values |
184 | | objects | Object
\| Array
| either a single or multiple CSG/CAG objects to color |
185 |
186 | **Example**
187 | ```js
188 | let redSphere = color([1,0,0,1], sphere())
189 | ```
190 |
191 |
192 | ## rgb2hsl(Number, Number, Number) ⇒
193 | Converts an RGB color value to HSL. Conversion formula
194 | adapted from http://en.wikipedia.org/wiki/HSL_color_space.
195 | Assumes r, g, and b are contained in the set [0, 1] and
196 | returns h, s, and l in the set [0, 1].
197 |
198 | **Kind**: global function
199 | **Returns**: Array The HSL representation
200 |
201 | | Param | Description |
202 | | --- | --- |
203 | | Number | r The red color value |
204 | | Number | g The green color value |
205 | | Number | b The blue color value |
206 |
207 |
208 |
209 | ## hsl2rgb(Number, Number, Number) ⇒
210 | Converts an HSL color value to RGB. Conversion formula
211 | adapted from http://en.wikipedia.org/wiki/HSL_color_space.
212 | Assumes h, s, and l are contained in the set [0, 1] and
213 | returns r, g, and b in the set [0, 1].
214 |
215 | **Kind**: global function
216 | **Returns**: Array The RGB representation
217 |
218 | | Param | Description |
219 | | --- | --- |
220 | | Number | h The hue |
221 | | Number | s The saturation |
222 | | Number | l The lightness |
223 |
224 |
225 |
226 | ## rgb2hsv(Number, Number, Number) ⇒
227 | Converts an RGB color value to HSV. Conversion formula
228 | adapted from http://en.wikipedia.org/wiki/HSV_color_space.
229 | Assumes r, g, and b are contained in the set [0, 1] and
230 | returns h, s, and v in the set [0, 1].
231 |
232 | **Kind**: global function
233 | **Returns**: Array The HSV representation
234 |
235 | | Param | Description |
236 | | --- | --- |
237 | | Number | r The red color value |
238 | | Number | g The green color value |
239 | | Number | b The blue color value |
240 |
241 |
242 |
243 | ## hsv2rgb(Number, Number, Number) ⇒
244 | Converts an HSV color value to RGB. Conversion formula
245 | adapted from http://en.wikipedia.org/wiki/HSV_color_space.
246 | Assumes h, s, and v are contained in the set [0, 1] and
247 | returns r, g, and b in the set [0, 1].
248 |
249 | **Kind**: global function
250 | **Returns**: Array The RGB representation
251 |
252 | | Param | Description |
253 | | --- | --- |
254 | | Number | h The hue |
255 | | Number | s The saturation |
256 | | Number | v The value |
257 |
258 |
259 |
260 | ## html2rgb()
261 | Converts a HTML5 color value (string) to RGB values
262 | See the color input type of HTML5 forms
263 | Conversion formula:
264 | - split the string; "#RRGGBB" into RGB components
265 | - convert the HEX value into RGB values
266 |
267 | **Kind**: global function
268 |
269 |
270 | ## rgb2html()
271 | Converts RGB color value to HTML5 color value (string)
272 | Conversion forumla:
273 | - convert R, G, B into HEX strings
274 | - return HTML formatted string "#RRGGBB"
275 |
276 | **Kind**: global function
277 |
278 |
279 | ## union(objects) ⇒ CSG
280 | union/ combine the given shapes
281 |
282 | **Kind**: global function
283 | **Returns**: CSG
- new CSG object, the union of all input shapes
284 |
285 | | Param | Type | Description |
286 | | --- | --- | --- |
287 | | objects | Object(s)
\| Array
| objects to combine : can be given - one by one: union(a,b,c) or - as an array: union([a,b,c]) |
288 |
289 | **Example**
290 | ```js
291 | let unionOfSpherAndCube = union(sphere(), cube())
292 | ```
293 |
294 |
295 | ## difference(objects) ⇒ CSG
296 | difference/ subtraction of the given shapes ie:
297 | cut out C From B From A ie : a - b - c etc
298 |
299 | **Kind**: global function
300 | **Returns**: CSG
- new CSG object, the difference of all input shapes
301 |
302 | | Param | Type | Description |
303 | | --- | --- | --- |
304 | | objects | Object(s)
\| Array
| objects to subtract can be given - one by one: difference(a,b,c) or - as an array: difference([a,b,c]) |
305 |
306 | **Example**
307 | ```js
308 | let differenceOfSpherAndCube = difference(sphere(), cube())
309 | ```
310 |
311 |
312 | ## intersection(objects) ⇒ CSG
313 | intersection of the given shapes: ie keep only the common parts between the given shapes
314 |
315 | **Kind**: global function
316 | **Returns**: CSG
- new CSG object, the intersection of all input shapes
317 |
318 | | Param | Type | Description |
319 | | --- | --- | --- |
320 | | objects | Object(s)
\| Array
| objects to intersect can be given - one by one: intersection(a,b,c) or - as an array: intersection([a,b,c]) |
321 |
322 | **Example**
323 | ```js
324 | let intersectionOfSpherAndCube = intersection(sphere(), cube())
325 | ```
326 |
327 |
328 | ## linear_extrude([options], baseShape) ⇒ CSG
329 | linear extrusion of the input 2d shape
330 |
331 | **Kind**: global function
332 | **Returns**: CSG
- new extruded shape
333 |
334 | | Param | Type | Default | Description |
335 | | --- | --- | --- | --- |
336 | | [options] | Object
| | options for construction |
337 | | [options.height] | Float
| 1
| height of the extruded shape |
338 | | [options.slices] | Integer
| 10
| number of intermediary steps/slices |
339 | | [options.twist] | Integer
| 0
| angle (in degrees to twist the extusion by) |
340 | | [options.center] | Boolean
| false
| whether to center extrusion or not |
341 | | baseShape | CAG
| | input 2d shape |
342 |
343 | **Example**
344 | ```js
345 | let revolved = linear_extrude({height: 10}, square())
346 | ```
347 |
348 |
349 | ## rotate_extrude([options], baseShape) ⇒ CSG
350 | rotate extrusion / revolve of the given 2d shape
351 |
352 | **Kind**: global function
353 | **Returns**: CSG
- new extruded shape
354 |
355 | | Param | Type | Default | Description |
356 | | --- | --- | --- | --- |
357 | | [options] | Object
| | options for construction |
358 | | [options.fn] | Integer
| 1
| resolution/number of segments of the extrusion |
359 | | [options.startAngle] | Float
| 1
| start angle of the extrusion, in degrees |
360 | | [options.angle] | Float
| 1
| angle of the extrusion, in degrees |
361 | | [options.overflow] | Float
| 'cap'
| what to do with points outside of bounds (+ / - x) : defaults to capping those points to 0 (only supported behaviour for now) |
362 | | baseShape | CAG
| | input 2d shape |
363 |
364 | **Example**
365 | ```js
366 | let revolved = rotate_extrude({fn: 10}, square())
367 | ```
368 |
369 |
370 | ## rectangular_extrude(basePoints, [options]) ⇒ CSG
371 | rectangular extrusion of the given array of points
372 |
373 | **Kind**: global function
374 | **Returns**: CSG
- new extruded shape
375 |
376 | | Param | Type | Default | Description |
377 | | --- | --- | --- | --- |
378 | | basePoints | Array
| | array of points (nested) to extrude from layed out like [ [0,0], [10,0], [5,10], [0,10] ] |
379 | | [options] | Object
| | options for construction |
380 | | [options.h] | Float
| 1
| height of the extruded shape |
381 | | [options.w] | Float
| 10
| width of the extruded shape |
382 | | [options.fn] | Integer
| 1
| resolution/number of segments of the extrusion |
383 | | [options.closed] | Boolean
| false
| whether to close the input path for the extrusion or not |
384 | | [options.round] | Boolean
| true
| whether to round the extrusion or not |
385 |
386 | **Example**
387 | ```js
388 | let revolved = rectangular_extrude({height: 10}, square())
389 | ```
390 |
391 |
392 | ## translate(vector, ...objects) ⇒ CSG
393 | translate an object in 2D/3D space
394 |
395 | **Kind**: global function
396 | **Returns**: CSG
- new CSG object , translated by the given amount
397 |
398 | | Param | Type | Description |
399 | | --- | --- | --- |
400 | | vector | Object
| 3D vector to translate the given object(s) by |
401 | | ...objects | Object(s)
\| Array
| either a single or multiple CSG/CAG objects to translate |
402 |
403 | **Example**
404 | ```js
405 | let movedSphere = translate([10,2,0], sphere())
406 | ```
407 |
408 |
409 | ## scale(scale, ...objects) ⇒ CSG
410 | scale an object in 2D/3D space
411 |
412 | **Kind**: global function
413 | **Returns**: CSG
- new CSG object , scaled by the given amount
414 |
415 | | Param | Type | Description |
416 | | --- | --- | --- |
417 | | scale | Float
\| Array
| either an array or simple number to scale object(s) by |
418 | | ...objects | Object(s)
\| Array
| either a single or multiple CSG/CAG objects to scale |
419 |
420 | **Example**
421 | ```js
422 | let scaledSphere = scale([0.2,15,1], sphere())
423 | ```
424 |
425 |
426 | ## rotate(rotation, objects) ⇒ CSG
427 | rotate an object in 2D/3D space
428 |
429 | **Kind**: global function
430 | **Returns**: CSG
- new CSG object , rotated by the given amount
431 |
432 | | Param | Type | Description |
433 | | --- | --- | --- |
434 | | rotation | Float
\| Array
| either an array or simple number to rotate object(s) by |
435 | | objects | Object(s)
\| Array
| either a single or multiple CSG/CAG objects to rotate |
436 |
437 | **Example**
438 | ```js
439 | let rotatedSphere = rotate([0.2,15,1], sphere())
440 | ```
441 |
442 |
443 | ## transform(matrix, ...objects) ⇒ CSG
444 | apply the given matrix transform to the given objects
445 |
446 | **Kind**: global function
447 | **Returns**: CSG
- new CSG object , transformed
448 |
449 | | Param | Type | Description |
450 | | --- | --- | --- |
451 | | matrix | Array
| the 4x4 matrix to apply, as a simple 1d array of 16 elements |
452 | | ...objects | Object(s)
\| Array
| either a single or multiple CSG/CAG objects to transform |
453 |
454 | **Example**
455 | ```js
456 | const angle = 45
457 | let transformedShape = transform([
458 | cos(angle), -sin(angle), 0, 10,
459 | sin(angle), cos(angle), 0, 20,
460 | 0 , 0, 1, 30,
461 | 0, 0, 0, 1
462 | ], sphere())
463 | ```
464 |
465 |
466 | ## center(axis, ...objects) ⇒ CSG
467 | center an object in 2D/3D space
468 |
469 | **Kind**: global function
470 | **Returns**: CSG
- new CSG object , translated by the given amount
471 |
472 | | Param | Type | Description |
473 | | --- | --- | --- |
474 | | axis | Boolean
\| Array
| either an array or single boolean to indicate which axis you want to center on |
475 | | ...objects | Object(s)
\| Array
| either a single or multiple CSG/CAG objects to translate |
476 |
477 | **Example**
478 | ```js
479 | let movedSphere = center(false, sphere())
480 | ```
481 |
482 |
483 | ## mirror(vector, ...objects) ⇒ CSG
484 | mirror an object in 2D/3D space
485 |
486 | **Kind**: global function
487 | **Returns**: CSG
- new CSG object , mirrored
488 |
489 | | Param | Type | Description |
490 | | --- | --- | --- |
491 | | vector | Array
| the axes to mirror the object(s) by |
492 | | ...objects | Object(s)
\| Array
| either a single or multiple CSG/CAG objects to mirror |
493 |
494 | **Example**
495 | ```js
496 | let rotatedSphere = mirror([0.2,15,1], sphere())
497 | ```
498 |
499 |
500 | ## expand(radius, object) ⇒ CSG/CAG
501 | expand an object in 2D/3D space
502 |
503 | **Kind**: global function
504 | **Returns**: CSG/CAG
- new CSG/CAG object , expanded
505 |
506 | | Param | Type | Description |
507 | | --- | --- | --- |
508 | | radius | float
| the radius to expand by |
509 | | object | Object
| a CSG/CAG objects to expand |
510 |
511 | **Example**
512 | ```js
513 | let expanededShape = expand([0.2,15,1], sphere())
514 | ```
515 |
516 |
517 | ## contract(radius, object) ⇒ CSG/CAG
518 | contract an object(s) in 2D/3D space
519 |
520 | **Kind**: global function
521 | **Returns**: CSG/CAG
- new CSG/CAG object , contracted
522 |
523 | | Param | Type | Description |
524 | | --- | --- | --- |
525 | | radius | float
| the radius to contract by |
526 | | object | Object
| a CSG/CAG objects to contract |
527 |
528 | **Example**
529 | ```js
530 | let contractedShape = contract([0.2,15,1], sphere())
531 | ```
532 |
533 |
534 | ## minkowski(objects) ⇒ CSG
535 | create a minkowski sum of the given shapes
536 |
537 | **Kind**: global function
538 | **Returns**: CSG
- new CSG object , mirrored
539 |
540 | | Param | Type | Description |
541 | | --- | --- | --- |
542 | | objects | Object(s)
\| Array
| either a single or multiple CSG/CAG objects to create a hull around |
543 |
544 | **Example**
545 | ```js
546 | let hulled = hull(rect(), circle())
547 | ```
548 |
549 |
550 | ## hull(objects) ⇒ CSG
551 | create a convex hull of the given shapes
552 |
553 | **Kind**: global function
554 | **Returns**: CSG
- new CSG object , a hull around the given shapes
555 |
556 | | Param | Type | Description |
557 | | --- | --- | --- |
558 | | objects | Object(s)
\| Array
| either a single or multiple CSG/CAG objects to create a hull around |
559 |
560 | **Example**
561 | ```js
562 | let hulled = hull(rect(), circle())
563 | ```
564 |
565 |
566 | ## chain_hull(objects) ⇒ CSG
567 | create a chain hull of the given shapes
568 | Originally "Whosa whatsis" suggested "Chain Hull" ,
569 | as described at https://plus.google.com/u/0/105535247347788377245/posts/aZGXKFX1ACN
570 | essentially hull A+B, B+C, C+D and then union those
571 |
572 | **Kind**: global function
573 | **Returns**: CSG
- new CSG object ,which a chain hull of the inputs
574 |
575 | | Param | Type | Description |
576 | | --- | --- | --- |
577 | | objects | Object(s)
\| Array
| either a single or multiple CSG/CAG objects to create a chain hull around |
578 |
579 | **Example**
580 | ```js
581 | let hulled = chain_hull(rect(), circle())
582 | ```
583 |
584 |
585 | ## square([options]) ⇒ CAG
586 | Construct a square/rectangle
587 |
588 | **Kind**: global function
589 | **Returns**: CAG
- new square
590 |
591 | | Param | Type | Default | Description |
592 | | --- | --- | --- | --- |
593 | | [options] | Object
| | options for construction |
594 | | [options.size] | Float
| 1
| size of the square, either as array or scalar |
595 | | [options.center] | Boolean
| true
| wether to center the square/rectangle or not |
596 |
597 | **Example**
598 | ```js
599 | let square1 = square({
600 | size: 10
601 | })
602 | ```
603 |
604 |
605 | ## circle([options]) ⇒ CAG
606 | Construct a circle
607 |
608 | **Kind**: global function
609 | **Returns**: CAG
- new circle
610 |
611 | | Param | Type | Default | Description |
612 | | --- | --- | --- | --- |
613 | | [options] | Object
| | options for construction |
614 | | [options.r] | Float
| 1
| radius of the circle |
615 | | [options.fn] | Integer
| 32
| segments of circle (ie quality/ resolution) |
616 | | [options.center] | Boolean
| true
| wether to center the circle or not |
617 |
618 | **Example**
619 | ```js
620 | let circle1 = circle({
621 | r: 10
622 | })
623 | ```
624 |
625 |
626 | ## polygon([options]) ⇒ CAG
627 | Construct a polygon either from arrays of paths and points, or just arrays of points
628 | nested paths (multiple paths) and flat paths are supported
629 |
630 | **Kind**: global function
631 | **Returns**: CAG
- new polygon
632 |
633 | | Param | Type | Description |
634 | | --- | --- | --- |
635 | | [options] | Object
| options for construction |
636 | | [options.paths] | Array
| paths of the polygon : either flat or nested array |
637 | | [options.points] | Array
| points of the polygon : either flat or nested array |
638 |
639 | **Example**
640 | ```js
641 | let poly = polygon([0,1,2,3,4])
642 | or
643 | let poly = polygon({path: [0,1,2,3,4]})
644 | or
645 | let poly = polygon({path: [0,1,2,3,4], points: [2,1,3]})
646 | ```
647 |
648 |
649 | ## triangle() ⇒ CAG
650 | Construct a triangle
651 |
652 | **Kind**: global function
653 | **Returns**: CAG
- new triangle
654 | **Example**
655 | ```js
656 | let triangle = trangle({
657 | length: 10
658 | })
659 | ```
660 |
661 |
662 | ## cube([options]) ⇒ CSG
663 | Construct a cuboid
664 |
665 | **Kind**: global function
666 | **Returns**: CSG
- new sphere
667 |
668 | | Param | Type | Default | Description |
669 | | --- | --- | --- | --- |
670 | | [options] | Object
| | options for construction |
671 | | [options.size] | Float
| 1
| size of the side of the cuboid : can be either: - a scalar : ie a single float, in which case all dimensions will be the same - or an array: to specify different dimensions along x/y/z |
672 | | [options.fn] | Integer
| 32
| segments of the sphere (ie quality/resolution) |
673 | | [options.fno] | Integer
| 32
| segments of extrusion (ie quality) |
674 | | [options.type] | String
| 'normal'
| type of sphere : either 'normal' or 'geodesic' |
675 |
676 | **Example**
677 | ```js
678 | let cube1 = cube({
679 | r: 10,
680 | fn: 20
681 | })
682 | ```
683 |
684 |
685 | ## sphere([options]) ⇒ CSG
686 | Construct a sphere
687 |
688 | **Kind**: global function
689 | **Returns**: CSG
- new sphere
690 |
691 | | Param | Type | Default | Description |
692 | | --- | --- | --- | --- |
693 | | [options] | Object
| | options for construction |
694 | | [options.r] | Float
| 1
| radius of the sphere |
695 | | [options.fn] | Integer
| 32
| segments of the sphere (ie quality/resolution) |
696 | | [options.fno] | Integer
| 32
| segments of extrusion (ie quality) |
697 | | [options.type] | String
| 'normal'
| type of sphere : either 'normal' or 'geodesic' |
698 |
699 | **Example**
700 | ```js
701 | let sphere1 = sphere({
702 | r: 10,
703 | fn: 20
704 | })
705 | ```
706 |
707 |
708 | ## cylinder([options]) ⇒ CSG
709 | Construct a cylinder
710 |
711 | **Kind**: global function
712 | **Returns**: CSG
- new cylinder
713 |
714 | | Param | Type | Default | Description |
715 | | --- | --- | --- | --- |
716 | | [options] | Object
| | options for construction |
717 | | [options.r] | Float
| 1
| radius of the cylinder |
718 | | [options.r1] | Float
| 1
| radius of the top of the cylinder |
719 | | [options.r2] | Float
| 1
| radius of the bottom of the cylinder |
720 | | [options.d] | Float
| 1
| diameter of the cylinder |
721 | | [options.d1] | Float
| 1
| diameter of the top of the cylinder |
722 | | [options.d2] | Float
| 1
| diameter of the bottom of the cylinder |
723 | | [options.fn] | Integer
| 32
| number of sides of the cylinder (ie quality/resolution) |
724 |
725 | **Example**
726 | ```js
727 | let cylinder = cylinder({
728 | d: 10,
729 | fn: 20
730 | })
731 | ```
732 |
733 |
734 | ## torus([options]) ⇒ CSG
735 | Construct a torus
736 |
737 | **Kind**: global function
738 | **Returns**: CSG
- new torus
739 |
740 | | Param | Type | Default | Description |
741 | | --- | --- | --- | --- |
742 | | [options] | Object
| | options for construction |
743 | | [options.ri] | Float
| 1
| radius of base circle |
744 | | [options.ro] | Float
| 4
| radius offset |
745 | | [options.fni] | Integer
| 16
| segments of base circle (ie quality) |
746 | | [options.fno] | Integer
| 32
| segments of extrusion (ie quality) |
747 | | [options.roti] | Integer
| 0
| rotation angle of base circle |
748 |
749 | **Example**
750 | ```js
751 | let torus1 = torus({
752 | ri: 10
753 | })
754 | ```
755 |
756 |
757 | ## polyhedron([options]) ⇒ CSG
758 | Construct a polyhedron from the given triangles/ polygons/points
759 |
760 | **Kind**: global function
761 | **Returns**: CSG
- new polyhedron
762 |
763 | | Param | Type | Description |
764 | | --- | --- | --- |
765 | | [options] | Object
| options for construction |
766 | | [options.triangles] | Array
| triangles to build the polyhedron from |
767 | | [options.polygons] | Array
| polygons to build the polyhedron from |
768 | | [options.points] | Array
| points to build the polyhedron from |
769 | | [options.colors] | Array
| colors to apply to the polyhedron |
770 |
771 | **Example**
772 | ```js
773 | let torus1 = polyhedron({
774 | points: [...]
775 | })
776 | ```
777 |
778 |
779 | ## vector_char(x, y, char) ⇒ Object
780 | Construct a with, segments tupple from a character
781 |
782 | **Kind**: global function
783 | **Returns**: Object
- { width: X, segments: [...] }
784 |
785 | | Param | Type | Description |
786 | | --- | --- | --- |
787 | | x | Float
| x offset |
788 | | y | Float
| y offset |
789 | | char | Float
| character |
790 |
791 | **Example**
792 | ```js
793 | let charData = vector_char(0, 12.2, 'b')
794 | ```
795 |
796 |
797 | ## vector_text(x, y, string) ⇒ Array
798 | Construct an array of with, segments tupple from a string
799 |
800 | **Kind**: global function
801 | **Returns**: Array
- [{ width: X, segments: [...] }]
802 |
803 | | Param | Type | Description |
804 | | --- | --- | --- |
805 | | x | Float
| x offset |
806 | | y | Float
| y offset |
807 | | string | Float
| string |
808 |
809 | **Example**
810 | ```js
811 | let stringData = vector_text(0, 12.2, 'b')
812 | ```
813 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jscad/scad-api",
3 | "version": "0.5.1",
4 | "description": "OpenSCAD like modeling api for OpenJSCAD & co",
5 | "repository": "https://github.com/jscad/scad-api",
6 | "main": "src/index.js",
7 | "module": "src/index.js",
8 | "scripts": {
9 | "test": "nyc ava './src/**/*.test.js' --verbose --timeout=1m",
10 | "docs": "jsdoc2md --files 'src/*.js' > docs/api.md",
11 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
12 | "preversion": "npm test",
13 | "version": "npm run changelog && npm run docs && git add -A ",
14 | "postversion": "git push origin master && git push origin master --tags",
15 | "release-patch": "git checkout master && git pull origin master && npm version patch",
16 | "release-minor": "git checkout master && git pull origin master && npm version minor",
17 | "release-major": "git checkout master && git pull origin master && npm version major"
18 | },
19 | "contributors": [
20 | {
21 | "name": "Rene K. Mueller",
22 | "url": "http://renekmueller.com"
23 | },
24 | {
25 | "name": "z3dev",
26 | "url": "http://www.z3d.jp"
27 | },
28 | {
29 | "name": "Mark 'kaosat-dev' Moissette",
30 | "url": "http://kaosat.net"
31 | }
32 | ],
33 | "keywords": [
34 | "openscad",
35 | "openjscad",
36 | "jscad",
37 | "parametric",
38 | "modeling",
39 | "cad",
40 | "api"
41 | ],
42 | "license": "MIT",
43 | "dependencies": {
44 | "@jscad/csg": "0.3.7"
45 | },
46 | "devDependencies": {
47 | "ava": "^0.23.0",
48 | "conventional-changelog-cli": "^1.3.4",
49 | "jsdoc-to-markdown": "^3.0.0",
50 | "nyc": "^11.0.2"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/clone.js:
--------------------------------------------------------------------------------
1 |
2 | /** clone the given object
3 | * @param {Object} obj - the object to clone by
4 | * @returns {CSG} new CSG object , a copy of the input
5 | *
6 | * @example
7 | * let copy = clone(sphere())
8 | */
9 | function clone (obj) {
10 | if (obj === null || typeof obj !== 'object') return obj
11 | var copy = obj.constructor()
12 | for (var attr in obj) {
13 | if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr]
14 | }
15 | return copy
16 | }
17 |
18 | module.exports = {
19 | clone
20 | }
21 |
--------------------------------------------------------------------------------
/src/clone.test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava')
2 | const { cube } = require('./primitives3d')
3 | const { clone } = require('./clone')
4 |
5 | test('clone', t => {
6 | const obs = clone(cube())
7 | const expFirstPoly = {
8 | vertices: [ { pos: { _x: 0, _y: 0, _z: 0 } },
9 | { pos: { _x: 0, _y: 0, _z: 1 } },
10 | { pos: { _x: 0, _y: 1, _z: 1 } },
11 | { pos: { _x: 0, _y: 1, _z: 0 } } ],
12 | shared: { color: null },
13 | plane: { normal: { _x: -1, _y: -0, _z: -0 }, w: -0 }
14 | }
15 |
16 | const expLastPoly = { vertices: [ { pos: { _x: 0, _y: 0, _z: 1 } },
17 | { pos: { _x: 1, _y: 0, _z: 1 } },
18 | { pos: { _x: 1, _y: 1, _z: 1 } },
19 | { pos: { _x: 0, _y: 1, _z: 1 } } ],
20 | shared: { color: null },
21 | plane: { normal: { _x: 0, _y: -0, _z: 1 }, w: 1 } }
22 |
23 | t.deepEqual(obs.properties.cube.center, {_x: 0.5, _y: 0.5, _z: 0.5})
24 | t.deepEqual(obs.polygons.length, 6)
25 | t.deepEqual(obs.polygons[0], expFirstPoly)
26 | t.deepEqual(obs.polygons[obs.polygons.length - 1], expLastPoly)
27 | })
28 |
--------------------------------------------------------------------------------
/src/color.js:
--------------------------------------------------------------------------------
1 | // color table from http://www.w3.org/TR/css3-color/
2 | const cssColors = {
3 | // basic color keywords
4 | 'black': [ 0 / 255, 0 / 255, 0 / 255 ],
5 | 'silver': [ 192 / 255, 192 / 255, 192 / 255 ],
6 | 'gray': [ 128 / 255, 128 / 255, 128 / 255 ],
7 | 'white': [ 255 / 255, 255 / 255, 255 / 255 ],
8 | 'maroon': [ 128 / 255, 0 / 255, 0 / 255 ],
9 | 'red': [ 255 / 255, 0 / 255, 0 / 255 ],
10 | 'purple': [ 128 / 255, 0 / 255, 128 / 255 ],
11 | 'fuchsia': [ 255 / 255, 0 / 255, 255 / 255 ],
12 | 'green': [ 0 / 255, 128 / 255, 0 / 255 ],
13 | 'lime': [ 0 / 255, 255 / 255, 0 / 255 ],
14 | 'olive': [ 128 / 255, 128 / 255, 0 / 255 ],
15 | 'yellow': [ 255 / 255, 255 / 255, 0 / 255 ],
16 | 'navy': [ 0 / 255, 0 / 255, 128 / 255 ],
17 | 'blue': [ 0 / 255, 0 / 255, 255 / 255 ],
18 | 'teal': [ 0 / 255, 128 / 255, 128 / 255 ],
19 | 'aqua': [ 0 / 255, 255 / 255, 255 / 255 ],
20 | // extended color keywords
21 | 'aliceblue': [ 240 / 255, 248 / 255, 255 / 255 ],
22 | 'antiquewhite': [ 250 / 255, 235 / 255, 215 / 255 ],
23 | // 'aqua': [ 0 / 255, 255 / 255, 255 / 255 ],
24 | 'aquamarine': [ 127 / 255, 255 / 255, 212 / 255 ],
25 | 'azure': [ 240 / 255, 255 / 255, 255 / 255 ],
26 | 'beige': [ 245 / 255, 245 / 255, 220 / 255 ],
27 | 'bisque': [ 255 / 255, 228 / 255, 196 / 255 ],
28 | // 'black': [ 0 / 255, 0 / 255, 0 / 255 ],
29 | 'blanchedalmond': [ 255 / 255, 235 / 255, 205 / 255 ],
30 | // 'blue': [ 0 / 255, 0 / 255, 255 / 255 ],
31 | 'blueviolet': [ 138 / 255, 43 / 255, 226 / 255 ],
32 | 'brown': [ 165 / 255, 42 / 255, 42 / 255 ],
33 | 'burlywood': [ 222 / 255, 184 / 255, 135 / 255 ],
34 | 'cadetblue': [ 95 / 255, 158 / 255, 160 / 255 ],
35 | 'chartreuse': [ 127 / 255, 255 / 255, 0 / 255 ],
36 | 'chocolate': [ 210 / 255, 105 / 255, 30 / 255 ],
37 | 'coral': [ 255 / 255, 127 / 255, 80 / 255 ],
38 | 'cornflowerblue': [ 100 / 255, 149 / 255, 237 / 255 ],
39 | 'cornsilk': [ 255 / 255, 248 / 255, 220 / 255 ],
40 | 'crimson': [ 220 / 255, 20 / 255, 60 / 255 ],
41 | 'cyan': [ 0 / 255, 255 / 255, 255 / 255 ],
42 | 'darkblue': [ 0 / 255, 0 / 255, 139 / 255 ],
43 | 'darkcyan': [ 0 / 255, 139 / 255, 139 / 255 ],
44 | 'darkgoldenrod': [ 184 / 255, 134 / 255, 11 / 255 ],
45 | 'darkgray': [ 169 / 255, 169 / 255, 169 / 255 ],
46 | 'darkgreen': [ 0 / 255, 100 / 255, 0 / 255 ],
47 | 'darkgrey': [ 169 / 255, 169 / 255, 169 / 255 ],
48 | 'darkkhaki': [ 189 / 255, 183 / 255, 107 / 255 ],
49 | 'darkmagenta': [ 139 / 255, 0 / 255, 139 / 255 ],
50 | 'darkolivegreen': [ 85 / 255, 107 / 255, 47 / 255 ],
51 | 'darkorange': [ 255 / 255, 140 / 255, 0 / 255 ],
52 | 'darkorchid': [ 153 / 255, 50 / 255, 204 / 255 ],
53 | 'darkred': [ 139 / 255, 0 / 255, 0 / 255 ],
54 | 'darksalmon': [ 233 / 255, 150 / 255, 122 / 255 ],
55 | 'darkseagreen': [ 143 / 255, 188 / 255, 143 / 255 ],
56 | 'darkslateblue': [ 72 / 255, 61 / 255, 139 / 255 ],
57 | 'darkslategray': [ 47 / 255, 79 / 255, 79 / 255 ],
58 | 'darkslategrey': [ 47 / 255, 79 / 255, 79 / 255 ],
59 | 'darkturquoise': [ 0 / 255, 206 / 255, 209 / 255 ],
60 | 'darkviolet': [ 148 / 255, 0 / 255, 211 / 255 ],
61 | 'deeppink': [ 255 / 255, 20 / 255, 147 / 255 ],
62 | 'deepskyblue': [ 0 / 255, 191 / 255, 255 / 255 ],
63 | 'dimgray': [ 105 / 255, 105 / 255, 105 / 255 ],
64 | 'dimgrey': [ 105 / 255, 105 / 255, 105 / 255 ],
65 | 'dodgerblue': [ 30 / 255, 144 / 255, 255 / 255 ],
66 | 'firebrick': [ 178 / 255, 34 / 255, 34 / 255 ],
67 | 'floralwhite': [ 255 / 255, 250 / 255, 240 / 255 ],
68 | 'forestgreen': [ 34 / 255, 139 / 255, 34 / 255 ],
69 | // 'fuchsia': [ 255 / 255, 0 / 255, 255 / 255 ],
70 | 'gainsboro': [ 220 / 255, 220 / 255, 220 / 255 ],
71 | 'ghostwhite': [ 248 / 255, 248 / 255, 255 / 255 ],
72 | 'gold': [ 255 / 255, 215 / 255, 0 / 255 ],
73 | 'goldenrod': [ 218 / 255, 165 / 255, 32 / 255 ],
74 | // 'gray': [ 128 / 255, 128 / 255, 128 / 255 ],
75 | // 'green': [ 0 / 255, 128 / 255, 0 / 255 ],
76 | 'greenyellow': [ 173 / 255, 255 / 255, 47 / 255 ],
77 | 'grey': [ 128 / 255, 128 / 255, 128 / 255 ],
78 | 'honeydew': [ 240 / 255, 255 / 255, 240 / 255 ],
79 | 'hotpink': [ 255 / 255, 105 / 255, 180 / 255 ],
80 | 'indianred': [ 205 / 255, 92 / 255, 92 / 255 ],
81 | 'indigo': [ 75 / 255, 0 / 255, 130 / 255 ],
82 | 'ivory': [ 255 / 255, 255 / 255, 240 / 255 ],
83 | 'khaki': [ 240 / 255, 230 / 255, 140 / 255 ],
84 | 'lavender': [ 230 / 255, 230 / 255, 250 / 255 ],
85 | 'lavenderblush': [ 255 / 255, 240 / 255, 245 / 255 ],
86 | 'lawngreen': [ 124 / 255, 252 / 255, 0 / 255 ],
87 | 'lemonchiffon': [ 255 / 255, 250 / 255, 205 / 255 ],
88 | 'lightblue': [ 173 / 255, 216 / 255, 230 / 255 ],
89 | 'lightcoral': [ 240 / 255, 128 / 255, 128 / 255 ],
90 | 'lightcyan': [ 224 / 255, 255 / 255, 255 / 255 ],
91 | 'lightgoldenrodyellow': [ 250 / 255, 250 / 255, 210 / 255 ],
92 | 'lightgray': [ 211 / 255, 211 / 255, 211 / 255 ],
93 | 'lightgreen': [ 144 / 255, 238 / 255, 144 / 255 ],
94 | 'lightgrey': [ 211 / 255, 211 / 255, 211 / 255 ],
95 | 'lightpink': [ 255 / 255, 182 / 255, 193 / 255 ],
96 | 'lightsalmon': [ 255 / 255, 160 / 255, 122 / 255 ],
97 | 'lightseagreen': [ 32 / 255, 178 / 255, 170 / 255 ],
98 | 'lightskyblue': [ 135 / 255, 206 / 255, 250 / 255 ],
99 | 'lightslategray': [ 119 / 255, 136 / 255, 153 / 255 ],
100 | 'lightslategrey': [ 119 / 255, 136 / 255, 153 / 255 ],
101 | 'lightsteelblue': [ 176 / 255, 196 / 255, 222 / 255 ],
102 | 'lightyellow': [ 255 / 255, 255 / 255, 224 / 255 ],
103 | // 'lime': [ 0 / 255, 255 / 255, 0 / 255 ],
104 | 'limegreen': [ 50 / 255, 205 / 255, 50 / 255 ],
105 | 'linen': [ 250 / 255, 240 / 255, 230 / 255 ],
106 | 'magenta': [ 255 / 255, 0 / 255, 255 / 255 ],
107 | // 'maroon': [ 128 / 255, 0 / 255, 0 / 255 ],
108 | 'mediumaquamarine': [ 102 / 255, 205 / 255, 170 / 255 ],
109 | 'mediumblue': [ 0 / 255, 0 / 255, 205 / 255 ],
110 | 'mediumorchid': [ 186 / 255, 85 / 255, 211 / 255 ],
111 | 'mediumpurple': [ 147 / 255, 112 / 255, 219 / 255 ],
112 | 'mediumseagreen': [ 60 / 255, 179 / 255, 113 / 255 ],
113 | 'mediumslateblue': [ 123 / 255, 104 / 255, 238 / 255 ],
114 | 'mediumspringgreen': [ 0 / 255, 250 / 255, 154 / 255 ],
115 | 'mediumturquoise': [ 72 / 255, 209 / 255, 204 / 255 ],
116 | 'mediumvioletred': [ 199 / 255, 21 / 255, 133 / 255 ],
117 | 'midnightblue': [ 25 / 255, 25 / 255, 112 / 255 ],
118 | 'mintcream': [ 245 / 255, 255 / 255, 250 / 255 ],
119 | 'mistyrose': [ 255 / 255, 228 / 255, 225 / 255 ],
120 | 'moccasin': [ 255 / 255, 228 / 255, 181 / 255 ],
121 | 'navajowhite': [ 255 / 255, 222 / 255, 173 / 255 ],
122 | // 'navy': [ 0 / 255, 0 / 255, 128 / 255 ],
123 | 'oldlace': [ 253 / 255, 245 / 255, 230 / 255 ],
124 | // 'olive': [ 128 / 255, 128 / 255, 0 / 255 ],
125 | 'olivedrab': [ 107 / 255, 142 / 255, 35 / 255 ],
126 | 'orange': [ 255 / 255, 165 / 255, 0 / 255 ],
127 | 'orangered': [ 255 / 255, 69 / 255, 0 / 255 ],
128 | 'orchid': [ 218 / 255, 112 / 255, 214 / 255 ],
129 | 'palegoldenrod': [ 238 / 255, 232 / 255, 170 / 255 ],
130 | 'palegreen': [ 152 / 255, 251 / 255, 152 / 255 ],
131 | 'paleturquoise': [ 175 / 255, 238 / 255, 238 / 255 ],
132 | 'palevioletred': [ 219 / 255, 112 / 255, 147 / 255 ],
133 | 'papayawhip': [ 255 / 255, 239 / 255, 213 / 255 ],
134 | 'peachpuff': [ 255 / 255, 218 / 255, 185 / 255 ],
135 | 'peru': [ 205 / 255, 133 / 255, 63 / 255 ],
136 | 'pink': [ 255 / 255, 192 / 255, 203 / 255 ],
137 | 'plum': [ 221 / 255, 160 / 255, 221 / 255 ],
138 | 'powderblue': [ 176 / 255, 224 / 255, 230 / 255 ],
139 | // 'purple': [ 128 / 255, 0 / 255, 128 / 255 ],
140 | // 'red': [ 255 / 255, 0 / 255, 0 / 255 ],
141 | 'rosybrown': [ 188 / 255, 143 / 255, 143 / 255 ],
142 | 'royalblue': [ 65 / 255, 105 / 255, 225 / 255 ],
143 | 'saddlebrown': [ 139 / 255, 69 / 255, 19 / 255 ],
144 | 'salmon': [ 250 / 255, 128 / 255, 114 / 255 ],
145 | 'sandybrown': [ 244 / 255, 164 / 255, 96 / 255 ],
146 | 'seagreen': [ 46 / 255, 139 / 255, 87 / 255 ],
147 | 'seashell': [ 255 / 255, 245 / 255, 238 / 255 ],
148 | 'sienna': [ 160 / 255, 82 / 255, 45 / 255 ],
149 | // 'silver': [ 192 / 255, 192 / 255, 192 / 255 ],
150 | 'skyblue': [ 135 / 255, 206 / 255, 235 / 255 ],
151 | 'slateblue': [ 106 / 255, 90 / 255, 205 / 255 ],
152 | 'slategray': [ 112 / 255, 128 / 255, 144 / 255 ],
153 | 'slategrey': [ 112 / 255, 128 / 255, 144 / 255 ],
154 | 'snow': [ 255 / 255, 250 / 255, 250 / 255 ],
155 | 'springgreen': [ 0 / 255, 255 / 255, 127 / 255 ],
156 | 'steelblue': [ 70 / 255, 130 / 255, 180 / 255 ],
157 | 'tan': [ 210 / 255, 180 / 255, 140 / 255 ],
158 | // 'teal': [ 0 / 255, 128 / 255, 128 / 255 ],
159 | 'thistle': [ 216 / 255, 191 / 255, 216 / 255 ],
160 | 'tomato': [ 255 / 255, 99 / 255, 71 / 255 ],
161 | 'turquoise': [ 64 / 255, 224 / 255, 208 / 255 ],
162 | 'violet': [ 238 / 255, 130 / 255, 238 / 255 ],
163 | 'wheat': [ 245 / 255, 222 / 255, 179 / 255 ],
164 | // 'white': [ 255 / 255, 255 / 255, 255 / 255 ],
165 | 'whitesmoke': [ 245 / 255, 245 / 255, 245 / 255 ],
166 | // 'yellow': [ 255 / 255, 255 / 255, 0 / 255 ],
167 | 'yellowgreen': [ 154 / 255, 205 / 255, 50 / 255 ]
168 | }
169 |
170 | /**
171 | * Converts an CSS color name to RGB color.
172 | *
173 | * @param String s The CSS color name
174 | * @return Array The RGB representation, or [0,0,0] default
175 | */
176 | function css2rgb (s) {
177 | return cssColors[s.toLowerCase()]
178 | }
179 |
180 | // color( (array[r,g,b] | css-string) [,alpha] (,array[objects] | list of objects) )
181 | /** apply the given color to the input object(s)
182 | * @param {Object} color - either an array or a hex string of color values
183 | * @param {Object|Array} objects either a single or multiple CSG/CAG objects to color
184 | * @returns {CSG} new CSG object , with the given color
185 | *
186 | * @example
187 | * let redSphere = color([1,0,0,1], sphere())
188 | */
189 | function color (color) {
190 | let object
191 | let i = 1
192 | let a = arguments
193 |
194 | // assume first argument is RGB array
195 | // but check if first argument is CSS string
196 | if (typeof color === 'string') {
197 | color = css2rgb(color)
198 | }
199 | // check if second argument is alpha
200 | if (Number.isFinite(a[i])) {
201 | color = color.concat(a[i])
202 | i++
203 | }
204 | // check if next argument is an an array
205 | if (Array.isArray(a[i])) {
206 | a = a[i]
207 | i = 0
208 | } // use this as the list of objects
209 | for (object = a[i++]; i < a.length; i++) {
210 | object = object.union(a[i])
211 | }
212 | return object.setColor(color)
213 | }
214 |
215 | // from http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
216 | /**
217 | * Converts an RGB color value to HSL. Conversion formula
218 | * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
219 | * Assumes r, g, and b are contained in the set [0, 1] and
220 | * returns h, s, and l in the set [0, 1].
221 | *
222 | * @param Number r The red color value
223 | * @param Number g The green color value
224 | * @param Number b The blue color value
225 | * @return Array The HSL representation
226 | */
227 | function rgb2hsl (r, g, b) {
228 | if (r.length) {
229 | b = r[2]
230 | g = r[1]
231 | r = r[0]
232 | }
233 | let max = Math.max(r, g, b)
234 | let min = Math.min(r, g, b)
235 | let h
236 | let s
237 | let l = (max + min) / 2
238 |
239 | if (max === min) {
240 | h = s = 0 // achromatic
241 | } else {
242 | let d = max - min
243 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
244 | switch (max) {
245 | case r:
246 | h = (g - b) / d + (g < b ? 6 : 0)
247 | break
248 | case g:
249 | h = (b - r) / d + 2
250 | break
251 | case b:
252 | h = (r - g) / d + 4
253 | break
254 | }
255 | h /= 6
256 | }
257 |
258 | return [h, s, l]
259 | }
260 |
261 | /**
262 | * Converts an HSL color value to RGB. Conversion formula
263 | * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
264 | * Assumes h, s, and l are contained in the set [0, 1] and
265 | * returns r, g, and b in the set [0, 1].
266 | *
267 | * @param Number h The hue
268 | * @param Number s The saturation
269 | * @param Number l The lightness
270 | * @return Array The RGB representation
271 | */
272 | function hsl2rgb (h, s, l) {
273 | if (h.length) {
274 | h = h[0]
275 | s = h[1]
276 | l = h[2]
277 | }
278 | let r
279 | let g
280 | let b
281 |
282 | if (s === 0) {
283 | r = g = b = l // achromatic
284 | } else {
285 | let q = l < 0.5 ? l * (1 + s) : l + s - l * s
286 | let p = 2 * l - q
287 | r = hue2rgb(p, q, h + 1 / 3)
288 | g = hue2rgb(p, q, h)
289 | b = hue2rgb(p, q, h - 1 / 3)
290 | }
291 |
292 | return [r, g, b]
293 | }
294 |
295 | function hue2rgb (p, q, t) {
296 | if (t < 0) t += 1
297 | if (t > 1) t -= 1
298 | if (t < 1 / 6) return p + (q - p) * 6 * t
299 | if (t < 1 / 2) return q
300 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
301 | return p
302 | }
303 |
304 | /**
305 | * Converts an RGB color value to HSV. Conversion formula
306 | * adapted from http://en.wikipedia.org/wiki/HSV_color_space.
307 | * Assumes r, g, and b are contained in the set [0, 1] and
308 | * returns h, s, and v in the set [0, 1].
309 | *
310 | * @param Number r The red color value
311 | * @param Number g The green color value
312 | * @param Number b The blue color value
313 | * @return Array The HSV representation
314 | */
315 |
316 | function rgb2hsv (r, g, b) {
317 | if (r.length) {
318 | r = r[0]
319 | g = r[1]
320 | b = r[2]
321 | }
322 | let max = Math.max(r, g, b)
323 | let min = Math.min(r, g, b)
324 | let h
325 | let s
326 | let v = max
327 |
328 | let d = max - min
329 | s = max === 0 ? 0 : d / max
330 |
331 | if (max === min) {
332 | h = 0 // achromatic
333 | } else {
334 | switch (max) {
335 | case r:
336 | h = (g - b) / d + (g < b ? 6 : 0)
337 | break
338 | case g:
339 | h = (b - r) / d + 2
340 | break
341 | case b:
342 | h = (r - g) / d + 4
343 | break
344 | }
345 | h /= 6
346 | }
347 |
348 | return [h, s, v]
349 | }
350 |
351 | /**
352 | * Converts an HSV color value to RGB. Conversion formula
353 | * adapted from http://en.wikipedia.org/wiki/HSV_color_space.
354 | * Assumes h, s, and v are contained in the set [0, 1] and
355 | * returns r, g, and b in the set [0, 1].
356 | *
357 | * @param Number h The hue
358 | * @param Number s The saturation
359 | * @param Number v The value
360 | * @return Array The RGB representation
361 | */
362 | function hsv2rgb (h, s, v) {
363 | if (h.length) {
364 | h = h[0]
365 | s = h[1]
366 | v = h[2]
367 | }
368 | let r, g, b
369 |
370 | let i = Math.floor(h * 6)
371 | let f = h * 6 - i
372 | let p = v * (1 - s)
373 | let q = v * (1 - f * s)
374 | let t = v * (1 - (1 - f) * s)
375 |
376 | switch (i % 6) {
377 | case 0:
378 | r = v, g = t, b = p
379 | break
380 | case 1:
381 | r = q, g = v, b = p
382 | break
383 | case 2:
384 | r = p, g = v, b = t
385 | break
386 | case 3:
387 | r = p, g = q, b = v
388 | break
389 | case 4:
390 | r = t, g = p, b = v
391 | break
392 | case 5:
393 | r = v, g = p, b = q
394 | break
395 | }
396 |
397 | return [r, g, b]
398 | }
399 |
400 | /**
401 | * Converts a HTML5 color value (string) to RGB values
402 | * See the color input type of HTML5 forms
403 | * Conversion formula:
404 | * - split the string; "#RRGGBB" into RGB components
405 | * - convert the HEX value into RGB values
406 | */
407 | function html2rgb (s) {
408 | let r = 0
409 | let g = 0
410 | let b = 0
411 | if (s.length === 7) {
412 | r = parseInt('0x' + s.slice(1, 3)) / 255
413 | g = parseInt('0x' + s.slice(3, 5)) / 255
414 | b = parseInt('0x' + s.slice(5, 7)) / 255
415 | }
416 | return [r, g, b]
417 | }
418 |
419 | /**
420 | * Converts RGB color value to HTML5 color value (string)
421 | * Conversion forumla:
422 | * - convert R, G, B into HEX strings
423 | * - return HTML formatted string "#RRGGBB"
424 | */
425 | function rgb2html (r, g, b) {
426 | if (r.length) {
427 | r = r[0]
428 | g = r[1]
429 | b = r[2]
430 | }
431 | let s = '#' +
432 | Number(0x1000000 + r * 255 * 0x10000 + g * 255 * 0x100 + b * 255).toString(16).substring(1, 7)
433 | return s
434 | }
435 |
436 | module.exports = {
437 | css2rgb,
438 | color,
439 | rgb2hsl,
440 | hsl2rgb,
441 | rgb2hsv,
442 | hsv2rgb,
443 | html2rgb,
444 | rgb2html
445 | }
446 |
--------------------------------------------------------------------------------
/src/color.test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava')
2 | const { cube, sphere, cylinder } = require('./primitives3d')
3 | const { square, circle } = require('./primitives2d')
4 | const { color, rgb2hsl, hsl2rgb, rgb2hsv, hsv2rgb, html2rgb, rgb2html, css2rgb } = require('./color')
5 |
6 | test('css2rgb', t => {
7 | const c1 = css2rgb('black')
8 | const e1 = [0 / 255, 0 / 255, 0 / 255]
9 | t.deepEqual(c1, e1)
10 |
11 | const c2 = css2rgb('yellowgreen')
12 | const e2 = [154 / 255, 205 / 255, 50 / 255]
13 | t.deepEqual(c2, e2)
14 |
15 | const c3 = css2rgb('jscad')
16 | })
17 |
18 | test('rgb2hsl', t => {
19 | const obs = rgb2hsl(1, 0, 0)
20 | const expColor = [0, 1, 0.5]
21 |
22 | t.deepEqual(obs, expColor)
23 | })
24 |
25 | test('hsl2rgb', t => {
26 | const obs = hsl2rgb(0, 1, 0)
27 | const expColor = [0, 0, 0]
28 |
29 | t.deepEqual(obs, expColor)
30 | })
31 |
32 | test('rgb2hsv', t => {
33 | const obs = rgb2hsv(1, 0, 0.5)
34 | const expColor = [0.9166666666666666, 1, 1]
35 |
36 | t.deepEqual(obs, expColor)
37 | })
38 |
39 | test('hsv2rgb', t => {
40 | const obs = hsv2rgb(0, 0.2, 0)
41 | const expColor = [0, 0, 0]
42 |
43 | t.deepEqual(obs, expColor)
44 | })
45 |
46 | test('html2rgb', t => {
47 | const obs = html2rgb('#000000')
48 | const expColor = [0, 0, 0]
49 |
50 | t.deepEqual(obs, expColor)
51 | })
52 |
53 | test('rgb2html', t => {
54 | const html = rgb2html(1, 0, 0.5)
55 | const expHtml = '#ff007f'
56 |
57 | t.deepEqual(html, expHtml)
58 | })
59 |
60 | test('color (rgb, on 3d objects)', t => {
61 | const obs = color([1, 0, 0], cube(), sphere())
62 | const expColor = { color: [ 1, 0, 0, 1 ] }
63 |
64 | t.deepEqual(obs.polygons[0].shared, expColor)
65 | t.deepEqual(obs.polygons[obs.polygons.length - 1].shared, expColor)
66 | })
67 |
68 | test.failing('color (rgb, on 2d objects)', t => {
69 | const obs = color([1, 0, 0], square(), circle())
70 | const expColor = { color: [ 1, 0, 0, 1 ] }
71 |
72 | t.deepEqual(obs.sides[0].shared, expColor)
73 | t.deepEqual(obs.sides[obs.sides.length - 1].shared, expColor)
74 | })
75 |
76 | test('color (rgba, on 3d objects)', t => {
77 | const obs = color([1, 0, 0, 0.5], cube(), sphere())
78 | const expColor = { color: [ 1, 0, 0, 0.5 ] }
79 |
80 | t.deepEqual(obs.polygons[0].shared, expColor)
81 | t.deepEqual(obs.polygons[obs.polygons.length - 1].shared, expColor)
82 | })
83 |
84 | test.failing('color (rgba, on 2d objects)', t => {
85 | const obs = color([1, 0, 0, 0.5], square(), circle())
86 | const expColor = { color: [ 1, 0, 0, 0.5 ] }
87 |
88 | t.deepEqual(obs.sides[0].shared, expColor)
89 | t.deepEqual(obs.sides[obs.sides.length - 1].shared, expColor)
90 | })
91 |
92 | test('color (rgba, on array of 3D objects)', t => {
93 | const obs = color([1, 0, 0, 0.5], [cube(), sphere()])
94 | const expColor = { color: [ 1, 0, 0, 0.5 ] }
95 |
96 | t.deepEqual(obs.polygons[0].shared, expColor)
97 | t.deepEqual(obs.polygons[obs.polygons.length - 1].shared, expColor)
98 | })
99 |
100 | test.failing('color (rgba, on array of 2d objects)', t => {
101 | const obs = color([1, 0, 0, 0.5], [square(), circle()])
102 | const expColor = { color: [ 1, 0, 0, 0.5 ] }
103 |
104 | t.deepEqual(obs.sides[0].shared, expColor)
105 | t.deepEqual(obs.sides[obs.sides.length - 1].shared, expColor)
106 | })
107 |
108 | test('color (by name, on 3d objects)', t => {
109 | var obs = color('red', cube())
110 | var expColor = { color: [ 1, 0, 0, 1 ] }
111 |
112 | t.deepEqual(obs.polygons[0].shared, expColor)
113 | t.deepEqual(obs.polygons[obs.polygons.length - 1].shared, expColor)
114 |
115 | obs = color('green', cube(), sphere())
116 | expColor = { color: [ 0, 128 / 255, 0, 1 ] }
117 |
118 | t.deepEqual(obs.polygons[0].shared, expColor)
119 | t.deepEqual(obs.polygons[obs.polygons.length - 1].shared, expColor)
120 |
121 | obs = color('blue', cube(), sphere(), cylinder())
122 | expColor = { color: [ 0, 0, 1, 1 ] }
123 |
124 | t.deepEqual(obs.polygons[0].shared, expColor)
125 | t.deepEqual(obs.polygons[obs.polygons.length - 1].shared, expColor)
126 | })
127 |
128 | test.failing('color (by name, on 2d objects)', t => {
129 | const obs = color('red', square(), circle())
130 | const expColor = { color: [ 1, 0, 0, 1 ] }
131 |
132 | t.deepEqual(obs.sides[0].shared, expColor)
133 | t.deepEqual(obs.sides[obs.sides.length - 1].shared, expColor)
134 | })
135 |
136 | test('color (by name and alpha, on 3d objects)', t => {
137 | var obs = color('red', 0.5, cube())
138 | var expColor = { color: [ 1, 0, 0, 0.5 ] }
139 |
140 | t.deepEqual(obs.polygons[0].shared, expColor)
141 | t.deepEqual(obs.polygons[obs.polygons.length - 1].shared, expColor)
142 |
143 | obs = color('green', 0.8, cube(), sphere())
144 | expColor = { color: [ 0, 128 / 255, 0, 0.8 ] }
145 |
146 | t.deepEqual(obs.polygons[0].shared, expColor)
147 | t.deepEqual(obs.polygons[obs.polygons.length - 1].shared, expColor)
148 |
149 | obs = color('blue', 0.2, cube(), sphere(), cylinder())
150 | expColor = { color: [ 0, 0, 1, 0.2 ] }
151 |
152 | t.deepEqual(obs.polygons[0].shared, expColor)
153 | t.deepEqual(obs.polygons[obs.polygons.length - 1].shared, expColor)
154 | })
155 |
156 | test.failing('color (by name and alpha, on 2d objects)', t => {
157 | const obs = color('red', 0.1, square(), circle())
158 | const expColor = { color: [ 1, 0, 0, 0.5 ] }
159 |
160 | t.deepEqual(obs.sides[0].shared, expColor)
161 | t.deepEqual(obs.sides[obs.sides.length - 1].shared, expColor)
162 | })
163 |
164 | test('color (by name and alpha, on array of 3d objects)', t => {
165 | const obs = color('red', 0.7, [cube(), sphere()])
166 | const expColor = { color: [ 1, 0, 0, 0.7 ] }
167 |
168 | t.deepEqual(obs.polygons[0].shared, expColor)
169 | t.deepEqual(obs.polygons[obs.polygons.length - 1].shared, expColor)
170 | })
171 |
172 | test.failing('color (by name and alpha, on array of 2d objects)', t => {
173 | const obs = color('red', 0.5, [square(), circle()])
174 | const expColor = { color: [ 1, 0, 0, 0.5 ] }
175 |
176 | t.deepEqual(obs.sides[0].shared, expColor)
177 | t.deepEqual(obs.sides[obs.sides.length - 1].shared, expColor)
178 | })
179 |
--------------------------------------------------------------------------------
/src/debug.js:
--------------------------------------------------------------------------------
1 | function echo () {
2 | console.warn('echo() will be deprecated in the near future: please use console.log/warn/error instead')
3 | var s = '', a = arguments
4 | for (var i = 0; i < a.length; i++) {
5 | if (i) s += ', '
6 | s += a[i]
7 | }
8 | // var t = (new Date()-global.time)/1000
9 | // console.log(t,s)
10 | console.log(s)
11 | }
12 |
13 | module.exports = {
14 | echo
15 | }
16 |
--------------------------------------------------------------------------------
/src/group.js:
--------------------------------------------------------------------------------
1 | const {CAG} = require('@jscad/csg')
2 |
3 | // FIXME : is this used anywhere ?
4 | function group () { // experimental
5 | let o
6 | let i = 0
7 | let a = arguments
8 | if (a[0].length) a = a[0]
9 |
10 | if ((typeof (a[i]) === 'object') && (a[i] instanceof CAG)) {
11 | o = a[i].extrude({offset: [0, 0, 0.1]}) // -- convert a 2D shape to a thin solid, note: do not a[i] = a[i].extrude()
12 | } else {
13 | o = a[i++]
14 | }
15 | for (; i < a.length; i++) {
16 | let obj = a[i]
17 | if ((typeof (a[i]) === 'object') && (a[i] instanceof CAG)) {
18 | obj = a[i].extrude({offset: [0, 0, 0.1]}) // -- convert a 2D shape to a thin solid:
19 | }
20 | o = o.unionForNonIntersecting(obj)
21 | }
22 | return o
23 | }
24 |
25 | module.exports = group
26 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | const { CSG } = require('@jscad/csg')
2 |
3 | // FIXME: this is to have more readable/less extremely verbose code below
4 | const vertexFromVectorArray = array => {
5 | return new CSG.Vertex(new CSG.Vector3D(array))
6 | }
7 |
8 | const polygonFromPoints = points => {
9 | // EEK talk about wrapping wrappers !
10 | const vertices = points.map(point => new CSG.Vertex(new CSG.Vector3D(point)))
11 | return new CSG.Polygon(vertices)
12 | }
13 |
14 | // Simplified, array vector rightMultiply1x3Vector
15 | const rightMultiply1x3VectorToArray = (matrix, vector) => {
16 | const [v0, v1, v2] = vector
17 | const v3 = 1
18 | let x = v0 * matrix.elements[0] + v1 * matrix.elements[1] + v2 * matrix.elements[2] + v3 * matrix.elements[3]
19 | let y = v0 * matrix.elements[4] + v1 * matrix.elements[5] + v2 * matrix.elements[6] + v3 * matrix.elements[7]
20 | let z = v0 * matrix.elements[8] + v1 * matrix.elements[9] + v2 * matrix.elements[10] + v3 * matrix.elements[11]
21 | let w = v0 * matrix.elements[12] + v1 * matrix.elements[13] + v2 * matrix.elements[14] + v3 * matrix.elements[15]
22 |
23 | // scale such that fourth element becomes 1:
24 | if (w !== 1) {
25 | const invw = 1.0 / w
26 | x *= invw
27 | y *= invw
28 | z *= invw
29 | }
30 | return [x, y, z]
31 | }
32 |
33 | function clamp (value, min, max) {
34 | return Math.min(Math.max(value, min), max)
35 | }
36 |
37 | const cagToPointsArray = input => {
38 | let points
39 | if ('sides' in input) {//this is a cag
40 | points = []
41 | input.sides.forEach(side => {
42 | points.push([side.vertex0.pos.x, side.vertex0.pos.y])
43 | points.push([side.vertex1.pos.x, side.vertex1.pos.y])
44 | })
45 | // cag.sides.map(side => [side.vertex0.pos.x, side.vertex0.pos.y])
46 | //, side.vertex1.pos.x, side.vertex1.pos.y])
47 | // due to the logic of CAG.fromPoints()
48 | // move the first point to the last
49 | /* if (points.length > 0) {
50 | points.push(points.shift())
51 | } */
52 | } else if ('points' in input) {
53 | points = input.points.map(p => ([p.x, p.y]))
54 | }
55 |
56 | return points
57 | }
58 |
59 | const degToRad = deg => (Math.PI / 180) * deg
60 |
61 | module.exports = {cagToPointsArray, clamp, rightMultiply1x3VectorToArray, polygonFromPoints}
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 |
2 | const primitives3d = require('./primitives3d')
3 | const primitives2d = require('./primitives2d')
4 | const booleanOps = require('./ops-booleans')
5 | const transformations = require('./ops-transformations')
6 | const extrusions = require('./ops-extrusions')
7 | const color = require('./color')
8 | const maths = require('./maths')
9 | const text = require('./text')
10 | const { echo } = require('./debug')
11 |
12 | // these are 'external' to this api and we basically just re-export for old api compatibility
13 | // ...needs to be reviewed
14 | const { CAG, CSG } = require('@jscad/csg')
15 | const { log } = require('./log') // FIXME: this is a duplicate of the one in openjscad itself,*/
16 |
17 | // mostly likely needs to be removed since it is in the OpenJsCad namespace anyway, leaving here
18 | // for now
19 |
20 | const exportedApi = {
21 | csg: {CAG, CSG},
22 | primitives2d,
23 | primitives3d,
24 | booleanOps,
25 | transformations,
26 | extrusions,
27 | color,
28 | maths,
29 | text,
30 | OpenJsCad: {OpenJsCad: {log}},
31 | debug: {echo}
32 | }
33 |
34 | module.exports = exportedApi
35 |
--------------------------------------------------------------------------------
/src/log.js:
--------------------------------------------------------------------------------
1 | function log (txt) {
2 | var timeInMs = Date.now()
3 | var prevtime// OpenJsCad.log.prevLogTime
4 | if (!prevtime) prevtime = timeInMs
5 | var deltatime = timeInMs - prevtime
6 | log.prevLogTime = timeInMs
7 | var timefmt = (deltatime * 0.001).toFixed(3)
8 | txt = '[' + timefmt + '] ' + txt
9 | if ((typeof (console) === 'object') && (typeof (console.log) === 'function')) {
10 | console.log(txt)
11 | } else if ((typeof (self) === 'object') && (typeof (self.postMessage) === 'function')) {
12 | self.postMessage({cmd: 'log', txt: txt})
13 | } else throw new Error('Cannot log')
14 | }
15 |
16 | // See Processor.setStatus()
17 | // Note: leave for compatibility
18 | function status (s) {
19 | log(s)
20 | }
21 |
22 | module.exports = {
23 | log,
24 | status
25 | }
26 |
--------------------------------------------------------------------------------
/src/maths.js:
--------------------------------------------------------------------------------
1 | // -- Math functions (360 deg based vs 2pi)
2 | function sin (a) {
3 | return Math.sin(a / 360 * Math.PI * 2)
4 | }
5 | function cos (a) {
6 | return Math.cos(a / 360 * Math.PI * 2)
7 | }
8 | function asin (a) {
9 | return Math.asin(a) / (Math.PI * 2) * 360
10 | }
11 | function acos (a) {
12 | return Math.acos(a) / (Math.PI * 2) * 360
13 | }
14 | function tan (a) {
15 | return Math.tan(a / 360 * Math.PI * 2)
16 | }
17 | function atan (a) {
18 | return Math.atan(a) / (Math.PI * 2) * 360
19 | }
20 | function atan2 (a, b) {
21 | return Math.atan2(a, b) / (Math.PI * 2) * 360
22 | }
23 | function ceil (a) {
24 | return Math.ceil(a)
25 | }
26 | function floor (a) {
27 | return Math.floor(a)
28 | }
29 | function abs (a) {
30 | return Math.abs(a)
31 | }
32 | function min (a, b) {
33 | return a < b ? a : b
34 | }
35 | function max (a, b) {
36 | return a > b ? a : b
37 | }
38 | function rands (min, max, vn, seed) {
39 | // -- seed is ignored for now, FIX IT (requires reimplementation of random())
40 | // see http://stackoverflow.com/questions/424292/how-to-create-my-own-javascript-random-number-generator-that-i-can-also-set-the
41 | var v = new Array(vn)
42 | for (var i = 0; i < vn; i++) {
43 | v[i] = Math.random() * (max - min) + min
44 | }
45 | }
46 | function log (a) {
47 | return Math.log(a)
48 | }
49 | function lookup (ix, v) {
50 | var r = 0
51 | for (var i = 0; i < v.length; i++) {
52 | var a0 = v[i]
53 | if (a0[0] >= ix) {
54 | i--
55 | a0 = v[i]
56 | var a1 = v[i + 1]
57 | var m = 0
58 | if (a0[0] !== a1[0]) {
59 | m = abs((ix - a0[0]) / (a1[0] - a0[0]))
60 | }
61 | // echo(">>",i,ix,a0[0],a1[0],";",m,a0[1],a1[1])
62 | if (m > 0) {
63 | r = a0[1] * (1 - m) + a1[1] * m
64 | } else {
65 | r = a0[1]
66 | }
67 | return r
68 | }
69 | }
70 | return r
71 | }
72 |
73 | function pow (a, b) {
74 | return Math.pow(a, b)
75 | }
76 |
77 | function sign (a) {
78 | return a < 0 ? -1 : (a > 1 ? 1 : 0)
79 | }
80 |
81 | function sqrt (a) {
82 | return Math.sqrt(a)
83 | }
84 |
85 | function round (a) {
86 | return floor(a + 0.5)
87 | }
88 |
89 | module.exports = {
90 | sin,
91 | cos,
92 | asin,
93 | acos,
94 | tan,
95 | atan,
96 | atan2,
97 | ceil,
98 | floor,
99 | abs,
100 | min,
101 | max,
102 | rands,
103 | log,
104 | lookup,
105 | pow,
106 | sign,
107 | sqrt,
108 | round
109 | }
110 |
--------------------------------------------------------------------------------
/src/maths.test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava')
2 | const { lookup } = require('./maths')
3 |
4 | test('lookup', t => {
5 | const values = [
6 | [ -200, 5 ],
7 | [ -50, 20 ],
8 | [ -20, 18 ],
9 | [ +80, 25 ],
10 | [ +150, 2 ]]
11 |
12 | const obs1 = lookup(2, values)
13 | const obs2 = lookup(4.2, values)
14 | const obs3 = lookup(20, values)
15 |
16 | t.deepEqual(obs1, 19.54)
17 | t.deepEqual(obs2, 19.694)
18 | t.deepEqual(obs3, 20.799999999999997)
19 | })
20 |
--------------------------------------------------------------------------------
/src/meta.js:
--------------------------------------------------------------------------------
1 | //FIXME : are ANY of these used anywhere ??
2 | const json = require('../package.json')
3 |
4 | function version () {
5 | return json.version
6 | }
7 |
8 | function JStoMeta (src) {
9 | var l = src.split(/\n/)
10 | var n = 0
11 | var m = []
12 | for (var i = 0; ; i++) {
13 | if (l[i].match(/^\/\/\s*(\S[^:]+):\s*(\S.*)/)) {
14 | var k = RegExp.$1
15 | var v = RegExp.$2
16 | m[k] = v
17 | n++
18 | } else {
19 | if (i > 5 && n === 0)
20 | break
21 | else if (n > 0)
22 | break
23 | }
24 | }
25 | return m
26 | }
27 |
28 | function MetaToJS (m) {
29 | var s = ''
30 | for (var k in m) {
31 | s += '// ' + k + ': ' + m[k] + '\n'
32 | }
33 | return s
34 | }
35 |
36 | module.exports = {
37 | version
38 | }
39 |
--------------------------------------------------------------------------------
/src/ops-booleans.js:
--------------------------------------------------------------------------------
1 | const { CAG } = require('@jscad/csg')
2 |
3 | // -- 3D boolean operations
4 |
5 | // FIXME should this be lazy ? in which case, how do we deal with 2D/3D combined
6 | // TODO we should have an option to set behaviour as first parameter
7 | /** union/ combine the given shapes
8 | * @param {Object(s)|Array} objects - objects to combine : can be given
9 | * - one by one: union(a,b,c) or
10 | * - as an array: union([a,b,c])
11 | * @returns {CSG} new CSG object, the union of all input shapes
12 | *
13 | * @example
14 | * let unionOfSpherAndCube = union(sphere(), cube())
15 | */
16 | function union () {
17 | let options = {}
18 | const defaults = {
19 | extrude2d: false
20 | }
21 | let o
22 | let i = 0
23 | let a = arguments
24 | if (a[0].length) a = a[0]
25 | if ('extrude2d' in a[0]) { // first parameter is options
26 | options = Object.assign({}, defaults, a[0])
27 | o = a[i++]
28 | }
29 |
30 | o = a[i++]
31 |
32 | // TODO: add option to be able to set this?
33 | if ((typeof (a[i]) === 'object') && a[i] instanceof CAG && options.extrude2d) {
34 | o = a[i].extrude({offset: [0, 0, 0.1]}) // -- convert a 2D shape to a thin solid, note: do not a[i] = a[i].extrude()
35 | }
36 | for (; i < a.length; i++) {
37 | let obj = a[i]
38 |
39 | if ((typeof (a[i]) === 'object') && a[i] instanceof CAG && options.extrude2d) {
40 | obj = a[i].extrude({offset: [0, 0, 0.1]}) // -- convert a 2D shape to a thin solid:
41 | }
42 | o = o.union(obj)
43 | }
44 | return o
45 | }
46 |
47 | /** difference/ subtraction of the given shapes ie:
48 | * cut out C From B From A ie : a - b - c etc
49 | * @param {Object(s)|Array} objects - objects to subtract
50 | * can be given
51 | * - one by one: difference(a,b,c) or
52 | * - as an array: difference([a,b,c])
53 | * @returns {CSG} new CSG object, the difference of all input shapes
54 | *
55 | * @example
56 | * let differenceOfSpherAndCube = difference(sphere(), cube())
57 | */
58 | function difference () {
59 | let object
60 | let i = 0
61 | let a = arguments
62 | if (a[0].length) a = a[0]
63 | for (object = a[i++]; i < a.length; i++) {
64 | if (a[i] instanceof CAG) {
65 | object = object.subtract(a[i])
66 | } else {
67 | object = object.subtract(a[i].setColor(1, 1, 0)) // -- color the cuts
68 | }
69 | }
70 | return object
71 | }
72 |
73 | /** intersection of the given shapes: ie keep only the common parts between the given shapes
74 | * @param {Object(s)|Array} objects - objects to intersect
75 | * can be given
76 | * - one by one: intersection(a,b,c) or
77 | * - as an array: intersection([a,b,c])
78 | * @returns {CSG} new CSG object, the intersection of all input shapes
79 | *
80 | * @example
81 | * let intersectionOfSpherAndCube = intersection(sphere(), cube())
82 | */
83 | function intersection () {
84 | let object
85 | let i = 0
86 | let a = arguments
87 | if (a[0].length) a = a[0]
88 | for (object = a[i++]; i < a.length; i++) {
89 | if (a[i] instanceof CAG) {
90 | object = object.intersect(a[i])
91 | } else {
92 | object = object.intersect(a[i].setColor(1, 1, 0)) // -- color the cuts
93 | }
94 | }
95 | return object
96 | }
97 |
98 | module.exports = {
99 | union,
100 | difference,
101 | intersection
102 | }
103 |
--------------------------------------------------------------------------------
/src/ops-booleans.test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava')
2 | const { cube, sphere } = require('./primitives3d')
3 | const { square } = require('./primitives2d')
4 | const { union, difference, intersection } = require('./ops-booleans')
5 |
6 | test('union (defaults)', t => {
7 | const op1 = cube()
8 | const op2 = cube({size: 10})
9 |
10 | const obs = union(op1, op2)
11 |
12 | t.deepEqual(obs.polygons.length, 6)
13 | })
14 |
15 | test('union (more than 2 operands)', t => {
16 | const op1 = cube()
17 | const op2 = cube()
18 | const op3 = cube({size: 10})
19 |
20 | const obs = union(op1, op2, op3)
21 |
22 | t.deepEqual(obs.polygons.length, 6)
23 | })
24 |
25 | test('union (complex)', t => {
26 | const obs = union(
27 | difference(
28 | cube({size: 3, center: true}),
29 | sphere({r:2, center: true})
30 | ),
31 | intersection(
32 | sphere({r: 1.3, center: true}),
33 | cube({size: 2.1, center: true})
34 | )
35 | )
36 |
37 | t.deepEqual(obs.polygons.length, 610)
38 | })
39 |
40 | test('union (2d & 3d shapes)', t => {
41 | const op1 = cube()
42 | const op2 = square([10, 2])
43 |
44 | const obs = union({extrude2d: true}, op1, op2)
45 |
46 | t.deepEqual(obs.polygons.length, 6)
47 | })
48 |
49 | test('difference (defaults)', t => {
50 | const op1 = cube({size: [10, 10, 1]})
51 | const op2 = cube({size: [1, 1, 10]})
52 |
53 | const obs = difference(op1, op2)
54 |
55 | t.deepEqual(obs.polygons.length, 10)
56 | })
57 |
58 | test('difference (more than 2 operands)', t => {
59 | const op1 = cube({size: [10, 10, 1]})
60 | const op2 = cube({size: [1, 1, 10]})
61 | const op3 = cube({size: [3, 3, 10]})
62 |
63 | const obs = difference(op1, op2, op3)
64 |
65 | t.deepEqual(obs.polygons.length, 10)
66 | })
67 |
68 | test('difference (2d & 3d shapes)', t => {
69 | const op1 = cube({size: [10, 10, 1]})
70 | const op2 = square([10, 2])
71 |
72 | const obs = difference(op1, op2)
73 |
74 | t.deepEqual(obs.polygons.length, 6)
75 | })
76 |
77 | test('intersection (defaults)', t => {
78 | const op1 = cube({size: [10, 10, 1]})
79 | const op2 = cube({size: [1, 1, 10]})
80 |
81 | const obs = difference(op1, op2)
82 |
83 | t.deepEqual(obs.polygons.length, 10)
84 | })
85 |
86 | test('intersection (more than 2 operands)', t => {
87 | const op1 = cube({size: [10, 10, 1]})
88 | const op2 = cube({size: [1, 1, 10]})
89 | const op3 = cube({size: [3, 3, 10]})
90 |
91 | const obs = intersection(op1, op2)
92 |
93 | t.deepEqual(obs.polygons.length, 6)
94 | })
95 |
96 | test('intersection (2d & 3d shapes)', t => {
97 | const op1 = cube({size: [10, 10, 1]})
98 | const op2 = cube({size: [1, 1, 10]})
99 |
100 | const obs = intersection(op1, op2)
101 |
102 | t.deepEqual(obs.polygons.length, 6)
103 | })
104 |
--------------------------------------------------------------------------------
/src/ops-combined.test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava')
2 | const { cube, torus } = require('./primitives3d')
3 | const { circle } = require('./primitives2d')
4 | const { linear_extrude } = require('./ops-extrusions')
5 | const { union, intersection } = require('./ops-booleans')
6 |
7 | // any tests that involve multiple operands (extrude with union translate with difference etc)
8 | // and are not testing a specific feature (union, difference, translate etc)
9 | // belong here
10 |
11 | test('linear_extrude of union of 2d shapes', t => {
12 | const obs = linear_extrude({height: 0.1}, union([
13 | circle({r: 8, center: true}).translate([0, 20, 0]),
14 | circle({r: 8, center: true})
15 | ]))
16 |
17 | t.deepEqual(obs.polygons.length, 142)
18 | })
19 |
20 | test('intersection of torus where ro===r1 and cube', t => {
21 | const obs = intersection(
22 | torus({ro: 0.5, ri: 0.5}),
23 | cube()
24 | )
25 |
26 | t.deepEqual(obs.polygons.length, 67)
27 | })
28 |
--------------------------------------------------------------------------------
/src/ops-extrusions.js:
--------------------------------------------------------------------------------
1 | const { CSG, CAG } = require('@jscad/csg')
2 | const {cagToPointsArray, clamp, rightMultiply1x3VectorToArray, polygonFromPoints} = require('./helpers')
3 | // -- 2D to 3D primitives
4 |
5 | // FIXME: right now linear & rotate extrude take params first, while rectangular_extrude
6 | // takes params second ! confusing and incoherent ! needs to be changed (BREAKING CHANGE !)
7 |
8 | /** linear extrusion of the input 2d shape
9 | * @param {Object} [options] - options for construction
10 | * @param {Float} [options.height=1] - height of the extruded shape
11 | * @param {Integer} [options.slices=10] - number of intermediary steps/slices
12 | * @param {Integer} [options.twist=0] - angle (in degrees to twist the extusion by)
13 | * @param {Boolean} [options.center=false] - whether to center extrusion or not
14 | * @param {CAG} baseShape input 2d shape
15 | * @returns {CSG} new extruded shape
16 | *
17 | * @example
18 | * let revolved = linear_extrude({height: 10}, square())
19 | */
20 | function linear_extrude (params, baseShape) {
21 | const defaults = {
22 | height: 1,
23 | slices: 10,
24 | twist: 0,
25 | center: false
26 | }
27 | /* convexity = 10, */
28 | const {height, twist, slices, center} = Object.assign({}, defaults, params)
29 |
30 | // if(params.convexity) convexity = params.convexity // abandoned
31 | let output = baseShape.extrude({offset: [0, 0, height], twistangle: twist, twiststeps: slices})
32 | if (center === true) {
33 | const b = output.getBounds() // b[0] = min, b[1] = max
34 | const offset = (b[1].plus(b[0])).times(-0.5)
35 | output = output.translate(offset)
36 | }
37 | return output
38 | }
39 |
40 | /** rotate extrusion / revolve of the given 2d shape
41 | * @param {Object} [options] - options for construction
42 | * @param {Integer} [options.fn=1] - resolution/number of segments of the extrusion
43 | * @param {Float} [options.startAngle=1] - start angle of the extrusion, in degrees
44 | * @param {Float} [options.angle=1] - angle of the extrusion, in degrees
45 | * @param {Float} [options.overflow='cap'] - what to do with points outside of bounds (+ / - x) :
46 | * defaults to capping those points to 0 (only supported behaviour for now)
47 | * @param {CAG} baseShape input 2d shape
48 | * @returns {CSG} new extruded shape
49 | *
50 | * @example
51 | * let revolved = rotate_extrude({fn: 10}, square())
52 | */
53 | function rotate_extrude (params, baseShape) {
54 | // note, we should perhaps alias this to revolve() as well
55 | const defaults = {
56 | fn: 32,
57 | startAngle: 0,
58 | angle: 360,
59 | overflow: 'cap'
60 | }
61 | params = Object.assign({}, defaults, params)
62 | let {fn, startAngle, angle, overflow} = params
63 | if (overflow !== 'cap') {
64 | throw new Error('only capping of overflowing points is supported !')
65 | }
66 |
67 | if (arguments.length < 2) { // FIXME: what the hell ??? just put params second !
68 | baseShape = params
69 | }
70 | // are we dealing with a positive or negative angle (for normals flipping)
71 | const flipped = angle > 0
72 | // limit actual angle between 0 & 360, regardless of direction
73 | const totalAngle = flipped ? clamp((startAngle + angle), 0, 360) : clamp((startAngle + angle), -360, 0)
74 | // adapt to the totalAngle : 1 extra segment per 45 degs if not 360 deg extrusion
75 | // needs to be at least one and higher then the input resolution
76 | const segments = Math.max(
77 | Math.floor(Math.abs(totalAngle) / 45),
78 | 1,
79 | fn
80 | )
81 | // maximum distance per axis between two points before considering them to be the same
82 | const overlapTolerance = 0.00001
83 | // convert baseshape to just an array of points, easier to deal with
84 | let shapePoints = cagToPointsArray(baseShape)
85 |
86 | // determine if the rotate_extrude can be computed in the first place
87 | // ie all the points have to be either x > 0 or x < 0
88 |
89 | // generic solution to always have a valid solid, even if points go beyond x/ -x
90 | // 1. split points up between all those on the 'left' side of the axis (x<0) & those on the 'righ' (x>0)
91 | // 2. for each set of points do the extrusion operation IN OPOSITE DIRECTIONS
92 | // 3. union the two resulting solids
93 |
94 | // 1. alt : OR : just cap of points at the axis ?
95 |
96 | // console.log('shapePoints BEFORE', shapePoints, baseShape.sides)
97 |
98 | const pointsWithNegativeX = shapePoints.filter(x => x[0] < 0)
99 | const pointsWithPositiveX = shapePoints.filter(x => x[0] >= 0)
100 | const arePointsWithNegAndPosX = pointsWithNegativeX.length > 0 && pointsWithPositiveX.length > 0
101 |
102 | if (arePointsWithNegAndPosX && overflow === 'cap') {
103 | if (pointsWithNegativeX.length > pointsWithPositiveX.length) {
104 | shapePoints = shapePoints.map(function (point) {
105 | return [Math.min(point[0], 0), point[1]]
106 | })
107 | } else if (pointsWithPositiveX.length >= pointsWithNegativeX.length) {
108 | shapePoints = shapePoints.map(function (point) {
109 | return [Math.max(point[0], 0), point[1]]
110 | })
111 | }
112 | }
113 |
114 | // console.log('negXs', pointsWithNegativeX, 'pointsWithPositiveX', pointsWithPositiveX, 'arePointsWithNegAndPosX', arePointsWithNegAndPosX)
115 | // console.log('shapePoints AFTER', shapePoints, baseShape.sides)
116 |
117 | let polygons = []
118 |
119 | // for each of the intermediary steps in the extrusion
120 | for (let i = 1; i < segments + 1; i++) {
121 | // for each side of the 2d shape
122 | for (let j = 0; j < shapePoints.length - 1; j++) {
123 | // 2 points of a side
124 | const curPoint = shapePoints[j]
125 | const nextPoint = shapePoints[j + 1]
126 |
127 | // compute matrix for current and next segment angle
128 | let prevMatrix = CSG.Matrix4x4.rotationZ((i - 1) / segments * angle + startAngle)
129 | let curMatrix = CSG.Matrix4x4.rotationZ(i / segments * angle + startAngle)
130 |
131 | const pointA = rightMultiply1x3VectorToArray(prevMatrix, [curPoint[0], 0, curPoint[1]])
132 | const pointAP = rightMultiply1x3VectorToArray(curMatrix, [curPoint[0], 0, curPoint[1]])
133 | const pointB = rightMultiply1x3VectorToArray(prevMatrix, [nextPoint[0], 0, nextPoint[1]])
134 | const pointBP = rightMultiply1x3VectorToArray(curMatrix, [nextPoint[0], 0, nextPoint[1]])
135 |
136 | // console.log(`point ${j} edge connecting ${j} to ${j + 1}`)
137 | let overlappingPoints = false
138 | if (Math.abs(pointA[0] - pointAP[0]) < overlapTolerance && Math.abs(pointB[1] - pointBP[1]) < overlapTolerance) {
139 | // console.log('identical / overlapping points (from current angle and next one), what now ?')
140 | overlappingPoints = true
141 | }
142 |
143 | // we do not generate a single quad because:
144 | // 1. it does not allow eliminating unneeded triangles in case of overlapping points
145 | // 2. the current cleanup routines of csg.js create degenerate shapes from those quads
146 | // let polyPoints = [pointA, pointB, pointBP, pointAP]
147 | // polygons.push(polygonFromPoints(polyPoints))
148 |
149 | if (flipped) {
150 | // CW
151 | polygons.push(polygonFromPoints([pointA, pointB, pointBP]))
152 | if (!overlappingPoints) {
153 | polygons.push(polygonFromPoints([pointBP, pointAP, pointA]))
154 | }
155 | } else {
156 | // CCW
157 | if (!overlappingPoints) {
158 | polygons.push(polygonFromPoints([pointA, pointAP, pointBP]))
159 | }
160 | polygons.push(polygonFromPoints([pointBP, pointB, pointA]))
161 | }
162 | }
163 | // if we do not do a full extrusion, we want caps at both ends (closed volume)
164 | if (Math.abs(angle) < 360) {
165 | // we need to recreate the side with capped points where applicable
166 | const sideShape = CAG.fromPoints(shapePoints)
167 | const endMatrix = CSG.Matrix4x4.rotationX(90).multiply(
168 | CSG.Matrix4x4.rotationZ(-startAngle)
169 | )
170 | const endCap = sideShape._toPlanePolygons({flipped: flipped})
171 | .map(x => x.transform(endMatrix))
172 |
173 | const startMatrix = CSG.Matrix4x4.rotationX(90).multiply(
174 | CSG.Matrix4x4.rotationZ(-angle - startAngle)
175 | )
176 | const startCap = sideShape._toPlanePolygons({flipped: !flipped})
177 | .map(x => x.transform(startMatrix))
178 | polygons = polygons.concat(endCap).concat(startCap)
179 | }
180 | }
181 | return CSG.fromPolygons(polygons).reTesselated().canonicalized()
182 | }
183 |
184 | /** rectangular extrusion of the given array of points
185 | * @param {Array} basePoints array of points (nested) to extrude from
186 | * layed out like [ [0,0], [10,0], [5,10], [0,10] ]
187 | * @param {Object} [options] - options for construction
188 | * @param {Float} [options.h=1] - height of the extruded shape
189 | * @param {Float} [options.w=10] - width of the extruded shape
190 | * @param {Integer} [options.fn=1] - resolution/number of segments of the extrusion
191 | * @param {Boolean} [options.closed=false] - whether to close the input path for the extrusion or not
192 | * @param {Boolean} [options.round=true] - whether to round the extrusion or not
193 | * @returns {CSG} new extruded shape
194 | *
195 | * @example
196 | * let revolved = rectangular_extrude({height: 10}, square())
197 | */
198 | function rectangular_extrude (basePoints, params) {
199 | const defaults = {
200 | w: 1,
201 | h: 1,
202 | fn: 8,
203 | closed: false,
204 | round: true
205 | }
206 | const {w, h, fn, closed, round} = Object.assign({}, defaults, params)
207 | return new CSG.Path2D(basePoints, closed).rectangularExtrude(w, h, fn, round)
208 | }
209 |
210 | module.exports = {
211 | linear_extrude,
212 | rotate_extrude,
213 | rectangular_extrude
214 | }
215 |
--------------------------------------------------------------------------------
/src/ops-extrusions.test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava')
2 | const { square } = require('./primitives2d')
3 | const { linear_extrude, rotate_extrude, rectangular_extrude } = require('./ops-extrusions')
4 | const { simplifiedPolygon } = require('./test-helpers')
5 |
6 | test('linear_extrude (height)', t => {
7 | const op1 = square()
8 | const obs = linear_extrude({ height: 10 }, op1)
9 |
10 | const expFirstPoly = {
11 | vertices: [ { pos: { _x: 1, _y: 1, _z: 0 } },
12 | { pos: { _x: 1, _y: 0, _z: 0 } },
13 | { pos: { _x: 0, _y: 0, _z: 0 } },
14 | { pos: { _x: 0, _y: 1, _z: 0 } } ],
15 | shared: { color: null },
16 | plane: { normal: { _x: -0, _y: -0, _z: -1 }, w: -0 }
17 | }
18 |
19 | const expLastPoly = {
20 | vertices: [ { pos: { _x: 0, _y: 1, _z: 10 } },
21 | { pos: { _x: 1, _y: 1, _z: 0 } },
22 | { pos: { _x: 0, _y: 1, _z: 0 } } ],
23 | shared: { color: null },
24 | plane: { normal: { _x: 0, _y: 1, _z: 0 }, w: 1 }
25 | }
26 |
27 | t.deepEqual(obs.polygons.length, 10)
28 | t.deepEqual(obs.polygons[0], expFirstPoly)
29 | t.deepEqual(obs.polygons[obs.polygons.length - 1], expLastPoly)
30 | })
31 |
32 | test('linear_extrude (height, twist, slices, center)', t => {
33 | const op1 = square()
34 | const obs = linear_extrude({ height: 10, twist: 360, slices: 50, center: true }, op1)
35 |
36 | const expFirstPoly = {
37 | vertices: [ { pos: { _x: 1, _y: 1, _z: -5 } },
38 | { pos: { _x: 1, _y: 0, _z: -5 } },
39 | { pos: { _x: 1.1102230246251565e-16, _y: 0, _z: -5 } },
40 | { pos: { _x: 1.1102230246251565e-16, _y: 1, _z: -5 } } ],
41 | shared: { color: null },
42 | plane: { normal: { _x: -0, _y: -0, _z: -1 }, w: 5 }
43 | }
44 |
45 | const expLastPoly = {
46 | vertices: [ { pos: { _x: 3.3306690738754696e-16, _y: 1, _z: 5 } },
47 | { pos: { _x: 1.1174479348787818, _y: 0.8667814677501742, _z: 4.800000000000001 } },
48 | { pos: { _x: 0.12533323356430387, _y: 0.9921147013144779, _z: 4.800000000000001 } } ],
49 | shared: { color: null },
50 | plane: { normal: { _x: 0.12523593496267418, _y: 0.9913445035756271, _z: 0.03939588588188166 },
51 | w: 1.188323932985035 }
52 | }
53 |
54 | t.deepEqual(obs.polygons.length, 402)
55 | t.deepEqual(obs.polygons[0], expFirstPoly)
56 | t.deepEqual(obs.polygons[obs.polygons.length - 1], expLastPoly)
57 | })
58 |
59 | test('rotate_extrude (defaults)', t => {
60 | const op1 = square()
61 | const obs = rotate_extrude(op1.translate([4, 0, 0]))
62 |
63 | const expFirstPoly = { positions:
64 | [ [ 3.923141121612922, -0.780361288064513, 1.0000000000000004 ],
65 | [ 4.000000000000001, 0, 1.0000000000000004 ],
66 | [ 4.000000000000001, 0, 0 ],
67 | [ 3.923141121612922, -0.780361288064513, 0 ] ],
68 | plane:
69 | { normal: [ -0.9951847266721969, 0.09801714032956071, 0 ],
70 | w: -3.9807389066887877 },
71 | shared: { color: null, tag: 1612 } }
72 |
73 | const expLastPoly = { positions:
74 | [ [ 4.903926402016151, 0.9754516100806419, 1 ],
75 | [ 4.999999999999999, 1.6653345369377348e-16, 1 ],
76 | [ 5, 0, 0 ],
77 | [ 4.903926402016151, 0.9754516100806419, 0 ] ],
78 | plane:
79 | { normal: [ 0.9951847266721968, 0.09801714032956149, -0 ],
80 | w: 4.975923633360985 },
81 | shared: { color: null, tag: 1612 } }
82 |
83 | t.deepEqual(obs.polygons.length, 132)
84 | t.deepEqual(simplifiedPolygon(obs.polygons[0]), expFirstPoly)
85 | t.deepEqual(simplifiedPolygon(obs.polygons[obs.polygons.length - 1]), expLastPoly)
86 | })
87 |
88 | test('rotate_extrude (custom resolution)', t => {
89 | const op1 = square()
90 | const obs = rotate_extrude({fn: 4}, op1.translate([4, 0, 0]))
91 |
92 | const expFirstPoly = { positions:
93 | [ [ 2.82842712474619, -2.82842712474619, 1 ],
94 | [ 4, 0, 1 ],
95 | [ 4, 0, 0 ],
96 | [ 2.82842712474619, -2.82842712474619, 0 ] ],
97 | plane:
98 | { normal: [ -0.9238795325112867, 0.3826834323650898, 0 ],
99 | w: -3.695518130045147 },
100 | shared: { color: null, tag: 1612 } }
101 |
102 | const expLastPoly = { positions:
103 | [ [ 3.53553390593274, 3.5355339059327373, 1 ],
104 | [ 5, 0, 1 ],
105 | [ 5, 0, 0 ],
106 | [ 3.53553390593274, 3.5355339059327373, 0 ] ],
107 | plane:
108 | { normal: [ 0.9238795325112867, 0.38268343236508956, -0 ],
109 | w: 4.619397662556434 },
110 | shared: { color: null, tag: 1612 } }
111 |
112 | t.deepEqual(obs.polygons.length, 36)
113 | t.deepEqual(simplifiedPolygon(obs.polygons[0]), expFirstPoly)
114 | t.deepEqual(simplifiedPolygon(obs.polygons[obs.polygons.length - 1]), expLastPoly)
115 | })
116 |
117 | test('rotate_extrude (custom angle)', t => {
118 | const op1 = square()
119 | const obs = rotate_extrude({angle: 20}, op1.translate([4, 0, 0]))
120 |
121 | const expFirstPoly = { positions:
122 | [ [ 3.999762020000599, -0.043632365976729495, 1 ],
123 | [ 4, 0, 1 ],
124 | [ 4, 0, 0 ],
125 | [ 3.999762020000599, -0.043632365976729495, 0 ] ],
126 | plane:
127 | { normal: [ -0.9999851261394216, 0.00545412687101576, 0 ],
128 | w: -3.9999405045576863 },
129 | shared: { color: null, tag: 1612 } }
130 |
131 | const expLastPoly = { positions:
132 | [ [ 4.716837503949126, -1.6587477087667604, 1 ],
133 | [ 4.698463103929543, -1.7101007166283435, 1 ],
134 | [ 4.698463103929543, -1.7101007166283435, 0 ],
135 | [ 4.716837503949126, -1.6587477087667604, 0 ] ],
136 | plane:
137 | { normal: [ 0.9415440651830257, -0.33688985339220645, 0 ],
138 | w: 4.999925630697108 },
139 | shared: { color: null, tag: 1612 } }
140 | t.deepEqual(obs.polygons.length, 192)
141 | t.deepEqual(simplifiedPolygon(obs.polygons[0]), expFirstPoly)
142 | t.deepEqual(simplifiedPolygon(obs.polygons[obs.polygons.length - 1]), expLastPoly)
143 | })
144 |
145 | test('rotate_extrude (custom negative angle)', t => {
146 | const op1 = square()
147 | const obs = rotate_extrude({angle: -20}, op1.translate([4, 0, 0]))
148 |
149 | const expFirstPoly = { positions:
150 | [ [ 4, 0, 1 ],
151 | [ 3.999762020000599, 0.043632365976729495, 1 ],
152 | [ 3.999762020000599, 0.043632365976729495, 0 ],
153 | [ 4, 0, 0 ] ],
154 | plane:
155 | { normal: [ -0.9999851261394216, -0.00545412687101576, 0 ],
156 | w: -3.9999405045576863 },
157 | shared: { color: null, tag: 1612 } }
158 |
159 | const expLastPoly = { positions:
160 | [ [ 4.698463103929543, 1.7101007166283435, 1 ],
161 | [ 4.716837503949126, 1.6587477087667604, 1 ],
162 | [ 4.716837503949126, 1.6587477087667604, 0 ],
163 | [ 4.698463103929543, 1.7101007166283435, 0 ] ],
164 | plane:
165 | { normal: [ 0.9415440651830257, 0.33688985339220645, 0 ],
166 | w: 4.999925630697108 },
167 | shared: { color: null, tag: 1612 } }
168 | t.deepEqual(obs.polygons.length, 192)
169 | t.deepEqual(simplifiedPolygon(obs.polygons[0]), expFirstPoly)
170 | t.deepEqual(simplifiedPolygon(obs.polygons[obs.polygons.length - 1]), expLastPoly)
171 | })
172 |
173 | test('rotate_extrude (custom negative angle, custom start angle)', t => {
174 | const op1 = square()
175 | const obs = rotate_extrude({angle: -20, startAngle: 27}, op1.translate([4, 0, 0]))
176 |
177 | const expFirstPoly = { positions:
178 | [ [ 3.5640260967534716, -1.815961998958187, 1 ],
179 | [ 3.583622734655973, -1.7769772355482905, 1 ],
180 | [ 3.583622734655973, -1.7769772355482905, 0 ],
181 | [ 3.5640260967534716, -1.815961998958187, 0 ] ],
182 | plane:
183 | { normal: [ -0.8934693932653679, 0.4491240845223238, 0 ],
184 | w: -3.9999405045576863 },
185 | shared: { color: null, tag: 1612 } }
186 |
187 | const expLastPoly = { positions:
188 | [ [ 4.96273075820661, -0.6093467170257374, 1 ],
189 | [ 4.955788690799897, -0.66344438511441, 1 ],
190 | [ 4.955788690799897, -0.66344438511441, 0 ],
191 | [ 4.96273075820661, -0.6093467170257374, 0 ] ],
192 | plane:
193 | { normal: [ 0.991866697787626, -0.12728100337391368, 0 ],
194 | w: 4.999925630697108 },
195 | shared: { color: null, tag: 1612 } }
196 | t.deepEqual(obs.polygons.length, 192)
197 | t.deepEqual(simplifiedPolygon(obs.polygons[0]), expFirstPoly)
198 | t.deepEqual(simplifiedPolygon(obs.polygons[obs.polygons.length - 1]), expLastPoly)
199 | })
200 |
201 | test('rotate_extrude (custom negative angle, custom start angle, capped points)', t => {
202 | const op1 = square().translate([-0.5, 0, 0])
203 | const obs = rotate_extrude({angle: -20, startAngle: 27}, op1)
204 |
205 | const expFirstPoly = { positions:
206 | [ [ 0.496273075820661, -0.06093467170257374, 0 ],
207 | [ 0.49557886907998977, -0.06634443851144099, 0 ],
208 | [ 0.4948256934098351, -0.07174631099558966, 0 ],
209 | [ 0.49401363843056983, -0.07713964638621444, 0 ],
210 | [ 0.4931428007686157, -0.08252380293033881, 0 ],
211 | [ 0.4922132840449458, -0.08789813996717726, 0 ],
212 | [ 0.49122519886275484, -0.09326201800436731, 0 ],
213 | [ 0.4901786627942984, -0.09861479879406297, 0 ],
214 | [ 0.48907380036690284, -0.10395584540887967, 0 ],
215 | [ 0.4879107430481481, -0.10928452231768229, 0 ],
216 | [ 0.48668962923022424, -0.11460019546120709, 0 ],
217 | [ 0.48541060421346405, -0.11990223232750827, 0 ],
218 | [ 0.48407382018905387, -0.12519000202722072, 0 ],
219 | [ 0.48267943622092446, -0.13046287536862944, 0 ],
220 | [ 0.48122761822682364, -0.13572022493253713, 0 ],
221 | [ 0.4797185389585742, -0.14096142514692075, 0 ],
222 | [ 0.4781523779815177, -0.14618585236136838, 0 ],
223 | [ 0.4765293216531485, -0.1513928849212873, 0 ],
224 | [ 0.4748495631009385, -0.15658190324187476, 0 ],
225 | [ 0.4731133021993573, -0.1617522898818424, 0 ],
226 | [ 0.4713207455460892, -0.16690342961688545, 0 ],
227 | [ 0.4694721064374497, -0.1720347095128884, 0 ],
228 | [ 0.46756760484300586, -0.17714551899885794, 0 ],
229 | [ 0.4656074673794018, -0.18223524993957482, 0 ],
230 | [ 0.4635919272833937, -0.187303296707956, 0 ],
231 | [ 0.46152122438409704, -0.19234905625711804, 0 ],
232 | [ 0.45939560507444915, -0.1973719281921336, 0 ],
233 | [ 0.45721532228189105, -0.20237131484147275, 0 ],
234 | [ 0.4549806354382716, -0.2073466213281195, 0 ],
235 | [ 0.4526918104489776, -0.21229725564035656, 0 ],
236 | [ 0.45034911966129393, -0.21722262870220851, 0 ],
237 | [ 0.44795284183199663, -0.22212215444353633, 0 ],
238 | [ 0.44550326209418395, -0.22699524986977337, 0 ],
239 | [ 0, 0, 0 ] ],
240 | plane: { normal: [ -0, 0, -1 ], w: -0 },
241 | shared: { color: null, tag: 1612 } }
242 |
243 | const expLastPoly = { positions:
244 | [ [ 0.496273075820661, -0.06093467170257374, 1 ],
245 | [ 0.49557886907998977, -0.06634443851144099, 1 ],
246 | [ 0.49557886907998977, -0.06634443851144099, 0 ],
247 | [ 0.496273075820661, -0.06093467170257374, 0 ] ],
248 | plane:
249 | { normal: [ 0.991866697787627, -0.12728100337390572, 0 ],
250 | w: 0.4999925630697108 },
251 | shared: { color: null, tag: 1612 } }
252 | t.deepEqual(obs.polygons.length, 98)
253 | // console.log('first', simplifiedPolygon(obs.polygons[0]))
254 | // console.log('last', simplifiedPolygon(obs.polygons[obs.polygons.length - 1]))
255 |
256 | t.deepEqual(simplifiedPolygon(obs.polygons[0]), expFirstPoly)
257 | t.deepEqual(simplifiedPolygon(obs.polygons[obs.polygons.length - 1]), expLastPoly)
258 | })
259 |
260 | test('rotate_extrude (invalid overflow setting should throw an exception)', t => {
261 | const op1 = square().translate([-0.5, 0, 0])
262 | t.throws(() => {
263 | rotate_extrude({angle: -20, startAngle: 27, overflow: undefined}, op1)
264 | }, 'only capping of overflowing points is supported !')
265 | })
266 |
267 | test('rectangular_extrude ', t => {
268 | const op1 = square()
269 | const obs = rectangular_extrude([ [10, 10], [-10, 10], [-20, 0], [-10, -10], [10, -10] ], // path is an array of 2d coords
270 | {w: 1, h: 3, closed: true}, op1)
271 |
272 | const expFirstPoly = {
273 | vertices: [
274 | { pos: { _x: -11.207106781186544, _y: 9.5, _z: 0 } },
275 | { pos: { _x: -10.35355339059327, _y: 10.353553390593275, _z: 0 } },
276 | { pos: { _x: -10.000000000000002, _y: 10.5, _z: 0 } },
277 | { pos: { _x: 9.5, _y: 10.5, _z: 0 } },
278 | { pos: { _x: 9.5, _y: 9.5, _z: 0 } } ],
279 | shared: { color: null, tag: 1612 },
280 | plane: { normal: { _x: -0, _y: -0, _z: -1 }, w: -0 }
281 | }
282 |
283 | const expLastPoly = {
284 | vertices: [
285 | { pos: { _x: -9.792893218813454, _y: 9.5, _z: 3 } },
286 | { pos: { _x: -19.292893218813454, _y: -1.1102230246251565e-16, _z: 0 } },
287 | { pos: { _x: -9.792893218813454, _y: 9.5, _z: 0 } } ],
288 | shared: { color: null, tag: 1612 },
289 | plane: { normal: { _x: 0.7071067811865476, _y: -0.7071067811865476, _z: 0 }, w: -13.642135623730951 }
290 | }
291 | /* console.log(obs.polygons[0])
292 | console.log(obs.polygons[0].vertices)
293 | console.log(obs.polygons[obs.polygons.length - 1])
294 | console.log(obs.polygons[obs.polygons.length - 1].vertices) */
295 | t.deepEqual(obs.polygons.length, 46)
296 | t.deepEqual(obs.polygons[0], expFirstPoly)
297 | t.deepEqual(obs.polygons[obs.polygons.length - 1], expLastPoly)
298 | })
299 |
--------------------------------------------------------------------------------
/src/ops-transformations.js:
--------------------------------------------------------------------------------
1 | const { CSG, CAG } = require('@jscad/csg')
2 | const { union } = require('./ops-booleans')
3 | // -- 3D transformations (OpenSCAD like notion)
4 |
5 | /** translate an object in 2D/3D space
6 | * @param {Object} vector - 3D vector to translate the given object(s) by
7 | * @param {Object(s)|Array} objects either a single or multiple CSG/CAG objects to translate
8 | * @returns {CSG} new CSG object , translated by the given amount
9 | *
10 | * @example
11 | * let movedSphere = translate([10,2,0], sphere())
12 | */
13 | function translate (vector, ...objects) { // v, obj or array
14 | // workaround needed to determine if we are dealing with an array of objects
15 | const _objects = (objects.length >= 1 && objects[0].length) ? objects[0] : objects
16 | let object = _objects[0]
17 |
18 | if (_objects.length > 1) {
19 | for (let i = 1; i < _objects.length; i++) { // FIXME/ why is union really needed ??
20 | object = object.union(_objects[i])
21 | }
22 | }
23 | return object.translate(vector)
24 | }
25 |
26 | /** scale an object in 2D/3D space
27 | * @param {Float|Array} scale - either an array or simple number to scale object(s) by
28 | * @param {Object(s)|Array} objects either a single or multiple CSG/CAG objects to scale
29 | * @returns {CSG} new CSG object , scaled by the given amount
30 | *
31 | * @example
32 | * let scaledSphere = scale([0.2,15,1], sphere())
33 | */
34 | function scale (scale, ...objects) { // v, obj or array
35 | const _objects = (objects.length >= 1 && objects[0].length) ? objects[0] : objects
36 | let object = _objects[0]
37 |
38 | if (_objects.length > 1) {
39 | for (let i = 1; i < _objects.length; i++) { // FIXME/ why is union really needed ??
40 | object = object.union(_objects[i])
41 | }
42 | }
43 | return object.scale(scale)
44 | }
45 |
46 | /** rotate an object in 2D/3D space
47 | * @param {Float|Array} rotation - either an array or simple number to rotate object(s) by
48 | * @param {Object(s)|Array} objects either a single or multiple CSG/CAG objects to rotate
49 | * @returns {CSG} new CSG object , rotated by the given amount
50 | *
51 | * @example
52 | * let rotatedSphere = rotate([0.2,15,1], sphere())
53 | */
54 | function rotate () {
55 | let o
56 | let i
57 | let v
58 | let r = 1
59 | let a = arguments
60 | if (!a[0].length) { // rotate(r,[x,y,z],o)
61 | r = a[0]
62 | v = a[1]
63 | i = 2
64 | if (a[2].length) { a = a[2]; i = 0 }
65 | } else { // rotate([x,y,z],o)
66 | v = a[0]
67 | i = 1
68 | if (a[1].length) { a = a[1]; i = 0 }
69 | }
70 | for (o = a[i++]; i < a.length; i++) {
71 | o = o.union(a[i])
72 | }
73 | if (r !== 1) {
74 | return o.rotateX(v[0] * r).rotateY(v[1] * r).rotateZ(v[2] * r)
75 | } else {
76 | return o.rotateX(v[0]).rotateY(v[1]).rotateZ(v[2])
77 | }
78 | }
79 |
80 | /** apply the given matrix transform to the given objects
81 | * @param {Array} matrix - the 4x4 matrix to apply, as a simple 1d array of 16 elements
82 | * @param {Object(s)|Array} objects either a single or multiple CSG/CAG objects to transform
83 | * @returns {CSG} new CSG object , transformed
84 | *
85 | * @example
86 | * const angle = 45
87 | * let transformedShape = transform([
88 | * cos(angle), -sin(angle), 0, 10,
89 | * sin(angle), cos(angle), 0, 20,
90 | * 0 , 0, 1, 30,
91 | * 0, 0, 0, 1
92 | * ], sphere())
93 | */
94 | function transform (matrix, ...objects) { // v, obj or array
95 | const _objects = (objects.length >= 1 && objects[0].length) ? objects[0] : objects
96 | let object = _objects[0]
97 |
98 | if (_objects.length > 1) {
99 | for (let i = 1; i < _objects.length; i++) { // FIXME/ why is union really needed ??
100 | object = object.union(_objects[i])
101 | }
102 | }
103 |
104 | let transformationMatrix
105 | if (!Array.isArray(matrix)) {
106 | throw new Error('Matrix needs to be an array')
107 | }
108 | matrix.forEach(element => {
109 | if (!Number.isFinite(element)) {
110 | throw new Error('you can only use a flat array of valid, finite numbers (float and integers)')
111 | }
112 | })
113 | transformationMatrix = new CSG.Matrix4x4(matrix)
114 | return object.transform(transformationMatrix)
115 | }
116 |
117 | /** center an object in 2D/3D space
118 | * @param {Boolean|Array} axis - either an array or single boolean to indicate which axis you want to center on
119 | * @param {Object(s)|Array} objects either a single or multiple CSG/CAG objects to translate
120 | * @returns {CSG} new CSG object , translated by the given amount
121 | *
122 | * @example
123 | * let movedSphere = center(false, sphere())
124 | */
125 | function center (axis, ...objects) { // v, obj or array
126 | const _objects = (objects.length >= 1 && objects[0].length) ? objects[0] : objects
127 | let object = _objects[0]
128 |
129 | if (_objects.length > 1) {
130 | for (let i = 1; i < _objects.length; i++) { // FIXME/ why is union really needed ??
131 | object = object.union(_objects[i])
132 | }
133 | }
134 | return object.center(axis)
135 | }
136 |
137 | /** mirror an object in 2D/3D space
138 | * @param {Array} vector - the axes to mirror the object(s) by
139 | * @param {Object(s)|Array} objects either a single or multiple CSG/CAG objects to mirror
140 | * @returns {CSG} new CSG object , mirrored
141 | *
142 | * @example
143 | * let rotatedSphere = mirror([0.2,15,1], sphere())
144 | */
145 | function mirror (vector, ...objects) {
146 | const _objects = (objects.length >= 1 && objects[0].length) ? objects[0] : objects
147 | let object = _objects[0]
148 |
149 | if (_objects.length > 1) {
150 | for (let i = 1; i < _objects.length; i++) { // FIXME/ why is union really needed ??
151 | object = object.union(_objects[i])
152 | }
153 | }
154 | const plane = new CSG.Plane(new CSG.Vector3D(vector[0], vector[1], vector[2]).unit(), 0)
155 | return object.mirrored(plane)
156 | }
157 |
158 | /** expand an object in 2D/3D space
159 | * @param {float} radius - the radius to expand by
160 | * @param {Object} object a CSG/CAG objects to expand
161 | * @returns {CSG/CAG} new CSG/CAG object , expanded
162 | *
163 | * @example
164 | * let expanededShape = expand([0.2,15,1], sphere())
165 | */
166 | function expand (radius, n, object) {
167 | return object.expand(radius, n)
168 | }
169 |
170 | /** contract an object(s) in 2D/3D space
171 | * @param {float} radius - the radius to contract by
172 | * @param {Object} object a CSG/CAG objects to contract
173 | * @returns {CSG/CAG} new CSG/CAG object , contracted
174 | *
175 | * @example
176 | * let contractedShape = contract([0.2,15,1], sphere())
177 | */
178 | function contract (radius, n, object) {
179 | return object.contract(radius, n)
180 | }
181 |
182 | /** create a minkowski sum of the given shapes
183 | * @param {Object(s)|Array} objects either a single or multiple CSG/CAG objects to create a hull around
184 | * @returns {CSG} new CSG object , mirrored
185 | *
186 | * @example
187 | * let hulled = hull(rect(), circle())
188 | */
189 | function minkowski () {
190 | console.log('minkowski() not yet implemented')
191 | }
192 |
193 | /** create a convex hull of the given shapes
194 | * @param {Object(s)|Array} objects either a single or multiple CSG/CAG objects to create a hull around
195 | * @returns {CSG} new CSG object , a hull around the given shapes
196 | *
197 | * @example
198 | * let hulled = hull(rect(), circle())
199 | */
200 | function hull () {
201 | let pts = []
202 |
203 | let a = arguments
204 | if (a[0].length) a = a[0]
205 | let done = []
206 |
207 | for (let i = 0; i < a.length; i++) { // extract all points of the CAG in the argument list
208 | let cag = a[i]
209 | if (!(cag instanceof CAG)) {
210 | throw new Error('ERROR: hull() accepts only 2D forms / CAG')
211 | }
212 | for (let j = 0; j < cag.sides.length; j++) {
213 | let x = cag.sides[j].vertex0.pos.x
214 | let y = cag.sides[j].vertex0.pos.y
215 | // avoid some coord to appear multiple times
216 | if (done['' + x + ',' + y]) {
217 | continue
218 | }
219 | pts.push({ x: x, y: y })
220 | done['' + x + ',' + y]++
221 | // echo(x,y);
222 | }
223 | }
224 | // echo(pts.length+" points in",pts);
225 |
226 | // from http://www.psychedelicdevelopment.com/grahamscan/
227 | // see also at https://github.com/bkiers/GrahamScan/blob/master/src/main/cg/GrahamScan.java
228 | let ConvexHullPoint = function (i, a, d) {
229 | this.index = i
230 | this.angle = a
231 | this.distance = d
232 |
233 | this.compare = function (p) {
234 | if (this.angle < p.angle) {
235 | return -1
236 | } else if (this.angle > p.angle) {
237 | return 1
238 | } else {
239 | if (this.distance < p.distance) {
240 | return -1
241 | } else if (this.distance > p.distance) {
242 | return 1
243 | }
244 | }
245 | return 0
246 | }
247 | }
248 |
249 | let ConvexHull = function () {
250 | this.points = null
251 | this.indices = null
252 |
253 | this.getIndices = function () {
254 | return this.indices
255 | }
256 |
257 | this.clear = function () {
258 | this.indices = null
259 | this.points = null
260 | }
261 |
262 | this.ccw = function (p1, p2, p3) {
263 | let ccw = (this.points[p2].x - this.points[p1].x) * (this.points[p3].y - this.points[p1].y) -
264 | (this.points[p2].y - this.points[p1].y) * (this.points[p3].x - this.points[p1].x)
265 | // we need this, otherwise sorting never ends, see https://github.com/Spiritdude/OpenJSCAD.org/issues/18
266 | if (ccw < 1e-5) {
267 | return 0
268 | }
269 | return ccw
270 | }
271 |
272 | this.angle = function (o, a) {
273 | // return Math.atan((this.points[a].y-this.points[o].y) / (this.points[a].x - this.points[o].x));
274 | return Math.atan2((this.points[a].y - this.points[o].y), (this.points[a].x - this.points[o].x))
275 | }
276 |
277 | this.distance = function (a, b) {
278 | return ((this.points[b].x - this.points[a].x) * (this.points[b].x - this.points[a].x) +
279 | (this.points[b].y - this.points[a].y) * (this.points[b].y - this.points[a].y))
280 | }
281 |
282 | this.compute = function (_points) {
283 | this.indices = null
284 | if (_points.length < 3) {
285 | return
286 | }
287 | this.points = _points
288 |
289 | // Find the lowest point
290 | let min = 0
291 | for (let i = 1; i < this.points.length; i++) {
292 | if (this.points[i].y === this.points[min].y) {
293 | if (this.points[i].x < this.points[min].x) {
294 | min = i
295 | }
296 | } else if (this.points[i].y < this.points[min].y) {
297 | min = i
298 | }
299 | }
300 |
301 | // Calculate angle and distance from base
302 | let al = []
303 | let ang = 0.0
304 | let dist = 0.0
305 | for (let i = 0; i < this.points.length; i++) {
306 | if (i === min) {
307 | continue
308 | }
309 | ang = this.angle(min, i)
310 | if (ang < 0) {
311 | ang += Math.PI
312 | }
313 | dist = this.distance(min, i)
314 | al.push(new ConvexHullPoint(i, ang, dist))
315 | }
316 |
317 | al.sort(function (a, b) { return a.compare(b) })
318 |
319 | // Create stack
320 | let stack = new Array(this.points.length + 1)
321 | let j = 2
322 | for (let i = 0; i < this.points.length; i++) {
323 | if (i === min) {
324 | continue
325 | }
326 | stack[j] = al[j - 2].index
327 | j++
328 | }
329 | stack[0] = stack[this.points.length]
330 | stack[1] = min
331 |
332 | let tmp
333 | let M = 2
334 | for (let i = 3; i <= this.points.length; i++) {
335 | while (this.ccw(stack[M - 1], stack[M], stack[i]) <= 0) {
336 | M--
337 | }
338 | M++
339 | tmp = stack[i]
340 | stack[i] = stack[M]
341 | stack[M] = tmp
342 | }
343 |
344 | this.indices = new Array(M)
345 | for (let i = 0; i < M; i++) {
346 | this.indices[i] = stack[i + 1]
347 | }
348 | }
349 | }
350 |
351 | let hull = new ConvexHull()
352 |
353 | hull.compute(pts)
354 | let indices = hull.getIndices()
355 |
356 | if (indices && indices.length > 0) {
357 | let ch = []
358 | for (let i = 0; i < indices.length; i++) {
359 | ch.push(pts[indices[i]])
360 | }
361 | return CAG.fromPoints(ch)
362 | }
363 | }
364 |
365 | /** create a chain hull of the given shapes
366 | * Originally "Whosa whatsis" suggested "Chain Hull" ,
367 | * as described at https://plus.google.com/u/0/105535247347788377245/posts/aZGXKFX1ACN
368 | * essentially hull A+B, B+C, C+D and then union those
369 | * @param {Object(s)|Array} objects either a single or multiple CSG/CAG objects to create a chain hull around
370 | * @returns {CSG} new CSG object ,which a chain hull of the inputs
371 | *
372 | * @example
373 | * let hulled = chain_hull(rect(), circle())
374 | */
375 | function chain_hull (params, objects) {
376 | /*
377 | const defaults = {
378 | closed: false
379 | }
380 | const closed = Object.assign({}, defaults, params) */
381 | let a = arguments
382 | let closed = false
383 | let j = 0
384 |
385 | if (a[j].closed !== undefined) {
386 | closed = a[j++].closed
387 | }
388 |
389 | if (a[j].length) { a = a[j] }
390 |
391 | let hulls = []
392 | let hullsAmount = a.length - (closed ? 0 : 1)
393 | for (let i = 0; i < hullsAmount; i++) {
394 | hulls.push(hull(a[i], a[(i + 1) % a.length]))
395 | }
396 | return union(hulls)
397 | }
398 |
399 | module.exports = {
400 | translate,
401 | center,
402 | scale,
403 | rotate,
404 | transform,
405 | mirror,
406 | expand,
407 | contract,
408 | minkowski,
409 | hull,
410 | chain_hull
411 | }
412 |
--------------------------------------------------------------------------------
/src/ops-transformations.test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava')
2 | const { sideEquals } = require('./test-helpers')
3 | const { cube, sphere } = require('./primitives3d')
4 | const { square, circle } = require('./primitives2d')
5 | const { translate, rotate, scale, transform, center, mirror, expand, contract, minkowski, hull, chain_hull } = require('./ops-transformations')
6 |
7 | // TODO: since cube, sphere etc rely on some of the transformations, we should be creating csg objects 'from scratch' instead
8 | // of using those since it is not a very good independant test otherwise
9 |
10 | test('translate (single item, 3d)', t => {
11 | const op1 = cube()
12 | const obs = translate([0, 10, 0], op1)
13 |
14 | t.deepEqual(obs.properties.cube.center, {_x: 0.5, _y: 10.5, _z: 0.5})
15 | })
16 |
17 | test('translate (multiple items, 3d)', t => {
18 | const op1 = cube()
19 | const op2 = sphere()
20 | const obs = translate([0, 10, 0], op1, op2)
21 |
22 | t.deepEqual(obs.properties.cube.center, {_x: 0.5, _y: 10.5, _z: 0.5})
23 | t.deepEqual(obs.properties.sphere.center, {_x: 0, _y: 10, _z: 0})
24 | })
25 |
26 | test('translate (multiple items in array, 3d)', t => {
27 | const op1 = cube()
28 | const op2 = sphere()
29 | const obs = translate([0, 10, 0], [op1, op2])
30 |
31 | t.deepEqual(obs.properties.cube.center, {_x: 0.5, _y: 10.5, _z: 0.5})
32 | t.deepEqual(obs.properties.sphere.center, {_x: 0, _y: 10, _z: 0})
33 | })
34 |
35 | test('translate (single item , 2d)', t => {
36 | const op1 = square()
37 | const obs = translate([0, 10, 0], op1)
38 |
39 | t.deepEqual(obs.sides[0], { vertex0: { pos: { _x: 0, _y: 11 } }, vertex1: { pos: { _x: 0, _y: 10 } } })
40 | t.deepEqual(obs.sides[obs.sides.length - 1], { vertex0: { pos: { _x: 1, _y: 11 } }, vertex1: { pos: { _x: 0, _y: 11 } } })
41 | })
42 |
43 | test('translate (multiple items, 2d)', t => {
44 | const op1 = square()
45 | const op2 = circle()
46 | const obs = translate([0, 10, 0], op1, op2)
47 |
48 | sideEquals(t, obs.sides[0], [[1.9807852804032304, 10.804909677983872], [2, 11]])
49 | sideEquals(t, obs.sides[obs.sides.length - 1], [[0, 10], [0.9999999999999998, 10]])
50 | })
51 |
52 | test('translate (multiple items in array, 2d)', t => {
53 | const op1 = square()
54 | const op2 = circle()
55 | const obs = translate([0, 10, 0], op1, op2)
56 |
57 | sideEquals(t, obs.sides[0], [[1.9807852804032304, 10.804909677983872], [2, 11]])
58 | sideEquals(t, obs.sides[obs.sides.length - 1], [[0, 10], [0.9999999999999998, 10]])
59 | })
60 |
61 | test('rotate (single item)', t => {
62 | const op1 = cube()
63 | const obs = rotate([0, Math.PI, 0], op1)
64 | t.deepEqual(obs.properties.cube.center, {_x: 0.5266504075063266, _y: 0.5, _z: 0.47184674235753715})
65 | })
66 |
67 | test('rotate (multiple items)', t => {
68 | const op1 = cube()
69 | const op2 = sphere({center: false})
70 |
71 | const obs = rotate([0, Math.PI, 0], op1, op2)
72 | t.deepEqual(obs.properties.cube.center, {_x: 0.5266504075063266, _y: 0.5, _z: 0.47184674235753715})
73 | t.deepEqual(obs.properties.sphere.center, {_x: 1.0533008150126533, _y: 1, _z: 0.9436934847150743})
74 | })
75 |
76 | test('rotate (multiple items in array)', t => {
77 | const op1 = cube()
78 | const op2 = sphere({center: false})
79 |
80 | const obs = rotate([0, Math.PI, 0], [op1, op2])
81 | t.deepEqual(obs.properties.cube.center, {_x: 0.5266504075063266, _y: 0.5, _z: 0.47184674235753715})
82 | t.deepEqual(obs.properties.sphere.center, {_x: 1.0533008150126533, _y: 1, _z: 0.9436934847150743})
83 | })
84 |
85 | test('rotate (multiple items, 2d)', t => {
86 | const op1 = square()
87 | const op2 = circle()
88 | const obs = rotate([0, 10, 0], op1, op2)
89 |
90 | sideEquals(t, obs.sides[0], [[1.9506927011935618, 0.8049096779838713], [1.969615506024416, 1]])
91 | sideEquals(t, obs.sides[obs.sides.length - 1], [[0, 0], [0.9848077530122078, 0]])
92 | })
93 |
94 | test('scale (single item)', t => {
95 | const op1 = cube()
96 | const obs = scale([2, 1, 1], op1)
97 |
98 | t.deepEqual(obs.properties.cube.center, {_x: 1, _y: 0.5, _z: 0.5})
99 | })
100 |
101 | test('scale (multiple items)', t => {
102 | const op1 = cube()
103 | const op2 = sphere({center: false})
104 | const obs = scale([2, 1, 1], op1, op2)
105 |
106 | t.deepEqual(obs.properties.cube.center, {_x: 1, _y: 0.5, _z: 0.5})
107 | t.deepEqual(obs.properties.sphere.center, {_x: 2, _y: 1, _z: 1})
108 | })
109 |
110 | test('scale (multiple items in array)', t => {
111 | const op1 = cube()
112 | const op2 = sphere({center: false})
113 | const obs = scale([2, 1, 1], [op1, op2])
114 |
115 | t.deepEqual(obs.properties.cube.center, {_x: 1, _y: 0.5, _z: 0.5})
116 | t.deepEqual(obs.properties.sphere.center, {_x: 2, _y: 1, _z: 1})
117 | })
118 |
119 | test('scale (multiple items, 2d)', t => {
120 | const op1 = square()
121 | const op2 = circle()
122 | const obs = scale([0, 10, 0], op1, op2)
123 |
124 | sideEquals(t, obs.sides[0], [[0, 8.049096779838713], [0, 10]])
125 | sideEquals(t, obs.sides[obs.sides.length - 1], [[0, 0], [0, 0]])
126 | })
127 |
128 | test('transform (single item, translation)', t => {
129 | const op1 = cube()
130 | // translate by [10, -5, 0]
131 | const obs = transform(
132 | [1, 0, 0, 0,
133 | 0, 1, 0, 0,
134 | 0, 0, 1, 0,
135 | 10, -5, 0, 1], op1)
136 |
137 | t.deepEqual(obs.properties.cube.center, {_x: 10.5, _y: -4.5, _z: 0.5})
138 | })
139 |
140 | test('transform (multiple items, translation)', t => {
141 | const op1 = cube()
142 | const op2 = sphere({center: false})
143 | const obs = transform(
144 | [1, 0, 0, 0,
145 | 0, 1, 0, 0,
146 | 0, 0, 1, 0,
147 | 10, -5, 0, 1], op1, op2)
148 |
149 | t.deepEqual(obs.properties.cube.center, {_x: 10.5, _y: -4.5, _z: 0.5})
150 | t.deepEqual(obs.properties.sphere.center, {_x: 11, _y: -4, _z: 1})
151 | })
152 |
153 | test('transform should fail if provided with anything but a flat array of numbers', t => {
154 | t.throws(() => {
155 | transform(['1', 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, -5, 0, 1], cube())
156 | }, 'you can only use a flat array of valid, finite numbers (float and integers)')
157 |
158 | t.throws(() => {
159 | transform([[0, 0, 0, 0], [ 0, 1, 0, 0], [0, 0, 1, 0], [10, -5, 0, 1]], cube())
160 | }, 'you can only use a flat array of valid, finite numbers (float and integers)')
161 | /* const obs =
162 | t.th
163 | t.deepEqual(obs.properties.cube.center, {_x: 10.5, _y: -4.5, _z: 0.5}) */
164 | })
165 |
166 | test('transform (multiple items, 2d , translation)', t => {
167 | const op1 = square()
168 | const op2 = circle()
169 | const obs = transform(
170 | [1, 0, 0, 0,
171 | 0, 1, 0, 0,
172 | 0, 0, 1, 0,
173 | 10, -5, 0, 1], op1, op2)
174 |
175 | sideEquals(t, obs.sides[0], [[11.98078528040323, -4.195090322016129], [12, -4]])
176 | sideEquals(t, obs.sides[obs.sides.length - 1], [[10, -5], [11, -5]])
177 | })
178 |
179 | test('center (single item)', t => {
180 | const op1 = cube()
181 | const obs = center(true, op1)
182 | t.deepEqual(obs.properties.cube.center, {_x: 0.5, _y: 0.5, _z: 0.5})
183 | })
184 |
185 | test('center (multiple item)', t => {
186 | const op1 = cube()
187 | const op2 = sphere()
188 | const obs = center(true, op1, op2)
189 | t.deepEqual(obs.properties.cube.center, {_x: 0.5, _y: 0.5, _z: 0.5})
190 | t.deepEqual(obs.properties.sphere.center, {_x: 0, _y: 0, _z: 0})
191 | })
192 |
193 | test('center (multiple items, 2d)', t => {
194 | const op1 = square()
195 | const op2 = circle()
196 | const obs = center(true, op1, op2)
197 |
198 | sideEquals(t, obs.sides[0], [[1.9807852804032304, 0.8049096779838713], [2, 1]])
199 | sideEquals(t, obs.sides[obs.sides.length - 1], [[0, 0], [0.9999999999999998, 0]])
200 | })
201 |
202 | test('mirror (single item)', t => {
203 | const op1 = cube()
204 | const obs = mirror([10, 20, 90], op1)
205 | t.deepEqual(obs.properties.cube.center, {_x: 0.36046511627906974, _y: 0.2209302325581396, _z: -0.7558139534883721})
206 | })
207 |
208 | test('mirror (multiple item)', t => {
209 | const op1 = cube()
210 | const op2 = sphere()
211 | const obs = mirror([10, 20, 90], op1, op2)
212 | t.deepEqual(obs.properties.cube.center, {_x: 0.36046511627906974, _y: 0.2209302325581396, _z: -0.7558139534883721})
213 | t.deepEqual(obs.properties.sphere.center, {_x: 0, _y: 0, _z: 0})
214 | })
215 |
216 | test.failing('mirror (multiple items, 2d)', t => {
217 | const op1 = square().translate([0, 5])
218 | const op2 = circle().translate([5, 2])
219 | const obs = mirror(true, op1, op2)
220 |
221 | t.deepEqual(obs.sides[0].vertex0, 0)
222 | })
223 |
224 | test('expand (single item)', t => {
225 | const op1 = cube()
226 | const obs = expand(10, 5, op1)
227 |
228 | t.deepEqual(obs.polygons[0].vertices[0], {pos: {_x: -10, _y: 0, _z: 0}})
229 | })
230 |
231 | test.failing('expand (multiple items)', t => {
232 | const op1 = cube()
233 | const op2 = sphere()
234 | const obs = expand(10, 5, op1, op2)
235 |
236 | t.deepEqual(obs.polygons[0].vertices[0], {pos: {_x: -10, _y: 0, _z: 0}})
237 | t.deepEqual(obs.properties.sphere.center, {_x: 0, _y: 0, _z: 0})
238 | })
239 |
240 | test('expand (multiple items, 2d)', t => {
241 | const op1 = square()
242 | const op2 = circle()
243 | const obs = expand(10, 5, op1, op2)
244 |
245 | t.deepEqual(obs.sides[0], {vertex0: {pos: {_x: 11, _y: 0}}, vertex1: {pos: {_x: 11, _y: 1}}})
246 | t.deepEqual(obs.sides[obs.sides.length - 1], {vertex0: {pos: {_x: -1.8369701987210296e-15, _y: -10}}, vertex1: {pos: {_x: 0.9999999999999981, _y: -10}}})
247 | })
248 |
249 | // FIXME: I have NO idea why this one is failing, it is only a very thin wrapper over contract
250 | // which means contract itself is likely broken
251 | // seems to work for 2d shapes?
252 | test.failing('contract (single item)', t => {
253 | const op1 = cube({size: 10})
254 | const obs = contract(5, 1, op1)
255 | console.log('obs', obs)
256 |
257 | t.deepEqual(obs.polygons[0].vertices[0], {pos: {_x: -10, _y: 0, _z: 0}})
258 | })
259 |
260 | test.failing('contract (multiple items, 3d)', t => {
261 | const op1 = cube()
262 | const op2 = sphere()
263 | const obs = contract(5, 1, op1, op2)
264 |
265 | t.deepEqual(obs.polygons[0].vertices[0], {pos: {_x: -10, _y: 0, _z: 0}})
266 | t.deepEqual(obs.properties.sphere.center, {_x: 0, _y: 0, _z: 0})
267 | })
268 |
269 | test.failing('contract (multiple items, 2d)', t => {
270 | const op1 = square()
271 | const op2 = circle()
272 | const obs = contract(10, 5, op1, op2)
273 |
274 | // FIXME: these are fake values, but it does not work either way
275 | t.deepEqual(obs.sides[0], {vertex0: {pos: {_x: 11, _y: 0}}, vertex1: {pos: {_x: 11, _y: 1}}})
276 | t.deepEqual(obs.sides[obs.sides.length - 1], {vertex0: {pos: {_x: -1.8369701987210296e-15, _y: -10}}, vertex1: {pos: {_x: 0.9999999999999981, _y: -10}}})
277 | })
278 |
279 | test.failing('minkowski (multiple items)', t => {
280 | const op1 = cube()
281 | const op2 = sphere({center: true})
282 | const obs = minkowski(op1, op2)
283 |
284 | t.deepEqual(obs.properties.cube.center, {_x: 0.5, _y: 10.5, _z: 0.5})
285 | t.deepEqual(obs.properties.sphere.center, {_x: 0, _y: 10, _z: 0})
286 | })
287 |
288 | test.failing('hull (multiple items, 3d)', t => {
289 | const op1 = cube()
290 | const op2 = sphere()
291 | const obs = hull(op1, op2)
292 |
293 | t.deepEqual(obs.properties.cube.center, {_x: 0.5, _y: 10.5, _z: 0.5})
294 | t.deepEqual(obs.properties.sphere.center, {_x: 0, _y: 10, _z: 0})
295 | })
296 |
297 | test('hull (multiple items, 2d)', t => {
298 | const op1 = square()
299 | const op2 = circle()
300 | const obs = hull(op1, op2)
301 |
302 | t.deepEqual(obs.sides[0], {vertex0: {pos: {_x: 0, _y: 1.0000000000000002}}, vertex1: {pos: {_x: 0, _y: 0}}})
303 | t.deepEqual(obs.sides[obs.sides.length - 1], {vertex0: {pos: {_x: 0.01921471959676957, _y: 1.1950903220161286}}, vertex1: {pos: {_x: 0, _y: 1.0000000000000002}}})
304 | })
305 |
306 | test.failing('chain_hull (multiple items 3d)', t => {
307 | const op1 = cube()
308 | const op2 = sphere()
309 | const obs = chain_hull(op1, op2)
310 |
311 | t.deepEqual(obs.properties.cube.center, {_x: 0.5, _y: 10.5, _z: 0.5})
312 | t.deepEqual(obs.properties.sphere.center, {_x: 0, _y: 10, _z: 0})
313 | })
314 |
315 | test('chain_hull (multiple items, 2d)', t => {
316 | const op1 = square()
317 | const op2 = circle()
318 | const obs = chain_hull(op1, op2)
319 |
320 | t.deepEqual(obs.sides[0], {vertex0: {pos: {_x: 0, _y: 1.0000000000000002}}, vertex1: {pos: {_x: 0, _y: 0}}})
321 | t.deepEqual(obs.sides[obs.sides.length - 1], {vertex0: {pos: {_x: 0.01921471959676957, _y: 1.1950903220161286}}, vertex1: {pos: {_x: 0, _y: 1.0000000000000002}}})
322 | })
323 |
--------------------------------------------------------------------------------
/src/primitives2d.js:
--------------------------------------------------------------------------------
1 | const { CAG } = require('@jscad/csg')
2 |
3 | // -- 2D primitives (OpenSCAD like notion)
4 |
5 | /** Construct a square/rectangle
6 | * @param {Object} [options] - options for construction
7 | * @param {Float} [options.size=1] - size of the square, either as array or scalar
8 | * @param {Boolean} [options.center=true] - wether to center the square/rectangle or not
9 | * @returns {CAG} new square
10 | *
11 | * @example
12 | * let square1 = square({
13 | * size: 10
14 | * })
15 | */
16 | function square () {
17 | let v = [1, 1]
18 | let off
19 | let a = arguments
20 | let params = a[0]
21 |
22 | if (params && Number.isFinite(params)) v = [params, params]
23 | if (params && params.length) {
24 | v = a[0]
25 | params = a[1]
26 | }
27 | if (params && params.size && params.size.length) v = params.size
28 |
29 | off = [v[0] / 2, v[1] / 2]
30 | if (params && params.center === true) off = [0, 0]
31 |
32 | return CAG.rectangle({center: off, radius: [v[0] / 2, v[1] / 2]})
33 | }
34 |
35 | /** Construct a circle
36 | * @param {Object} [options] - options for construction
37 | * @param {Float} [options.r=1] - radius of the circle
38 | * @param {Integer} [options.fn=32] - segments of circle (ie quality/ resolution)
39 | * @param {Boolean} [options.center=true] - wether to center the circle or not
40 | * @returns {CAG} new circle
41 | *
42 | * @example
43 | * let circle1 = circle({
44 | * r: 10
45 | * })
46 | */
47 | function circle (params) {
48 | const defaults = {
49 | r: 1,
50 | fn: 32,
51 | center: false
52 | }
53 | let {r, fn, center} = Object.assign({}, defaults, params)
54 | if (params && !params.r && !params.fn && !params.center) r = params
55 | let offset = center === true ? [0, 0] : [r, r]
56 | return CAG.circle({center: offset, radius: r, resolution: fn})
57 | }
58 |
59 | /** Construct a polygon either from arrays of paths and points, or just arrays of points
60 | * nested paths (multiple paths) and flat paths are supported
61 | * @param {Object} [options] - options for construction
62 | * @param {Array} [options.paths] - paths of the polygon : either flat or nested array
63 | * @param {Array} [options.points] - points of the polygon : either flat or nested array
64 | * @returns {CAG} new polygon
65 | *
66 | * @example
67 | * let poly = polygon([0,1,2,3,4])
68 | * or
69 | * let poly = polygon({path: [0,1,2,3,4]})
70 | * or
71 | * let poly = polygon({path: [0,1,2,3,4], points: [2,1,3]})
72 | */
73 | function polygon (params) { // array of po(ints) and pa(ths)
74 | let points = [ ]
75 | if (params.paths && params.paths.length && params.paths[0].length) { // pa(th): [[0,1,2],[2,3,1]] (two paths)
76 | for (let j = 0; j < params.paths.length; j++) {
77 | for (let i = 0; i < params.paths[j].length; i++) {
78 | points[i] = params.points[params.paths[j][i]]
79 | }
80 | }
81 | } else if (params.paths && params.paths.length) { // pa(th): [0,1,2,3,4] (single path)
82 | for (let i = 0; i < params.paths.length; i++) {
83 | points[i] = params.points[params.paths[i]]
84 | }
85 | } else { // pa(th) = po(ints)
86 | if (params.length) {
87 | points = params
88 | } else {
89 | points = params.points
90 | }
91 | }
92 | return CAG.fromPoints(points)
93 | }
94 |
95 | // FIXME: errr this is kinda just a special case of a polygon , why do we need it ?
96 | /** Construct a triangle
97 | * @returns {CAG} new triangle
98 | *
99 | * @example
100 | * let triangle = trangle({
101 | * length: 10
102 | * })
103 | */
104 | function triangle () {
105 | let a = arguments
106 | if (a[0] && a[0].length) a = a[0]
107 | return CAG.fromPoints(a)
108 | }
109 |
110 | module.exports = {
111 | square,
112 | circle,
113 | polygon,
114 | triangle
115 | }
116 |
--------------------------------------------------------------------------------
/src/primitives2d.test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava')
2 | const { square, circle, triangle, polygon } = require('./primitives2d')
3 | const { sideEquals, shape2dToNestedArray } = require('./test-helpers')
4 |
5 | /* FIXME : not entirely sure how to deal with this, but for now relies on inspecting
6 | output data structures: we should have higher level primitives ... */
7 |
8 | // helper functions
9 | function comparePositonVertices (obs, exp) {
10 | for (let index = 0; index < obs.length; index++) {
11 | let side = obs[index]
12 | const same = side.vertex0.pos._x === exp[index][0][0] && side.vertex0.pos._y === exp[index][0][1]
13 | && side.vertex1.pos._x === exp[index][1][0] && side.vertex1.pos._y === exp[index][1][1]
14 | // console.log('side', side.vertex0.pos, same)
15 | if (!same) {
16 | return false
17 | }
18 | }
19 | return true
20 | }
21 |
22 | test.failing('triangle (defaults)', t => {
23 | const obs = triangle()
24 |
25 | const expSides = [
26 | [[0, 1], [0, 0]],
27 | [[0, 0], [1, 0]],
28 | [[1, 0], [1, 1]]
29 | ]
30 | t.deepEqual(obs.sides.length, 3)
31 | t.truthy(comparePositonVertices(obs.sides, expSides))
32 | })
33 |
34 | test.failing('triangle (custom size)', t => {
35 | const obs = triangle(5)
36 |
37 | const expSides = [
38 | [[0, 1], [0, 0]],
39 | [[0, 0], [1, 0]],
40 | [[1, 0], [1, 1]]
41 | ]
42 | t.deepEqual(obs.sides.length, 3)
43 | t.truthy(comparePositonVertices(obs.sides, expSides))
44 | })
45 |
46 | test('square (defaults)', t => {
47 | const obs = square()
48 |
49 | const expSides = [
50 | [[0, 1], [0, 0]],
51 | [[0, 0], [1, 0]],
52 | [[1, 0], [1, 1]],
53 | [[1, 1], [0, 1]]
54 | ]
55 | t.deepEqual(obs.sides.length, 4)
56 | t.truthy(comparePositonVertices(obs.sides, expSides))
57 | })
58 |
59 | test('square (custom size, 2d array parameter)', t => {
60 | const obs = square([2, 3])
61 |
62 | const expSides = [
63 | [[0, 3], [0, 0]],
64 | [[0, 0], [2, 0]],
65 | [[2, 0], [2, 3]],
66 | [[2, 3], [0, 3]]
67 | ]
68 |
69 | t.deepEqual(obs.sides.length, 4)
70 | t.truthy(comparePositonVertices(obs.sides, expSides))
71 | })
72 |
73 | test('square (custom size, size object parameter)', t => {
74 | const obs = square({size: [2, 3]})
75 |
76 | const expSides = [
77 | [[0, 3], [0, 0]],
78 | [[0, 0], [2, 0]],
79 | [[2, 0], [2, 3]],
80 | [[2, 3], [0, 3]]
81 | ]
82 |
83 | t.deepEqual(obs.sides.length, 4)
84 | t.truthy(comparePositonVertices(obs.sides, expSides))
85 | })
86 |
87 | test('square (default size, centered)', t => {
88 | const obs = square({center: true})
89 |
90 | const expSides = [
91 | [[-0.5, 0.5], [-0.5, -0.5]],
92 | [[-0.5, -0.5], [0.5, -0.5]],
93 | [[0.5, -0.5], [0.5, 0.5]],
94 | [[0.5, 0.5], [-0.5, 0.5]]
95 | ]
96 |
97 | t.deepEqual(obs.sides.length, 4)
98 | t.truthy(comparePositonVertices(obs.sides, expSides))
99 | })
100 |
101 | test('square (custom size, centered)', t => {
102 | const obs = square({size: [2, 3], center: true})
103 |
104 | const expSides = [ [ [ -1, 1.5 ], [ -1, -1.5 ] ],
105 | [ [ -1, -1.5 ], [ 1, -1.5 ] ],
106 | [ [ 1, -1.5 ], [ 1, 1.5 ] ],
107 | [ [ 1, 1.5 ], [ -1, 1.5 ] ] ]
108 |
109 | t.deepEqual(obs.sides.length, 4)
110 | t.truthy(comparePositonVertices(obs.sides, expSides))
111 | })
112 |
113 | test('circle (defaults)', t => {
114 | const obs = circle()
115 |
116 | // points that make up our circle
117 | const expected = [ [ 1.9807852804032304, 0.8049096779838713, 2, 1 ],
118 | [ 2, 1, 1.9807852804032304, 1.1950903220161282 ],
119 | [ 1.9807852804032304,
120 | 1.1950903220161282,
121 | 1.9238795325112867,
122 | 1.3826834323650898 ],
123 | [ 1.9238795325112867,
124 | 1.3826834323650898,
125 | 1.8314696123025453,
126 | 1.5555702330196022 ],
127 | [ 1.8314696123025453,
128 | 1.5555702330196022,
129 | 1.7071067811865475,
130 | 1.7071067811865475 ],
131 | [ 1.7071067811865475,
132 | 1.7071067811865475,
133 | 1.5555702330196022,
134 | 1.8314696123025453 ],
135 | [ 1.5555702330196022,
136 | 1.8314696123025453,
137 | 1.3826834323650898,
138 | 1.9238795325112867 ],
139 | [ 1.3826834323650898,
140 | 1.9238795325112867,
141 | 1.1950903220161284,
142 | 1.9807852804032304 ],
143 | [ 1.1950903220161284, 1.9807852804032304, 1, 2 ],
144 | [ 1, 2, 0.8049096779838718, 1.9807852804032304 ],
145 | [ 0.8049096779838718,
146 | 1.9807852804032304,
147 | 0.6173165676349103,
148 | 1.9238795325112867 ],
149 | [ 0.6173165676349103,
150 | 1.9238795325112867,
151 | 0.44442976698039804,
152 | 1.8314696123025453 ],
153 | [ 0.44442976698039804,
154 | 1.8314696123025453,
155 | 0.29289321881345254,
156 | 1.7071067811865475 ],
157 | [ 0.29289321881345254,
158 | 1.7071067811865475,
159 | 0.16853038769745465,
160 | 1.5555702330196022 ],
161 | [ 0.16853038769745465,
162 | 1.5555702330196022,
163 | 0.07612046748871326,
164 | 1.3826834323650898 ],
165 | [ 0.07612046748871326,
166 | 1.3826834323650898,
167 | 0.01921471959676957,
168 | 1.1950903220161286 ],
169 | [ 0.01921471959676957, 1.1950903220161286, 0, 1.0000000000000002 ],
170 | [ 0, 1.0000000000000002, 0.01921471959676957, 0.8049096779838716 ],
171 | [ 0.01921471959676957,
172 | 0.8049096779838716,
173 | 0.07612046748871315,
174 | 0.6173165676349104 ],
175 | [ 0.07612046748871315,
176 | 0.6173165676349104,
177 | 0.16853038769745454,
178 | 0.44442976698039804 ],
179 | [ 0.16853038769745454,
180 | 0.44442976698039804,
181 | 0.2928932188134523,
182 | 0.29289321881345254 ],
183 | [ 0.2928932188134523,
184 | 0.29289321881345254,
185 | 0.4444297669803978,
186 | 0.16853038769745476 ],
187 | [ 0.4444297669803978,
188 | 0.16853038769745476,
189 | 0.6173165676349097,
190 | 0.07612046748871348 ],
191 | [ 0.6173165676349097,
192 | 0.07612046748871348,
193 | 0.8049096779838714,
194 | 0.01921471959676968 ],
195 | [ 0.8049096779838714, 0.01921471959676968, 0.9999999999999998, 0 ],
196 | [ 0.9999999999999998, 0, 1.1950903220161284, 0.01921471959676957 ],
197 | [ 1.1950903220161284,
198 | 0.01921471959676957,
199 | 1.38268343236509,
200 | 0.07612046748871337 ],
201 | [ 1.38268343236509,
202 | 0.07612046748871337,
203 | 1.5555702330196017,
204 | 0.16853038769745454 ],
205 | [ 1.5555702330196017,
206 | 0.16853038769745454,
207 | 1.7071067811865475,
208 | 0.2928932188134523 ],
209 | [ 1.7071067811865475,
210 | 0.2928932188134523,
211 | 1.8314696123025453,
212 | 0.4444297669803978 ],
213 | [ 1.8314696123025453,
214 | 0.4444297669803978,
215 | 1.9238795325112865,
216 | 0.6173165676349096 ],
217 | [ 1.9238795325112865,
218 | 0.6173165676349096,
219 | 1.9807852804032304,
220 | 0.8049096779838713 ] ]
221 |
222 | t.deepEqual(obs.sides.length, 32)
223 | t.deepEqual(shape2dToNestedArray(obs), expected)
224 | })
225 |
226 | test('circle (custom radius)', t => {
227 | const obs = circle(10)
228 |
229 | // we just use a sample of points for simplicity
230 | t.deepEqual(obs.sides.length, 32)
231 | sideEquals(t, obs.sides[0], [[19.8078528040323, 8.049096779838713], [20, 10]])
232 | sideEquals(t, obs.sides[obs.sides.length - 1], [[19.238795325112864, 6.173165676349096], [19.8078528040323, 8.049096779838713]])
233 | })
234 |
235 | test('circle (custom radius, object as parameter)', t => {
236 | const obs = circle({r: 10})
237 |
238 | // we just use a sample of points for simplicity
239 | t.deepEqual(obs.sides.length, 32)
240 | sideEquals(t, obs.sides[0], [[19.8078528040323, 8.049096779838713], [20, 10]])
241 | sideEquals(t, obs.sides[obs.sides.length - 1], [[19.238795325112864, 6.173165676349096], [19.8078528040323, 8.049096779838713]])
242 | })
243 |
244 | test('circle (custom radius, custom resolution, object as parameter)', t => {
245 | const obs = circle({r: 10, fn: 5})
246 |
247 | // we just use a sample of points for simplicity
248 | t.deepEqual(obs.sides.length, 5)
249 | sideEquals(t, obs.sides[0], [[13.090169943749473, 0.4894348370484636], [20, 10]])
250 | sideEquals(t, obs.sides[obs.sides.length - 1], [[1.9098300562505255, 4.12214747707527], [13.090169943749473, 0.4894348370484636]])
251 | })
252 |
253 | test('circle (custom radius, custom resolution, centered object as parameter)', t => {
254 | const obs = circle({center: true, r: 10, fn: 5})
255 |
256 | // we just use a sample of points for simplicity
257 | t.deepEqual(obs.sides.length, 5)
258 | sideEquals(t, obs.sides[0], [[3.0901699437494723, -9.510565162951536], [10, 0]])
259 | sideEquals(t, obs.sides[obs.sides.length - 1], [[-8.090169943749475, -5.87785252292473], [3.0901699437494723, -9.510565162951536]])
260 | })
261 |
262 | test('polygon (direct params)', t => {
263 | const obs = polygon([ [0, 0], [3, 0], [3, 3] ])
264 |
265 | const expSides = [ [ [ 3, 3 ], [ 0, 0 ] ],
266 | [ [ 0, 0 ], [ 3, 0 ] ],
267 | [ [ 3, 0 ], [ 3, 3 ] ]
268 | ]
269 | // we just use a sample of points for simplicity
270 | t.deepEqual(obs.sides.length, 3)
271 | t.truthy(comparePositonVertices(obs.sides, expSides))
272 | })
273 |
274 | test('polygon (object params)', t => {
275 | const obs = polygon({points: [ [0, 0], [3, 0], [3, 3] ]})
276 |
277 | const expSides = [ [ [ 3, 3 ], [ 0, 0 ] ],
278 | [ [ 0, 0 ], [ 3, 0 ] ],
279 | [ [ 3, 0 ], [ 3, 3 ] ]
280 | ]
281 | // we just use a sample of points for simplicity
282 | t.deepEqual(obs.sides.length, 3)
283 | t.truthy(comparePositonVertices(obs.sides, expSides))
284 | })
285 |
286 | test.failing('polygon (object params, with custom paths)', t => {
287 | const obs = polygon({points: [ [0, 0], [3, 0], [3, 3] ], paths: [ [0, 1, 2], [1, 2, 3] ]})
288 |
289 | const expSides = [ [ [ 3, 3 ], [ 0, 0 ] ],
290 | [ [ 0, 0 ], [ 3, 0 ] ],
291 | [ [ 3, 0 ], [ 3, 3 ] ]
292 | ]
293 | // we just use a sample of points for simplicity
294 | t.deepEqual(obs.sides.length, 3)
295 | t.truthy(comparePositonVertices(obs.sides, expSides))
296 | })
297 |
--------------------------------------------------------------------------------
/src/primitives3d.js:
--------------------------------------------------------------------------------
1 | // -- 3D primitives (OpenSCAD like notion)
2 | const { CSG } = require('@jscad/csg')
3 | const { circle } = require('./primitives2d')
4 | const { rotate_extrude } = require('./ops-extrusions')
5 | const { translate, scale } = require('./ops-transformations')
6 |
7 | /** Construct a cuboid
8 | * @param {Object} [options] - options for construction
9 | * @param {Float} [options.size=1] - size of the side of the cuboid : can be either:
10 | * - a scalar : ie a single float, in which case all dimensions will be the same
11 | * - or an array: to specify different dimensions along x/y/z
12 | * @param {Integer} [options.fn=32] - segments of the sphere (ie quality/resolution)
13 | * @param {Integer} [options.fno=32] - segments of extrusion (ie quality)
14 | * @param {String} [options.type='normal'] - type of sphere : either 'normal' or 'geodesic'
15 | * @returns {CSG} new sphere
16 | *
17 | * @example
18 | * let cube1 = cube({
19 | * r: 10,
20 | * fn: 20
21 | * })
22 | */
23 | function cube(params) {
24 | const defaults = {
25 | size: 1,
26 | offset: [0, 0, 0],
27 | round: false,
28 | radius: 0,
29 | fn: 8
30 | }
31 |
32 | let {round, radius, fn, size} = Object.assign({}, defaults, params)
33 | let offset = [0, 0, 0]
34 | let v = null
35 | if (params && params.length) v = params
36 | if (params && params.size && params.size.length) v = params.size // { size: [1,2,3] }
37 | if (params && params.size && !params.size.length) size = params.size // { size: 1 }
38 | if (params && (typeof params !== 'object')) size = params// (2)
39 | if (params && params.round === true) {
40 | round = true
41 | radius = v && v.length ? (v[0] + v[1] + v[2]) / 30 : size / 10
42 | }
43 | if (params && params.radius) {
44 | round = true
45 | radius = params.radius
46 | }
47 |
48 | let x = size
49 | let y = size
50 | let z = size
51 | if (v && v.length) {
52 | [x, y, z] = v
53 | }
54 | offset = [x / 2, y / 2, z / 2] // center: false default
55 | let object = round
56 | ? CSG.roundedCube({radius: [x / 2, y / 2, z / 2], roundradius: radius, resolution: fn})
57 | : CSG.cube({radius: [x / 2, y / 2, z / 2]})
58 | if (params && params.center && params.center.length) {
59 | offset = [params.center[0] ? 0 : x / 2, params.center[1] ? 0 : y / 2, params.center[2] ? 0 : z / 2]
60 | } else if (params && params.center === true) {
61 | offset = [0, 0, 0]
62 | } else if (params && params.center === false) {
63 | offset = [x / 2, y / 2, z / 2]
64 | }
65 | return (offset[0] || offset[1] || offset[2]) ? translate(offset, object) : object
66 | }
67 |
68 | /** Construct a sphere
69 | * @param {Object} [options] - options for construction
70 | * @param {Float} [options.r=1] - radius of the sphere
71 | * @param {Integer} [options.fn=32] - segments of the sphere (ie quality/resolution)
72 | * @param {Integer} [options.fno=32] - segments of extrusion (ie quality)
73 | * @param {String} [options.type='normal'] - type of sphere : either 'normal' or 'geodesic'
74 | * @returns {CSG} new sphere
75 | *
76 | * @example
77 | * let sphere1 = sphere({
78 | * r: 10,
79 | * fn: 20
80 | * })
81 | */
82 | function sphere (params) {
83 | const defaults = {
84 | r: 1,
85 | fn: 32,
86 | type: 'normal'
87 | }
88 |
89 | let {r, fn, type} = Object.assign({}, defaults, params)
90 | let offset = [0, 0, 0] // center: false (default)
91 | if (params && (typeof params !== 'object')) {
92 | r = params
93 | }
94 | // let zoffset = 0 // sphere() in openscad has no center:true|false
95 |
96 | let output = type === 'geodesic' ? geodesicSphere(params) : CSG.sphere({radius: r, resolution: fn})
97 |
98 | // preparing individual x,y,z center
99 | if (params && params.center && params.center.length) {
100 | offset = [params.center[0] ? 0 : r, params.center[1] ? 0 : r, params.center[2] ? 0 : r]
101 | } else if (params && params.center === true) {
102 | offset = [0, 0, 0]
103 | } else if (params && params.center === false) {
104 | offset = [r, r, r]
105 | }
106 | return (offset[0] || offset[1] || offset[2]) ? translate(offset, output) : output
107 | }
108 |
109 | function geodesicSphere (params) {
110 | const defaults = {
111 | r: 1,
112 | fn: 5
113 | }
114 | let {r, fn} = Object.assign({}, defaults, params)
115 |
116 | let ci = [ // hard-coded data of icosahedron (20 faces, all triangles)
117 | [0.850651, 0.000000, -0.525731],
118 | [0.850651, -0.000000, 0.525731],
119 | [-0.850651, -0.000000, 0.525731],
120 | [-0.850651, 0.000000, -0.525731],
121 | [0.000000, -0.525731, 0.850651],
122 | [0.000000, 0.525731, 0.850651],
123 | [0.000000, 0.525731, -0.850651],
124 | [0.000000, -0.525731, -0.850651],
125 | [-0.525731, -0.850651, -0.000000],
126 | [0.525731, -0.850651, -0.000000],
127 | [0.525731, 0.850651, 0.000000],
128 | [-0.525731, 0.850651, 0.000000]]
129 |
130 | let ti = [ [0, 9, 1], [1, 10, 0], [6, 7, 0], [10, 6, 0], [7, 9, 0], [5, 1, 4], [4, 1, 9], [5, 10, 1], [2, 8, 3], [3, 11, 2], [2, 5, 4],
131 | [4, 8, 2], [2, 11, 5], [3, 7, 6], [6, 11, 3], [8, 7, 3], [9, 8, 4], [11, 10, 5], [10, 11, 6], [8, 9, 7]]
132 |
133 | let geodesicSubDivide = function (p, fn, offset) {
134 | let p1 = p[0]
135 | let p2 = p[1]
136 | let p3 = p[2]
137 | let n = offset
138 | let c = []
139 | let f = []
140 |
141 | // p3
142 | // /\
143 | // /__\ fn = 3
144 | // i /\ /\
145 | // /__\/__\ total triangles = 9 (fn*fn)
146 | // /\ /\ /\
147 | // 0/__\/__\/__\
148 | // p1 0 j p2
149 |
150 | for (let i = 0; i < fn; i++) {
151 | for (let j = 0; j < fn - i; j++) {
152 | let t0 = i / fn
153 | let t1 = (i + 1) / fn
154 | let s0 = j / (fn - i)
155 | let s1 = (j + 1) / (fn - i)
156 | let s2 = fn - i - 1 ? j / (fn - i - 1) : 1
157 | let q = []
158 |
159 | q[0] = mix3(mix3(p1, p2, s0), p3, t0)
160 | q[1] = mix3(mix3(p1, p2, s1), p3, t0)
161 | q[2] = mix3(mix3(p1, p2, s2), p3, t1)
162 |
163 | // -- normalize
164 | for (let k = 0; k < 3; k++) {
165 | let r = Math.sqrt(q[k][0] * q[k][0] + q[k][1] * q[k][1] + q[k][2] * q[k][2])
166 | for (let l = 0; l < 3; l++) {
167 | q[k][l] /= r
168 | }
169 | }
170 | c.push(q[0], q[1], q[2])
171 | f.push([n, n + 1, n + 2]); n += 3
172 |
173 | if (j < fn - i - 1) {
174 | let s3 = fn - i - 1 ? (j + 1) / (fn - i - 1) : 1
175 | q[0] = mix3(mix3(p1, p2, s1), p3, t0)
176 | q[1] = mix3(mix3(p1, p2, s3), p3, t1)
177 | q[2] = mix3(mix3(p1, p2, s2), p3, t1)
178 |
179 | // -- normalize
180 | for (let k = 0; k < 3; k++) {
181 | let r = Math.sqrt(q[k][0] * q[k][0] + q[k][1] * q[k][1] + q[k][2] * q[k][2])
182 | for (let l = 0; l < 3; l++) {
183 | q[k][l] /= r
184 | }
185 | }
186 | c.push(q[0], q[1], q[2])
187 | f.push([n, n + 1, n + 2]); n += 3
188 | }
189 | }
190 | }
191 | return { points: c, triangles: f, offset: n }
192 | }
193 |
194 | const mix3 = function (a, b, f) {
195 | let _f = 1 - f
196 | let c = []
197 | for (let i = 0; i < 3; i++) {
198 | c[i] = a[i] * _f + b[i] * f
199 | }
200 | return c
201 | }
202 |
203 | if (params) {
204 | if (params.fn) fn = Math.floor(params.fn / 6)
205 | }
206 |
207 | if (fn <= 0) fn = 1
208 |
209 | let c = []
210 | let f = []
211 | let offset = 0
212 |
213 | for (let i = 0; i < ti.length; i++) {
214 | let g = geodesicSubDivide([ ci[ti[i][0]], ci[ti[i][1]], ci[ti[i][2]]], fn, offset)
215 | c = c.concat(g.points)
216 | f = f.concat(g.triangles)
217 | offset = g.offset
218 | }
219 | return scale(r, polyhedron({points: c, triangles: f}))
220 | }
221 |
222 | /** Construct a cylinder
223 | * @param {Object} [options] - options for construction
224 | * @param {Float} [options.r=1] - radius of the cylinder
225 | * @param {Float} [options.r1=1] - radius of the top of the cylinder
226 | * @param {Float} [options.r2=1] - radius of the bottom of the cylinder
227 | * @param {Float} [options.d=1] - diameter of the cylinder
228 | * @param {Float} [options.d1=1] - diameter of the top of the cylinder
229 | * @param {Float} [options.d2=1] - diameter of the bottom of the cylinder
230 | * @param {Integer} [options.fn=32] - number of sides of the cylinder (ie quality/resolution)
231 | * @returns {CSG} new cylinder
232 | *
233 | * @example
234 | * let cylinder = cylinder({
235 | * d: 10,
236 | * fn: 20
237 | * })
238 | */
239 | function cylinder (params) {
240 | const defaults = {
241 | r: 1,
242 | r1: 1,
243 | r2: 1,
244 | h: 1,
245 | fn: 32,
246 | round: false
247 | }
248 | let {r1, r2, h, fn, round} = Object.assign({}, defaults, params)
249 | let offset = [0, 0, 0]
250 | let a = arguments
251 | if (params && params.d) {
252 | r1 = r2 = params.d / 2
253 | }
254 | if (params && params.r) {
255 | r1 = params.r
256 | r2 = params.r
257 | }
258 | if (params && params.h) {
259 | h = params.h
260 | }
261 | if (params && (params.r1 || params.r2)) {
262 | r1 = params.r1
263 | r2 = params.r2
264 | if (params.h) h = params.h
265 | }
266 | if (params && (params.d1 || params.d2)) {
267 | r1 = params.d1 / 2
268 | r2 = params.d2 / 2
269 | }
270 |
271 | if (a && a[0] && a[0].length) {
272 | a = a[0]
273 | r1 = a[0]
274 | r2 = a[1]
275 | h = a[2]
276 | if (a.length === 4) fn = a[3]
277 | }
278 |
279 | let object
280 | if (params && (params.start && params.end)) {
281 | object = round
282 | ? CSG.roundedCylinder({start: params.start, end: params.end, radiusStart: r1, radiusEnd: r2, resolution: fn})
283 | : CSG.cylinder({start: params.start, end: params.end, radiusStart: r1, radiusEnd: r2, resolution: fn})
284 | } else {
285 | object = round
286 | ? CSG.roundedCylinder({start: [0, 0, 0], end: [0, 0, h], radiusStart: r1, radiusEnd: r2, resolution: fn})
287 | : CSG.cylinder({start: [0, 0, 0], end: [0, 0, h], radiusStart: r1, radiusEnd: r2, resolution: fn})
288 | let r = r1 > r2 ? r1 : r2
289 | if (params && params.center && params.center.length) { // preparing individual x,y,z center
290 | offset = [params.center[0] ? 0 : r, params.center[1] ? 0 : r, params.center[2] ? -h / 2 : 0]
291 | } else if (params && params.center === true) {
292 | offset = [0, 0, -h / 2]
293 | } else if (params && params.center === false) {
294 | offset = [0, 0, 0]
295 | }
296 | object = (offset[0] || offset[1] || offset[2]) ? translate(offset, object) : object
297 | }
298 | return object
299 | }
300 |
301 | /** Construct a torus
302 | * @param {Object} [options] - options for construction
303 | * @param {Float} [options.ri=1] - radius of base circle
304 | * @param {Float} [options.ro=4] - radius offset
305 | * @param {Integer} [options.fni=16] - segments of base circle (ie quality)
306 | * @param {Integer} [options.fno=32] - segments of extrusion (ie quality)
307 | * @param {Integer} [options.roti=0] - rotation angle of base circle
308 | * @returns {CSG} new torus
309 | *
310 | * @example
311 | * let torus1 = torus({
312 | * ri: 10
313 | * })
314 | */
315 | function torus (params) {
316 | const defaults = {
317 | ri: 1,
318 | ro: 4,
319 | fni: 16,
320 | fno: 32,
321 | roti: 0
322 | }
323 | params = Object.assign({}, defaults, params)
324 |
325 | /* possible enhancements ? declarative limits
326 | const limits = {
327 | fni: {min: 3},
328 | fno: {min: 3}
329 | }*/
330 |
331 | let {ri, ro, fni, fno, roti} = params
332 |
333 | if (fni < 3) fni = 3
334 | if (fno < 3) fno = 3
335 |
336 | let baseCircle = circle({r: ri, fn: fni, center: true})
337 |
338 | if (roti) baseCircle = baseCircle.rotateZ(roti)
339 | let result = rotate_extrude({fn: fno}, translate([ro, 0, 0], baseCircle))
340 | // result = result.union(result)
341 | return result
342 | }
343 |
344 | /** Construct a polyhedron from the given triangles/ polygons/points
345 | * @param {Object} [options] - options for construction
346 | * @param {Array} [options.triangles] - triangles to build the polyhedron from
347 | * @param {Array} [options.polygons] - polygons to build the polyhedron from
348 | * @param {Array} [options.points] - points to build the polyhedron from
349 | * @param {Array} [options.colors] - colors to apply to the polyhedron
350 | * @returns {CSG} new polyhedron
351 | *
352 | * @example
353 | * let torus1 = polyhedron({
354 | * points: [...]
355 | * })
356 | */
357 | function polyhedron (params) {
358 | let pgs = []
359 | let ref = params.triangles || params.polygons
360 | let colors = params.colors || null
361 |
362 | for (let i = 0; i < ref.length; i++) {
363 | let pp = []
364 | for (let j = 0; j < ref[i].length; j++) {
365 | pp[j] = params.points[ref[i][j]]
366 | }
367 |
368 | let v = []
369 | for (let j = ref[i].length - 1; j >= 0; j--) { // --- we reverse order for examples of OpenSCAD work
370 | v.push(new CSG.Vertex(new CSG.Vector3D(pp[j][0], pp[j][1], pp[j][2])))
371 | }
372 | let s = CSG.Polygon.defaultShared
373 | if (colors && colors[i]) {
374 | s = CSG.Polygon.Shared.fromColor(colors[i])
375 | }
376 | pgs.push(new CSG.Polygon(v, s))
377 | }
378 |
379 | return CSG.fromPolygons(pgs)
380 | }
381 |
382 | module.exports = {
383 | cube,
384 | sphere,
385 | geodesicSphere,
386 | cylinder,
387 | torus,
388 | polyhedron
389 | }
390 |
--------------------------------------------------------------------------------
/src/primitives3d.test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava')
2 | const { cube, sphere, geodesicSphere, cylinder, torus, polyhedron } = require('./primitives3d')
3 | const { simplifiedPolygon, comparePolygons } = require('./test-helpers.js')
4 |
5 | /* FIXME : not entirely sure how to deal with this, but for now relies on inspecting
6 | output data structures: we should have higher level primitives ... */
7 |
8 | test('cube (defaults)', t => {
9 | const obs = cube()
10 | const expFirstPoly = {
11 | vertices: [ { pos: { _x: 0, _y: 0, _z: 0 } },
12 | { pos: { _x: 0, _y: 0, _z: 1 } },
13 | { pos: { _x: 0, _y: 1, _z: 1 } },
14 | { pos: { _x: 0, _y: 1, _z: 0 } } ],
15 | shared: { color: null },
16 | plane: { normal: { _x: -1, _y: -0, _z: -0 }, w: -0 }
17 | }
18 |
19 | const expPoly5 = { vertices: [ { pos: { _x: 0, _y: 0, _z: 1 } },
20 | { pos: { _x: 1, _y: 0, _z: 1 } },
21 | { pos: { _x: 1, _y: 1, _z: 1 } },
22 | { pos: { _x: 0, _y: 1, _z: 1 } } ],
23 | shared: { color: null },
24 | plane: { normal: { _x: 0, _y: -0, _z: 1 }, w: 1 } }
25 |
26 | t.deepEqual(obs.properties.cube.center, {_x: 0.5, _y: 0.5, _z: 0.5})
27 | t.deepEqual(obs.polygons.length, 6)
28 | t.deepEqual(obs.polygons[0], expFirstPoly)
29 | t.deepEqual(obs.polygons[5], expPoly5)
30 | })
31 |
32 | test('cube (custom size, single parameter)', t => {
33 | const obs = cube(2)
34 | const expFirstPoly = {
35 | vertices: [ { pos: { _x: 0, _y: 0, _z: 0 } },
36 | { pos: { _x: 0, _y: 0, _z: 2 } },
37 | { pos: { _x: 0, _y: 2, _z: 2 } },
38 | { pos: { _x: 0, _y: 2, _z: 0 } } ],
39 | shared: { color: null },
40 | plane: { normal: { _x: -1, _y: -0, _z: -0 }, w: -0 }
41 | }
42 |
43 | const expPoly5 = { vertices: [ { pos: { _x: 0, _y: 0, _z: 2 } },
44 | { pos: { _x: 2, _y: 0, _z: 2 } },
45 | { pos: { _x: 2, _y: 2, _z: 2 } },
46 | { pos: { _x: 0, _y: 2, _z: 2 } } ],
47 | shared: { color: null },
48 | plane: { normal: { _x: 0, _y: -0, _z: 1 }, w: 2 } }
49 |
50 | t.deepEqual(obs.properties.cube.center, {_x: 1, _y: 1, _z: 1})
51 | t.deepEqual(obs.polygons.length, 6)
52 | t.deepEqual(obs.polygons[0], expFirstPoly)
53 | t.deepEqual(obs.polygons[5], expPoly5)
54 | })
55 |
56 | test('cube (custom size, single value, object parameter)', t => {
57 | const obs = cube({size: 2})
58 | const expFirstPoly = {
59 | vertices: [ { pos: { _x: 0, _y: 0, _z: 0 } },
60 | { pos: { _x: 0, _y: 0, _z: 2 } },
61 | { pos: { _x: 0, _y: 2, _z: 2 } },
62 | { pos: { _x: 0, _y: 2, _z: 0 } } ],
63 | shared: { color: null },
64 | plane: { normal: { _x: -1, _y: -0, _z: -0 }, w: -0 }
65 | }
66 |
67 | const expPoly5 = { vertices: [ { pos: { _x: 0, _y: 0, _z: 2 } },
68 | { pos: { _x: 2, _y: 0, _z: 2 } },
69 | { pos: { _x: 2, _y: 2, _z: 2 } },
70 | { pos: { _x: 0, _y: 2, _z: 2 } } ],
71 | shared: { color: null },
72 | plane: { normal: { _x: 0, _y: -0, _z: 1 }, w: 2 } }
73 |
74 | t.deepEqual(obs.properties.cube.center, {_x: 1, _y: 1, _z: 1})
75 | t.deepEqual(obs.polygons.length, 6)
76 | t.deepEqual(obs.polygons[0], expFirstPoly)
77 | t.deepEqual(obs.polygons[5], expPoly5)
78 | })
79 |
80 | test('cube (custom size, array value, object parameter)', t => {
81 | const obs = cube({size: [2, 1, 3]})
82 | const expFirstPoly = {
83 | vertices: [ { pos: { _x: 0, _y: 0, _z: 0 } },
84 | { pos: { _x: 0, _y: 0, _z: 3 } },
85 | { pos: { _x: 0, _y: 1, _z: 3 } },
86 | { pos: { _x: 0, _y: 1, _z: 0 } } ],
87 | shared: { color: null },
88 | plane: { normal: { _x: -1, _y: -0, _z: -0 }, w: -0 }
89 | }
90 |
91 | const expPoly5 = { vertices: [ { pos: { _x: 0, _y: 0, _z: 3 } },
92 | { pos: { _x: 2, _y: 0, _z: 3 } },
93 | { pos: { _x: 2, _y: 1, _z: 3 } },
94 | { pos: { _x: 0, _y: 1, _z: 3 } } ],
95 | shared: { color: null },
96 | plane: { normal: { _x: 0, _y: -0, _z: 1 }, w: 3 } }
97 |
98 | t.deepEqual(obs.properties.cube.center, {_x: 1, _y: 0.5, _z: 1.5})
99 | t.deepEqual(obs.polygons.length, 6)
100 | t.deepEqual(obs.polygons[0], expFirstPoly)
101 | t.deepEqual(obs.polygons[5], expPoly5)
102 | })
103 |
104 | test('cube (standard size, custom center(booleans), object parameter)', t => {
105 | const obs = cube({size: 1, center: [false, true, false]})
106 | const expFirstPoly = {
107 | vertices: [ { pos: { _x: 0, _y: -0.5, _z: 0 } },
108 | { pos: { _x: 0, _y: -0.5, _z: 1 } },
109 | { pos: { _x: 0, _y: 0.5, _z: 1 } },
110 | { pos: { _x: 0, _y: 0.5, _z: 0 } } ],
111 | shared: { color: null },
112 | plane: { normal: { _x: -1, _y: -0, _z: -0 }, w: -0 }
113 | }
114 |
115 | const expPoly5 = { vertices: [ { pos: { _x: 0, _y: -0.5, _z: 1 } },
116 | { pos: { _x: 1, _y: -0.5, _z: 1 } },
117 | { pos: { _x: 1, _y: 0.5, _z: 1 } },
118 | { pos: { _x: 0, _y: 0.5, _z: 1 } } ],
119 | shared: { color: null },
120 | plane: { normal: { _x: 0, _y: -0, _z: 1 }, w: 1 } }
121 |
122 | t.deepEqual(obs.properties.cube.center, {_x: 0.5, _y: 0, _z: 0.5})
123 | t.deepEqual(obs.polygons.length, 6)
124 | t.deepEqual(obs.polygons[0], expFirstPoly)
125 | t.deepEqual(obs.polygons[5], expPoly5)
126 | })
127 |
128 | test('cube (standard size, rounded)', t => {
129 | const obs = cube({round: true})
130 | const expFirstPoly = {
131 | vertices: [ { pos: { _x: 0.09999999999999998, _y: 0, _z: 0.09999999999999998 } },
132 | { pos: { _x: 0.029289321881345254,
133 | _y: 0.0292893218813452,
134 | _z: 0.09999999999999998 } },
135 | { pos: { _x: 0.04999999999999999,
136 | _y: 0.04999999999999993,
137 | _z: 0.029289321881345254 } },
138 | { pos: { _x: 0.09999999999999998,
139 | _y: 0.0292893218813452,
140 | _z: 0.029289321881345254 } } ],
141 | shared: { color: null, tag: 296 },
142 | plane: { normal: { _x: -0.3574067443365931,
143 | _y: -0.8628562094610168,
144 | _z: -0.3574067443365933 },
145 | w: -0.07148134886731874 }
146 | }
147 |
148 | const expPoly5 = { vertices: [ { pos: { _x: 0.8999999999999998, _y: 0.09999999999999998, _z: 0 } },
149 | { pos: { _x: 0.9000000000000005,
150 | _y: 0.0292893218813452,
151 | _z: 0.029289321881345254 } },
152 | { pos: { _x: 0.09999999999999998,
153 | _y: 0.0292893218813452,
154 | _z: 0.029289321881345254 } },
155 | { pos: { _x: 0.09999999999999998, _y: 0.09999999999999998, _z: 0 } } ],
156 | shared: { color: null, tag: 296 },
157 | plane: { normal: { _x: -0, _y: -0.3826834323650898, _z: -0.9238795325112868 },
158 | w: -0.03826834323650884 } }
159 |
160 | t.deepEqual(obs.properties.sphere.center, {_x: 0.09999999999999998, _y: 0.09999999999999998, _z: 0.09999999999999998})
161 | t.deepEqual(obs.properties.roundedCube.center, {pos: {_x: 0.5, _y: 0.5, _z: 0.5}})
162 | t.deepEqual(obs.polygons.length, 62)
163 | t.deepEqual(obs.polygons[0], expFirstPoly)
164 | t.deepEqual(obs.polygons[5], expPoly5)
165 | })
166 |
167 | test('cube (custom size, rounded)', t => {
168 | const obs = cube({size: [69, 20, 3], round: true, radius: 0.5})
169 |
170 | const expFirstPoly = {
171 | vertices: [ { pos: { _x: 0.5, _y: 0, _z: 0.5 } },
172 | { pos: { _x: 0.14644660940672338,
173 | _y: 0.14644660940672694,
174 | _z: 0.5 } },
175 | { pos: { _x: 0.25,
176 | _y: 0.25,
177 | _z: 0.14644660940672627 } },
178 | { pos: { _x: 0.5,
179 | _y: 0.14644660940672694,
180 | _z: 0.14644660940672627 } } ],
181 | shared: { color: null, tag: 296 },
182 | plane: { normal: { _x: -0.35740674433659303,
183 | _y: -0.8628562094610174,
184 | _z: -0.3574067443365919 },
185 | w: -0.3574067443365845 }
186 | }
187 |
188 | const expPoly5 = { vertices: [ { pos: { _x: 68.5, _y: 0.5, _z: 0 } },
189 | { pos: { _x: 68.50000000000003,
190 | _y: 0.14644660940672694,
191 | _z: 0.14644660940672627 } },
192 | { pos: { _x: 0.5,
193 | _y: 0.14644660940672694,
194 | _z: 0.14644660940672627 } },
195 | { pos: { _x: 0.5, _y: 0.5, _z: 0 } } ],
196 | shared: { color: null, tag: 296 },
197 | plane: { normal: { _x: -0, _y: -0.3826834323650899, _z: -0.9238795325112867 },
198 | w: -0.19134171618254614 } }
199 |
200 | t.deepEqual(obs.properties.sphere.center, {_x: 0.5, _y: 0.5, _z: 0.5})
201 | t.deepEqual(obs.properties.roundedCube.center, {pos: {_x: 34.5, _y: 10, _z: 1.5}})
202 | t.deepEqual(obs.polygons.length, 62)
203 | t.deepEqual(obs.polygons[0], expFirstPoly)
204 | t.deepEqual(obs.polygons[5], expPoly5)
205 | })
206 |
207 | test('sphere (defaults)', t => {
208 | const obs = sphere()
209 | const expFirstPoly = {
210 | vertices: [ { pos: { _x: 1, _y: 0, _z: 0 } },
211 | { pos: { _x: 0.9807852804032304, _y: -0.19509032201612825, _z: 0 } },
212 | { pos: { _x: 0.9619397662556434, _y: -0.19134171618254486, _z: -0.19509032201612825 } },
213 | { pos: { _x: 0.9807852804032304, _y: 0, _z: -0.19509032201612825 } } ],
214 | shared: { color: null, tag: 296 },
215 | plane: { normal: { _x: 0.9904383506609418, _y: -0.09754966309535142, _z: -0.09754966309535128 },
216 | w: 0.9904383506609418 } }
217 |
218 | const expPoly511 = { vertices: [
219 | { pos: { _x: 6.005577771483276e-17, _y: 1.1945836920083923e-17, _z: 1 } },
220 | { pos: { _x: 0.19509032201612833, _y: 4.778334768033559e-17, _z: 0.9807852804032304 } },
221 | { pos: { _x: 0.19134171618254492, _y: 0.03806023374435672, _z: 0.9807852804032304 } } ],
222 | shared: { color: null, tag: 296 },
223 | plane: { normal: { _x: 0.09801257320997024, _y: 0.009653395882096847, _z: 0.9951383559288144 },
224 | w: 0.9951383559288144 } }
225 |
226 | t.deepEqual(obs.properties.sphere.center, {_x: 0, _y: 0, _z: 0})
227 | t.deepEqual(obs.polygons.length, 512)
228 | t.deepEqual(obs.polygons[0], expFirstPoly)
229 | t.deepEqual(obs.polygons[511], expPoly511)
230 | })
231 |
232 | test('sphere (geodesic)', t => {
233 | const obs = sphere({type: 'geodesic'})
234 | const expFirstPoly = {
235 | vertices: [
236 | { pos: { _x: 0.9376113117392494, _y: 0, _z: -0.3476852428542286 } },
237 | { pos: { _x: 0.8659842041673648, _y: -0.1875222783339947, _z: -0.46358036332554414 } },
238 | { pos: { _x: 0.850650911463407, _y: 0, _z: -0.5257309452814002 } } ],
239 | shared: { color: null, tag: 296 },
240 | plane: { normal: { _x: 0.8962330678097445, _y: -0.07179554325990238, _z: -0.4377347234711329 },
241 | w: 0.9925121659689756 } }
242 |
243 | const expPoly499 = { vertices: [
244 | { pos: { _x: 0, _y: -0.5257309452814002, _z: -0.850650911463407 } },
245 | { pos: { _x: 0.11589509083138601, _y: -0.6511026416595388, _z: -0.7500891133359788 } },
246 | { pos: { _x: -0.11589509083138601, _y: -0.6511026416595388, _z: -0.7500891133359788 } } ],
247 | shared: { color: null, tag: 296 },
248 | plane: { normal: { _x: -0, _y: -0.6256977990747257, _z: -0.7800655512410763 },
249 | w: 0.9925121675324735 } }
250 |
251 | t.deepEqual(obs.polygons.length, 500)
252 | t.deepEqual(obs.polygons[0], expFirstPoly)
253 | t.deepEqual(obs.polygons[499], expPoly499)
254 | })
255 |
256 | test('sphere (custom radius , resolution, center)', t => {
257 | const obs = sphere({r: 2, fn: 10, center: [true, true, false]})
258 | const expFirstPoly = {
259 | vertices: [ { pos: { _x: 2, _y: 0, _z: 2 } },
260 | { pos: { _x: 1.618033988749895, _y: -1.1755705045849463, _z: 2 } },
261 | { pos: { _x: 1.4012585384440737, _y: -1.0180739209102545, _z: 1 } },
262 | { pos: { _x: 1.7320508075688774, _y: 0, _z: 1 } } ],
263 | shared: { color: null, tag: 296 },
264 | plane: { normal: { _x: 0.9216023954604601,
265 | _y: -0.29944677038053147,
266 | _z: -0.24694261760621833 },
267 | w: 1.3493195557084836 } }
268 |
269 | const expPoly59 = { vertices: [ { pos: { _x: 9.907600726170914e-17, _y: 7.19829327805997e-17, _z: 4 } },
270 | { pos: { _x: 1.0000000000000002, _y: 2.449293598294707e-16, _z: 3.732050807568877 } },
271 | { pos: { _x: 0.8090169943749475, _y: 0.5877852522924735, _z: 3.732050807568877 } } ],
272 | shared: { color: null, tag: 296 },
273 | plane: { normal: { _x: 0.2579086818975185,
274 | _y: 0.08379961057797093,
275 | _z: 0.9625283045546582 },
276 | w: 3.850113218218633 } }
277 |
278 | t.deepEqual(obs.properties.sphere.center, {_x: 0, _y: 0, _z: 2})
279 | t.deepEqual(obs.polygons.length, 60)
280 | t.is(comparePolygons(obs.polygons[0], expFirstPoly, 0.000001), true)
281 | t.is(comparePolygons(obs.polygons[59], expPoly59, 0.000001), true)
282 | })
283 |
284 | test('cylinder (defaults)', t => {
285 | const obs = cylinder()
286 | const expFirstPoly = {
287 | vertices: [ { pos: { _x: 0, _y: 0, _z: 0 } },
288 | { pos: { _x: 1, _y: 0, _z: 0 } },
289 | { pos: { _x: 0.9807852804032304, _y: -0.19509032201612825, _z: 0 } } ],
290 | shared: { color: null, tag: 296 },
291 | plane: { normal: { _x: 0, _y: 0, _z: -1 }, w: 0 } }
292 |
293 | const expPoly95 = { vertices: [ { pos: { _x: 0, _y: 0, _z: 1 } },
294 | { pos: { _x: 1, _y: 2.4492935982947064e-16, _z: 1 } },
295 | { pos: { _x: 0.9807852804032303, _y: 0.19509032201612872, _z: 1 } } ],
296 | shared: { color: null, tag: 296 },
297 | plane: { normal: { _x: 0, _y: 0, _z: 1 }, w: 1 } }
298 |
299 | t.deepEqual(obs.properties.cylinder.start, {point: {_x: 0, _y: 0, _z: 0}, axisvector: {_x: -0, _y: -0, _z: -1}, normalvector: {_x: 1, _y: 0, _z: 0}})
300 | t.deepEqual(obs.properties.cylinder.end, {point: {_x: 0, _y: 0, _z: 1}, axisvector: {_x: 0, _y: 0, _z: 1}, normalvector: {_x: 1, _y: 0, _z: 0}})
301 | t.deepEqual(obs.polygons.length, 96)
302 | t.deepEqual(obs.polygons[0], expFirstPoly)
303 | t.deepEqual(obs.polygons[95], expPoly95)
304 | })
305 |
306 | test('cylinder (defaults)', t => {
307 | const obs = cylinder()
308 | const expFirstPoly = {
309 | vertices: [ { pos: { _x: 0, _y: 0, _z: 0 } },
310 | { pos: { _x: 1, _y: 0, _z: 0 } },
311 | { pos: { _x: 0.9807852804032304, _y: -0.19509032201612825, _z: 0 } } ],
312 | shared: { color: null, tag: 296 },
313 | plane: { normal: { _x: 0, _y: 0, _z: -1 }, w: 0 } }
314 |
315 | const expPoly95 = { vertices: [ { pos: { _x: 0, _y: 0, _z: 1 } },
316 | { pos: { _x: 1, _y: 2.4492935982947064e-16, _z: 1 } },
317 | { pos: { _x: 0.9807852804032303, _y: 0.19509032201612872, _z: 1 } } ],
318 | shared: { color: null, tag: 296 },
319 | plane: { normal: { _x: 0, _y: 0, _z: 1 }, w: 1 } }
320 |
321 | t.deepEqual(obs.properties.cylinder.start, {point: {_x: 0, _y: 0, _z: 0}, axisvector: {_x: -0, _y: -0, _z: -1}, normalvector: {_x: 1, _y: 0, _z: 0}})
322 | t.deepEqual(obs.properties.cylinder.end, {point: {_x: 0, _y: 0, _z: 1}, axisvector: {_x: 0, _y: 0, _z: 1}, normalvector: {_x: 1, _y: 0, _z: 0}})
323 | t.deepEqual(obs.polygons.length, 96)
324 | t.deepEqual(obs.polygons[0], expFirstPoly)
325 | t.deepEqual(obs.polygons[95], expPoly95)
326 | })
327 |
328 | test('cylinder (custom radius, height, center, resolution)', t => {
329 | const obs = cylinder({r: 2, h: 10, center: [true, true, false], fn: 10})
330 | const expFirstPoly = {
331 | vertices: [ { pos: { _x: 0, _y: 0, _z: 0 } },
332 | { pos: { _x: 2, _y: 0, _z: 0 } },
333 | { pos: { _x: 1.618033988749895, _y: -1.1755705045849463, _z: 0 } } ],
334 | shared: { color: null, tag: 296 },
335 | plane: { normal: { _x: 0, _y: 0, _z: -1 }, w: 0 } }
336 |
337 | const expPoly29 = { vertices: [ { pos: { _x: 0, _y: 0, _z: 10 } },
338 | { pos: { _x: 2, _y: 4.898587196589413e-16, _z: 10 } },
339 | { pos: { _x: 1.6180339887498945, _y: 1.1755705045849467, _z: 10 } } ],
340 | shared: { color: null, tag: 296 },
341 | plane: { normal: { _x: 0, _y: 0, _z: 1 }, w: 10 } }
342 |
343 | t.deepEqual(obs.properties.cylinder.start, {point: {_x: 0, _y: 0, _z: 0}, axisvector: {_x: -0, _y: -0, _z: -1}, normalvector: {_x: 1, _y: 0, _z: 0}})
344 | t.deepEqual(obs.properties.cylinder.end, {point: {_x: 0, _y: 0, _z: 10}, axisvector: {_x: 0, _y: 0, _z: 1}, normalvector: {_x: 1, _y: 0, _z: 0}})
345 | t.deepEqual(obs.polygons.length, 30)
346 | t.is(comparePolygons(obs.polygons[0], expFirstPoly, 0.000001), true)
347 | t.is(comparePolygons(obs.polygons[29], expPoly29, 0.000001), true)
348 | })
349 |
350 | test('cylinder (custom double radius, rounded)', t => {
351 | const obs = cylinder({r1: 2, r2: 3, round: true})
352 | const expFirstPoly = {
353 | vertices: [ { pos: { _x: 0.19509032201612825, _y: 0.9807852804032304, _z: 0 } },
354 | { pos: { _x: 0, _y: 1, _z: 0 } },
355 | { pos: { _x: 0, _y: 1, _z: 1 } },
356 | { pos: { _x: 0.19509032201612825, _y: 0.9807852804032304, _z: 1 } } ],
357 | shared: { color: null, tag: 296 },
358 | plane: { normal: { _x: 0.09801714032956071, _y: 0.9951847266721969, _z: 0 },
359 | w: 0.9951847266721969 } }
360 |
361 | const expPoly543 = { vertices: [ { pos: { _x: -1.1945836920083923e-17, _y: 6.005577771483276e-17, _z: 2 } },
362 | { pos: { _x: -4.778334768033559e-17, _y: 0.19509032201612833, _z: 1.9807852804032304 } },
363 | { pos: { _x: -0.03806023374435672, _y: 0.19134171618254492, _z: 1.9807852804032304 } } ],
364 | shared: { color: null, tag: 296 },
365 | plane: { normal: { _x: -0.009653395882096847, _y: 0.09801257320997024, _z: 0.9951383559288144 },
366 | w: 1.9902767118576288 } }
367 |
368 | t.deepEqual(obs.properties.roundedCylinder.start, {point: {_x: 0, _y: 0, _z: 0}, axisvector: {_x: -0, _y: -0, _z: -1}, normalvector: {_x: 0, _y: 1, _z: 0}})
369 | t.deepEqual(obs.properties.roundedCylinder.end, {point: {_x: 0, _y: 0, _z: 1}, axisvector: {_x: 0, _y: 0, _z: 1}, normalvector: {_x: 0, _y: 1, _z: 0}})
370 | t.deepEqual(obs.polygons.length, 544)
371 | t.deepEqual(obs.polygons[0], expFirstPoly)
372 | t.deepEqual(obs.polygons[543], expPoly543)
373 | })
374 |
375 | test('cylinder (custom double diameter, rounded)', t => {
376 | const obs = cylinder({d1: 1, d2: 1.5, round: true})
377 | const expFirstPoly = {
378 | vertices: [ { pos: { _x: 0.19509032201612825, _y: 0.9807852804032304, _z: 0 } },
379 | { pos: { _x: 0, _y: 1, _z: 0 } },
380 | { pos: { _x: 0, _y: 1, _z: 1 } },
381 | { pos: { _x: 0.19509032201612825, _y: 0.9807852804032304, _z: 1 } } ],
382 | shared: { color: null, tag: 296 },
383 | plane: { normal: { _x: 0.09801714032956071, _y: 0.9951847266721969, _z: 0 },
384 | w: 0.9951847266721969 } }
385 |
386 | const expPoly543 = { vertices: [ { pos: { _x: -1.1945836920083923e-17, _y: 6.005577771483276e-17, _z: 2 } },
387 | { pos: { _x: -4.778334768033559e-17, _y: 0.19509032201612833, _z: 1.9807852804032304 } },
388 | { pos: { _x: -0.03806023374435672, _y: 0.19134171618254492, _z: 1.9807852804032304 } } ],
389 | shared: { color: null, tag: 296 },
390 | plane: { normal: { _x: -0.009653395882096847, _y: 0.09801257320997024, _z: 0.9951383559288144 },
391 | w: 1.9902767118576288 } }
392 |
393 | t.deepEqual(obs.properties.roundedCylinder.start, {point: {_x: 0, _y: 0, _z: 0}, axisvector: {_x: -0, _y: -0, _z: -1}, normalvector: {_x: 0, _y: 1, _z: 0}})
394 | t.deepEqual(obs.properties.roundedCylinder.end, {point: {_x: 0, _y: 0, _z: 1}, axisvector: {_x: 0, _y: 0, _z: 1}, normalvector: {_x: 0, _y: 1, _z: 0}})
395 | t.deepEqual(obs.polygons.length, 544)
396 | t.deepEqual(obs.polygons[0], expFirstPoly)
397 | t.deepEqual(obs.polygons[543], expPoly543)
398 | })
399 |
400 | test('cylinder (custom double diameter, rounded, start, end)', t => {
401 | const obs = cylinder({d1: 1, d2: 1.5, round: true, start: [0, 0, 0], end: [0, 0, 10]})
402 | const expFirstPoly = {
403 | vertices: [ { pos: { _x: 0.19509032201612825, _y: 0.9807852804032304, _z: 0 } },
404 | { pos: { _x: 0, _y: 1, _z: 0 } },
405 | { pos: { _x: 0, _y: 1, _z: 10 } },
406 | { pos: { _x: 0.19509032201612825, _y: 0.9807852804032304, _z: 10 } } ],
407 | shared: { color: null, tag: 296 },
408 | plane: { normal: { _x: 0.0980171403295607, _y: 0.9951847266721968, _z: 0 },
409 | w: 0.9951847266721968 } }
410 |
411 | const expPoly543 = { vertices: [ { pos: { _x: -1.1945836920083923e-17, _y: 6.005577771483276e-17, _z: 11 } },
412 | { pos: { _x: -4.778334768033559e-17, _y: 0.19509032201612833, _z: 10.98078528040323 } },
413 | { pos: { _x: -0.03806023374435672, _y: 0.19134171618254492, _z: 10.98078528040323 } } ],
414 | shared: { color: null, tag: 296 },
415 | plane: { normal: { _x: -0.009653395882096847, _y: 0.09801257320997024, _z: 0.9951383559288144 },
416 | w: 10.946521915216959 } }
417 |
418 | t.deepEqual(obs.properties.roundedCylinder.start, {point: {_x: 0, _y: 0, _z: 0}, axisvector: {_x: -0, _y: -0, _z: -1}, normalvector: {_x: 0, _y: 1, _z: 0}})
419 | t.deepEqual(obs.properties.roundedCylinder.end, {point: {_x: 0, _y: 0, _z: 10}, axisvector: {_x: 0, _y: 0, _z: 1}, normalvector: {_x: 0, _y: 1, _z: 0}})
420 | t.deepEqual(obs.polygons.length, 544)
421 | t.deepEqual(obs.polygons[0], expFirstPoly)
422 | t.deepEqual(obs.polygons[543], expPoly543)
423 | })
424 |
425 | test('torus (defaults)', t => {
426 | const obs = torus()
427 | const expFirstPoly = { positions:
428 | [ [ 4.923879532511286, 5.551115123125783e-17, -0.3826834323650904 ],
429 | [ 5, 5.551115123125783e-17, 0 ],
430 | [ 4.903926402016152, -0.9754516100806412, 0 ],
431 | [ 4.829268567965809,
432 | -0.96060124356625,
433 | -0.3826834323650906 ] ],
434 | plane:
435 | { normal: [ 0.9762410328686741, -0.09615134934208333, -0.1941864149810719 ],
436 | w: 4.8812051643433705 },
437 | shared: { color: null, tag: 296 } }
438 |
439 | const expLastPoly = { positions:
440 | [ [ 4.82926856796581,
441 | 0.9606012435662523,
442 | -0.3826834323650903 ],
443 | [ 4.923879532511286,
444 | 5.551115123125783e-17, -0.3826834323650904 ],
445 | [ 4.707106781186546,
446 | 0,
447 | -0.7071067811865475 ],
448 | [ 4.616661044273995,
449 | 0.9183109777059867,
450 | -0.7071067811865483 ] ],
451 | plane:
452 | { normal: [ 0.8286954742331524, 0.08161938021295423, -0.5537166132229949 ],
453 | w: 4.292294858367102 },
454 | shared: { color: null, tag: 296 } }
455 |
456 | t.deepEqual(obs.polygons.length, 512)
457 | t.deepEqual(simplifiedPolygon(obs.polygons[0]), expFirstPoly)
458 | t.deepEqual(simplifiedPolygon(obs.polygons[obs.polygons.length - 1]), expLastPoly)
459 | })
460 |
461 | test('torus (custom all)', t => {
462 | const obs = torus({ ro: 5, ri: 3, fni: 4, fno: 5, roti: 45 })
463 | const expFirstPoly = { positions:
464 | [ [ 7.121320343559641, 0, 2.1213203435596424 ],
465 | [ 5.035533905932736,
466 | -5.035533905932738, 2.1213203435596424 ],
467 | [ 5.035533905932736,
468 | -5.035533905932738,
469 | -2.1213203435596433 ],
470 | [ 7.121320343559641,
471 | 0, -2.1213203435596433 ] ],
472 | plane:
473 | { normal:
474 | [ 0.9238795325112867,
475 | -0.3826834323650898,
476 | -1.9341017156376838e-16 ],
477 | w: 6.5792421098709974 },
478 | shared: { color: null, tag: 296 } }
479 |
480 | const expLastPoly = { positions:
481 | [ [ 2.0355339059327378,
482 | -2.035533905932737,
483 | -2.121320343559643 ],
484 | [ 2.878679656440357,
485 | 2.220446049250313e-16,
486 | -2.121320343559643 ],
487 | [ 2.8786796564403567,
488 | -5.9289321881345245,
489 | -2.121320343559643 ],
490 | [ 2.0355339059327373,
491 | -6.2781745930520225,
492 | -2.1213203435596424 ] ],
493 | plane:
494 | { normal: [ -3.14018491736755e-16,
495 | 1.300707181133076e-16, -1 ],
496 | w: 2.121320343559641 },
497 | shared: { color: null, tag: 296 } }
498 |
499 | t.deepEqual(obs.polygons.length, 36)
500 | t.deepEqual(simplifiedPolygon(obs.polygons[0]), expFirstPoly)
501 | t.deepEqual(simplifiedPolygon(obs.polygons[19]), expLastPoly)
502 | })
503 |
504 | test('polyhedron (points & triangles)', t => {
505 | const obs = polyhedron({
506 | points: [ [10, 10, 0], [10, -10, 0], [-10, -10, 0], [-10, 10, 0], [0, 0, 10] ], // the apex point
507 | triangles: [ [0, 1, 4], [1, 2, 4], [2, 3, 4], [3, 0, 4], [1, 0, 3], [2, 1, 3] ] // two triangles for square base
508 | })
509 | const expFirstPoly = {
510 | vertices: [ { pos: { _x: 0, _y: 0, _z: 10 } },
511 | { pos: { _x: 10, _y: -10, _z: 0 } },
512 | { pos: { _x: 10, _y: 10, _z: 0 } } ],
513 | shared: { color: null, tag: 296 },
514 | plane: { normal: { _x: 0.7071067811865475, _y: 0, _z: 0.7071067811865475 },
515 | w: 7.071067811865475 } }
516 |
517 | const expLastPoly = { vertices: [ { pos: { _x: -10, _y: 10, _z: 0 } },
518 | { pos: { _x: 10, _y: -10, _z: 0 } },
519 | { pos: { _x: -10, _y: -10, _z: 0 } } ],
520 | shared: { color: null, tag: 296 },
521 | plane: { normal: { _x: 0, _y: 0, _z: -1 }, w: 0 } }
522 |
523 | t.deepEqual(obs.polygons.length, 6)
524 | t.deepEqual(obs.polygons[0], expFirstPoly)
525 | t.deepEqual(obs.polygons[5], expLastPoly)
526 | })
527 |
528 | test('polyhedron (points & polygons)', t => {
529 | const obs = polyhedron({
530 | points: [ [10, 10, 0], [10, -10, 0], [-10, -10, 0], [-10, 10, 0], [0, 0, 10] ], // the apex point
531 | polygons: [ [0, 1, 4], [1, 2, 4], [2, 3, 4], [3, 0, 4], [1, 0, 3], [2, 1, 3] ] // two triangles for square base
532 | })
533 | const expFirstPoly = {
534 | vertices: [ { pos: { _x: 0, _y: 0, _z: 10 } },
535 | { pos: { _x: 10, _y: -10, _z: 0 } },
536 | { pos: { _x: 10, _y: 10, _z: 0 } } ],
537 | shared: { color: null, tag: 296 },
538 | plane: { normal: { _x: 0.7071067811865475, _y: 0, _z: 0.7071067811865475 },
539 | w: 7.071067811865475 } }
540 |
541 | const expLastPoly = { vertices: [ { pos: { _x: -10, _y: 10, _z: 0 } },
542 | { pos: { _x: 10, _y: -10, _z: 0 } },
543 | { pos: { _x: -10, _y: -10, _z: 0 } } ],
544 | shared: { color: null, tag: 296 },
545 | plane: { normal: { _x: 0, _y: 0, _z: -1 }, w: 0 } }
546 |
547 | t.deepEqual(obs.polygons.length, 6)
548 | t.deepEqual(obs.polygons[0], expFirstPoly)
549 | t.deepEqual(obs.polygons[5], expLastPoly)
550 | })
551 |
--------------------------------------------------------------------------------
/src/test-helpers.js:
--------------------------------------------------------------------------------
1 | function vertex3Equals (t, observed, expected) {
2 | const obs = [observed.pos._x, observed.pos._y, observed.pos._z]
3 | return t.deepEqual(obs, expected)
4 | }
5 |
6 | function vertex2Equals (t, observed, expected) {
7 | const obs = [observed.pos._x, observed.pos._y]
8 | return t.deepEqual(obs, expected)
9 | }
10 |
11 | function vector3Equals (t, observed, expected) {
12 | const obs = [observed._x, observed._y, observed._z]
13 | return t.deepEqual(obs, expected)
14 | }
15 |
16 | function sideEquals (t, observed, expected) {
17 | vertex2Equals(t, observed.vertex0, expected[0], 'vertex0 are not equal')
18 | vertex2Equals(t, observed.vertex1, expected[1], 'vertex1 are not equal')
19 | }
20 |
21 | function shape2dToNestedArray (shape2d) {
22 | const sides = shape2d.sides.map(function (side) {
23 | return [side.vertex0.pos._x, side.vertex0.pos._y, side.vertex1.pos._x, side.vertex1.pos._y]
24 | })
25 | return sides
26 | }
27 |
28 | function shape3dToNestedArray (shape3d) {
29 | const polygons = shape3d.polygons.map(function (polygon) {
30 | return polygon.vertices.map(vertex => [vertex.pos._x, vertex.pos._y, vertex.pos._z])
31 | })
32 | return polygons
33 | }
34 |
35 | function simplifiedPolygon (polygon) {
36 | const vertices = polygon.vertices.map(vertex => [vertex.pos._x, vertex.pos._y, vertex.pos._z])
37 | const plane = {normal: [polygon.plane.normal._x, polygon.plane.normal._y, polygon.plane.normal._z], w: polygon.plane.w}
38 | return {positions: vertices, plane, shared: polygon.shared}
39 | }
40 |
41 | function simplifiedCSG (csg) {
42 | const polygonsData = csg.polygons.map(x => simplifiedPolygon(x).positions)
43 | return polygonsData
44 | }
45 |
46 | function almostEquals (t, observed, expected, precision) {
47 | t.is(Math.abs(expected - observed) < precision, true)
48 | }
49 |
50 | function compareNumbers (a, b, precision) {
51 | return Math.abs(a - b) < precision
52 | }
53 |
54 | function compareVertices (a, b, precision) {
55 | if ('_w' in a && !('_w' in b)) {
56 | return false
57 | }
58 | const fields = ['_x', '_y', '_z']
59 | for (let i = 0; i < fields.length; i++) {
60 | const field = fields[i]
61 | if (!compareNumbers(a[field], b[field], precision)) {
62 | return false
63 | }
64 | }
65 | return true
66 | }
67 |
68 | function comparePolygons (a, b, precision) {
69 | // First find one matching vertice
70 | // We try to find the first vertice of a inside b
71 | // If there is no such vertice, then a != b
72 | if (a.vertices.length !== b.vertices.length || a.vertices.length === 0) {
73 | return false
74 | }
75 | if (a.shared.color && a.shared.color !== b.shared.color) {
76 | return false
77 | }
78 | if (a.shared.tag && a.shared.tag !== b.shared.tag) {
79 | return false
80 | }
81 | if (a.shared.plane && a.shared.plane !== b.shared.plane) {
82 | return false
83 | }
84 |
85 | let start = a.vertices[0]
86 | let index = b.vertices.findIndex(v => {
87 | if (!v) { return false }
88 | return v._x === start._x && v._y === start._y && v._z === start._z
89 | })
90 | if (index === -1) {
91 | return false
92 | }
93 | // Rearrange b vertices so that they start with the same vertex as a
94 | let vs = b.vertices
95 | if (index !== 0) {
96 | vs = b.vertices.slice(index).concat(b.vertices.slice(0, index))
97 | }
98 |
99 | // Compare now vertices one by one
100 | for (let i = 0; i < a.vertices.length; i++) {
101 | const vertex = a.vertices[i].pos
102 | const otherVertex = vs[i].pos
103 | if (!compareVertices(vertex, otherVertex, precision)) {
104 | return false
105 | }
106 | /* if (a.vertices[i]._x !== vs[i]._x ||
107 | a.vertices[i]._y !== vs[i]._y ||
108 | a.vertices[i]._z !== vs[i]._z) { return false } */
109 | }
110 | return true
111 | }
112 |
113 | module.exports = {
114 | vertex2Equals,
115 | vertex3Equals,
116 | vector3Equals,
117 | sideEquals,
118 | shape2dToNestedArray,
119 | simplifiedPolygon,
120 | simplifiedCSG,
121 |
122 | compareNumbers,
123 | comparePolygons,
124 | compareVertices
125 | }
126 |
--------------------------------------------------------------------------------
/src/text.test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava')
2 | const { vector_text, vector_char } = require('./text')
3 |
4 | test('vector_char', t => {
5 | const obs = vector_char(0, 2, 'O')
6 | const expSegments = [ [ [ 9, 23 ],
7 | [ 7, 22 ],
8 | [ 5, 20 ],
9 | [ 4, 18 ],
10 | [ 3, 15 ],
11 | [ 3, 10 ],
12 | [ 4, 7 ],
13 | [ 5, 5 ],
14 | [ 7, 3 ],
15 | [ 9, 2 ],
16 | [ 13, 2 ],
17 | [ 15, 3 ],
18 | [ 17, 5 ],
19 | [ 18, 7 ],
20 | [ 19, 10 ],
21 | [ 19, 15 ],
22 | [ 18, 18 ],
23 | [ 17, 20 ],
24 | [ 15, 22 ],
25 | [ 13, 23 ],
26 | [ 9, 23 ] ] ]
27 |
28 | t.deepEqual(obs.width, 22)
29 | t.deepEqual(obs.segments, expSegments)
30 | })
31 |
32 | test('vector_text', t => {
33 | const obs = vector_text(0, 0, 'Hi')
34 | const exp = [ [ [ 4, 21 ], [ 4, 0 ] ],
35 | [ [ 18, 21 ], [ 18, 0 ] ],
36 | [ [ 4, 11 ], [ 18, 11 ] ],
37 | [ [ 25, 21 ], [ 26, 20 ], [ 27, 21 ], [ 26, 22 ], [ 25, 21 ] ],
38 | [ [ 26, 14 ], [ 26, 0 ] ] ]
39 |
40 | t.deepEqual(obs, exp)
41 | })
42 |
--------------------------------------------------------------------------------