├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets ├── examples.gif ├── intro-0.png ├── intro-1.png ├── intro-2.png ├── intro-3.png ├── intro-4.png ├── intro-5.png └── tiling.png ├── examples ├── examples.rs └── intro.rs └── src ├── color.rs ├── error.rs ├── lib.rs ├── model.rs └── shape.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tiling" 3 | version = "0.1.0" 4 | authors = ["Jonas Michel "] 5 | license = "MIT OR Apache-2.0" 6 | description = "A library for constructing tilings of regular polygons" 7 | readme = "README.md" 8 | keywords = ["tiling", "tiles", "polygon", "pattern", "geometric"] 9 | categories = ["graphics", "multimedia::images"] 10 | repository = "https://github.com/jonasrmichel/tiling" 11 | homepage = "https://github.com/jonasrmichel/tiling" 12 | documentation = "http://docs.rs/tiling" 13 | edition = "2018" 14 | 15 | [dependencies] 16 | cairo-rs = { version = "0.14.0", features = ["png"] } 17 | itertools = "0.10.0" 18 | png = "0.16.0" 19 | thiserror = "1.0" -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Jonas Michel 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2021 Jonas Michel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in the 5 | Software without restriction, including without limitation the rights to use, 6 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 7 | Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 17 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 18 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tiling 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/tiling?style=flat-square)](https://crates.io/crates/tiling) 4 | [![Crates.io](https://img.shields.io/crates/d/tiling?style=flat-square)](https://crates.io/crates/tiling) 5 | [![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](https://github.com/jonasrmichel/tiling/blob/main/LICENSE-APACHE) 6 | [![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://github.com/jonasrmichel/tiling/blob/main/LICENSE-MIT) 7 | 8 | *tiling* is a library for constructing tilings of regular polygons and their 9 | dual tilings. 10 | 11 | tiling 12 | 13 | # Resources 14 | 15 | - [Documentation](https://docs.rs/tiling) 16 | - [Tilings by regular polygons](http://en.wikipedia.org/wiki/Tiling_by_regular_polygons) 17 | - [List of Euclidian uniform tilings](https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings) 18 | 19 | # Examples 20 | 21 | Here are some tilings produced by the examples in the [`examples`](./examples) directory. 22 | 23 | examples 24 | 25 | # Requirements 26 | 27 | *tiling* uses [cairo-rs](https://crates.io/crates/cairo-rs) for rendering and 28 | requires [cairo](https://www.cairographics.org/download/) to be installed. 29 | 30 | # Usage 31 | 32 | Create an empty tiling model. 33 | 34 | ```rust 35 | let (width, height, scale) = (1024, 1024, 128.0); 36 | 37 | let mut model = Model::new(width, height, scale); 38 | ``` 39 | 40 | Place a polygon at the origin. 41 | This adds a hexagon. 42 | 43 | ```rust 44 | let stroke = Color::new(242, 60, 60)?; 45 | let fill_hexagon = Color::new(242, 194, 106)?; 46 | 47 | model.add(Shape::new(6, fill_hexagon, stroke)?); 48 | ``` 49 | 50 | At this point we can render the model. 51 | 52 | ```rust 53 | let background = Color::new(242, 242, 242)?; 54 | let margin = 0.1; 55 | let show_labels = false; 56 | let line_width = 0.1; 57 | 58 | let render = model.render(background, margin, line_width, show_labels)?; 59 | render.write_to_png("output.png")?; 60 | ``` 61 | 62 | hexagon 63 | 64 | Let's continue by attaching a square to each of the hexagon's sides. 65 | 66 | ```rust 67 | let fill_square = Color::new(23, 216, 146)?; 68 | 69 | let squares = model.add_multi(0..1, 0..6, Shape::new(4, fill_square, stroke)?)?; 70 | ``` 71 | 72 | The first parameter `0..1` is a range that indicates the shape(s) to attach to 73 | (by their index). 74 | In this example, the square is attached to the hexagon (index `0`). 75 | 76 | > When `show_labels` is `true`, each shape is labeled with its index. 77 | 78 | The second paramter `0..6` is a range that indicates the edge(s) to attach to 79 | (by their index). 80 | In this example, the square is attached to all six edges of the hexagon. 81 | 82 | > When `show_labels` is `true`, each edge is labeled with its index. 83 | 84 | The final paramter defines the shape to add (a square). 85 | 86 | The `add_multi` method returns a range containing the indexes of the added square 87 | shapes so they can be referenced later. 88 | We'll see how to do that next. 89 | 90 | hexagon squares 91 | 92 | Now, attach triangles to all of the squares using the previously returned range 93 | `squares`. 94 | Here, a triangle is attached to edge `1` of each square. 95 | 96 | ```rust 97 | let fill_triangle = Color::new(242, 209, 48)?; 98 | 99 | let _ = model.add_multi(squares.clone(), 1..2, Shape::new(3, fill_triangle, stroke)?)?; 100 | ``` 101 | 102 | hexagon squares triangles 103 | 104 | Let's wrap up by attaching a hexagon to the outer edge of each square. 105 | These hexagons will define the repeating positions of the tiling. 106 | 107 | ```rust 108 | let hexagons = model.add_multi(squares.clone(), 2..3, Shape::new(6, fill_hexagon, stroke)?)?; 109 | ``` 110 | 111 | hexagon squares triangles hexagons 112 | 113 | Now that the tiling's repeating pattern is complete, use the `repeat` method to 114 | fill the rest of the surface with the pattern. 115 | 116 | ```rust 117 | model.repeat(hexagons)?; 118 | ``` 119 | 120 | hexagon squares triangles hexagons repeated 121 | 122 | Once satisfied, disable the shape and edge labels and adjust the scale. 123 | 124 | The complete code for this example is in [`examples/intro.rs`](./examples/intro.rs). 125 | 126 | Dual tilings may be created using the `render_dual` method. 127 | A tiling's dual is formed by drawing edges between the centers of adjacent polygons. 128 | 129 | Here is the dual tiling of the above example. 130 | 131 | hexagon squares triangles hexagons dual tiling 132 | 133 | # Installation 134 | 135 | *tiling* is available on [crates.io](https://crates.io/crates/tiling) and can be 136 | included in your Cargo enabled project. 137 | 138 | ```toml 139 | [dependencies] 140 | tiling = "0.1.0" 141 | ``` 142 | 143 | # Future improvements 144 | 145 | - Declarative API 146 | - Reduce memory usage by reusing `Shape` references 147 | - Generate tiling models by interpreting their [*vertex configuration*](https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings) 148 | - Command line tool 149 | - Support shape and edge attachment via disjoint ranges 150 | - Support different image output types 151 | 152 | # Acknowledgements 153 | 154 | This library was inspired by Michael Fogleman's [Tiling](https://github.com/fogleman/Tiling) 155 | Python tool. 156 | Several of the low-level geometric computations in this crate are based on 157 | implementations found in Tiling. -------------------------------------------------------------------------------- /assets/examples.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasrmichel/tiling/27095c50d79cc8cfcbdea9958c8eee2cbff68184/assets/examples.gif -------------------------------------------------------------------------------- /assets/intro-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasrmichel/tiling/27095c50d79cc8cfcbdea9958c8eee2cbff68184/assets/intro-0.png -------------------------------------------------------------------------------- /assets/intro-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasrmichel/tiling/27095c50d79cc8cfcbdea9958c8eee2cbff68184/assets/intro-1.png -------------------------------------------------------------------------------- /assets/intro-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasrmichel/tiling/27095c50d79cc8cfcbdea9958c8eee2cbff68184/assets/intro-2.png -------------------------------------------------------------------------------- /assets/intro-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasrmichel/tiling/27095c50d79cc8cfcbdea9958c8eee2cbff68184/assets/intro-3.png -------------------------------------------------------------------------------- /assets/intro-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasrmichel/tiling/27095c50d79cc8cfcbdea9958c8eee2cbff68184/assets/intro-4.png -------------------------------------------------------------------------------- /assets/intro-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasrmichel/tiling/27095c50d79cc8cfcbdea9958c8eee2cbff68184/assets/intro-5.png -------------------------------------------------------------------------------- /assets/tiling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasrmichel/tiling/27095c50d79cc8cfcbdea9958c8eee2cbff68184/assets/tiling.png -------------------------------------------------------------------------------- /examples/examples.rs: -------------------------------------------------------------------------------- 1 | use tiling::{Color, Model, Result, Shape}; 2 | 3 | const WIDTH: i32 = 1024; 4 | const HEIGHT: i32 = 1024; 5 | const SCALE: f64 = 128.0; 6 | const MARGIN: f64 = 0.1; 7 | const SHOW_LABELS: bool = false; 8 | const LINE_WIDTH: f64 = 0.1; 9 | 10 | pub fn main() -> Result<()> { 11 | let blue = Color::new(56, 103, 165)?; 12 | let yellow = Color::new(242, 205, 21)?; 13 | let gold = Color::new(242, 174, 45)?; 14 | let orange = Color::new(216, 140, 73)?; 15 | let burnt = Color::new(191, 86, 47)?; 16 | 17 | Ex3636::render(blue, yellow, gold, orange, burnt)?; 18 | Ex33434::render(blue, yellow, gold, orange, burnt)?; 19 | Ex33336::render(blue, yellow, gold, orange, burnt)?; 20 | Ex333333::render(blue, yellow, gold, orange, burnt)?; 21 | 22 | Ok(()) 23 | } 24 | 25 | trait Example { 26 | fn render( 27 | background: Color, 28 | stroke: Color, 29 | fill_0: Color, 30 | fill_1: Color, 31 | fill_2: Color, 32 | ) -> Result<()>; 33 | } 34 | 35 | struct Ex3636(); 36 | 37 | impl Example for Ex3636 { 38 | fn render( 39 | background: Color, 40 | stroke: Color, 41 | fill_0: Color, 42 | fill_1: Color, 43 | fill_2: Color, 44 | ) -> Result<()> { 45 | let mut model = Model::new(WIDTH, HEIGHT, SCALE); 46 | model.add(Shape::new(6, fill_1, stroke)?); 47 | let a = model.add_multi(0..1, 0..6, Shape::new(3, fill_0, stroke)?)?; 48 | let b = model.add_multi(a, 1..2, Shape::new(6, fill_1, stroke)?)?; 49 | model.repeat(b)?; 50 | 51 | model 52 | .render(background, MARGIN, LINE_WIDTH, SHOW_LABELS)? 53 | .write_to_png("3.6.3.6.png")?; 54 | model 55 | .render_dual(background, fill_0, stroke, MARGIN, LINE_WIDTH)? 56 | .write_to_png("3.6.3.6-dual.png")?; 57 | 58 | Ok(()) 59 | } 60 | } 61 | 62 | struct Ex33434(); 63 | 64 | impl Example for Ex33434 { 65 | fn render( 66 | background: Color, 67 | stroke: Color, 68 | fill_0: Color, 69 | fill_1: Color, 70 | fill_2: Color, 71 | ) -> Result<()> { 72 | let mut model = Model::new(WIDTH, HEIGHT, SCALE); 73 | model.add(Shape::new(4, fill_1, stroke)?); 74 | let a = model.add_multi(0..1, 0..4, Shape::new(3, fill_2, stroke)?)?; 75 | let b = model.add_multi(a, 1..2, Shape::new(4, fill_1, stroke)?)?; 76 | let c = model.add_multi(b, 2..4, Shape::new(3, fill_2, stroke)?)?; 77 | let d = model.add_multi(c, 2..3, Shape::new(4, fill_1, stroke)?)?; 78 | model.repeat(d)?; 79 | 80 | model 81 | .render(background, MARGIN, LINE_WIDTH, SHOW_LABELS)? 82 | .write_to_png("3.3.4.3.4.png")?; 83 | model 84 | .render_dual(background, fill_0, stroke, MARGIN, LINE_WIDTH)? 85 | .write_to_png("3.3.4.3.4-dual.png")?; 86 | 87 | Ok(()) 88 | } 89 | } 90 | 91 | struct Ex33336(); 92 | 93 | impl Example for Ex33336 { 94 | fn render( 95 | background: Color, 96 | stroke: Color, 97 | fill_0: Color, 98 | fill_1: Color, 99 | fill_2: Color, 100 | ) -> Result<()> { 101 | let mut model = Model::new(WIDTH, HEIGHT, SCALE); 102 | model.add(Shape::new(6, fill_2, stroke)?); 103 | let a = model.add_multi(0..1, 0..6, Shape::new(3, fill_0, stroke)?)?; 104 | let b = model.add_multi(a.clone(), 1..2, Shape::new(3, fill_0, stroke)?)?; 105 | let c = model.add_multi(a.clone(), 2..3, Shape::new(3, fill_0, stroke)?)?; 106 | let d = model.add_multi(c, 1..2, Shape::new(6, fill_2, stroke)?)?; 107 | model.repeat(d)?; 108 | 109 | model 110 | .render(background, MARGIN, LINE_WIDTH, SHOW_LABELS)? 111 | .write_to_png("3.3.3.3.6.png")?; 112 | model 113 | .render_dual(background, fill_0, stroke, MARGIN, LINE_WIDTH)? 114 | .write_to_png("3.3.3.3.6-dual.png")?; 115 | 116 | Ok(()) 117 | } 118 | } 119 | 120 | struct Ex333333(); 121 | 122 | impl Example for Ex333333 { 123 | fn render( 124 | background: Color, 125 | stroke: Color, 126 | fill_0: Color, 127 | fill_1: Color, 128 | fill_2: Color, 129 | ) -> Result<()> { 130 | let mut model = Model::new(WIDTH, HEIGHT, SCALE); 131 | model.add(Shape::new(3, fill_2, stroke)?); 132 | let a = model.add_multi(0..1, 0..3, Shape::new(3, fill_1, stroke)?)?; 133 | let b = model.add_multi(a, 1..3, Shape::new(3, fill_2, stroke)?)?; 134 | model.repeat(b)?; 135 | 136 | model 137 | .render(background, MARGIN, LINE_WIDTH, SHOW_LABELS)? 138 | .write_to_png("3.3.3.3.3.3.png")?; 139 | model 140 | .render_dual(background, fill_0, stroke, MARGIN, LINE_WIDTH)? 141 | .write_to_png("3.3.3.3.3.3-dual.png")?; 142 | 143 | Ok(()) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /examples/intro.rs: -------------------------------------------------------------------------------- 1 | use tiling::{Color, Model, Result, Shape}; 2 | 3 | pub fn main() -> Result<()> { 4 | let width = 1024; 5 | let height = 1024; 6 | let scale = 128.0; 7 | let stroke = Color::new(242, 60, 60)?; 8 | let fill_hexagon = Color::new(242, 194, 106)?; 9 | let fill_square = Color::new(23, 216, 146)?; 10 | let fill_triangle = Color::new(242, 209, 48)?; 11 | let background = Color::new(242, 242, 242)?; 12 | let margin = 0.1; 13 | let show_labels = false; 14 | let line_width = 0.1; 15 | 16 | // create an empty model 17 | let mut model = Model::new(width, height, scale); 18 | 19 | // add a hexagon 20 | model.add(Shape::new(6, fill_hexagon, stroke)?); 21 | 22 | // attach a square to each side of the hexagon 23 | let squares = model.add_multi(0..1, 0..6, Shape::new(4, fill_square, stroke)?)?; 24 | 25 | // attach a triangle between the squares 26 | let _ = model.add_multi(squares.clone(), 1..2, Shape::new(3, fill_triangle, stroke)?)?; 27 | 28 | // attach a hexagon to the outer edge of each square 29 | let hexagons = model.add_multi(squares.clone(), 2..3, Shape::new(6, fill_hexagon, stroke)?)?; 30 | 31 | // fill the surface with the pattern 32 | model.repeat(hexagons)?; 33 | 34 | // render the tiling 35 | let render = model.render(background, margin, line_width, show_labels)?; 36 | render.write_to_png("intro.png")?; 37 | 38 | // render the dual tiling 39 | let render_dual = model.render_dual(background, fill_hexagon, stroke, margin, line_width)?; 40 | render_dual.write_to_png("intro-dual.png")?; 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /src/color.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use crate::{Error::*, Result}; 4 | 5 | /// The valid range of a color value (0 to 255 inclusive). 6 | const RGB_RANGE: RangeInclusive = 0..=255; 7 | 8 | /// A color with red, green, and blue components. 9 | #[derive(Clone, Copy, Debug)] 10 | pub struct Color { 11 | red: i32, 12 | green: i32, 13 | blue: i32, 14 | } 15 | 16 | impl Color { 17 | /// Returns a new color, validating each component is in the range [0, 255]. 18 | pub fn new(red: i32, green: i32, blue: i32) -> Result { 19 | if !(RGB_RANGE.contains(&red) && RGB_RANGE.contains(&green) && RGB_RANGE.contains(&blue)) { 20 | return Err(InvalidColor); 21 | } 22 | 23 | Ok(Color { red, green, blue }) 24 | } 25 | 26 | /// Returns the red component. 27 | pub fn red(&self) -> i32 { 28 | self.red 29 | } 30 | 31 | /// Returns the green component. 32 | pub fn green(&self) -> i32 { 33 | self.green 34 | } 35 | 36 | /// Returns the blue component. 37 | pub fn blue(&self) -> i32 { 38 | self.blue 39 | } 40 | 41 | /// Returns the red, green, and blue comonents as a tuple where each component 42 | /// has been translated into the unit interval (0 to 1 inclusive). 43 | pub fn rgb_unit_int(&self) -> (f64, f64, f64) { 44 | fn unit_int(i: i32) -> f64 { 45 | i as f64 / 255.0 46 | } 47 | 48 | ( 49 | unit_int(self.red), 50 | unit_int(self.green), 51 | unit_int(self.blue), 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{io, result}; 2 | 3 | use thiserror::Error; 4 | 5 | /// A type that represents a success or failure. 6 | pub type Result = result::Result; 7 | 8 | /// An enum that captures all possible error conditions of this crate. 9 | #[derive(Debug, Error)] 10 | pub enum Error { 11 | /// An out of bounds index was accessed in a data structure. 12 | #[error("out of bounds index {index} exceeds length {length} in {name}")] 13 | OutOfBounds { 14 | index: usize, 15 | length: usize, 16 | name: String, 17 | }, 18 | 19 | /// An error occurred while rendering a shape. 20 | #[error("render error: {0}")] 21 | Render(#[from] cairo::Error), 22 | 23 | /// An error occurred while cairo was doing I/O. 24 | #[error("cairo I/O error")] 25 | CairoIO(#[from] cairo::IoError), 26 | 27 | /// An I/O error occurred. 28 | #[error("file I/O error")] 29 | FileIO(#[from] io::Error), 30 | 31 | /// User-provided shape parameters were invalid. 32 | #[error("invalid shape parameters")] 33 | InvalidShape, 34 | 35 | /// User-provided color parameters were invalid. 36 | #[error("invalid color parameters")] 37 | InvalidColor, 38 | } 39 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # tiling 2 | //! 3 | //! *tiling* is a library for constructing tilings of regular polygons and their 4 | //! dual tilings. 5 | //! 6 | //! # Resources 7 | //! 8 | //! - [Tilings by regular polygons](http://en.wikipedia.org/wiki/Tiling_by_regular_polygons) 9 | //! - [List of Euclidian uniform tilings](https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings) 10 | //! 11 | //! # Requirements 12 | //! 13 | //! *tiling* uses [cairo-rs](https://crates.io/crates/cairo-rs) for rendering and 14 | //! requires [cairo](https://www.cairographics.org/download/) to be installed. 15 | //! 16 | //! # Example 17 | //! 18 | //! Create an empty tiling model. 19 | //! 20 | //! ```rust 21 | //! let (width, height, scale) = (1024, 1024, 128.0); 22 | //! 23 | //! let mut model = Model::new(width, height, scale); 24 | //! ``` 25 | //! 26 | //! Place a polygon at the origin. 27 | //! This adds a hexagon. 28 | //! 29 | //! ```rust 30 | //! let stroke = Color::new(242, 60, 60)?; 31 | //! let fill_hexagon = Color::new(242, 194, 106)?; 32 | //! 33 | //! model.add(Shape::new(6, fill_hexagon, stroke)?); 34 | //! ``` 35 | //! 36 | //! At this point we can render the model. 37 | //! 38 | //! ```rust 39 | //! let background = Color::new(242, 242, 242)?; 40 | //! let margin = 0.1; 41 | //! let show_labels = false; 42 | //! let line_width = 0.1; 43 | //! 44 | //! let render = model.render(background, margin, line_width, show_labels)?; 45 | //! render.write_to_png("output.png")?; 46 | //! ``` 47 | //! 48 | //! Let's continue by attaching a square to each of the hexagon's sides. 49 | //! 50 | //! ```rust 51 | //! let fill_square = Color::new(23, 216, 146)?; 52 | //! 53 | //! let squares = model.add_multi(0..1, 0..6, Shape::new(4, fill_square, stroke)?)?; 54 | //! ``` 55 | //! 56 | //! The first parameter `0..1` is a range that indicates the shape(s) to attach to 57 | //! (by their index). 58 | //! In this example, the square is attached to the hexagon (index `0`). 59 | //! 60 | //! > When `show_labels` is `true`, each shape is labeled with its index. 61 | //! 62 | //! The second paramter `0..6` is a range that indicates the edge(s) to attach to 63 | //! (by their index). 64 | //! In this example, the square is attached to all six edges of the hexagon. 65 | //! 66 | //! > When `show_labels` is `true`, each edge is labeled with its index. 67 | //! 68 | //! The final paramter defines the shape to add (a square). 69 | //! 70 | //! The `add_multi` method returns a range containing the indexes of the added square 71 | //! shapes so they can be referenced later. 72 | //! We'll see how to do that next. 73 | //! 74 | //! Now, attach triangles to all of the squares using the previously returned range 75 | //! `squares`. 76 | //! Here, a triangle is attached to edge `1` of each square. 77 | //! 78 | //! ```rust 79 | //! let fill_triangle = Color::new(242, 209, 48)?; 80 | //! 81 | //! let _ = model.add_multi(squares.clone(), 1..2, Shape::new(3, fill_triangle, stroke)?)?; 82 | //! ``` 83 | //! 84 | //! Let's wrap up by attaching a hexagon to the outer edge of each square. 85 | //! These hexagons will define the repeating positions of the tiling. 86 | //! 87 | //! ```rust 88 | //! let hexagons = model.add_multi(squares.clone(), 2..3, Shape::new(6, fill_hexagon, stroke)?)?; 89 | //! ``` 90 | //! 91 | //! Now that the tiling's repeating pattern is complete, use the `repeat` method to 92 | //! fill the rest of the surface with the pattern. 93 | //! 94 | //! ```rust 95 | //! model.repeat(hexagons)?; 96 | //! ``` 97 | //! 98 | //! Once satisfied, disable the shape and edge labels and adjust the scale. 99 | //! 100 | //! Dual tilings may be created using the `render_dual` method. 101 | //! A tiling's dual is formed by drawing edges between the centers of adjacent polygons. 102 | pub use color::Color; 103 | pub use error::{Error, Result}; 104 | pub use model::Model; 105 | pub use shape::{Dual, Point, Polygon, Shape}; 106 | 107 | pub mod color; 108 | pub mod error; 109 | pub mod model; 110 | pub mod shape; 111 | -------------------------------------------------------------------------------- /src/model.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Ordering::Less, collections::HashMap, fs::File, ops::Range, path::Path}; 2 | 3 | use crate::{Color, Dual, Error::*, Point, Polygon, Result, Shape}; 4 | 5 | /// Represents a tiling composed of an arbitrary number of regular polygons. 6 | /// A model is used to imperatively construct a tiling by building small patterns 7 | /// of shapes that are then repeated to fill a two-dimensional space. 8 | /// Use `render` to render the tiling. 9 | /// Use `render_dual` to render the dual tiling. 10 | #[derive(Debug)] 11 | pub struct Model { 12 | width: i32, 13 | height: i32, 14 | scale: f64, 15 | shapes: Vec, 16 | lookup: HashMap, 17 | } 18 | 19 | impl Model { 20 | /// Returns an empty model. 21 | pub fn new(width: i32, height: i32, scale: f64) -> Model { 22 | Model { 23 | width, 24 | height, 25 | scale, 26 | shapes: Vec::new(), 27 | lookup: HashMap::new(), 28 | } 29 | } 30 | 31 | /// Adds shape to the model. 32 | pub fn add(&mut self, shape: Shape) { 33 | self.shapes.push(shape); 34 | self.lookup.insert(shape.point(), shape); 35 | } 36 | 37 | /// Attaches shape to every edge in edges of each shape in indexes. 38 | pub fn add_multi( 39 | &mut self, 40 | indexes: Range, 41 | edges: Range, 42 | shape: Shape, 43 | ) -> Result> { 44 | let start = self.shapes.len(); 45 | for i in indexes { 46 | for e in edges.clone() { 47 | self.attach(i, e, shape)?; 48 | } 49 | } 50 | let end = self.shapes.len(); 51 | 52 | Ok(start..end) 53 | } 54 | 55 | /// Attaches shape to the edge with index edge of the shape with index index. 56 | fn attach(&mut self, index: usize, edge: usize, shape: Shape) -> Result<()> { 57 | let parent = self.shapes.get(index).ok_or(OutOfBounds { 58 | index: index, 59 | length: self.shapes.len(), 60 | name: String::from("model shapes"), 61 | })?; 62 | let shape = parent.adjacent(shape.sides(), edge, shape.fill(), shape.stroke())?; 63 | self.add(shape); 64 | 65 | Ok(()) 66 | } 67 | 68 | /// Fills the rest of the surface with the pattern contained by the shapes 69 | /// with index in indexes. 70 | pub fn repeat(&mut self, indexes: Range) -> Result<()> { 71 | let mut memo: HashMap = HashMap::new(); 72 | let mut depth = 0; 73 | 74 | loop { 75 | self.repeat_r(indexes.clone(), Point::origin(), depth, &mut memo)?; 76 | let w = self.width as f64 / 2.0 / self.scale; 77 | let h = self.height as f64 / 2.0 / self.scale; 78 | let tl = memo.keys().any(|p| p.x < -w && p.y < -h); 79 | let tr = memo.keys().any(|p| p.x > w && p.y < -h); 80 | let bl = memo.keys().any(|p| p.x < -w && p.y > h); 81 | let br = memo.keys().any(|p| p.x > w && p.y > h); 82 | if tl && tr && bl && br { 83 | break; 84 | } 85 | depth += 1; 86 | } 87 | 88 | Ok(()) 89 | } 90 | 91 | /// Recurisvely fills the surface by repeating a pattern of shapes. 92 | fn repeat_r( 93 | &mut self, 94 | indexes: Range, 95 | point: Point, 96 | depth: i32, 97 | memo: &mut HashMap, 98 | ) -> Result<()> { 99 | if depth < 0 { 100 | return Ok(()); 101 | } 102 | 103 | let prev_depth = *memo.get(&point).unwrap_or(&-1); 104 | if prev_depth >= depth { 105 | return Ok(()); 106 | } 107 | 108 | memo.insert(point, depth); 109 | 110 | if prev_depth == -1 { 111 | self.add_repeats(point); 112 | } 113 | 114 | let mut shapes = Vec::new(); 115 | for i in indexes.clone() { 116 | let s = self.shapes.get(i).ok_or(OutOfBounds { 117 | index: i, 118 | length: self.shapes.len(), 119 | name: String::from("model shapes"), 120 | })?; 121 | 122 | shapes.push(*s); 123 | } 124 | 125 | for s in shapes.iter() { 126 | self.repeat_r(indexes.clone(), point + s.point(), depth - 1, memo)?; 127 | } 128 | 129 | Ok(()) 130 | } 131 | 132 | /// Adds a shape to be repeated at point. 133 | fn add_repeats(&mut self, point: Point) { 134 | for s in self.shapes.iter() { 135 | let p = point + s.point(); 136 | if self.lookup.contains_key(&p) { 137 | continue; 138 | } 139 | 140 | self.lookup.insert(p, s.clone_at(p)); 141 | } 142 | } 143 | 144 | /// Returns the model's dual tiling. 145 | fn dual(&self, fill: Color, stroke: Color) -> Result> { 146 | let mut vertexes: HashMap> = HashMap::new(); 147 | for s in self.lookup.values() { 148 | let points = s.points(0.0)?; 149 | for p in &points[0..points.len() - 1] { 150 | if let Some(shapes) = vertexes.get_mut(p) { 151 | shapes.push(*s); 152 | } else { 153 | vertexes.insert(*p, vec![*s]); 154 | } 155 | } 156 | } 157 | 158 | let mut duals: Vec = Vec::new(); 159 | for (p, shapes) in vertexes.iter_mut() { 160 | if shapes.len() < 3 { 161 | continue; 162 | } 163 | 164 | let angle = |s: &Shape| (s.point().y - p.y).atan2(s.point().x - p.x); 165 | 166 | shapes.sort_by(|a, b| angle(b).partial_cmp(&angle(a)).unwrap_or(Less)); 167 | 168 | let mut points = shapes.iter().map(|s| s.point()).collect::>(); 169 | points.push(*points.first().ok_or(OutOfBounds { 170 | index: 0, 171 | length: shapes.len(), 172 | name: String::from("dual shapes"), 173 | })?); 174 | 175 | duals.push(Dual::new(points, fill, stroke)); 176 | } 177 | 178 | Ok(duals) 179 | } 180 | 181 | /// Renders the model. 182 | pub fn render( 183 | &self, 184 | background: Color, 185 | margin: f64, 186 | line_width: f64, 187 | show_labels: bool, 188 | ) -> Result { 189 | let (surface, context) = self.render_init(background, line_width)?; 190 | let shapes = self.lookup.values(); 191 | 192 | if show_labels { 193 | for s in shapes.clone() { 194 | s.render_edge_labels(&context, margin - 0.25)?; 195 | } 196 | } 197 | for s in shapes.clone() { 198 | s.render(&context, margin)?; 199 | } 200 | if show_labels { 201 | for (i, s) in shapes.clone().enumerate() { 202 | s.render_label(&context, &i.to_string())?; 203 | } 204 | } 205 | 206 | Ok(Render(surface)) 207 | } 208 | 209 | /// Renders the model's dual tiling. 210 | pub fn render_dual( 211 | &self, 212 | background: Color, 213 | fill: Color, 214 | stroke: Color, 215 | margin: f64, 216 | line_width: f64, 217 | ) -> Result { 218 | let (surface, context) = self.render_init(background, line_width)?; 219 | let shapes = self.dual(fill, stroke)?; 220 | 221 | for s in shapes.clone() { 222 | s.render(&context, margin)?; 223 | } 224 | 225 | Ok(Render(surface)) 226 | } 227 | 228 | /// Prepares a cairo surface and context for rendering. 229 | fn render_init( 230 | &self, 231 | background: Color, 232 | line_width: f64, 233 | ) -> Result<(cairo::ImageSurface, cairo::Context)> { 234 | let surface = cairo::ImageSurface::create(cairo::Format::Rgb24, self.width, self.height)?; 235 | let context = cairo::Context::new(&surface)?; 236 | let (red, green, blue) = background.rgb_unit_int(); 237 | context.set_line_cap(cairo::LineCap::Round); 238 | context.set_line_join(cairo::LineJoin::Round); 239 | context.set_line_width(line_width); 240 | context.set_font_size(18.0 / self.scale); 241 | context.translate(self.width as f64 / 2.0, self.height as f64 / 2.0); 242 | context.scale(self.scale, self.scale); 243 | context.set_source_rgb(red, green, blue); 244 | context.paint()?; 245 | 246 | Ok((surface, context)) 247 | } 248 | } 249 | 250 | /// Represents a rendered model. 251 | pub struct Render(cairo::ImageSurface); 252 | 253 | impl Render { 254 | /// Writes a rendered model to a PNG file at path. 255 | pub fn write_to_png>(&self, path: P) -> Result<()> { 256 | let mut file = File::create(path)?; 257 | self.0.write_to_png(&mut file)?; 258 | 259 | Ok(()) 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/shape.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | f64::consts::PI, 3 | hash::{Hash, Hasher}, 4 | ops, 5 | }; 6 | 7 | use itertools::multizip; 8 | 9 | use crate::{Color, Error::*, Result}; 10 | 11 | /// The number of decimal places to use when comparing points. 12 | const PRECISION: i32 = 6; 13 | 14 | /// A generic interface of a polygon. 15 | pub trait Polygon { 16 | /// Returns the polygon's points. 17 | fn points(&self, margin: f64) -> Result>; 18 | 19 | /// Renders the polygon. 20 | fn render(&self, context: &cairo::Context, margin: f64) -> Result<()>; 21 | } 22 | 23 | /// A representation of a regular polygon (all angles and sides are equal). 24 | #[derive(Clone, Copy, Debug)] 25 | pub struct Shape { 26 | sides: i32, 27 | point: Point, 28 | rotation: f64, 29 | fill: Color, 30 | stroke: Color, 31 | } 32 | 33 | impl Shape { 34 | /// Returns a new shape, ensuring the number of sides is at least three. 35 | pub fn new(sides: i32, fill: Color, stroke: Color) -> Result { 36 | if sides < 3 { 37 | return Err(InvalidShape); 38 | } 39 | 40 | Ok(Shape { 41 | sides, 42 | point: Point::origin(), 43 | rotation: 0.0, 44 | fill, 45 | stroke, 46 | }) 47 | } 48 | 49 | /// Returns the shape's sides. 50 | pub fn sides(&self) -> i32 { 51 | self.sides 52 | } 53 | 54 | /// Returns the shape's point. 55 | pub fn point(&self) -> Point { 56 | self.point 57 | } 58 | 59 | /// Returns the shape's rotation. 60 | pub fn rotation(&self) -> f64 { 61 | self.rotation 62 | } 63 | 64 | /// Returns the shape's fill. 65 | pub fn fill(&self) -> Color { 66 | self.fill 67 | } 68 | 69 | /// Returns the shape's stroke. 70 | pub fn stroke(&self) -> Color { 71 | self.stroke 72 | } 73 | 74 | /// Returns the the edge indexed by index. 75 | fn edge(&self, index: usize, margin: f64) -> Result { 76 | let es = self.edges(margin)?; 77 | es.get(index) 78 | .ok_or(OutOfBounds { 79 | index: index, 80 | length: es.len(), 81 | name: String::from("shape edges"), 82 | }) 83 | .map(|(p0, p1)| (p0.clone(), p1.clone())) 84 | } 85 | 86 | /// Returns the shape's edges. 87 | fn edges(&self, margin: f64) -> Result> { 88 | let ps = self.points(margin)?; 89 | 90 | let mut es = Vec::new(); 91 | for i in 0..self.sides { 92 | let i = i as usize; 93 | let p0 = ps.get(i).ok_or(OutOfBounds { 94 | index: i, 95 | length: ps.len(), 96 | name: String::from("shape vertices"), 97 | })?; 98 | let p1 = ps.get(i + 1).ok_or(OutOfBounds { 99 | index: i + 1, 100 | length: ps.len(), 101 | name: String::from("shape vertices"), 102 | })?; 103 | 104 | es.push((p0.clone(), p1.clone())); 105 | } 106 | 107 | Ok(es) 108 | } 109 | 110 | /// Returns the sides-sided shape adjacent to the edge with index edge. 111 | pub fn adjacent(&self, sides: i32, edge: usize, fill: Color, stroke: Color) -> Result { 112 | let (p0, p1) = self.edge(edge, 0.0)?; 113 | let angle = 2.0 * PI / sides as f64; 114 | let a = (p1.y - p0.y).atan2(p1.x - p0.x); 115 | let b = a - PI / 2.0; 116 | let d = 0.5 / (angle / 2.0).tan(); 117 | let p = Point { 118 | x: p0.x + (p1.x - p0.x) / 2.0 + b.cos() * d, 119 | y: p0.y + (p1.y - p0.y) / 2.0 + b.sin() * d, 120 | }; 121 | let r = a + angle * ((sides - 1) as f64 / 2.0); 122 | 123 | Ok(Shape { 124 | sides: sides, 125 | point: p, 126 | rotation: r, 127 | fill: fill, 128 | stroke: stroke, 129 | }) 130 | } 131 | 132 | /// Renders the index of each edge as an edge label. 133 | pub fn render_edge_labels(&self, context: &cairo::Context, margin: f64) -> Result<()> { 134 | let es = self.edges(margin)?; 135 | for (i, e) in es.iter().enumerate() { 136 | let text = i.to_string(); 137 | let (p0, p1) = e; 138 | let te = context.text_extents(&text)?; 139 | let x = p0.x + (p1.x - p0.x) / 2.0 - te.width / 2.0; 140 | let y = p0.y + (p1.y - p0.y) / 2.0 - te.height / 2.0; 141 | 142 | context.set_source_rgb(0.0, 0.0, 0.0); 143 | context.move_to(x, y); 144 | context.show_text(&text)?; 145 | } 146 | 147 | Ok(()) 148 | } 149 | 150 | /// Renders text as the shape's label. 151 | pub fn render_label(&self, context: &cairo::Context, text: &str) -> Result<()> { 152 | let te = context.text_extents(text)?; 153 | let x = self.point.x - te.width / 2.0; 154 | let y = self.point.y - te.height / 2.0; 155 | 156 | context.set_source_rgb(0.0, 0.0, 0.0); 157 | context.move_to(x, y); 158 | context.show_text(text)?; 159 | 160 | Ok(()) 161 | } 162 | 163 | /// Returns a copy of the shape centered at point. 164 | pub fn clone_at(&self, point: Point) -> Shape { 165 | let mut s = self.clone(); 166 | s.point = point; 167 | 168 | s 169 | } 170 | } 171 | 172 | impl Polygon for Shape { 173 | /// Returns the polygon's points. 174 | fn points(&self, margin: f64) -> Result> { 175 | let angle = 2.0 * PI / self.sides as f64; 176 | let rotation = self.rotation - PI / 2.0; 177 | let angles = (0..=self.sides) 178 | .map(|i| (i % self.sides) as f64 * angle + rotation) 179 | .collect::>(); 180 | let d = { 181 | let a = angle / 2.0; 182 | 0.5 / a.sin() - margin / a.cos() 183 | }; 184 | 185 | let points = angles 186 | .iter() 187 | .map(|a| Point { 188 | x: self.point.x + a.cos() * d, 189 | y: self.point.y + a.sin() * d, 190 | }) 191 | .collect(); 192 | 193 | Ok(points) 194 | } 195 | 196 | /// Renders the polygon. 197 | fn render(&self, context: &cairo::Context, margin: f64) -> Result<()> { 198 | render(context, self.points(margin)?, self.fill, self.stroke) 199 | } 200 | } 201 | 202 | /// A representation of a polygon within a dual tiling. 203 | #[derive(Clone, Debug)] 204 | pub struct Dual { 205 | points: Vec, 206 | fill: Color, 207 | stroke: Color, 208 | } 209 | 210 | impl Dual { 211 | /// Returns a new dual with vertices points. 212 | pub fn new(points: Vec, fill: Color, stroke: Color) -> Dual { 213 | Dual { 214 | points, 215 | fill, 216 | stroke, 217 | } 218 | } 219 | 220 | /// Computes the inset polygon for a polygon with vertices points. 221 | fn inset_polygon(points: Vec, margin: f64) -> Result> { 222 | let p = points.get(points.len() - 2).ok_or(OutOfBounds { 223 | index: points.len() - 2, 224 | length: points.len(), 225 | name: String::from("shape points"), 226 | })?; 227 | let mut points = points.clone(); 228 | points.insert(0, *p); 229 | 230 | let mut rs = multizip((&points, &points[1..], &points[2..])) 231 | .map(|(p0, p1, p2)| Dual::inset_corner((p0.clone(), p1.clone(), p2.clone()), margin)) 232 | .collect::>(); 233 | rs.push(rs[0]); 234 | 235 | Ok(rs) 236 | } 237 | 238 | /// Computes the inset corner for a tuple of three points. 239 | fn inset_corner(plane: Plane, margin: f64) -> Point { 240 | let (p0, p1, p2) = plane; 241 | let a0 = (p1.y - p0.y).atan2(p1.x - p0.x) - PI / 2.0; 242 | let a1 = (p2.y - p1.y).atan2(p2.x - p1.x) - PI / 2.0; 243 | let (ax0, ay0) = (p0.x + a0.cos() * margin, p0.y + a0.sin() * margin); 244 | let (ax1, ay1) = (p1.x + a0.cos() * margin, p1.y + a0.sin() * margin); 245 | let (bx0, by0) = (p1.x + a1.cos() * margin, p1.y + a1.sin() * margin); 246 | let (bx1, by1) = (p2.x + a1.cos() * margin, p2.y + a1.sin() * margin); 247 | let (ady, adx) = (ay1 - ay0, ax0 - ax1); 248 | let (bdy, bdx) = (by1 - by0, bx0 - bx1); 249 | let c0 = ady * ax0 + adx * ay0; 250 | let c1 = bdy * bx0 + bdx * by0; 251 | let d = ady * bdx - bdy * adx; 252 | 253 | Point { 254 | x: (bdx * c0 - adx * c1) / d, 255 | y: (ady * c1 - bdy * c0) / d, 256 | } 257 | } 258 | } 259 | 260 | impl Polygon for Dual { 261 | /// Returns the polygon's points. 262 | fn points(&self, margin: f64) -> Result> { 263 | if margin == 0.0 { 264 | Ok(self.points.clone()) 265 | } else { 266 | Dual::inset_polygon(self.points.clone(), margin) 267 | } 268 | } 269 | 270 | /// Renders the polygon. 271 | fn render(&self, context: &cairo::Context, margin: f64) -> Result<()> { 272 | render(context, self.points(margin)?, self.fill, self.stroke) 273 | } 274 | } 275 | 276 | /// Represents a point in two-dimensional space. 277 | #[derive(Clone, Copy, Debug)] 278 | pub struct Point { 279 | pub x: f64, 280 | pub y: f64, 281 | } 282 | 283 | impl Point { 284 | /// Returns a point at the origin. 285 | pub fn origin() -> Point { 286 | Point { x: 0.0, y: 0.0 } 287 | } 288 | 289 | /// Returns a normalized point component. 290 | /// This is used to simplify equality tests and hashing. 291 | fn normalize(n: f64) -> i32 { 292 | (n * 10_f64.powi(PRECISION)).round() as i32 293 | } 294 | } 295 | 296 | impl PartialEq for Point { 297 | fn eq(&self, other: &Self) -> bool { 298 | Point::normalize(self.x) == Point::normalize(other.x) 299 | && Point::normalize(self.y) == Point::normalize(other.y) 300 | } 301 | } 302 | 303 | impl Eq for Point {} 304 | 305 | impl Hash for Point { 306 | fn hash(&self, state: &mut H) { 307 | Point::normalize(self.x).hash(state); 308 | Point::normalize(self.y).hash(state); 309 | } 310 | } 311 | 312 | impl ops::Add for Point { 313 | type Output = Point; 314 | 315 | fn add(self, _rhs: Point) -> Point { 316 | Point { 317 | x: self.x + _rhs.x, 318 | y: self.y + _rhs.y, 319 | } 320 | } 321 | } 322 | 323 | /// A representation of an edge in two-dimensional space. 324 | type Edge = (Point, Point); 325 | 326 | /// A representation of a plane in two-dimensional space. 327 | type Plane = (Point, Point, Point); 328 | 329 | /// Renders the polygon defined by points. 330 | fn render(context: &cairo::Context, points: Vec, fill: Color, stroke: Color) -> Result<()> { 331 | for i in 0..points.len() { 332 | let p = points[i]; 333 | match i { 334 | 0 => context.move_to(p.x, p.y), 335 | _ => context.line_to(p.x, p.y), 336 | } 337 | } 338 | 339 | let (r, g, b) = fill.rgb_unit_int(); 340 | context.set_source_rgb(r, g, b); 341 | context.fill_preserve()?; 342 | 343 | let (r, g, b) = stroke.rgb_unit_int(); 344 | context.set_source_rgb(r, g, b); 345 | context.stroke()?; 346 | 347 | Ok(()) 348 | } 349 | --------------------------------------------------------------------------------