├── .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 | [![GitHub version](https://badge.fury.io/gh/jscad%2Fscad-api.svg)](https://badge.fury.io/gh/jscad%2Fscad-api) 4 | [![EXPERIMENTAL](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) 5 | [![Build Status](https://travis-ci.org/jscad/scad-api.svg?branch=master)](https://travis-ci.org/jscad/scad-api) 6 | [![Dependency Status](https://david-dm.org/jscad/scad-api.svg)](https://david-dm.org/jscad/scad-api) 7 | [![devDependency Status](https://david-dm.org/jscad/scad-api/dev-status.svg)](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 |
4 |
clone(obj)CSG
5 |

clone the given object

6 |
7 |
css2rgb(String)
8 |

Converts an CSS color name to RGB color.

9 |
10 |
color(color, objects)CSG
11 |

apply the given color to the input object(s)

12 |
13 |
rgb2hsl(Number, Number, Number)
14 |

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 |
19 |
hsl2rgb(Number, Number, Number)
20 |

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 |
25 |
rgb2hsv(Number, Number, Number)
26 |

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 |
31 |
hsv2rgb(Number, Number, Number)
32 |

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 |
37 |
html2rgb()
38 |

Converts a HTML5 color value (string) to RGB values 39 | See the color input type of HTML5 forms 40 | Conversion formula:

41 | 45 |
46 |
rgb2html()
47 |

Converts RGB color value to HTML5 color value (string) 48 | Conversion forumla:

49 | 53 |
54 |
union(objects)CSG
55 |

union/ combine the given shapes

56 |
57 |
difference(objects)CSG
58 |

difference/ subtraction of the given shapes ie: 59 | cut out C From B From A ie : a - b - c etc

60 |
61 |
intersection(objects)CSG
62 |

intersection of the given shapes: ie keep only the common parts between the given shapes

63 |
64 |
linear_extrude([options], baseShape)CSG
65 |

linear extrusion of the input 2d shape

66 |
67 |
rotate_extrude([options], baseShape)CSG
68 |

rotate extrusion / revolve of the given 2d shape

69 |
70 |
rectangular_extrude(basePoints, [options])CSG
71 |

rectangular extrusion of the given array of points

72 |
73 |
translate(vector, ...objects)CSG
74 |

translate an object in 2D/3D space

75 |
76 |
scale(scale, ...objects)CSG
77 |

scale an object in 2D/3D space

78 |
79 |
rotate(rotation, objects)CSG
80 |

rotate an object in 2D/3D space

81 |
82 |
transform(matrix, ...objects)CSG
83 |

apply the given matrix transform to the given objects

84 |
85 |
center(axis, ...objects)CSG
86 |

center an object in 2D/3D space

87 |
88 |
mirror(vector, ...objects)CSG
89 |

mirror an object in 2D/3D space

90 |
91 |
expand(radius, object)CSG/CAG
92 |

expand an object in 2D/3D space

93 |
94 |
contract(radius, object)CSG/CAG
95 |

contract an object(s) in 2D/3D space

96 |
97 |
minkowski(objects)CSG
98 |

create a minkowski sum of the given shapes

99 |
100 |
hull(objects)CSG
101 |

create a convex hull of the given shapes

102 |
103 |
chain_hull(objects)CSG
104 |

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 |
109 |
square([options])CAG
110 |

Construct a square/rectangle

111 |
112 |
circle([options])CAG
113 |

Construct a circle

114 |
115 |
polygon([options])CAG
116 |

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 |
119 |
triangle()CAG
120 |

Construct a triangle

121 |
122 |
cube([options])CSG
123 |

Construct a cuboid

124 |
125 |
sphere([options])CSG
126 |

Construct a sphere

127 |
128 |
cylinder([options])CSG
129 |

Construct a cylinder

130 |
131 |
torus([options])CSG
132 |

Construct a torus

133 |
134 |
polyhedron([options])CSG
135 |

Construct a polyhedron from the given triangles/ polygons/points

136 |
137 |
vector_char(x, y, char)Object
138 |

Construct a with, segments tupple from a character

139 |
140 |
vector_text(x, y, string)Array
141 |

Construct an array of with, segments tupple from a string

142 |
143 |
144 | 145 | 146 | 147 | ## clone(obj) ⇒ 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 | --------------------------------------------------------------------------------