├── .gitignore
├── .travis.yml
├── LDConfig.ldr
├── LICENSE
├── README.md
├── ldrawpy
├── __init__.py
├── constants.py
├── ldrarrows.py
├── ldrawpy.py
├── ldrcolour.py
├── ldrcolourdict.py
├── ldrhelpers.py
├── ldrmodel.py
├── ldrpprint.py
├── ldrprimitives.py
├── ldrshapes.py
├── ldvrender.py
└── scripts
│ ├── __init__.py
│ └── ldrcat.py
├── setup.py
├── tests
├── rerun.sh
├── test_files
│ ├── stylesheet.yml
│ ├── stylesheet_dark.yml
│ ├── test_model.ldr
│ └── testfile2.ldr
├── test_ldrcolour.py
├── test_ldrprimitives.py
└── test_misc.py
└── tox.ini
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | *.pdf
4 | *.png
5 | # C extensions
6 | *.so
7 | .vscode
8 |
9 | # OS litter
10 | .DS_Store
11 | Desktop.ini
12 | ._*
13 | Thumbs.db
14 | .Trashes
15 |
16 | # Packages
17 | *.egg
18 | *.egg-info
19 | dist
20 | build
21 | eggs
22 | parts
23 | bin
24 | var
25 | sdist
26 | develop-eggs
27 | .installed.cfg
28 | lib
29 | lib64
30 | __pycache__
31 |
32 | # Installer logs
33 | pip-log.txt
34 |
35 | # Unit test / coverage reports
36 | .coverage
37 | .tox
38 | nosetests.xml
39 |
40 | # Translations
41 | *.mo
42 |
43 | # Mr Developer
44 | .mr.developer.cfg
45 | .project
46 | .pydevproject
47 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | python:
4 | - "3.7"
5 |
6 | before_script:
7 | - pip install setuptools==60.8.2
8 | - wget https://github.com/michaelgale/toolbox-py/archive/master.zip -O /tmp/toolbox.zip
9 | - unzip /tmp/toolbox.zip
10 | - cd toolbox-py-master
11 | - python setup.py install
12 | - cd ..
13 |
14 | script:
15 | - python setup.py install
16 | - cd tests
17 | - pytest
18 |
--------------------------------------------------------------------------------
/LDConfig.ldr:
--------------------------------------------------------------------------------
1 | 0 LDraw.org Configuration File
2 | 0 Name: LDConfig.ldr
3 | 0 Author: LDraw.org
4 | 0 !LDRAW_ORG Configuration UPDATE 2017-12-15
5 |
6 | 0 // LDraw Solid Colours
7 | 0 // LEGOID 26 - Black
8 | 0 !COLOUR Black CODE 0 VALUE #05131D EDGE #FFFFFF
9 | 0 // LEGOID 23 - Bright Blue
10 | 0 !COLOUR Blue CODE 1 VALUE #0055BF EDGE #05131D
11 | 0 // LEGOID 28 - Dark Green
12 | 0 !COLOUR Green CODE 2 VALUE #257A3E EDGE #05131D
13 | 0 // LEGOID 107 - Bright Bluish Green
14 | 0 !COLOUR Dark_Turquoise CODE 3 VALUE #00838F EDGE #05131D
15 | 0 // LEGOID 21 - Bright Red
16 | 0 !COLOUR Red CODE 4 VALUE #C91A09 EDGE #05131D
17 | 0 // LEGOID 221 - Bright Purple
18 | 0 !COLOUR Dark_Pink CODE 5 VALUE #C870A0 EDGE #05131D
19 | 0 // LEGOID 217 - Brown
20 | 0 !COLOUR Brown CODE 6 VALUE #583927 EDGE #05131D
21 | 0 // LEGOID 2 - Grey
22 | 0 !COLOUR Light_Grey CODE 7 VALUE #9BA19D EDGE #05131D
23 | 0 // LEGOID 27 - Dark Grey
24 | 0 !COLOUR Dark_Grey CODE 8 VALUE #6D6E5C EDGE #05131D
25 | 0 // LEGOID 45 - Light Blue
26 | 0 !COLOUR Light_Blue CODE 9 VALUE #B4D2E3 EDGE #05131D
27 | 0 // LEGOID 37 - Bright Green
28 | 0 !COLOUR Bright_Green CODE 10 VALUE #4B9F4A EDGE #05131D
29 | 0 // LEGOID 116 - Medium Bluish Green
30 | 0 !COLOUR Light_Turquoise CODE 11 VALUE #55A5AF EDGE #05131D
31 | 0 // LEGOID 4 - Brick Red
32 | 0 !COLOUR Salmon CODE 12 VALUE #F2705E EDGE #05131D
33 | 0 // LEGOID 9 - Light Reddish Violet
34 | 0 !COLOUR Pink CODE 13 VALUE #FC97AC EDGE #05131D
35 | 0 // LEGOID 24 - Bright Yellow
36 | 0 !COLOUR Yellow CODE 14 VALUE #F2CD37 EDGE #05131D
37 | 0 // LEGOID 1 - White
38 | 0 !COLOUR White CODE 15 VALUE #FFFFFF EDGE #05131D
39 | 0 // LEGOID 6 - Light Green
40 | 0 !COLOUR Light_Green CODE 17 VALUE #C2DAB8 EDGE #05131D
41 | 0 // LEGOID 3 - Light Yellow
42 | 0 !COLOUR Light_Yellow CODE 18 VALUE #FBE696 EDGE #05131D
43 | 0 // LEGOID 5 - Brick Yellow
44 | 0 !COLOUR Tan CODE 19 VALUE #E4CD9E EDGE #05131D
45 | 0 // LEGOID 39 - Light Bluish Violet
46 | 0 !COLOUR Light_Violet CODE 20 VALUE #C9CAE2 EDGE #05131D
47 | 0 // LEGOID 104 - Bright Violet
48 | 0 !COLOUR Purple CODE 22 VALUE #81007B EDGE #05131D
49 | 0 // LEGOID 196 - Dark Royal Blue
50 | 0 !COLOUR Dark_Blue_Violet CODE 23 VALUE #2032B0 EDGE #05131D
51 | 0 // LEGOID 106 - Bright Orange
52 | 0 !COLOUR Orange CODE 25 VALUE #FE8A18 EDGE #05131D
53 | 0 // LEGOID 124 - Bright Reddish Violet
54 | 0 !COLOUR Magenta CODE 26 VALUE #923978 EDGE #05131D
55 | 0 // LEGOID 119 - Bright Yellowish Green
56 | 0 !COLOUR Lime CODE 27 VALUE #BBE90B EDGE #05131D
57 | 0 // LEGOID 138 - Sand Yellow
58 | 0 !COLOUR Dark_Tan CODE 28 VALUE #958A73 EDGE #05131D
59 | 0 // LEGOID 222 - Light Purple
60 | 0 !COLOUR Bright_Pink CODE 29 VALUE #E4ADC8 EDGE #05131D
61 | 0 // LEGOID 324 - Medium Lavender
62 | 0 !COLOUR Medium_Lavender CODE 30 VALUE #AC78BA EDGE #05131D
63 | 0 // LEGOID 325 - Lavender
64 | 0 !COLOUR Lavender CODE 31 VALUE #E1D5ED EDGE #05131D
65 | 0 // LEGOID 36 - Light Yellowish Orange
66 | 0 !COLOUR Very_Light_Orange CODE 68 VALUE #F3CF9B EDGE #05131D
67 | 0 // LEGOID 198 - Bright Reddish Lilac
68 | 0 !COLOUR Bright_Reddish_Lilac CODE 69 VALUE #CD6298 EDGE #05131D
69 | 0 // LEGOID 192 - Reddish Brown
70 | 0 !COLOUR Reddish_Brown CODE 70 VALUE #582A12 EDGE #05131D
71 | 0 // LEGOID 194 - Medium Stone Grey
72 | 0 !COLOUR Light_Bluish_Grey CODE 71 VALUE #A0A5A9 EDGE #05131D
73 | 0 // LEGOID 199 - Dark Stone Grey
74 | 0 !COLOUR Dark_Bluish_Grey CODE 72 VALUE #6C6E68 EDGE #05131D
75 | 0 // LEGOID 102 - Medium Blue
76 | 0 !COLOUR Medium_Blue CODE 73 VALUE #5C9DD1 EDGE #05131D
77 | 0 // LEGOID 29 - Medium Green
78 | 0 !COLOUR Medium_Green CODE 74 VALUE #73DCA1 EDGE #05131D
79 | 0 // LEGOID 223 - Light Pink
80 | 0 !COLOUR Light_Pink CODE 77 VALUE #FECCCF EDGE #05131D
81 | 0 // LEGOID 283 - Light Nougat
82 | 0 !COLOUR Light_Flesh CODE 78 VALUE #F6D7B3 EDGE #05131D
83 | 0 // LEGOID 38 - Dark Orange
84 | 0 !COLOUR Medium_Dark_Flesh CODE 84 VALUE #CC702A EDGE #05131D
85 | 0 // LEGOID 268 - Medium Lilac
86 | 0 !COLOUR Medium_Lilac CODE 85 VALUE #3F3691 EDGE #05131D
87 | 0 // LEGOID 312 - Medium Nougat
88 | 0 !COLOUR Dark_Flesh CODE 86 VALUE #7C503A EDGE #05131D
89 | 0 // LEGOID 195 - Medium Royal Blue
90 | 0 !COLOUR Blue_Violet CODE 89 VALUE #4C61DB EDGE #05131D
91 | 0 // LEGOID 18 - Nougat
92 | 0 !COLOUR Flesh CODE 92 VALUE #D09168 EDGE #05131D
93 | 0 // LEGOID 100 - Light Red
94 | 0 !COLOUR Light_Salmon CODE 100 VALUE #FEBABD EDGE #05131D
95 | 0 // LEGOID 110 - Bright Bluish Violet
96 | 0 !COLOUR Violet CODE 110 VALUE #4354A3 EDGE #05131D
97 | 0 // LEGOID 112 - Medium Bluish Violet
98 | 0 !COLOUR Medium_Violet CODE 112 VALUE #6874CA EDGE #05131D
99 | 0 // LEGOID 115 - Medium Yellowish Green
100 | 0 !COLOUR Medium_Lime CODE 115 VALUE #C7D23C EDGE #05131D
101 | 0 // LEGOID 118 - Light Bluish Green
102 | 0 !COLOUR Aqua CODE 118 VALUE #B3D7D1 EDGE #05131D
103 | 0 // LEGOID 120 - Light Yellowish Green
104 | 0 !COLOUR Light_Lime CODE 120 VALUE #D9E4A7 EDGE #05131D
105 | 0 // LEGOID 125 - Light Orange
106 | 0 !COLOUR Light_Orange CODE 125 VALUE #F9BA61 EDGE #05131D
107 | 0 // LEGOID 208 - Light Stone Grey
108 | 0 !COLOUR Very_Light_Bluish_Grey CODE 151 VALUE #E6E3E0 EDGE #05131D
109 | 0 // LEGOID 191 - Flame Yellowish Orange
110 | 0 !COLOUR Bright_Light_Orange CODE 191 VALUE #F8BB3D EDGE #05131D
111 | 0 // LEGOID 212 - Light Royal Blue
112 | 0 !COLOUR Bright_Light_Blue CODE 212 VALUE #86C1E1 EDGE #05131D
113 | 0 // LEGOID 216 - Rust
114 | 0 !COLOUR Rust CODE 216 VALUE #B31004 EDGE #05131D
115 | 0 // LEGOID 226 - Cool Yellow
116 | 0 !COLOUR Bright_Light_Yellow CODE 226 VALUE #FFF03A EDGE #05131D
117 | 0 // LEGOID 232 - Dove Blue
118 | 0 !COLOUR Sky_Blue CODE 232 VALUE #56BED6 EDGE #05131D
119 | 0 // LEGOID 140 - Earth Blue
120 | 0 !COLOUR Dark_Blue CODE 272 VALUE #0D325B EDGE #05131D
121 | 0 // LEGOID 141 - Earth Green
122 | 0 !COLOUR Dark_Green CODE 288 VALUE #184632 EDGE #05131D
123 | 0 // LEGOID 308 - Dark Brown
124 | 0 !COLOUR Dark_Brown CODE 308 VALUE #352100 EDGE #05131D
125 | 0 // LEGOID 11 - Pastel Blue
126 | 0 !COLOUR Maersk_Blue CODE 313 VALUE #54A9C8 EDGE #05131D
127 | 0 // LEGOID 154 - New Dark Red
128 | 0 !COLOUR Dark_Red CODE 320 VALUE #720E0F EDGE #05131D
129 | 0 // LEGOID 321 - Dark Azur
130 | 0 !COLOUR Dark_Azure CODE 321 VALUE #1498D7 EDGE #05131D
131 | 0 // LEGOID 322 - Medium Azur
132 | 0 !COLOUR Medium_Azure CODE 322 VALUE #3EC2DD EDGE #05131D
133 | 0 // LEGOID 323 - Aqua
134 | 0 !COLOUR Light_Aqua CODE 323 VALUE #BDDCD8 EDGE #05131D
135 | 0 // LEGOID 326 - Spring Yellowish Green
136 | 0 !COLOUR Yellowish_Green CODE 326 VALUE #DFEEA5 EDGE #05131D
137 | 0 // LEGOID 330 - Olive Green
138 | 0 !COLOUR Olive_Green CODE 330 VALUE #9B9A5A EDGE #05131D
139 | 0 // LEGOID 153 - Sand Red
140 | 0 !COLOUR Sand_Red CODE 335 VALUE #D67572 EDGE #05131D
141 | 0 // LEGOID 22 - Medium Reddish Violet
142 | 0 !COLOUR Medium_Dark_Pink CODE 351 VALUE #F785B1 EDGE #05131D
143 | 0 // LEGOID 25 - Earth Orange
144 | 0 !COLOUR Earth_Orange CODE 366 VALUE #FA9C1C EDGE #05131D
145 | 0 // LEGOID 136 - Sand Violet
146 | 0 !COLOUR Sand_Purple CODE 373 VALUE #845E84 EDGE #05131D
147 | 0 // LEGOID 151 - Sand Green
148 | 0 !COLOUR Sand_Green CODE 378 VALUE #A0BCAC EDGE #05131D
149 | 0 // LEGOID 135 - Sand Blue
150 | 0 !COLOUR Sand_Blue CODE 379 VALUE #597184 EDGE #05131D
151 | 0 // LEGOID 12 - Light Orange Brown
152 | 0 !COLOUR Fabuland_Brown CODE 450 VALUE #B67B50 EDGE #05131D
153 | 0 // LEGOID 105 - Bright Yellowish Orange
154 | 0 !COLOUR Medium_Orange CODE 462 VALUE #FFA70B EDGE #05131D
155 | 0 // LEGOID 38 - Dark Orange
156 | 0 !COLOUR Dark_Orange CODE 484 VALUE #A95500 EDGE #05131D
157 | 0 // LEGOID 103 - Light Grey
158 | 0 !COLOUR Very_Light_Grey CODE 503 VALUE #E6E3DA EDGE #05131D
159 | 0 // LEGOID 218 - Reddish Lilac
160 | 0 !COLOUR Reddish_Lilac CODE 218 VALUE #8E5597 EDGE #05131D
161 | 0 // LEGOID 295 - Flamingo Pink
162 | 0 !COLOUR Flamingo_Pink CODE 295 VALUE #FF94C2 EDGE #05131D
163 | 0 // LEGOID 219 - Lilac
164 | 0 !COLOUR Lilac CODE 219 VALUE #564E9D EDGE #05131D
165 | 0 // LEGOID 128 - Dark Nougat
166 | 0 !COLOUR Dark_Nougat CODE 128 VALUE #AD6140 EDGE #05131D
167 |
168 |
169 | 0 // LDraw Transparent Colours
170 | 0 // LEGOID 40 - Transparent
171 | 0 !COLOUR Trans_Clear CODE 47 VALUE #ECECEC EDGE #222222 ALPHA 128
172 | 0 // LEGOID 111 - Transparent Brown
173 | 0 !COLOUR Trans_Black CODE 40 VALUE #635F52 EDGE #171316 ALPHA 128
174 | 0 // LEGOID 41 - Transparent Red
175 | 0 !COLOUR Trans_Red CODE 36 VALUE #C91A09 EDGE #880000 ALPHA 128
176 | 0 // LEGOID 47 - Transparent Fluorescent Reddish Orange
177 | 0 !COLOUR Trans_Neon_Orange CODE 38 VALUE #FF800D EDGE #BD2400 ALPHA 128
178 | 0 // LEGOID 182 - Trans Bright Orange
179 | 0 !COLOUR Trans_Orange CODE 57 VALUE #F08F1C EDGE #A45C28 ALPHA 128
180 | 0 // LEGOID 157 - Transparent Fluorescent Yellow
181 | 0 !COLOUR Trans_Neon_Yellow CODE 54 VALUE #DAB000 EDGE #C3BA3F ALPHA 128
182 | 0 // LEGOID 44 - Transparent Yellow
183 | 0 !COLOUR Trans_Yellow CODE 46 VALUE #F5CD2F EDGE #8E7400 ALPHA 128
184 | 0 // LEGOID 49 - Transparent Fluorescent Green
185 | 0 !COLOUR Trans_Neon_Green CODE 42 VALUE #C0FF00 EDGE #84C300 ALPHA 128
186 | 0 // LEGOID 311 / 227 - Transparent Bright Green / Transparent Bright Yellowish Green
187 | 0 !COLOUR Trans_Bright_Green CODE 35 VALUE #56E646 EDGE #9DA86B ALPHA 128
188 | 0 // LEGOID 48 - Transparent Green
189 | 0 !COLOUR Trans_Green CODE 34 VALUE #237841 EDGE #1E6239 ALPHA 128
190 | 0 // LEGOID 43 - Transparent Blue
191 | 0 !COLOUR Trans_Dark_Blue CODE 33 VALUE #0020A0 EDGE #000064 ALPHA 128
192 | 0 // LEGOID 143 - Transparent Fluorescent Blue
193 | 0 !COLOUR Trans_Medium_Blue CODE 41 VALUE #559AB7 EDGE #196973 ALPHA 128
194 | 0 // LEGOID 42 - Transparent Light Blue
195 | 0 !COLOUR Trans_Light_Blue CODE 43 VALUE #AEE9EF EDGE #224340 ALPHA 128
196 | 0 // LEGOID 229 - Transparent Light Bluish Green
197 | 0 !COLOUR Trans_Very_Light_Blue CODE 39 VALUE #C1DFF0 EDGE #85A3B4 ALPHA 128
198 | 0 // LEGOID 236 - Transparent Bright Reddish Lilac
199 | 0 !COLOUR Trans_Bright_Reddish_Lilac CODE 44 VALUE #96709F EDGE #5A3463 ALPHA 128
200 | 0 // LEGOID 126 - Transparent Bright Bluish Violet
201 | 0 !COLOUR Trans_Purple CODE 52 VALUE #A5A5CB EDGE #280025 ALPHA 128
202 | 0 // LEGOID 113 - Transparent Medium Reddish Violet
203 | 0 !COLOUR Trans_Dark_Pink CODE 37 VALUE #DF6695 EDGE #A32A59 ALPHA 128
204 | 0 // LEGOID 230 - Transparent Bright Pink
205 | 0 !COLOUR Trans_Pink CODE 45 VALUE #FC97AC EDGE #A8718C ALPHA 128
206 | 0 // LEGOID 285 - Transparent Light Green
207 | 0 !COLOUR Trans_Light_Green CODE 285 VALUE #7DC291 EDGE #52805F ALPHA 128
208 | 0 // LEGOID 234 - Transparent Fire Yellow
209 | 0 !COLOUR Trans_Fire_Yellow CODE 234 VALUE #FBE890 EDGE #BAAB6A ALPHA 128
210 | 0 // LEGOID 293 - Transparent Light Royal Blue
211 | 0 !COLOUR Trans_Light_Blue_Violet CODE 293 VALUE #6BABE4 EDGE #4D7BA3 ALPHA 128
212 | 0 // LEGOID 231 - Transparent Flame Yellowish Orange
213 | 0 !COLOUR Trans_Bright_Light_Orange CODE 231 VALUE #FCB76D EDGE #BD8951 ALPHA 128
214 | 0 // LEGOID 284 - Transparent Reddish Lilac
215 | 0 !COLOUR Trans_Reddish_Lilac CODE 284 VALUE #C281A5 EDGE #82566E ALPHA 128
216 |
217 |
218 | 0 // LDraw Chrome Colours
219 | 0 // LEGOID 299 - Warm Gold Drum Lacq
220 | 0 !COLOUR Chrome_Gold CODE 334 VALUE #BBA53D EDGE #BBB23D CHROME
221 | 0 // LEGOID 298 - Cool Silver Drum Lacq
222 | 0 !COLOUR Chrome_Silver CODE 383 VALUE #E0E0E0 EDGE #A4A4A4 CHROME
223 | 0 // LEGOID 187 - Metallic Earth Orange
224 | 0 !COLOUR Chrome_Antique_Brass CODE 60 VALUE #645A4C EDGE #281E10 CHROME
225 | 0 !COLOUR Chrome_Black CODE 64 VALUE #1B2A34 EDGE #595959 CHROME
226 | 0 // LEGOID 185 - Metallic Bright Blue
227 | 0 !COLOUR Chrome_Blue CODE 61 VALUE #6C96BF EDGE #202A68 CHROME
228 | 0 // LEGOID 147 - Metallic Dark Green
229 | 0 !COLOUR Chrome_Green CODE 62 VALUE #3CB371 EDGE #007735 CHROME
230 | 0 !COLOUR Chrome_Pink CODE 63 VALUE #AA4D8E EDGE #6E1152 CHROME
231 |
232 |
233 | 0 // LDraw Pearl Colours
234 | 0 // LEGOID 183 - Metallic White
235 | 0 !COLOUR Pearl_White CODE 183 VALUE #F2F3F2 EDGE #05131D PEARLESCENT
236 | 0 // LEGOID 150 - Metallic Light Grey
237 | 0 !COLOUR Pearl_Very_Light_Grey CODE 150 VALUE #BBBDBC EDGE #05131D PEARLESCENT
238 | 0 // LEGOID 179 / 296 / 131 / 315 - Silver Flip-flop / Cool Silver / Silver / Silver Metallic
239 | 0 !COLOUR Pearl_Light_Grey CODE 135 VALUE #9CA3A8 EDGE #05131D PEARLESCENT
240 | 0 // LEGOID 131 - Silver
241 | 0 !COLOUR Flat_Silver CODE 179 VALUE #898788 EDGE #05131D PEARLESCENT
242 | 0 // LEGOID 148 - Metallic Dark Grey
243 | 0 !COLOUR Pearl_Dark_Grey CODE 148 VALUE #575857 EDGE #05131D PEARLESCENT
244 | 0 // LEGOID 145 - Sand Blue Metallic
245 | 0 !COLOUR Metal_Blue CODE 137 VALUE #5677BA EDGE #05131D PEARLESCENT
246 | 0 // LEGOID 127 - Gold
247 | 0 !COLOUR Pearl_Light_Gold CODE 142 VALUE #DCBE61 EDGE #05131D PEARLESCENT
248 | 0 // LEGOID 297 - Warm Gold
249 | 0 !COLOUR Pearl_Gold CODE 297 VALUE #CC9C2B EDGE #05131D PEARLESCENT
250 | 0 // LEGOID 147 - Metallic Sand Yellow
251 | 0 !COLOUR Flat_Dark_Gold CODE 178 VALUE #B4883E EDGE #05131D PEARLESCENT
252 | 0 // LEGOID 139 - Copper
253 | 0 !COLOUR Copper CODE 134 VALUE #964A27 EDGE #05131D PEARLESCENT
254 | 0 // LEGOID 189 - Reddish Gold
255 | 0 !COLOUR Reddish_Gold CODE 189 VALUE #AC8247 EDGE #05131D PEARLESCENT
256 |
257 |
258 | 0 // LDraw Metallic Colours
259 | 0 // LEGOID 315 - Silver Metallic
260 | 0 !COLOUR Metallic_Silver CODE 80 VALUE #A5A9B4 EDGE #05131D METAL
261 | 0 // LEGOID 200 - Lemon Metallic
262 | 0 !COLOUR Metallic_Green CODE 81 VALUE #899B5F EDGE #05131D METAL
263 | 0 // LEGOID 310 / 335 Metalized Gold / Gold Ink
264 | 0 !COLOUR Metallic_Gold CODE 82 VALUE #DBAC34 EDGE #05131D METAL
265 | 0 // LEGOID 149 - Metallic Black
266 | 0 !COLOUR Metallic_Black CODE 83 VALUE #1A2831 EDGE #05131D METAL
267 | 0 // LEGOID 309 / 336 - Metalized Silver / Silver Ink
268 | 0 !COLOUR Metallic_Dark_Grey CODE 87 VALUE #6D6E5C EDGE #05131D METAL
269 | 0 // LEGOID 300 / 334 - Copper Drum Lacq / Copper Ink
270 | 0 !COLOUR Metallic_Copper CODE 300 VALUE #C27F53 EDGE #05131D METAL
271 | 0 // LEGOID 184 - Metallic Bright Red
272 | 0 !COLOUR Metallic_Bright_Red CODE 184 VALUE #D60026 EDGE #05131D METAL
273 | 0 // LEGOID 186 - Metallic Dark Green
274 | 0 !COLOUR Metallic_Dark_Green CODE 186 VALUE #008E3C EDGE #05131D METAL
275 |
276 |
277 | 0 // LDraw Milky Colours
278 | 0 // LEGOID 20 - Nature
279 | 0 !COLOUR Milky_White CODE 79 VALUE #FFFFFF EDGE #C3C3C3 ALPHA 240
280 | 0 // LEGOID 294 - Phosphorescent Green
281 | 0 !COLOUR Glow_In_Dark_Opaque CODE 21 VALUE #E0FFB0 EDGE #A4C374 ALPHA 240 LUMINANCE 15
282 | 0 // LEGOID 50 - Phosphorescent White
283 | 0 !COLOUR Glow_In_Dark_Trans CODE 294 VALUE #BDC6AD EDGE #818A71 ALPHA 240 LUMINANCE 15
284 | 0 // LEGOID 329 - White Glow
285 | 0 !COLOUR Glow_In_Dark_White CODE 329 VALUE #F5F3D7 EDGE #B5B49F ALPHA 240 LUMINANCE 15
286 |
287 |
288 | 0 // LDraw Glitter Colours
289 | 0 // LEGOID 114 - Tr. Medium Reddish-Violet w. Glitter 2%
290 | 0 !COLOUR Glitter_Trans_Dark_Pink CODE 114 VALUE #DF6695 EDGE #9A2A66 ALPHA 128 MATERIAL GLITTER VALUE #923978 FRACTION 0.17 VFRACTION 0.2 SIZE 1
291 | 0 // LEGOID 117 - Transparent Glitter
292 | 0 !COLOUR Glitter_Trans_Clear CODE 117 VALUE #FFFFFF EDGE #C3C3C3 ALPHA 128 MATERIAL GLITTER VALUE #FFFFFF FRACTION 0.08 VFRACTION 0.1 SIZE 1
293 | 0 // LEGOID 129 - Tr. Bright Bluish Violet w. Glitter 2%
294 | 0 !COLOUR Glitter_Trans_Purple CODE 129 VALUE #640061 EDGE #280025 ALPHA 128 MATERIAL GLITTER VALUE #8C00FF FRACTION 0.3 VFRACTION 0.4 SIZE 1
295 | 0 // LEGOID 302 Tr. Light Blue with Glitter 2%
296 | 0 !COLOUR Glitter_Trans_Light_Blue CODE 302 VALUE #AEE9EF EDGE #72B3B0 ALPHA 128 MATERIAL GLITTER VALUE #923978 FRACTION 0.17 VFRACTION 0.2 SIZE 1
297 | 0 // LEGOID 339 Tr Fluorescent Green with Glitter 2%
298 | 0 !COLOUR Glitter_Trans_Neon_Green CODE 339 VALUE #C0FF00 EDGE #84C300 ALPHA 128 MATERIAL GLITTER VALUE #923978 FRACTION 0.17 VFRACTION 0.2 SIZE 1
299 |
300 |
301 | 0 // LDraw Speckle Colours
302 | 0 !COLOUR Speckle_Black_Silver CODE 132 VALUE #000000 EDGE #898788 MATERIAL SPECKLE VALUE #898788 FRACTION 0.4 MINSIZE 1 MAXSIZE 3
303 | 0 // LEGOID 132 - Black Glitter
304 | 0 !COLOUR Speckle_Black_Gold CODE 133 VALUE #000000 EDGE #DBAC34 MATERIAL SPECKLE VALUE #DBAC34 FRACTION 0.4 MINSIZE 1 MAXSIZE 3
305 | 0 !COLOUR Speckle_Black_Copper CODE 75 VALUE #000000 EDGE #AB6038 MATERIAL SPECKLE VALUE #AB6038 FRACTION 0.4 MINSIZE 1 MAXSIZE 3
306 | 0 !COLOUR Speckle_Dark_Bluish_Grey_Silver CODE 76 VALUE #635F61 EDGE #898788 MATERIAL SPECKLE VALUE #898788 FRACTION 0.4 MINSIZE 1 MAXSIZE 3
307 |
308 |
309 | 0 // LDraw Rubber Colours
310 | 0 !COLOUR Rubber_Yellow CODE 65 VALUE #F5CD2F EDGE #05131D RUBBER
311 | 0 !COLOUR Rubber_Trans_Yellow CODE 66 VALUE #CAB000 EDGE #8E7400 ALPHA 128 RUBBER
312 | 0 !COLOUR Rubber_Trans_Clear CODE 67 VALUE #FFFFFF EDGE #C3C3C3 ALPHA 128 RUBBER
313 | 0 !COLOUR Rubber_Black CODE 256 VALUE #212121 EDGE #595959 RUBBER
314 | 0 !COLOUR Rubber_Blue CODE 273 VALUE #0033B2 EDGE #05131D RUBBER
315 | 0 !COLOUR Rubber_Red CODE 324 VALUE #C40026 EDGE #05131D RUBBER
316 | 0 !COLOUR Rubber_Orange CODE 350 VALUE #D06610 EDGE #05131D RUBBER
317 | 0 !COLOUR Rubber_Light_Grey CODE 375 VALUE #C1C2C1 EDGE #05131D RUBBER
318 | 0 !COLOUR Rubber_Dark_Blue CODE 406 VALUE #001D68 EDGE #595959 RUBBER
319 | 0 !COLOUR Rubber_Purple CODE 449 VALUE #81007B EDGE #05131D RUBBER
320 | 0 !COLOUR Rubber_Lime CODE 490 VALUE #D7F000 EDGE #05131D RUBBER
321 | 0 !COLOUR Rubber_Light_Bluish_Grey CODE 496 VALUE #A3A2A4 EDGE #05131D RUBBER
322 | 0 !COLOUR Rubber_Flat_Silver CODE 504 VALUE #898788 EDGE #05131D RUBBER
323 | 0 !COLOUR Rubber_White CODE 511 VALUE #FAFAFA EDGE #05131D RUBBER
324 |
325 |
326 | 0 // LDraw Internal Common Material Colours
327 | 0 !COLOUR Main_Colour CODE 16 VALUE #FFFF80 EDGE #05131D
328 | 0 !COLOUR Edge_Colour CODE 24 VALUE #7F7F7F EDGE #05131D
329 | 0 // LEGOID 109 - Black IR
330 | 0 !COLOUR Trans_Black_IR_Lens CODE 32 VALUE #000000 EDGE #05131D ALPHA 210
331 | 0 !COLOUR Magnet CODE 493 VALUE #656761 EDGE #595959 METAL
332 | 0 !COLOUR Electric_Contact_Alloy CODE 494 VALUE #D0D0D0 EDGE #05131D METAL
333 | 0 !COLOUR Electric_Contact_Copper CODE 495 VALUE #AE7A59 EDGE #05131D METAL
334 | 0
335 |
336 | 0 !COLOUR TrueBlack CODE 500 VALUE #05131D EDGE #05131D
337 | 0 !COLOUR TrueWhite CODE 501 VALUE #FFFFFF EDGE #FFFFFF
338 | 0 !COLOUR TrueClear CODE 502 VALUE #80FF80 EDGE #20FF20 ALPHA 1
339 |
340 | 0 !COLOUR ArrowRed CODE 804 VALUE #C91A09 EDGE #05131D LUMINANCE 15
341 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Michael Gale
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ldraw-py
2 |
3 | 
4 | 
5 |
6 |
7 | 
8 |
9 | A utility package for creating, modifying, and reading LDraw files and data structures.
10 |
11 | LDraw is an open standard for LEGO® CAD software. It is based on a hierarchy of elements describing primitive shapes up to complex LEGO models and scenes.
12 |
13 | ## Installation
14 |
15 | The **ldraw-py** package can be installed directly from the source code:
16 |
17 | ```shell
18 | $ git clone https://github.com/michaelgale/ldraw-py.git
19 | $ cd ldraw-py
20 | $ python setup.py install
21 | ```
22 |
23 | ## Usage
24 |
25 | After installation, the package can imported:
26 |
27 | ```shell
28 | $ python
29 | >>> import ldrawpy
30 | >>> ldrawpy.__version__
31 | ```
32 |
33 | An example of the package can be seen below
34 |
35 | ```python
36 | from ldrawpy import LDRColour
37 |
38 | # Create a white colour using LDraw colour code 15 for white
39 | mycolour = LDRColour(15)
40 | print(mycolour)
41 | ```
42 |
43 | ```shell
44 | White
45 | ```
46 |
47 | ## Requirements
48 |
49 | * Python 3.7+
50 | * toolbox-py
51 |
52 | ## References
53 |
54 | - [LDraw.org](https://www.ldraw.org) - Official maintainer of the LDraw file format specification and the LDraw official part library.
55 | - [ldraw-vscode](https://github.com/michaelgale/ldraw-vscode) - Visual Studio Code language extension plug-in for LDraw files
56 |
57 | ### Lego CAD Tools
58 |
59 | - [Bricklink stud.io](https://www.bricklink.com/v3/studio/download.page) new and modern design tool designed and maintained by Bricklink
60 | - [LeoCAD](https://www.leocad.org) cross platform tool
61 | - [MLCAD](http://mlcad.lm-software.com) for Windows
62 | - [Bricksmith](http://bricksmith.sourceforge.net) for macOS by Allen Smith (no longer maintained)
63 | - [LDView](http://ldview.sourceforge.net) real-time 3D viewer for LDraw models
64 |
65 | ### LPub Instructions Tools
66 |
67 | - Original [LPub](http://lpub.binarybricks.nl) publishing tool by Kevin Clague
68 | - [LPub3D](https://trevorsandy.github.io/lpub3d/) successor to LPub by Trevor Sandy
69 | - [Manual](https://sites.google.com/site/workingwithlpub/lpub-4) for Legacy LPub 4 tool (last version by Kevin Clague)
70 |
71 | ## Authors
72 |
73 | `ldraw-py` was written by [Michael Gale](https://github.com/michaelgale)
74 |
--------------------------------------------------------------------------------
/ldrawpy/__init__.py:
--------------------------------------------------------------------------------
1 | """ldrawpy - A utility package for creating, modifying, and reading LDraw files and data structures."""
2 |
3 | import os
4 |
5 | # fmt: off
6 | __project__ = 'ldrawpy'
7 | __version__ = '0.6.0'
8 | # fmt: on
9 |
10 | VERSION = __project__ + "-" + __version__
11 |
12 | script_dir = os.path.dirname(__file__)
13 |
14 | from .constants import *
15 | from .ldrawpy import brick_name_strip, xyz_to_ldr, mesh_to_ldr
16 | from .ldrcolourdict import *
17 | from .ldrhelpers import *
18 | from .ldrcolour import LDRColour
19 | from .ldrprimitives import LDRAttrib, LDRHeader, LDRLine, LDRTriangle, LDRQuad, LDRPart
20 | from .ldrshapes import *
21 | from .ldrmodel import LDRModel, parse_special_tokens, sort_parts, get_sha1_hash
22 | from .ldvrender import LDViewRender
23 | from .ldrarrows import ArrowContext, arrows_for_step, remove_offset_parts
24 | from .ldrpprint import pprint_line, clean_line, clean_file
25 |
--------------------------------------------------------------------------------
/ldrawpy/constants.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #
3 | # Copyright (C) 2020 Michael Gale
4 | # This file is part of the legocad python module.
5 | # Permission is hereby granted, free of charge, to any person
6 | # obtaining a copy of this software and associated documentation
7 | # files (the "Software"), to deal in the Software without restriction,
8 | # including without limitation the rights to use, copy, modify, merge,
9 | # publish, distribute, sublicense, and/or sell copies of the Software,
10 | # and to permit persons to whom the Software is furnished to do so,
11 | # subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be
14 | # included in all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | #
24 | # Constants
25 |
26 | LDR_OPT_COLOUR = 24
27 | LDR_DEF_COLOUR = 16
28 |
29 | #
30 | # special colour codes for use with labels
31 | #
32 | LDR_ALL_COLOUR = 1000
33 | LDR_ANY_COLOUR = 1001
34 | LDR_OTHER_COLOUR = 1002
35 | LDR_MONO_COLOUR = 1003
36 | LDR_BLKWHT_COLOUR = 1004
37 | LDR_GRAY_COLOUR = 1005
38 | LDR_REDYLW_COLOUR = 1006
39 | LDR_BLUYLW_COLOUR = 1007
40 | LDR_REDBLUYLW_COLOUR = 1008
41 | LDR_GRNBRN_COLOUR = 1009
42 | LDR_BLUBRN_COLOUR = 1010
43 | LDR_BRGREEN_COLOUR = 1011
44 | LDR_LAVENDER_COLOUR = 1012
45 | LDR_PINK_COLOUR = 1013
46 | LDR_LTYLW_COLOUR = 1014
47 | LDR_BLUBLU_COLOUR = 1015
48 | LDR_DKREDBLU_COLOUR = 1016
49 | LDR_ORGYLW_COLOUR = 1017
50 | LDR_ORGBRN_COLOUR = 1018
51 | LDR_BLUES_COLOUR = 1019
52 | LDR_GREENS_COLOUR = 1020
53 | LDR_YELLOWS_COLOUR = 1021
54 | LDR_REDORG_COLOUR = 1022
55 | LDR_TANBRN_COLOUR = 1023
56 | LDR_REDORGYLW_COLOUR = 1024
57 | LDR_BLUGRN_COLOUR = 1025
58 | LDR_TAN_COLOUR = 1026
59 | LDR_PINKPURP_COLOUR = 1027
60 |
61 | LDR_ANY_COLOUR_FILL = ["E6E6E6", "8EDAFF", "FFFF66", "FD908F"]
62 |
63 | LDRAW_TOKENS = [
64 | "STEP",
65 | "FILE",
66 | "NOFILE",
67 | "WRITE",
68 | "PRINT",
69 | "CLEAR",
70 | "PAUSE",
71 | "SAVE",
72 | "BFC",
73 | ]
74 | META_TOKENS = [
75 | "ROTSTEP",
76 | "BACKGROUND",
77 | "GHOST",
78 | "GROUP",
79 | "MLCAD",
80 | "ROTATION",
81 | "SYNTH",
82 | "L3P",
83 | "COLOR",
84 | "COLOUR",
85 | "TRANSLATE",
86 | "ROTATE",
87 | "SCALE",
88 | "TRANSFORM",
89 | "COLOURNAME",
90 | "COLORNAME",
91 | "POINT",
92 | "MATRIX",
93 | "CMDLINE",
94 | "BEGIN",
95 | "END",
96 | ]
97 |
98 | SPECIAL_TOKENS = {
99 | "scale": ["!LPUB ASSEM MODEL_SCALE %-1", "!PY SCALE %-1"],
100 | "rotation_abs": ["ROTSTEP %2 %3 %4 ABS"],
101 | "rotation_rel": ["ROTSTEP %2 %3 %4 REL"],
102 | "page_break": ["!LPUB INSERT PAGE", "!PY PAGE_BREAK"],
103 | "columns": ["!PY COLUMNS %-1"],
104 | "column_break": ["!PY COLUMN_BREAK"],
105 | "arrow_begin": ["!PY ARROW BEGIN %4 %5 %6 %7 %8 %9 %10 %11 %12"],
106 | "arrow_colour": ["!PY ARROW COLOUR %-1"],
107 | "arrow_length": ["!PY ARROW LENGTH %-1"],
108 | "arrow_end": ["!PY ARROW END"],
109 | "callout": ["!PY CALLOUT"],
110 | "bom": ["!LPUB INSERT BOM", "!PY BOM"],
111 | "no_callout": ["!PY NO_CALLOUT"],
112 | "no_fullscale": ["!PY NO_FULLSCALE"],
113 | "no_rotate_icon": ["!PY NO_ROTATE_ICON"],
114 | "rotation_pre": ["!PY ROT %3 %4 %5"],
115 | "no_preview": ["!PY NO_PREVIEW"],
116 | "model_scale": ["!PY MODEL_SCALE %-1"],
117 | "preview_aspect": ["!PY PREVIEW_ASPECT %3 %4 %5"],
118 | "preview_scale": ["!PY PREVIEW_SCALE %3"],
119 | "pli_proxy": ["!PY PLI_PROXY %3 %4 %5 %6 %7"],
120 | }
121 |
122 | LDR_DEFAULT_SCALE = 1.0
123 | LDR_DEFAULT_ASPECT = (40, -55, 0)
124 |
125 | ASPECT_DICT = {
126 | "front": (0, 0, 0),
127 | "back": (0, 180, 0),
128 | "right": (0, 90, 0),
129 | "left": (0, -90, 0),
130 | "top": (90, 0, 0),
131 | "bottom": (-90, 0, 0),
132 | "iso35": (35, 35, 0),
133 | "iso45": (35, 45, 0),
134 | "iso55": (35, 55, 0),
135 | "iso90": (35, 90, 0),
136 | "iso125": (35, 125, 0),
137 | "iso135": (35, 135, 0),
138 | "iso145": (35, 145, 0),
139 | "iso-35": (35, -35, 0),
140 | "iso-45": (35, -45, 0),
141 | "iso-55": (35, -55, 0),
142 | "iso-90": (35, -90, 0),
143 | "iso-125": (35, -125, 0),
144 | "iso-135": (35, -135, 0),
145 | "iso-145": (35, -145, 0),
146 | "iso180": (35, 180, 0),
147 | "iso-180": (35, 180, 0),
148 | "n": (35, 0, 0),
149 | "nnw": (35, 35, 0),
150 | "nw": (35, 45, 0),
151 | "wnw": (35, 55, 0),
152 | "w": (35, 90, 0),
153 | "wsw": (35, 125, 0),
154 | "sw": (35, 135, 0),
155 | "ssw": (35, 145, 0),
156 | "s": (35, 180, 0),
157 | "sse": (35, -145, 0),
158 | "se": (35, -135, 0),
159 | "ese": (35, -125, 0),
160 | "e": (35, -90, 0),
161 | "ene": (35, -55, 0),
162 | "ne": (35, -45, 0),
163 | "nne": (35, -35, 0),
164 | }
165 |
166 | FLIP_DICT = {
167 | "flip-x": (180, 0, 0),
168 | "flip-y": (0, 180, 0),
169 | "flip-z": (0, 0, 180),
170 | "flipx": (180, 0, 0),
171 | "flipy": (0, 180, 0),
172 | "flipz": (0, 0, 180),
173 | "rot90": (0, 90, 0),
174 | "rot-90": (0, -90, 0),
175 | }
176 |
--------------------------------------------------------------------------------
/ldrawpy/ldrarrows.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #
3 | # Copyright (C) 2020 Michael Gale
4 | # This file is part of the legocad python module.
5 | # Permission is hereby granted, free of charge, to any person
6 | # obtaining a copy of this software and associated documentation
7 | # files (the "Software"), to deal in the Software without restriction,
8 | # including without limitation the rights to use, copy, modify, merge,
9 | # publish, distribute, sublicense, and/or sell copies of the Software,
10 | # and to permit persons to whom the Software is furnished to do so,
11 | # subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be
14 | # included in all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | #
24 | # LDraw arrow callout utilties
25 |
26 | import os
27 | import copy
28 | from math import sin, cos, pi
29 | from functools import reduce
30 | import rich
31 | from toolbox import *
32 | from ldrawpy import *
33 |
34 | ARROW_PREFIX = """0 BUFEXCHG A STORE"""
35 | ARROW_PLI = """0 !LPUB PLI BEGIN IGN"""
36 | ARROW_SUFFIX = """0 !LPUB PLI END
37 | 0 STEP
38 | 0 BUFEXCHG A RETRIEVE"""
39 | ARROW_PLI_SUFFIX = """0 !LPUB PLI END"""
40 |
41 | ARROW_PARTS = ["hashl2", "hashl3", "hashl4", "hashl5", "hashl6"]
42 |
43 | ARROW_MZ = Matrix([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
44 | ARROW_PZ = Matrix([[0, -1, 0], [-1, 0, 0], [0, 0, -1]])
45 | ARROW_MX = Matrix([[0, 0, 1], [1, 0, 0], [0, 1, 0]])
46 | ARROW_PX = Matrix([[0, 0, -1], [-1, 0, 0], [0, 1, 0]])
47 | ARROW_MY = Matrix([[0, -1, 0], [0, 0, -1], [1, 0, 0]])
48 | ARROW_PY = Matrix([[0, -1, 0], [0, 0, 1], [-1, 0, 0]])
49 |
50 |
51 | def value_after_token(tokens, value_token, x, xtype=int):
52 | for i, token in enumerate(tokens):
53 | if not token == value_token or not (i + 1) < len(tokens):
54 | continue
55 | try:
56 | v = xtype(tokens[i + 1])
57 | return v
58 | except ValueError:
59 | return x
60 | return x
61 |
62 |
63 | def norm_angle(angle):
64 | return angle % 45
65 |
66 |
67 | def vectorize(s):
68 | try:
69 | v = Vector(*(float(x) for x in s))
70 | except ValueError:
71 | return None
72 | return v
73 |
74 |
75 | class ArrowContext:
76 | def __init__(self, colour=804, length=2):
77 | self.colour = colour
78 | self.length = length
79 | self.scale = 25
80 | self.yscale = 20
81 | self.offset = Vector(0, 0, 0)
82 | self.rotstep = Vector(0, 0, 0)
83 | self.ratio = 0.5
84 | self.outline_colour = 804
85 |
86 | def part_for_length(self, length):
87 | if length <= 2:
88 | return "hashl2"
89 | if length <= 3:
90 | return "hashl3"
91 | if length <= 4:
92 | return "hashl4"
93 | if length <= 5:
94 | return "hashl5"
95 | return "hashl5"
96 |
97 | def matrix_for_offset(self, offset, mask="", invert=False, tilt=0):
98 | rotxy = norm_angle(self.rotstep.x + self.rotstep.y)
99 | rotxz = norm_angle(self.rotstep.x + self.rotstep.z)
100 | rotyz = norm_angle(self.rotstep.y + self.rotstep.z)
101 |
102 | if invert:
103 | rotxy = rotxy + 180
104 | rotxz = rotxz + 180
105 | rotyz = rotyz + 180
106 | if (offset.x > 0) and "x" not in mask:
107 | return ARROW_MX.rotate(-rotyz + tilt, ZAxis)
108 | elif (offset.x < 0) and "x" not in mask:
109 | return ARROW_PX.rotate(rotyz + tilt, ZAxis)
110 |
111 | if (offset.y > 0) and "y" not in mask:
112 | return ARROW_PY.rotate(rotxz + tilt, ZAxis)
113 | elif (offset.y < 0) and "y" not in mask:
114 | return ARROW_MY.rotate(-rotxz + tilt, ZAxis)
115 |
116 | if (offset.z > 0) and "z" not in mask:
117 | return ARROW_MZ.rotate(-rotxy + tilt, ZAxis)
118 | elif (offset.z < 0) and "z" not in mask:
119 | return ARROW_PZ.rotate(rotxy + tilt, ZAxis)
120 | return Identity()
121 |
122 | def loc_for_offset(self, offset, length, mask="", ratio=0.5):
123 | loc_offset = Vector(0, 0, 0)
124 | if (offset.x > 0) and "x" not in mask:
125 | loc_offset.x = offset.x / 2 - float(length) * ratio * self.scale
126 | elif (offset.x < 0) and "x" not in mask:
127 | loc_offset.x = offset.x / 2 + float(length) * ratio * self.scale
128 | if (offset.y > 0) and "y" not in mask:
129 | loc_offset.y = offset.y / 2 - float(length) * ratio * self.yscale
130 | elif (offset.y < 0) and "y" not in mask:
131 | loc_offset.y = offset.y / 2 + float(length) * ratio * self.yscale
132 | if (offset.z > 0) and "z" not in mask:
133 | loc_offset.z = offset.z / 2 - float(length) * ratio * self.scale
134 | elif (offset.z < 0) and "z" not in mask:
135 | loc_offset.z = offset.z / 2 + float(length) * ratio * self.scale
136 | if "x" in mask:
137 | loc_offset += Vector(offset.x, 0, 0)
138 | if "y" in mask:
139 | loc_offset += Vector(0, offset.y, 0)
140 | if "z" in mask:
141 | loc_offset += Vector(0, 0, offset.z)
142 | return loc_offset
143 |
144 | def part_loc_for_offset(self, offset, mask=""):
145 | loc_offset = Vector(0, 0, 0)
146 | if abs(offset.x) > 0.1 and "x" not in mask:
147 | loc_offset.x = offset.x
148 | if abs(offset.y) > 0.1 and "y" not in mask:
149 | loc_offset.y = offset.y
150 | if abs(offset.z) > 0.1 and "z" not in mask:
151 | loc_offset.z = offset.z
152 | return loc_offset
153 |
154 | def _mask_axis(self, offsets):
155 | """Determines which axis has a changing value and is not consistent in a list"""
156 | if len(offsets) == 1:
157 | return ""
158 | mx, my, mz = 1e18, 1e18, 1e18
159 | nx, ny, nz = -1e18, -1e18, -1e18
160 | mask = ""
161 | for o in offsets:
162 | mx, my, mz = min(mx, o.x), min(my, o.y), min(mz, o.z)
163 | nx, ny, nz = max(nx, o.x), max(ny, o.y), max(nz, o.z)
164 | if abs(nx - mx) > 0:
165 | mask = mask + "x"
166 | if abs(ny - my) > 0:
167 | mask = mask + "y"
168 | if abs(nz - mz) > 0:
169 | mask = mask + "z"
170 | return mask
171 |
172 | def arrow_from_dict(self, dict):
173 | arrows = []
174 | mask = self._mask_axis(dict["offset"])
175 | for i, o in enumerate(dict["offset"]):
176 | ldrpart = LDRPart()
177 | ldrpart.wrapcallout = False
178 | ldrpart.from_str(dict["line"])
179 | arrpart = LDRPart()
180 | arrpart.name = self.part_for_length(dict["length"])
181 | arrpart.attrib.loc = copy.copy(ldrpart.attrib.loc)
182 | arrpart.attrib.loc += self.loc_for_offset(
183 | o, dict["length"], mask, ratio=dict["ratio"]
184 | )
185 | arrpart.attrib.matrix = self.matrix_for_offset(
186 | o, mask, invert=dict["invert"], tilt=dict["tilt"]
187 | )
188 | arrpart.attrib.colour = dict["colour"]
189 | arrows.append(str(arrpart))
190 | return "".join(arrows)
191 |
192 | def dict_for_line(self, line, invert, ratio, colour=None, tilt=0):
193 | item = {}
194 | item["line"] = line
195 | c = colour if colour is not None else self.colour
196 | item["colour"] = c
197 | item["length"] = self.length
198 | item["offset"] = copy.copy(self.offset)
199 | item["invert"] = invert
200 | item["ratio"] = ratio
201 | item["tilt"] = tilt
202 | return item
203 |
204 |
205 | def arrows_for_step(arrow_ctx, step, as_lpub=True, only_arrows=False, as_dict=False):
206 | step_lines = []
207 | arrow_parts = []
208 | arrow_dict = []
209 | lines = step.splitlines()
210 | in_arrow = False
211 | offset = Vector(0, 0, 0)
212 | arrow_ratio = 0.5
213 | arrow_tilt = 0
214 | arrow_colour = arrow_ctx.colour
215 | for line in lines:
216 | lineType = int(line.lstrip()[0] if line.lstrip() else -1)
217 | if lineType == 0:
218 | ls = line.upper().split()
219 | if "!PY ARROW" in line:
220 | if in_arrow == False and "BEGIN" in line:
221 | in_arrow = True
222 | arrow_ctx.offset = []
223 | nv = int((len(ls) - 4) / 3)
224 | for _, i in enumerate(range(nv)):
225 | v = vectorize(ls[4 + (i * 3) : 7 + (i * 3)])
226 | if v is not None:
227 | arrow_ctx.offset.append(v)
228 | elif in_arrow and "END" in line:
229 | in_arrow = False
230 | continue
231 | arrow_colour = value_after_token(ls, "COLOUR", arrow_colour, int)
232 | if not in_arrow:
233 | arrow_ctx.colour = arrow_colour
234 | arrow_ctx.length = value_after_token(
235 | ls, "LENGTH", arrow_ctx.length, int
236 | )
237 | arrow_ratio = value_after_token(ls, "RATIO", arrow_ratio, float)
238 | arrow_tilt = value_after_token(ls, "TILT", arrow_tilt, float)
239 | if (
240 | any(x in line for x in ["COLOUR", "LENGTH", "RATIO", "TILT"])
241 | and not in_arrow
242 | ):
243 | continue
244 |
245 | if in_arrow and lineType == 1:
246 | item = arrow_ctx.dict_for_line(
247 | line,
248 | invert=False,
249 | ratio=arrow_ratio,
250 | colour=arrow_colour,
251 | tilt=arrow_tilt,
252 | )
253 | arrow_parts.append(item)
254 | item = arrow_ctx.dict_for_line(
255 | line,
256 | invert=True,
257 | ratio=arrow_ratio,
258 | colour=arrow_colour,
259 | tilt=arrow_tilt,
260 | )
261 | arrow_parts.append(item)
262 | elif not in_arrow and lineType == 1:
263 | if not only_arrows:
264 | step_lines.append(line)
265 |
266 | if len(arrow_parts) > 0:
267 | if as_lpub:
268 | step_lines.append(ARROW_PREFIX)
269 | for part in arrow_parts:
270 | ldrpart = LDRPart()
271 | ldrpart.wrapcallout = False
272 | ldrpart.from_str(part["line"])
273 | mask = arrow_ctx._mask_axis(part["offset"])
274 | ldrpart.attrib.loc += arrow_ctx.part_loc_for_offset(
275 | part["offset"][0], mask
276 | )
277 | step_lines.append(str(ldrpart).strip("\n"))
278 | step_lines.append(ARROW_PLI)
279 | for part in arrow_parts:
280 | arrow_part = arrow_ctx.arrow_from_dict(part)
281 | step_lines.append(arrow_part.strip("\n"))
282 | step_lines.append(ARROW_SUFFIX)
283 | step_lines.append(ARROW_PLI)
284 | for part in arrow_parts:
285 | step_lines.append(part["line"])
286 | step_lines.append(ARROW_PLI_SUFFIX)
287 | else:
288 | for i, part in enumerate(arrow_parts):
289 | ad = {}
290 | ldrpart = LDRPart()
291 | ldrpart.wrapcallout = False
292 | ldrpart.from_str(part["line"])
293 | mask = arrow_ctx._mask_axis(part["offset"])
294 | offset = arrow_ctx.part_loc_for_offset(part["offset"][0], mask)
295 | ldrpart.attrib.loc += offset
296 | ad["offset"] = offset
297 | if i % 2 == 0:
298 | step_lines.append(str(ldrpart).strip("\n"))
299 | ad["part"] = str(ldrpart)
300 | arrow_part = arrow_ctx.arrow_from_dict(part)
301 | step_lines.append(arrow_part.strip("\n"))
302 | ad["arrow"] = arrow_part
303 | arrow_dict.append(ad)
304 |
305 | else:
306 | if "NOFILE" not in step:
307 | if len(step_lines) > 0:
308 | step_lines.append("0 STEP")
309 | if as_dict:
310 | return arrow_dict
311 | if len(step_lines) > 0:
312 | return "\n".join(step_lines)
313 | return ""
314 |
315 |
316 | def arrows_for_lpub_file(filename, outfile):
317 | arrow_ctx = ArrowContext()
318 | with open(filename, "rt") as fp:
319 | with open(outfile, "w") as fpo:
320 | files = fp.read().split("0 FILE")
321 | for i, file in enumerate(files):
322 | mfile = file if i == 0 else "0 FILE " + file.strip()
323 | steps = mfile.split("0 STEP")
324 | for j, step in enumerate(steps):
325 | new_step = arrows_for_step(arrow_ctx, step)
326 | fpo.write(new_step)
327 | if len(steps) > 1:
328 | fpo.write("\n")
329 |
330 |
331 | def remove_offset_parts(parts, oparts, arrow_dict, as_str=False):
332 | """Removes parts which are offset versions of the same part."""
333 | pp = ldrlist_from_parts(parts)
334 | op = ldrlist_from_parts(oparts)
335 | offsets = []
336 | arrows = []
337 | for ad in arrow_dict:
338 | p = LDRPart().from_str(ad["part"])
339 | offset = ad["offset"] * p.attrib.matrix
340 | offsets.append(offset)
341 | a = LDRPart().from_str(ad["arrow"])
342 | arrows.append(a.name)
343 | np = []
344 | for p in pp:
345 | matched = False
346 | for o in op:
347 | if not o.name == p.name or not o.attrib.colour == p.attrib.colour:
348 | continue
349 | elif p.name in arrows:
350 | continue
351 | else:
352 | v1 = p.attrib.loc * p.attrib.matrix
353 | v2 = o.attrib.loc * o.attrib.matrix
354 | for offset in offsets:
355 | vd = abs(v2.copy() - v1.copy())
356 | vo = abs(offset)
357 | ld = abs(p.attrib.loc - o.attrib.loc)
358 | if abs(vd - vo) < 0.1 and abs(ld - vo) < 0.1:
359 | matched = True
360 | if matched:
361 | break
362 |
363 | if not matched:
364 | np.append(p)
365 | if as_str:
366 | return ldrstring_from_list(np)
367 | return np
368 |
--------------------------------------------------------------------------------
/ldrawpy/ldrawpy.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #
3 | # Copyright (C) 2020 Michael Gale
4 | # This file is part of the legocad python module.
5 | # Permission is hereby granted, free of charge, to any person
6 | # obtaining a copy of this software and associated documentation
7 | # files (the "Software"), to deal in the Software without restriction,
8 | # including without limitation the rights to use, copy, modify, merge,
9 | # publish, distribute, sublicense, and/or sell copies of the Software,
10 | # and to permit persons to whom the Software is furnished to do so,
11 | # subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be
14 | # included in all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | #
24 | # LDraw python module
25 |
26 | from toolbox import *
27 |
28 | from .constants import *
29 | from .ldrprimitives import LDRTriangle, LDRLine
30 |
31 |
32 | def xyz_to_ldr(point, as_tuple=False):
33 | """Converts a typical x,y,z 3D coordinate to the somewhat unconventional
34 | LDraw representations of x, -z, y, i.e. the vertical axis extends in
35 | the -y direction as opposed to +z."""
36 | if isinstance(point, (tuple, list)):
37 | v = Vector(point[0], -point[2], point[1])
38 | else:
39 | v = Vector(point.x, -point.z, point.y)
40 | if as_tuple:
41 | return v.as_tuple()
42 | return v
43 |
44 |
45 | def mesh_to_ldr(
46 | faces, vertices, mesh_colour=LDR_DEF_COLOUR, edges=None, edge_colour=None
47 | ):
48 | """Converts a triangular mesh into a LDraw formatted string of triangles
49 | and optionally specified edge lines.
50 | faces - list of triangle vertex indices into the vertices list
51 | vertices - list of mesh 3D vertices (x, y, z)
52 | mesh_colour - LDraw colour code for mesh triangles
53 | edges - list of ((x0, y0, z0), (x1, y1, z1)) line tuples
54 | edge_colour - LDraw colour code for edge lines
55 | """
56 | s = []
57 | triangles = []
58 | for face in faces:
59 | tri = LDRTriangle(mesh_colour, "mm")
60 | tri.p1 = xyz_to_ldr(vertices[face[0]])
61 | tri.p2 = xyz_to_ldr(vertices[face[1]])
62 | tri.p3 = xyz_to_ldr(vertices[face[2]])
63 | triangles.append(tri)
64 | for triangle in triangles:
65 | s.append(str(triangle))
66 | if edges is not None:
67 | lines = []
68 | ec = edge_colour if edge_colour is not None else LDR_OPT_COLOUR
69 | for edge in edges:
70 | line = LDRLine(ec, "mm")
71 | line.p1 = xyz_to_ldr(edge[0])
72 | line.p2 = xyz_to_ldr(edge[1])
73 | lines.append(line)
74 | for line in lines:
75 | s.append(str(line))
76 | return "".join(s)
77 |
78 |
79 | def brick_name_strip(s, level=0):
80 | """Progressively strips (with increasing levels) a part description
81 | by making substitutions with abreviations, removing spaces, etc.
82 | This can be useful for labelling or BOM part lists where space is limited."""
83 | sn = s
84 | if level == 0:
85 | sn = sn.replace(" ", " ")
86 | sn = sn.replace(
87 | "Plate 1 x 2 with Groove with 1 Centre Stud, without Understud",
88 | "Plate 1 x 2 Jumper",
89 | )
90 | sn = sn.replace(
91 | "Plate 1 x 2 without Groove with 1 Centre Stud", "Plate 1 x 2 Jumper"
92 | )
93 | sn = sn.replace(
94 | "Plate 1 x 2 with Groove with 1 Centre Stud", "Plate 1 x 2 Jumper"
95 | )
96 | sn = sn.replace("Brick 1 x 1 with Headlight", "Brick 1 x 1 Erling")
97 | sn = sn.replace("with Groove", "")
98 | sn = sn.replace("Bluish ", "Bl ")
99 | sn = sn.replace("Slope Brick", "Slope")
100 | sn = sn.replace("0.667", "2/3")
101 | sn = sn.replace("1.667", "1-2/3")
102 | sn = sn.replace("1.333", "1-1/3")
103 | sn = sn.replace("1 And 1/3", "1-1/3")
104 | sn = sn.replace("1 and 1/3", "1-1/3")
105 | sn = sn.replace("1 & 1/3", "1-1/3")
106 | sn = sn.replace("with Headlight", "Erling")
107 | sn = sn.replace("Angle Connector", "Conn")
108 | sn = sn.replace("~Plate", "Plate")
109 | elif level == 1:
110 | sn = sn.replace("with ", "w/")
111 | sn = sn.replace("With ", "w/")
112 | sn = sn.replace("Ribbed", "ribbed")
113 | sn = sn.replace("without ", "wo/")
114 | sn = sn.replace("Without ", "wo/")
115 | sn = sn.replace("One", "1")
116 | sn = sn.replace("Two", "2")
117 | sn = sn.replace("Three", "3 ")
118 | sn = sn.replace("Four", "4")
119 | sn = sn.replace(" and ", " & ")
120 | sn = sn.replace(" And ", " & ")
121 | sn = sn.replace("Dark", "Dk")
122 | sn = sn.replace("Light", "Lt")
123 | sn = sn.replace("Bright", "Br")
124 | sn = sn.replace("Reddish Brown", "Red Brown")
125 | sn = sn.replace("Reddish", "Red")
126 | sn = sn.replace("Yellowish", "Ylwish")
127 | sn = sn.replace("Medium", "Med")
128 | sn = sn.replace("Offset", "offs")
129 | sn = sn.replace("Adjacent", "adj")
130 | sn = sn.replace(" degree", "°")
131 | elif level == 2:
132 | sn = sn.replace("Trans", "Tr")
133 | sn = sn.replace(" x ", "x")
134 | sn = sn.replace("Bl ", " ")
135 | elif level == 3:
136 | sn = sn.replace("Orange", "Org")
137 | sn = sn.replace("Yellow", "Ylw")
138 | sn = sn.replace("Black", "Blk")
139 | sn = sn.replace("White", "Wht")
140 | sn = sn.replace("Green", "Grn")
141 | sn = sn.replace("Brown", "Brn")
142 | sn = sn.replace("Purple", "Prpl")
143 | sn = sn.replace("Violet", "Vlt")
144 | sn = sn.replace("Gray", "Gry")
145 | sn = sn.replace("Grey", "Gry")
146 | sn = sn.replace("Axlehole", "axle")
147 | sn = sn.replace("Cylinder", "Cyl")
148 | sn = sn.replace("cylinder", "cyl")
149 | sn = sn.replace("Inverted", "Inv")
150 | sn = sn.replace("inverted", "inv")
151 | sn = sn.replace("Centre", "Ctr")
152 | sn = sn.replace("centre", "ctr")
153 | sn = sn.replace("Center", "Ctr")
154 | sn = sn.replace("center", "ctr")
155 | sn = sn.replace("Figure", "Fig")
156 | sn = sn.replace("figure", "fig")
157 | sn = sn.replace("Rounded", "Round")
158 | sn = sn.replace("rounded", "round")
159 | sn = sn.replace("Underside", "under")
160 | sn = sn.replace("Vertical", "vert")
161 | sn = sn.replace("Horizontal", "horz")
162 | sn = sn.replace("vertical", "vert")
163 | sn = sn.replace("horizontal", "horz")
164 | sn = sn.replace("Flex-System", "Flex")
165 | sn = sn.replace("Flanges", "Flange")
166 | sn = sn.replace("Joiner", "joiner")
167 | sn = sn.replace("Joint", "joint")
168 | sn = sn.replace("Type 1", "")
169 | sn = sn.replace("Type 2", "")
170 | elif level == 4:
171 | sn = sn.replace("Technic", "")
172 | sn = sn.replace("Single", "1")
173 | sn = sn.replace("Dual", "2")
174 | sn = sn.replace("Double", "Dbl")
175 | sn = sn.replace("Stud on", "stud")
176 | sn = sn.replace("Studs on Sides", "stud sides")
177 | sn = sn.replace("Studs on Side", "side studs")
178 | sn = sn.replace("Hinge Plate", "Hinge")
179 | elif level == 5:
180 | sn = sn.replace(" on ", " ")
181 | sn = sn.replace(" On ", " ")
182 | sn = sn.replace("Round", "Rnd")
183 | sn = sn.replace("round", "rnd")
184 | sn = sn.replace("Side", "Sd")
185 | sn = sn.replace("Groove", "Grv")
186 | sn = sn.replace("Minifig", "")
187 | sn = sn.replace("Curved", "Curv")
188 | sn = sn.replace("curved", "curv")
189 | sn = sn.replace("Notched", "notch")
190 | sn = sn.replace("Friction", "fric")
191 | sn = sn.replace("(Complete)", "")
192 | sn = sn.replace("Cross", "X")
193 | sn = sn.replace("Embossed", "Emb")
194 | sn = sn.replace("Extension", "Ext")
195 | sn = sn.replace("Bottom", "Bot")
196 | sn = sn.replace("bottom", "bot")
197 | sn = sn.replace("Inside", "Insd")
198 | sn = sn.replace("inside", "insd")
199 | sn = sn.replace("Locking", "click")
200 | sn = sn.replace("Axleholder", "axle")
201 | sn = sn.replace("axleholder", "axle")
202 | sn = sn.replace("End", "end")
203 | sn = sn.replace("Open", "open")
204 | sn = sn.replace("Rod", "rod")
205 | sn = sn.replace("Hole", "hole")
206 | sn = sn.replace("Ball", "ball")
207 | sn = sn.replace("Thin", "thin")
208 | sn = sn.replace("Thick", "thick")
209 | sn = sn.replace(" - ", "-")
210 | elif level == 6:
211 | sn = sn.replace("Up", "up")
212 | sn = sn.replace("Down", "dn")
213 | sn = sn.replace("Bot", "bot")
214 | sn = sn.replace("Rnd", "rnd")
215 | sn = sn.replace("Studs", "St")
216 | sn = sn.replace("studs", "St")
217 | sn = sn.replace("Stud", "St")
218 | sn = sn.replace("stud", "St")
219 | sn = sn.replace("Corners", "edge")
220 | sn = sn.replace("w/Curv Top", "curved")
221 | sn = sn.replace("Domed", "dome")
222 | sn = sn.replace("Clip", "clip")
223 | sn = sn.replace("Clips", "clip")
224 | sn = sn.replace("Convex", "cvx")
225 | sn = sn[0].upper() + sn[1:]
226 | return sn
227 |
--------------------------------------------------------------------------------
/ldrawpy/ldrcolour.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #
3 | # Copyright (C) 2020 Michael Gale
4 | # This file is part of the legocad python module.
5 | # Permission is hereby granted, free of charge, to any person
6 | # obtaining a copy of this software and associated documentation
7 | # files (the "Software"), to deal in the Software without restriction,
8 | # including without limitation the rights to use, copy, modify, merge,
9 | # publish, distribute, sublicense, and/or sell copies of the Software,
10 | # and to permit persons to whom the Software is furnished to do so,
11 | # subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be
14 | # included in all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | #
24 | # LDraw colour class
25 |
26 | import string
27 |
28 | from ldrawpy import *
29 |
30 |
31 | class LDRColour(object):
32 | """LDraw colour helper class. This class can be used to store a
33 | colour and to perform conversions among:
34 | LDraw colour code, Bricklink colour code, colour name,
35 | RGB floating point, RGB hex"""
36 |
37 | def __init__(self, colour=LDR_DEF_COLOUR):
38 | self.code = LDR_DEF_COLOUR
39 | self.r = 0.8
40 | self.g = 0.8
41 | self.b = 0.8
42 | if isinstance(colour, (tuple, list)):
43 | if (colour[0] > 1.0) or (colour[1] > 1.0) or (colour[2] > 1.0):
44 | self.r = min(colour[0] / 255.0, 1.0)
45 | self.g = min(colour[1] / 255.0, 1.0)
46 | self.b = min(colour[2] / 255.0, 1.0)
47 | else:
48 | self.r = colour[0]
49 | self.g = colour[1]
50 | self.b = colour[2]
51 | rgbstr = self.as_hex().lower()
52 | newCode = LDR_DEF_COLOUR
53 | for code, hexrgb in LDR_COLOUR_RGB.items():
54 | if rgbstr == hexrgb.lower():
55 | newCode = code
56 | if newCode != LDR_DEF_COLOUR:
57 | self.code = newCode
58 |
59 | else:
60 | if isinstance(colour, str):
61 | self.code = LDRColour.ColourCodeFromString(colour)
62 | if self.code == LDR_DEF_COLOUR:
63 | self.r, self.g, self.b = LDRColour.RGBFromHex(colour)
64 | elif isinstance(colour, LDRColour):
65 | self.code = colour.code
66 | else:
67 | self.code = colour
68 | self.code_to_rgb()
69 |
70 | def __repr__(self):
71 | return "%s(%s, r: %.2f g: %.2f b: %.2f, #%s)" % (
72 | self.__class__.__name__,
73 | self.code,
74 | self.r,
75 | self.g,
76 | self.b,
77 | self.as_hex(),
78 | )
79 |
80 | def __str__(self):
81 | return LDRColour.SafeLDRColourName(self.code)
82 |
83 | def __eq__(self, other):
84 | if isinstance(other, int):
85 | if self.code == other:
86 | return True
87 | if isinstance(other, LDRColour):
88 | if self.code == other.code and self.code != LDR_DEF_COLOUR:
89 | return True
90 | if self.r == other.r and self.g == other.g and self.b == other.b:
91 | return True
92 | return False
93 |
94 | def code_to_rgb(self):
95 | if self.code == LDR_DEF_COLOUR:
96 | self.r = 0.62
97 | self.g = 0.62
98 | self.b = 0.62
99 | return
100 | if self.code in LDR_COLOUR_RGB:
101 | rgb = LDR_COLOUR_RGB[self.code]
102 | [rd, gd, bd] = tuple(int(rgb[i : i + 2], 16) for i in (0, 2, 4))
103 | self.r = float(rd) / 255.0
104 | self.g = float(gd) / 255.0
105 | self.b = float(bd) / 255.0
106 |
107 | def as_tuple(self):
108 | return (self.r, self.g, self.b)
109 |
110 | def as_bgr(self):
111 | return (int(self.b * 255), int(self.g * 255), int(self.r * 255))
112 |
113 | def as_hex(self):
114 | return "%02X%02X%02X" % (
115 | int(self.r * 255.0),
116 | int(self.g * 255.0),
117 | int(self.b * 255.0),
118 | )
119 |
120 | def ldvcode(self):
121 | pc = self.code
122 | if self.code >= 1000:
123 | pc = LDR_DEF_COLOUR
124 | return pc
125 |
126 | def name(self):
127 | theName = LDRColour.SafeLDRColourName(self.code)
128 | if theName == "":
129 | return self.as_hex()
130 | return theName
131 |
132 | def high_contrast_complement(self):
133 | level = self.r * self.r + self.g * self.g + self.b * self.b
134 | if level < 1.25:
135 | return (1.0, 1.0, 1.0)
136 | return (0.0, 0.0, 0.0)
137 |
138 | def to_bricklink(self):
139 | for blcode, ldrcode in BL_TO_LDR_COLOUR.items():
140 | if ldrcode == self.code:
141 | return blcode
142 | return 0
143 |
144 | @staticmethod
145 | def SafeLDRColourName(ldrCode):
146 | if ldrCode in LDR_COLOUR_NAME:
147 | return LDR_COLOUR_NAME[ldrCode]
148 | elif ldrCode in LDR_COLOUR_TITLE:
149 | return LDR_COLOUR_TITLE[ldrCode]
150 | return ""
151 |
152 | @staticmethod
153 | def SafeLDRColourRGB(ldrCode):
154 | if ldrCode in LDR_COLOUR_RGB:
155 | return LDR_COLOUR_RGB[ldrCode]
156 | return "FFFFFF"
157 |
158 | @staticmethod
159 | def BLColourCodeFromLDR(colour):
160 | for blcode, ldrcode in BL_TO_LDR_COLOUR.items():
161 | if ldrcode == colour:
162 | return blcode
163 | return 0
164 |
165 | @staticmethod
166 | def ColourCodeFromString(colourStr):
167 | for code, label in LDR_COLOUR_NAME.items():
168 | if label.lower() == colourStr.lower():
169 | return code
170 | if len(colourStr) == 6 or len(colourStr) == 7:
171 | hs = colourStr.lstrip("#").lower()
172 | if not all(c in string.hexdigits for c in hs):
173 | return LDR_DEF_COLOUR
174 | for code, rgbhex in LDR_COLOUR_RGB.items():
175 | if hs == rgbhex.lower():
176 | return code
177 | return LDR_DEF_COLOUR
178 |
179 | @staticmethod
180 | def RGBFromHex(hexStr):
181 | if len(hexStr) < 6:
182 | return 0, 0, 0
183 | hs = hexStr.lstrip("#")
184 | if not all(c in string.hexdigits for c in hs):
185 | return 0, 0, 0
186 | [rd, gd, bd] = tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4))
187 | r = float(rd) / 255.0
188 | g = float(gd) / 255.0
189 | b = float(bd) / 255.0
190 | return r, g, b
191 |
192 |
193 | def FillColoursFromLDRCode(ldrCode):
194 | fillColours = []
195 | if ldrCode in LDR_COLOUR_RGB:
196 | fillColours.append(LDR_COLOUR_RGB[ldrCode])
197 | elif ldrCode in LDR_FILL_CODES:
198 | fillColours = LDR_FILL_CODES[ldrCode]
199 | return [LDRColour.RGBFromHex(x) for x in fillColours]
200 |
201 |
202 | def FillTitlesFromLDRCode(ldrCode):
203 | fillTitles = []
204 | if ldrCode in LDR_COLOUR_RGB:
205 | fillTitles.append(LDR_COLOUR_NAME[ldrCode])
206 | elif ldrCode in LDR_FILL_TITLES:
207 | fillTitles = LDR_FILL_TITLES[ldrCode]
208 | return fillTitles
209 |
--------------------------------------------------------------------------------
/ldrawpy/ldrcolourdict.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #
3 | # Copyright (C) 2020 Michael Gale
4 | # This file is part of the legocad python module.
5 | # Permission is hereby granted, free of charge, to any person
6 | # obtaining a copy of this software and associated documentation
7 | # files (the "Software"), to deal in the Software without restriction,
8 | # including without limitation the rights to use, copy, modify, merge,
9 | # publish, distribute, sublicense, and/or sell copies of the Software,
10 | # and to permit persons to whom the Software is furnished to do so,
11 | # subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be
14 | # included in all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | #
24 | # Colour lookup dictionaries
25 |
26 | from .constants import *
27 |
28 | BL_TO_LDR_COLOUR = {
29 | 1: 15,
30 | 49: 503,
31 | 99: 151,
32 | 86: 71,
33 | 9: 7,
34 | 10: 8,
35 | 85: 72,
36 | 11: 0,
37 | 59: 320,
38 | 5: 4,
39 | 27: 216,
40 | 25: 12,
41 | 26: 100,
42 | 58: 335,
43 | 88: 70,
44 | 8: 6,
45 | 120: 308,
46 | 69: 28,
47 | 2: 19,
48 | 90: 78,
49 | 28: 92,
50 | 150: 84,
51 | 91: 86,
52 | 106: 450,
53 | 29: 366,
54 | 68: 484,
55 | 4: 25,
56 | 31: 462,
57 | 110: 191,
58 | 32: 125,
59 | 96: 68,
60 | 3: 14,
61 | 103: 226,
62 | 33: 18,
63 | 35: 120,
64 | 158: 326,
65 | 34: 27,
66 | 155: 330,
67 | 80: 288,
68 | 6: 2,
69 | 36: 10,
70 | 37: 74,
71 | 38: 17,
72 | 48: 378,
73 | 39: 3,
74 | 40: 11,
75 | 41: 118,
76 | 152: 323,
77 | 63: 272,
78 | 7: 1,
79 | 153: 321,
80 | 156: 322,
81 | 42: 73,
82 | 72: 313,
83 | 105: 212,
84 | 62: 9,
85 | 87: 232,
86 | 55: 379,
87 | 97: 89,
88 | 109: 23,
89 | 43: 110,
90 | 73: 112,
91 | 44: 20,
92 | 24: 22,
93 | 157: 30,
94 | 154: 31,
95 | 54: 373,
96 | 71: 26,
97 | 94: 351,
98 | 104: 29,
99 | 23: 13,
100 | 56: 77,
101 | 12: 47,
102 | 13: 40,
103 | 17: 36,
104 | 18: 38,
105 | 98: 57,
106 | 164: 231,
107 | 121: 54,
108 | 19: 46,
109 | 16: 42,
110 | 108: 35,
111 | 20: 34,
112 | 14: 33,
113 | 74: 41,
114 | 15: 43,
115 | 113: 39,
116 | 51: 52,
117 | 50: 37,
118 | 107: 45,
119 | 21: 334,
120 | 22: 383,
121 | 57: 60,
122 | 122: 64,
123 | 52: 61,
124 | 64: 62,
125 | 82: 63,
126 | 83: 183,
127 | 119: 150,
128 | 66: 135,
129 | 95: 179,
130 | 77: 148,
131 | 78: 137,
132 | 61: 142,
133 | 115: 297,
134 | 81: 178,
135 | 84: 134,
136 | 67: 80,
137 | 70: 81,
138 | 65: 82,
139 | 89: 85,
140 | 46: 21,
141 | 353: 220,
142 | 368: 236,
143 | }
144 |
145 | LDR_COLOUR_NAME = {
146 | 0: "Black",
147 | 1: "Blue",
148 | 2: "Green",
149 | 3: "Dark Turquoise",
150 | 4: "Red",
151 | 5: "Dark Pink",
152 | 6: "Brown",
153 | 7: "Old Light Grey",
154 | 8: "Old Dark Grey",
155 | 9: "Light Blue",
156 | 10: "Bright Green",
157 | 11: "Light Turquoise",
158 | 12: "Salmon",
159 | 13: "Pink",
160 | 14: "Yellow",
161 | 15: "White",
162 | 16: "Default",
163 | 17: "Light Green",
164 | 18: "Light Yellow",
165 | 19: "Tan",
166 | 20: "Light Violet",
167 | 21: "Glow in Dark Opaque",
168 | 22: "Purple",
169 | 23: "Dark Blue Violet",
170 | 24: "Outline",
171 | 25: "Orange",
172 | 26: "Magenta",
173 | 27: "Lime",
174 | 28: "Dark Tan",
175 | 29: "Bright Pink",
176 | 30: "Medium Lavender",
177 | 31: "Lavender",
178 | 68: "Very Light Orange",
179 | 69: "Bright Reddish Lilac",
180 | 70: "Reddish Brown",
181 | 71: "Light Bluish Gray",
182 | 72: "Dark Bluish Gray",
183 | 73: "Medium Blue",
184 | 74: "Medium Green",
185 | 77: "Light Pink",
186 | 78: "Light Nougat",
187 | 84: "Medium Nougat",
188 | 85: "Dark Purple",
189 | 86: "Dark Flesh",
190 | 89: "Blue Violet",
191 | 92: "Nougat",
192 | 100: "Light Salmon",
193 | 110: "Violet",
194 | 112: "Medium Violet",
195 | 115: "Medium Lime",
196 | 118: "Aqua",
197 | 120: "Light Lime",
198 | 125: "Light Orange",
199 | 151: "Very Light Bluish Grey",
200 | 191: "Bright Light Orange",
201 | 212: "Bright Light Blue",
202 | 216: "Rust",
203 | 226: "Bright Light Yellow",
204 | 232: "Sky Blue",
205 | 272: "Dark Blue",
206 | 288: "Dark Green",
207 | 308: "Dark Brown",
208 | 313: "Maersk Blue",
209 | 320: "Dark Red",
210 | 321: "Dark Azur",
211 | 322: "Medium Azur",
212 | 323: "Light Aqua",
213 | 326: "Yellowish Green",
214 | 330: "Olive Green",
215 | 335: "Sand Red",
216 | 351: "Medium Dark Pink",
217 | 353: "Coral",
218 | 366: "Earth Orange",
219 | 373: "Sand Purple",
220 | 378: "Sand Green",
221 | 379: "Sand Blue",
222 | 450: "Fabuland Brown",
223 | 462: "Medium Orange",
224 | 484: "Dark Orange",
225 | 503: "Very Light Grey",
226 | 218: "Reddish Lilac",
227 | 295: "Flamingo Pink",
228 | 219: "Lilac",
229 | 128: "Dark Nougat",
230 | 47: "Trans Clear",
231 | 40: "Trans Black",
232 | 36: "Trans Red",
233 | 38: "Trans Neon Orange",
234 | 57: "Trans Orange",
235 | 54: "Trans Neon Yellow",
236 | 46: "Trans Yellow",
237 | 42: "Trans Neon Green",
238 | 35: "Trans Bright Green",
239 | 34: "Trans Green",
240 | 33: "Trans Dark Blue",
241 | 41: "Trans Medium Blue",
242 | 43: "Trans Light Blue",
243 | 39: "Trans Very Light Blue",
244 | 44: "Trans Bright Reddish Lilac",
245 | 52: "Trans Purple",
246 | 37: "Trans Dark Pink",
247 | 45: "Trans Pink",
248 | 285: "Trans Light Green",
249 | 234: "Trans Fire Yellow",
250 | 293: "Trans Light Blue Violet",
251 | 231: "Trans Bright Light Orange",
252 | 284: "Trans Reddish Lilac",
253 | 334: "Chrome Gold",
254 | 383: "Chrome Silver",
255 | 60: "Chrome Antique Brass",
256 | 64: "Chrome Black",
257 | 61: "Chrome Blue",
258 | 62: "Chrome Green",
259 | 63: "Chrome Pink",
260 | 183: "Pearl White",
261 | 150: "Pearl Very Light Grey",
262 | 135: "Pearl Light Grey",
263 | 179: "Flat Silver",
264 | 148: "Pearl Dark Grey",
265 | 137: "Metal Blue",
266 | 142: "Pearl Light Gold",
267 | 297: "Pearl Gold",
268 | 178: "Flat Dark Gold",
269 | 134: "Copper",
270 | 189: "Reddish Gold",
271 | 80: "Metallic Silver",
272 | 81: "Metallic Green",
273 | 82: "Metallic Gold",
274 | 83: "Metallic Black",
275 | 87: "Metallic Dark Grey",
276 | 300: "Metallic Copper",
277 | 184: "Metallic Bright Red",
278 | 186: "Metallic Dark Green",
279 | 368: "Neon Yellow",
280 | 801: "Arrow Blue",
281 | 802: "Arrow Green",
282 | 804: "Arrow Red",
283 | }
284 |
285 | LDR_COLOUR_RGB = {
286 | 0: "05131D", # 'Black',
287 | 1: "0055bf", #'Blue',
288 | 2: "257a3e", #'Green',
289 | 3: "00838f", #'Dark Turquoise',
290 | 4: "c91a09", #'Red',
291 | 5: "c870a0", #'Dark Pink',
292 | 6: "583927", #'Brown',
293 | 7: "9ba19d", #'Light Grey',
294 | 8: "6d6e5c", #'Dark Grey',
295 | 9: "b4d2e3", #'Light Blue',
296 | 10: "4b9f4a", #'Bright Green',
297 | 11: "55a5af", #'Light Turquoise',
298 | 12: "f2705e", #'Salmon',
299 | 13: "fc97ac", #'Pink',
300 | 14: "f2cd37", #'Yellow',
301 | 15: "ffffff", #'White',
302 | 16: "101010", #'Default',
303 | 17: "c2dab8", #'Light Green',
304 | 18: "fbe696", #'Light Yellow',
305 | 19: "e4cd9e", #'Tan',
306 | 20: "c9cae2", #'Light Violet',
307 | 21: "ECE8DE", #'Glow in Dark Opaque'
308 | 22: "81007b", #'Purple',
309 | 23: "2032b0", #'Dark Blue Violet',
310 | 24: "101010", #'Outline',
311 | 25: "fe8a18", #'Orange',
312 | 26: "923978", #'Magenta',
313 | 27: "bbe90b", #'Lime',
314 | 28: "958a73", #'Dark Tan',
315 | 29: "e4adc8", #'Bright Pink',
316 | 30: "ac78ba", #'Medium Lavender',
317 | 31: "e1d5ed", #'Lavender',
318 | 68: "f3cf9b", #'Very Light Orange',
319 | 69: "cd6298", #'Bright Reddish Lilac',
320 | 70: "582a12", #'Reddish Brown',
321 | 71: "a0a5a9", #'Light Bluish Gray',
322 | 72: "6c6e68", #'Dark Bluish Gray',
323 | 73: "5c9dd1", #'Medium Blue',
324 | 74: "73dca1", #'Medium Green',
325 | 77: "fecccf", #'Light Pink',
326 | 78: "f6d7b3", #'Light Flesh',
327 | 84: "cc702a", #'Medium Dark Flesh',
328 | 85: "3f3691", #'Medium Lilac', / Dark Purple
329 | 86: "7c503a", #'Dark Flesh',
330 | 89: "4c61db", #'Blue Violet',
331 | 92: "d09168", #'Flesh',
332 | 100: "febabd", #'Light Salmon',
333 | 110: "4354a3", #'Violet',
334 | 112: "6874ca", #'Medium Violet',
335 | 115: "c7d23c", #'Medium Lime',
336 | 118: "b3d7d1", #'Aqua',
337 | 120: "d9e4a7", #'Light Lime',
338 | 125: "f9ba61", #'Light Orange',
339 | 151: "e6e3e0", #'Very Light Bluish Grey',
340 | 191: "f8bb3d", #'Bright Light Orange',
341 | 212: "86c1e1", #'Bright Light Blue',
342 | 216: "b31004", #'Rust',
343 | 226: "fff03a", #'Bright Light Yellow',
344 | 232: "56bed6", #'Sky Blue',
345 | 272: "0d325b", #'Dark Blue',
346 | 288: "184632", #'Dark Green',
347 | 308: "352100", #'Dark Brown',
348 | 313: "54a9c8", #'Maersk Blue',
349 | 320: "720e0f", #'Dark Red',
350 | 321: "1498d7", #'Dark Azur',
351 | 322: "3ec2dd", #'Medium Azur',
352 | 323: "bddcd8", #'Light Aqua',
353 | 326: "dfeea5", #'Yellowish Green',
354 | 330: "9b9a5a", #'Olive Green',
355 | 335: "d67572", #'Sand Red',
356 | 351: "f785b1", #'Medium Dark Pink',
357 | 353: "FF6D77", # Coral
358 | 366: "fa9c1c", #'Earth Orange',
359 | 373: "845e84", #'Sand Purple',
360 | 378: "a0bcac", #'Sand Green',
361 | 379: "597184", #'Sand Blue',
362 | 450: "b67b50", #'Fabuland Brown',
363 | 462: "ffa70b", #'Medium Orange',
364 | 484: "a95500", #'Dark Orange',
365 | 503: "e6e3da", #'Very Light Grey',
366 | 218: "8e5597", #'Reddish Lilac',
367 | 295: "ff94c2", #'Flamingo Pink',
368 | 219: "564e9d", #'Lilac',
369 | 128: "ad6140", #'Dark Nougat',
370 | 47: "fcfcfc", #'Trans Clear',
371 | 40: "635f52", #'Trans Black',
372 | 36: "c91a09", #'Trans Red',
373 | 38: "ff800d", #'Trans Neon Orange',
374 | 57: "f08f1c", #'Trans Orange',
375 | 54: "dab000", #'Trans Neon Yellow',
376 | 46: "f5cd2f", #'Trans Yellow',
377 | 42: "c0ff00", #'Trans Neon Green',
378 | 35: "56e646", #'Trans Bright Green',
379 | 34: "237841", #'Trans Green',
380 | 33: "0020a0", #'Trans Dark Blue',
381 | 41: "559ab7", #'Trans Medium Blue',
382 | 43: "aee9ef", #'Trans Light Blue',
383 | 39: "c1dff0", #'Trans Very Light Blue',
384 | 44: "96709f", #'Trans Bright Reddish Lilac',
385 | 52: "a5a5cb", #'Trans Purple',
386 | 37: "df6695", #'Trans Dark Pink',
387 | 45: "fc97ac", #'Trans Pink',
388 | 285: "7dc291", #'Trans Light Green',
389 | 234: "fbe890", #'Trans Fire Yellow',
390 | 293: "68abe4", #'Trans Light Blue Violet',
391 | 231: "fcb76d", #'Trans Bright Light Orange',
392 | 284: "c281a5", #'Trans Reddish Lilac',
393 | 334: "bba53d", #'Chrome Gold',
394 | 383: "e0e0e0", #'Chrome Silver',
395 | 60: "645a4c", #'Chrome Antique Brass',
396 | 64: "1b2a34", #'Chrome Black',
397 | 61: "6c96bf", #'Chrome Blue',
398 | 62: "3cb371", #'Chrome Green',
399 | 63: "aa4d8e", #'Chrome Pink',
400 | 183: "f2f3f2", #'Pearl White',
401 | 150: "bbbdbc", #'Pearl Very Light Grey',
402 | 135: "9ca3a8", #'Pearl Light Grey',
403 | 179: "898788", #'Flat Silver',
404 | 148: "575857", #'Pearl Dark Grey',
405 | 137: "5677ba", #'Metal Blue',
406 | 142: "dcbe61", #'Pearl Light Gold',
407 | 297: "cc9c2b", #'Pearl Gold',
408 | 178: "b4883e", #'Flat Dark Gold',
409 | 134: "964a27", #'Copper',
410 | 189: "ac8247", #'Reddish Gold',
411 | 80: "a5a9b4", #'Metallic Silver',
412 | 81: "899b5f", #'Metallic Green',
413 | 82: "dbac34", #'Metallic Gold',
414 | 83: "1a2831", #'Metallic Black',
415 | 87: "6d6e5c", #'Metallic Dark Grey',
416 | 300: "c27f53", #'Metallic Copper',
417 | 184: "d60026", #'Metallic Bright Red',
418 | 186: "008e3c", #'Metallic Dark Green'
419 | 368: "EBD800", # Neon Yellow
420 | 801: "0830FF", # Arrow Blue
421 | 802: "08B010", # Arrow Green
422 | 804: "FF0000", # Arrow Red
423 | }
424 |
425 | LDR_COLOUR_TITLE = {
426 | LDR_ALL_COLOUR: "All Colours",
427 | LDR_ANY_COLOUR: "Any Colour",
428 | LDR_OTHER_COLOUR: "Other Colours",
429 | LDR_MONO_COLOUR: "Monochrome",
430 | LDR_BLUES_COLOUR: "Blues",
431 | LDR_GREENS_COLOUR: "Greens",
432 | LDR_YELLOWS_COLOUR: "Yellows",
433 | LDR_PINKPURP_COLOUR: "Pinks/Purples",
434 | LDR_BLKWHT_COLOUR: "Black/White",
435 | LDR_GRAY_COLOUR: "Light/Dark Gray",
436 | LDR_REDYLW_COLOUR: "Red/Yellow",
437 | LDR_BLUYLW_COLOUR: "Blue/Yellow",
438 | LDR_REDBLUYLW_COLOUR: "Red/Blue/Yellow",
439 | LDR_GRNBRN_COLOUR: "Green/Brown",
440 | LDR_BLUBRN_COLOUR: "Blue/Brown",
441 | LDR_BRGREEN_COLOUR: "Bright Green/Ylwish Green",
442 | LDR_LAVENDER_COLOUR: "Light/Med Lavender",
443 | LDR_PINK_COLOUR: "Light/Med Pink",
444 | LDR_LTYLW_COLOUR: "Br Light Yellow/Light Nougat",
445 | LDR_BLUBLU_COLOUR: "Blue/Med Blue",
446 | LDR_ORGYLW_COLOUR: "Orange/Yellow",
447 | LDR_ORGBRN_COLOUR: "Orange/Brown",
448 | LDR_REDORG_COLOUR: "Red/Orange",
449 | LDR_REDORGYLW_COLOUR: "Red/Org/Ylw",
450 | LDR_BLUGRN_COLOUR: "Blue/Green",
451 | LDR_TAN_COLOUR: "Light/Dark Tan",
452 | }
453 |
454 | LDR_FILL_CODES = {
455 | LDR_ALL_COLOUR: LDR_ANY_COLOUR_FILL,
456 | LDR_ANY_COLOUR: LDR_ANY_COLOUR_FILL,
457 | LDR_OTHER_COLOUR: LDR_ANY_COLOUR_FILL,
458 | LDR_MONO_COLOUR: [
459 | LDR_COLOUR_RGB[15],
460 | LDR_COLOUR_RGB[151],
461 | LDR_COLOUR_RGB[71],
462 | LDR_COLOUR_RGB[72],
463 | ],
464 | LDR_BLUES_COLOUR: [
465 | LDR_COLOUR_RGB[212],
466 | LDR_COLOUR_RGB[73],
467 | LDR_COLOUR_RGB[1],
468 | LDR_COLOUR_RGB[272],
469 | ],
470 | LDR_GREENS_COLOUR: [
471 | LDR_COLOUR_RGB[378],
472 | LDR_COLOUR_RGB[10],
473 | LDR_COLOUR_RGB[2],
474 | LDR_COLOUR_RGB[288],
475 | ],
476 | LDR_YELLOWS_COLOUR: [
477 | LDR_COLOUR_RGB[19],
478 | LDR_COLOUR_RGB[226],
479 | LDR_COLOUR_RGB[14],
480 | LDR_COLOUR_RGB[191],
481 | ],
482 | LDR_PINKPURP_COLOUR: [
483 | LDR_COLOUR_RGB[29],
484 | LDR_COLOUR_RGB[13],
485 | LDR_COLOUR_RGB[30],
486 | LDR_COLOUR_RGB[26],
487 | ],
488 | LDR_BLKWHT_COLOUR: [LDR_COLOUR_RGB[0], LDR_COLOUR_RGB[15]],
489 | LDR_GRAY_COLOUR: [LDR_COLOUR_RGB[71], LDR_COLOUR_RGB[72]],
490 | LDR_REDYLW_COLOUR: [LDR_COLOUR_RGB[4], LDR_COLOUR_RGB[14]],
491 | LDR_BLUYLW_COLOUR: [LDR_COLOUR_RGB[1], LDR_COLOUR_RGB[14]],
492 | LDR_REDBLUYLW_COLOUR: [LDR_COLOUR_RGB[4], LDR_COLOUR_RGB[1], LDR_COLOUR_RGB[14]],
493 | LDR_GRNBRN_COLOUR: [LDR_COLOUR_RGB[2], LDR_COLOUR_RGB[70]],
494 | LDR_BLUBRN_COLOUR: [LDR_COLOUR_RGB[1], LDR_COLOUR_RGB[70]],
495 | LDR_BLUBLU_COLOUR: [LDR_COLOUR_RGB[1], LDR_COLOUR_RGB[73]],
496 | LDR_DKREDBLU_COLOUR: [LDR_COLOUR_RGB[272], LDR_COLOUR_RGB[320]],
497 | LDR_BRGREEN_COLOUR: [LDR_COLOUR_RGB[10], LDR_COLOUR_RGB[326]],
498 | LDR_LAVENDER_COLOUR: [LDR_COLOUR_RGB[30], LDR_COLOUR_RGB[31]],
499 | LDR_PINK_COLOUR: [LDR_COLOUR_RGB[13], LDR_COLOUR_RGB[29]],
500 | LDR_LTYLW_COLOUR: [LDR_COLOUR_RGB[226], LDR_COLOUR_RGB[78]],
501 | LDR_ORGYLW_COLOUR: [LDR_COLOUR_RGB[25], LDR_COLOUR_RGB[14]],
502 | LDR_ORGBRN_COLOUR: [LDR_COLOUR_RGB[25], LDR_COLOUR_RGB[6]],
503 | LDR_REDORG_COLOUR: [LDR_COLOUR_RGB[4], LDR_COLOUR_RGB[25]],
504 | LDR_TANBRN_COLOUR: [LDR_COLOUR_RGB[19], LDR_COLOUR_RGB[6]],
505 | LDR_REDORGYLW_COLOUR: [LDR_COLOUR_RGB[4], LDR_COLOUR_RGB[25], LDR_COLOUR_RGB[14]],
506 | LDR_BLUGRN_COLOUR: [LDR_COLOUR_RGB[73], LDR_COLOUR_RGB[10]],
507 | LDR_TAN_COLOUR: [LDR_COLOUR_RGB[19], LDR_COLOUR_RGB[28]],
508 | }
509 |
510 | LDR_FILL_TITLES = {
511 | LDR_ALL_COLOUR: ["All Colours"],
512 | LDR_ANY_COLOUR: ["Any Colour"],
513 | LDR_OTHER_COLOUR: ["Other Colours"],
514 | LDR_MONO_COLOUR: ["Monochrome"],
515 | LDR_BLUES_COLOUR: ["Blues"],
516 | LDR_GREENS_COLOUR: ["Greens"],
517 | LDR_YELLOWS_COLOUR: ["Yellows"],
518 | LDR_PINKPURP_COLOUR: ["Pinks/Purples"],
519 | LDR_BLKWHT_COLOUR: [LDR_COLOUR_NAME[0], LDR_COLOUR_NAME[15]],
520 | LDR_GRAY_COLOUR: [LDR_COLOUR_NAME[71], LDR_COLOUR_NAME[72]],
521 | LDR_REDYLW_COLOUR: [LDR_COLOUR_NAME[4], LDR_COLOUR_NAME[14]],
522 | LDR_BLUYLW_COLOUR: [LDR_COLOUR_NAME[1], LDR_COLOUR_NAME[14]],
523 | LDR_REDBLUYLW_COLOUR: [LDR_COLOUR_NAME[4], LDR_COLOUR_NAME[1], LDR_COLOUR_NAME[14]],
524 | LDR_GRNBRN_COLOUR: [LDR_COLOUR_NAME[2], LDR_COLOUR_NAME[70]],
525 | LDR_BLUBRN_COLOUR: [LDR_COLOUR_NAME[1], LDR_COLOUR_NAME[70]],
526 | LDR_BLUBLU_COLOUR: [LDR_COLOUR_NAME[1], LDR_COLOUR_NAME[73]],
527 | LDR_DKREDBLU_COLOUR: [LDR_COLOUR_NAME[272], LDR_COLOUR_NAME[320]],
528 | LDR_BRGREEN_COLOUR: [LDR_COLOUR_NAME[10], LDR_COLOUR_NAME[326]],
529 | LDR_LAVENDER_COLOUR: [LDR_COLOUR_NAME[30], LDR_COLOUR_NAME[31]],
530 | LDR_PINK_COLOUR: [LDR_COLOUR_NAME[13], LDR_COLOUR_NAME[29]],
531 | LDR_LTYLW_COLOUR: [LDR_COLOUR_NAME[226], LDR_COLOUR_NAME[78]],
532 | LDR_ORGYLW_COLOUR: [LDR_COLOUR_NAME[25], LDR_COLOUR_NAME[14]],
533 | LDR_ORGBRN_COLOUR: [LDR_COLOUR_NAME[25], LDR_COLOUR_NAME[6]],
534 | LDR_REDORG_COLOUR: [LDR_COLOUR_NAME[4], LDR_COLOUR_NAME[25]],
535 | LDR_TANBRN_COLOUR: [LDR_COLOUR_NAME[19], LDR_COLOUR_NAME[6]],
536 | LDR_REDORGYLW_COLOUR: [
537 | LDR_COLOUR_NAME[4],
538 | LDR_COLOUR_NAME[25],
539 | LDR_COLOUR_NAME[14],
540 | ],
541 | LDR_BLUGRN_COLOUR: [LDR_COLOUR_NAME[1], LDR_COLOUR_NAME[2]],
542 | LDR_TAN_COLOUR: [LDR_COLOUR_NAME[19], LDR_COLOUR_NAME[28]],
543 | }
544 |
--------------------------------------------------------------------------------
/ldrawpy/ldrhelpers.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #
3 | # Copyright (C) 2020 Michael Gale
4 | # This file is part of the legocad python module.
5 | # Permission is hereby granted, free of charge, to any person
6 | # obtaining a copy of this software and associated documentation
7 | # files (the "Software"), to deal in the Software without restriction,
8 | # including without limitation the rights to use, copy, modify, merge,
9 | # publish, distribute, sublicense, and/or sell copies of the Software,
10 | # and to permit persons to whom the Software is furnished to do so,
11 | # subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be
14 | # included in all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | #
24 | # LDraw related helper functions
25 |
26 | import decimal
27 | from toolbox import *
28 | from ldrawpy import *
29 |
30 |
31 | def quantize(x):
32 | """Quantizes an string LDraw value to 4 decimal places"""
33 | v = decimal.Decimal(x.strip()).quantize(decimal.Decimal(10) ** -4)
34 | return float(v)
35 |
36 |
37 | def MM2LDU(x):
38 | return x * 2.5
39 |
40 |
41 | def LDU2MM(x):
42 | return x * 0.4
43 |
44 |
45 | def val_units(value, units="ldu"):
46 | """
47 | Writes a floating point value in units of either mm or ldu.
48 | It restricts the number of decimal places to 4 and minimizes
49 | redundant trailing zeros (as recommended by ldraw.org)
50 | """
51 | x = value * 2.5 if units == "mm" else value
52 | xs = "%.5f" % (x)
53 | ns = str(quantize(xs)).replace("0E-4", "0.")
54 | if "E" not in ns:
55 | ns = ns.rstrip("0")
56 | ns = ns.rstrip(".")
57 | if ns == "-0":
58 | return "0 "
59 | return ns + " "
60 |
61 |
62 | def mat_str(m):
63 | """
64 | Writes the values of a matrix formatted by PUnits.
65 | """
66 | return "".join([val_units(v, "ldu") for v in m])
67 |
68 |
69 | def vector_str(p, attrib):
70 | return (
71 | val_units(p.x, attrib.units)
72 | + val_units(p.y, attrib.units)
73 | + val_units(p.z, attrib.units)
74 | )
75 |
76 |
77 | def get_circle_segments(radius, segments, attrib):
78 | from .ldrprimitives import LDRLine
79 |
80 | lines = []
81 | for seg in range(segments):
82 | p1 = Vector(0, 0, 0)
83 | p2 = Vector(0, 0, 0)
84 | a1 = seg / segments * 2.0 * pi
85 | a2 = (seg + 1) / segments * 2.0 * pi
86 | p1.x = radius * cos(a1)
87 | p1.z = radius * sin(a1)
88 | p2.x = radius * cos(a2)
89 | p2.z = radius * sin(a2)
90 | l = LDRLine(attrib.colour, attrib.units)
91 | l.p1 = p1
92 | l.p2 = p2
93 | lines.append(l)
94 | return lines
95 |
96 |
97 | def ldrlist_from_parts(parts):
98 | """Returns a list of LDRPart objects from either a list of LDRParts,
99 | a list of strings representing parts or a string with line feed
100 | delimited parts."""
101 | from .ldrprimitives import LDRPart
102 |
103 | p = []
104 | if isinstance(parts, str):
105 | # assume its a string of LDraw lines of text
106 | parts = parts.splitlines()
107 | if isinstance(parts, list):
108 | if len(parts) < 1:
109 | return p
110 | if isinstance(parts[0], LDRPart):
111 | p.extend(parts)
112 | else:
113 | p = [LDRPart().from_str(e) for e in parts]
114 | p = [e for e in p if e is not None]
115 | return p
116 |
117 |
118 | def ldrstring_from_list(parts):
119 | """Returns a LDraw formatted string from a list of parts. Each part
120 | is represented in a line feed terminated string concatenated together."""
121 | from .ldrprimitives import LDRPart
122 |
123 | s = []
124 | for p in parts:
125 | if isinstance(p, LDRPart):
126 | s.append(str(p))
127 | else:
128 | if not p[-1] == "\n":
129 | s.append(p + "\n")
130 | else:
131 | s.append(p)
132 | return "".join(s)
133 |
134 |
135 | def merge_same_parts(parts, other, ignore_colour=False, as_str=False):
136 | """Merges parts + other where the the parts in other take precedence."""
137 | from .ldrprimitives import LDRPart
138 |
139 | op = ldrlist_from_parts(other)
140 | p = ldrlist_from_parts(other)
141 | np = ldrlist_from_parts(parts)
142 | for n in np:
143 | if not any(
144 | [
145 | n.is_same(o, ignore_location=False, ignore_colour=ignore_colour)
146 | for o in op
147 | ]
148 | ):
149 | p.append(n)
150 | if as_str:
151 | return ldrstring_from_list(p)
152 | return p
153 |
154 |
155 | def remove_parts_from_list(
156 | parts, other, ignore_colour=True, ignore_location=True, exact=False, as_str=False
157 | ):
158 | """Returns a list based on removing the parts from other from parts."""
159 | pp = ldrlist_from_parts(parts)
160 | op = ldrlist_from_parts(other)
161 | np = []
162 | for p in pp:
163 | if ignore_colour and ignore_location:
164 | if not any([o.name == p.name for o in op]):
165 | np.append(p)
166 | elif not any(
167 | [
168 | p.is_same(
169 | o,
170 | ignore_location=ignore_colour,
171 | ignore_colour=ignore_colour,
172 | exact=exact,
173 | )
174 | for o in op
175 | ]
176 | ):
177 | np.append(p)
178 | if as_str:
179 | return ldrstring_from_list(np)
180 | return np
181 |
182 |
183 | def norm_angle(a):
184 | """Normalizes an angle in degrees to -180 ~ +180 deg."""
185 | a = a % 360
186 | if a >= 0 and a <= 180:
187 | return a
188 | if a > 180:
189 | return -180 + (-180 + a)
190 | if a >= -180 and a < 0:
191 | return a
192 | return 180 + (a + 180)
193 |
194 |
195 | def norm_aspect(a):
196 | """Normalizes the three angle components of aspect angle to -180 ~ +180 deg."""
197 | return tuple([norm_angle(v) for v in a])
198 |
199 |
200 | def _flip_x(a):
201 | return (-a[0], a[1], a[2])
202 |
203 |
204 | def _add_aspect(a, b):
205 | return norm_aspect(
206 | new_aspect=(
207 | a[0] + b[0],
208 | a[1] + b[1],
209 | a[2] + b[2],
210 | )
211 | )
212 |
213 |
214 | def preset_aspect(current_aspect, aspect_change):
215 | if isinstance(aspect_change, list):
216 | changes = aspect_change
217 | else:
218 | changes = [aspect_change]
219 | new_aspect = tuple(current_aspect)
220 | for aspect in changes:
221 | a = aspect.lower()
222 | if a in ASPECT_DICT:
223 | new_aspect = _flip_x(ASPECT_DICT[a])
224 | elif a in FLIP_DICT:
225 | r = FLIP_DICT[a]
226 | new_aspect = _add_aspect(new_aspect, r)
227 | elif a == "down":
228 | if new_aspect[0] < 0:
229 | new_aspect = (145, new_aspect[1], new_aspect[2])
230 | elif a == "up":
231 | if new_aspect[0] > 0:
232 | new_aspect = (-35, new_aspect[1], new_aspect[2])
233 | return norm_aspect(new_aspect)
234 |
235 | def clean_line(line):
236 | sl = line.split()
237 | nl = []
238 | for i, s in enumerate(sl):
239 | xs = s
240 | if i > 0 and "_" not in s:
241 | try:
242 | x = float(s)
243 | xs = val_units(float(x)).rstrip()
244 | except ValueError:
245 | pass
246 | nl.append(xs)
247 | nl.append(" ")
248 | nl = "".join(nl).rstrip()
249 | return nl
250 |
251 | def clean_file(fn, fno=None, verbose=False, as_str=False):
252 | """Cleans an LDraw file by changing all floating point numbers to
253 | an optimum representation within the suggested precision of up to
254 | 4 decimal places.
255 | """
256 | if fno is None:
257 | fno = fn.replace(".ldr", "_clean.ldr")
258 | ns = []
259 | bytes_in = 0
260 | bytes_out = 0
261 | with open(fn, "r") as f:
262 | lines = f.readlines()
263 | for line in lines:
264 | bytes_in += len(line)
265 | nl = clean_line(line)
266 | # sl = line.split()
267 | # nl = []
268 | # for i, s in enumerate(sl):
269 | # xs = s
270 | # if i > 0 and "_" not in s:
271 | # try:
272 | # x = float(s)
273 | # xs = val_units(float(x)).rstrip()
274 | # except ValueError:
275 | # pass
276 | # nl.append(xs)
277 | # nl.append(" ")
278 | # nl = "".join(nl).rstrip()
279 | bytes_out += len(nl)
280 | ns.append(nl)
281 | if verbose:
282 | print(
283 | "%s : %d bytes in / %d bytes out (%.1f%% saved)"
284 | % (fn, bytes_in, bytes_out, ((bytes_in - bytes_out) / bytes_in * 100.0))
285 | )
286 | if as_str:
287 | return ns
288 | ns = "\n".join(ns)
289 | with open(fno, "w") as f:
290 | f.write(ns)
291 |
--------------------------------------------------------------------------------
/ldrawpy/ldrmodel.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #
3 | # Copyright (C) 2020 Michael Gale
4 | # This file is part of the legocad python module.
5 | # Permission is hereby granted, free of charge, to any person
6 | # obtaining a copy of this software and associated documentation
7 | # files (the "Software"), to deal in the Software without restriction,
8 | # including without limitation the rights to use, copy, modify, merge,
9 | # publish, distribute, sublicense, and/or sell copies of the Software,
10 | # and to permit persons to whom the Software is furnished to do so,
11 | # subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be
14 | # included in all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | #
24 | # LDraw model classes and helper functions
25 |
26 | import hashlib
27 |
28 | import crayons
29 | from collections import defaultdict
30 |
31 | from toolbox import *
32 | from ldrawpy import *
33 |
34 | # import brickbom if available, otherwise don't raise since it is not
35 | # necessary for testing.
36 | try:
37 | from brickbom import BOM, BOMPart
38 | except:
39 | pass
40 | try:
41 | from rich import print
42 |
43 | has_rich = True
44 | except:
45 | has_rich = False
46 |
47 | START_TOKENS = ["PLI BEGIN IGN", "BUFEXCHG STORE"]
48 | END_TOKENS = ["PLI END", "BUFEXCHG RETRIEVE"]
49 | # START_TOKENS = ["PLI BEGIN IGN", "BUFEXCHG STORE", "SYNTH BEGIN"]
50 | # END_TOKENS = ["PLI END", "BUFEXCHG RETRIEVE", "SYNTH END"]
51 | EXCEPTION_LIST = ["2429c01.dat"]
52 | IGNORE_LIST = ["LS02"]
53 |
54 | COMMON_SUBSTITUTIONS = [
55 | ("3070a", "3070b"), # 1 x 1 tile
56 | ("3069a", "3069b"), # 1 x 2 tile
57 | ("3068a", "3068b"), # 2 x 2 tile
58 | ("x224", "41751"), # windscreen
59 | ("4864a", "87552"), # 1 x 2 x 2 panel with side supports
60 | ("4864b", "87552"), # 1 x 2 x 2 panel with side supports
61 | ("2362a", "87544"), # 1 x 2 x 3 panel with side supports
62 | ("2362b", "87544"), # 1 x 2 x 3 panel with side supports
63 | ("60583", "60583b"), # 1 x 1 x 3 brick with clips
64 | ("60583a", "60583b"),
65 | ("3245a", "3245c"), # 1 x 2 x 2 brick
66 | ("3245b", "3245c"),
67 | ("3794", "15573"), # 1 x 2 jumper plate
68 | ("3794a", "15573"),
69 | ("3794b", "15573"),
70 | ("4215a", "60581"), # 1 x 4 x 3 panel with side supports
71 | ("4215b", "60581"),
72 | ("4215", "60581"),
73 | # ["2429c01", "73983"], # 1 x 4 hinge plate complete
74 | ("73983", "2429c01"), # 1 x 4 hinge plate complete
75 | ("3665a", "3665"),
76 | ("3665b", "3665"), # 2 x 1 45 deg inv slope
77 | ("4081a", "4081b"), # 1x1 plate with light ring
78 | ("4085a", "60897"), # 1x1 plate with vert clip
79 | ("4085b", "60897"),
80 | ("4085c", "60897"),
81 | ("6019", "61252"), # 1x1 plate with horz clip
82 | ("59426", "32209"), # technic 5.5 axle
83 | ("48729", "48729b"), # bar with clip
84 | ("48729a", "48729b"),
85 | ("41005", "48729b"),
86 | ("4459", "2780"), # Technic friction pin
87 | ("44302", "44302a"), # 1x2 click hinge plate
88 | ("44302b", "44302a"), # 1x2 click hinge plate
89 | ("2436", "28802"), # 1x2 x 1x4 bracket
90 | ("2436a", "28802"),
91 | ("2436b", "28802"),
92 | ("2454", "2454b"), # 1x2x5 brick
93 | ("64567", "577b"), # minifig lightsabre holder
94 | ("30241b", "60475b"),
95 | ("2861", "2861c01"),
96 | ("2859", "2859c01"),
97 | ("70026a", "70026"),
98 | ("4707pb01", "4707c01"),
99 | ("4707pb02", "4707c02"),
100 | ("4707pb03", "4707c03"),
101 | ("4707pb04", "4707c04"),
102 | ("4707pb05", "4707c05"),
103 | ("3242", "3240a"),
104 | ("2776c28", "766bc03"),
105 | ("2776c28", "766bc03"),
106 | ("766c96", "766bc03"),
107 | ("7864-1", "u9058c02"),
108 | ("bb0012vb","501bc01"),
109 | ("bb0012v2","867"),
110 | ("70026b", "70026"),
111 | ("3242c", "3240a"),
112 | ("4623b", "4623"),
113 | ("4623a", "4623"),
114 | ]
115 |
116 |
117 | def substitute_part(part):
118 | for e in COMMON_SUBSTITUTIONS:
119 | if part.name == e[0]:
120 | part.name = e[1]
121 | return part
122 |
123 |
124 | def line_has_all_tokens(line, tokenlist):
125 | for t in tokenlist:
126 | tokens = t.split()
127 | tcount = 0
128 | tlen = len(tokens)
129 | for te in tokens:
130 | if te in line.split():
131 | tcount += 1
132 | if tcount == tlen:
133 | return True
134 | return False
135 |
136 |
137 | def parse_special_tokens(line):
138 | ls = line.split()
139 | metas = []
140 | for k, v in SPECIAL_TOKENS.items():
141 | for t in v:
142 | tokens = t.split()
143 | tcount = 0
144 | tlen = len([x for x in tokens if x[0] != "%"])
145 | for token in tokens:
146 | if token in ls:
147 | tcount += 1
148 | if tcount == tlen:
149 | linelen = len(ls)
150 | captures = [int(x[1:]) for x in tokens if x[0] == "%"]
151 | if len(captures) > 0:
152 | values = [ls[x] for x in captures if x < linelen]
153 | meta = {k: {"values": values, "text": line}}
154 | else:
155 | meta = {k: {"text": line}}
156 | metas.append(meta)
157 | return metas
158 |
159 |
160 | def get_meta_commands(ldr_string):
161 | """Parses an LDraw string looking for known meta commands. Identified meta
162 | commands are returned in a dictionary."""
163 | cmd = []
164 | lines = ldr_string.splitlines()
165 | for line in lines:
166 | lineType = int(line.lstrip()[0] if line.lstrip() else -1)
167 | if lineType == -1:
168 | continue
169 | if lineType == 0:
170 | meta = parse_special_tokens(line)
171 | if meta is not None:
172 | cmd.extend(meta)
173 | return cmd
174 |
175 |
176 | def get_parts_from_model(ldr_string):
177 | """Extracts a list of parts representing LDraw parts (line type 1) from a
178 | string of LDraw text. The returned list is contains dictionary for each part
179 | with the keys "partname" and "ldrtext"."""
180 | parts = []
181 | lines = ldr_string.splitlines()
182 | mask_depth = 0
183 | bufex = False
184 | for line in lines:
185 | pd = {}
186 | if line_has_all_tokens(line, ["BUFEXCHG STORE"]):
187 | bufex = True
188 | if line_has_all_tokens(line, ["BUFEXCHG RETRIEVE"]):
189 | bufex = False
190 | if line_has_all_tokens(line, START_TOKENS):
191 | mask_depth += 1
192 | if line_has_all_tokens(line, END_TOKENS):
193 | if mask_depth > 0:
194 | mask_depth -= 1
195 |
196 | lineType = int(line.lstrip()[0] if line.lstrip() else -1)
197 | if lineType == -1:
198 | continue
199 | if lineType == 1:
200 | splitLine = line.lower().split()
201 | pd["ldrtext"] = line
202 | pd["partname"] = " ".join([str(i) for i in splitLine[14:]])
203 | if mask_depth == 0:
204 | parts.append(pd)
205 | else:
206 | if pd["partname"] in EXCEPTION_LIST:
207 | parts.append(pd)
208 | elif not bufex and pd["partname"].endswith(".ldr"):
209 | parts.append(pd)
210 | return parts
211 |
212 |
213 | def recursive_parse_model(
214 | model,
215 | submodels,
216 | parts,
217 | offset=None,
218 | matrix=None,
219 | reset_parts=False,
220 | only_submodel=None,
221 | ):
222 | """Recursively parses an LDraw model dictionary plus any submodels and
223 | populates a parts list representing that model. To support selective
224 | parsing of only one submodel, only_submodel can be set to the desired
225 | submodel."""
226 | o = offset if offset is not None else Vector(0, 0, 0)
227 | m = matrix if matrix is not None else Identity()
228 | if reset_parts:
229 | parts.clear()
230 | for e in model:
231 | if only_submodel is not None:
232 | if not e["partname"] == only_submodel:
233 | continue
234 | if e["partname"] in submodels:
235 | submodel = submodels[e["partname"]]
236 | p = LDRPart()
237 | p.from_str(e["ldrtext"])
238 | new_matrix = m * p.attrib.matrix
239 | new_loc = m * p.attrib.loc
240 | new_loc += o
241 | recursive_parse_model(
242 | submodel,
243 | submodels,
244 | parts,
245 | offset=new_loc,
246 | matrix=new_matrix,
247 | )
248 | else:
249 | if only_submodel is None:
250 | part = LDRPart()
251 | part.from_str(e["ldrtext"])
252 | part = substitute_part(part)
253 | part.transform(matrix=m, offset=o)
254 | if (
255 | not part.name in IGNORE_LIST
256 | and not part.name.upper() in IGNORE_LIST
257 | ):
258 | parts.append(part)
259 |
260 |
261 | def unique_set(items):
262 | udict = {}
263 | if len(items) > 0:
264 | udict = defaultdict(int)
265 | for e in items:
266 | udict[e] += 1
267 | return udict
268 |
269 |
270 | def key_name(elem):
271 | return elem.name
272 |
273 |
274 | def key_colour(elem):
275 | return elem.attrib.colour
276 |
277 |
278 | def key_colour(elem):
279 | return elem.attrib.colour
280 |
281 |
282 | def get_sha1_hash(parts):
283 | """Gets a normalized sha1 hash LDRPart objects"""
284 | sp = [(p, p.sha1hash()) for p in parts]
285 | sp.sort(key=lambda x: x[1])
286 | shash = hashlib.sha1()
287 | for p in sp:
288 | shash.update(bytes(p[1], encoding="utf8"))
289 | return shash.hexdigest()
290 |
291 |
292 | def sort_parts(parts, key="name", order="ascending"):
293 | """Sorts a list of LDRPart objects by key"""
294 | if key.lower() == "sha1":
295 | sp = [(p, p.sha1hash()) for p in parts]
296 | sp.sort(key=lambda x: x[1])
297 | return [p[0] for p in sp]
298 | sp = [p for p in parts]
299 | if key.lower() == "name":
300 | sp.sort(key=key_name, reverse=True if order.lower() == "descending" else False)
301 | elif key.lower() == "colour":
302 | sp.sort(
303 | key=key_colour, reverse=True if order.lower() == "descending" else False
304 | )
305 | return sp
306 |
307 |
308 | class LDRModel:
309 | PARAMS = {
310 | "global_origin": (0, 0, 0),
311 | "global_aspect": (-40, 55, 0),
312 | "global_scale": 1.0,
313 | "pli_aspect": (-25, -40, 0),
314 | "pli_exceptions": {
315 | "32001": (-50, -25, 0),
316 | "3676": (-25, 50, 0),
317 | "3045": (-25, 50, 0),
318 | },
319 | "callout_step_thr": 6,
320 | "continuous_step_numbers": False,
321 | }
322 |
323 | def __init__(self, filename, **kwargs):
324 | from brickbom import BOM, BOMPart
325 |
326 | self.filename = filename
327 | apply_params(self, kwargs, locals())
328 | _, self.title = split_path(filename)
329 | self.bom = BOM()
330 | self.steps = {}
331 | self.pli = {}
332 | self.sub_models = {}
333 | self.sub_model_str = {}
334 | self.unwrapped = None
335 | self.callouts = {}
336 | self.continuous_step_count = 0
337 |
338 | def __str__(self):
339 | s = []
340 | s.append("LDRModel:")
341 | s.append(
342 | " Global origin: %s Global aspect: %s"
343 | % (self.global_origin, self.global_aspect)
344 | )
345 | s.append(" Number of steps: %d" % (len(self.steps)))
346 | s.append(" Number of sub-models: %d" % (len(self.sub_models)))
347 | return "\n".join(s)
348 |
349 | def __getitem__(self, key):
350 | return self.unwrapped[key]
351 |
352 | def print_step_dict(self, key):
353 | if key in self.steps:
354 | s = self.steps[key]
355 | for k, v in s.items():
356 | if k == "sub_parts":
357 | for ks, vs in v.items():
358 | print("%s: " % (ks))
359 | for e in vs:
360 | print(" %s" % (str(e).rstrip()))
361 | elif isinstance(v, list):
362 | print("%s: " % (k))
363 | for vx in v:
364 | print(" %s" % (str(vx).rstrip()))
365 | else:
366 | print("%s: %s" % (k, v))
367 |
368 | def print_unwrapped_dict(self, idx):
369 | s = self.unwrapped[idx]
370 | for k, v in s.items():
371 | if isinstance(v, list):
372 | print("%s: " % (k))
373 | for vx in v:
374 | print(" %s" % (str(vx).rstrip()))
375 | else:
376 | print("%s: %s" % (k, v))
377 |
378 | def print_unwrapped_verbose(self):
379 | for i, v in enumerate(self.unwrapped):
380 | print(
381 | "%3d. idx:%3d [pl:%d l:%d nl:%d] [s:%2d ns:%2d sc:%2d] %-16s q:%d sc:%.2f (%3.0f,%4.0f,%3.0f)"
382 | % (
383 | i,
384 | v["idx"],
385 | v["prev_level"],
386 | v["level"],
387 | v["next_level"],
388 | v["step"],
389 | v["next_step"],
390 | v["num_steps"],
391 | v["model"],
392 | v["qty"],
393 | v["scale"],
394 | v["aspect"][0],
395 | v["aspect"][1],
396 | v["aspect"][2],
397 | )
398 | )
399 |
400 | def print_unwrapped(self):
401 | for v in self.unwrapped:
402 | self.print_step(v)
403 |
404 | def print_step(self, v):
405 | pb = "break" if v["page_break"] else ""
406 | co = str(v["callout"])
407 | model_name = v["model"].replace(".ldr", "")
408 | model_name = model_name[:16]
409 | qty = "(%2dx)" % (v["qty"]) if v["qty"] > 0 else " "
410 | level = " " * v["level"] + "Level %d" % (v["level"])
411 | level = "%-11s" % (level)
412 | parts = "(%2dx pcs)" % (len(v["pli_bom"]))
413 | meta = [v.keys() for v in v["meta"]]
414 | meta = [list(x) for x in meta if "columns" not in x]
415 | for e in v["meta"]:
416 | if "columns" in e:
417 | meta.append("[green]COL%s[/]" % (e["columns"]["values"][0]))
418 | meta = ["".join(x) for x in meta]
419 | meta = " ".join(meta)
420 | meta = meta.replace("arrow_begin", ":arrow_down:")
421 | meta = meta.replace(" arrow_end", "")
422 | meta = meta.replace(" arrow_length", "")
423 | meta = meta.replace("rotation_rel", ":arrows_counterclockwise:R")
424 | meta = meta.replace("rotation_abs", ":arrows_counterclockwise:A")
425 | meta = meta.replace("rotation_pre", ":arrows_counterclockwise:P")
426 | meta = meta.replace("preview_aspect", ":arrows_counterclockwise:M")
427 | meta = meta.replace("model_scale", ":triangular_ruler:M")
428 | meta = meta.replace("scale", ":triangular_ruler:")
429 | meta = meta.replace("page_break", ":page_facing_up:")
430 | meta = meta.replace("no_callout", ":prohibited:CA")
431 | meta = meta.replace("no_preview", ":prohibited:PR")
432 | if has_rich:
433 | if not co == "0":
434 | fmt = "%3d. %s Step [yellow]%3d/%3d[/] Model: [red]%-16s[/]"
435 | else:
436 | if model_name == "root":
437 | fmt = "%3d. %s Step [green]%3d/%3d[/] Model: [green]%-16s[/]"
438 | else:
439 | fmt = "%3d. %s Step [green]%3d/%3d[/] Model: [red]%-16s[/]"
440 | else:
441 | fmt = "%3d. %s Step %3d/%3d Model: %-16s"
442 | fmt += " %s %s scale: %.2f (%3.0f,%4.0f,%3.0f)"
443 | if co == "0":
444 | fmt += " [bright_black]%1s[/]"
445 | else:
446 | fmt += " [yellow]%1s[/]"
447 | if pb == "break":
448 | fmt += " [magenta]BR[/] %s"
449 | else:
450 | fmt += " %s"
451 | print(
452 | fmt
453 | % (
454 | v["idx"],
455 | level,
456 | v["step"],
457 | v["num_steps"],
458 | model_name,
459 | qty,
460 | parts,
461 | v["scale"],
462 | v["aspect"][0],
463 | v["aspect"][1],
464 | v["aspect"][2],
465 | co,
466 | meta,
467 | )
468 | )
469 |
470 | def idx_range_from_steps(self, steps):
471 | """Returns a start and stop index into the unwrapped model from
472 | either a list or range."""
473 | if isinstance(steps, list):
474 | start_idx = self.idx_from_step(steps[0], as_start_idx=True)
475 | stop_idx = self.idx_from_step(steps[len(steps) - 1])
476 | elif isinstance(steps, range):
477 | start_idx = self.idx_from_step(steps.start, as_start_idx=True)
478 | stop_idx = self.idx_from_step(steps.stop)
479 | return start_idx, stop_idx
480 |
481 | def idx_from_step(self, step, as_start_idx=False):
482 | """Returns the index from the unwrapped model corresponding to a specified
483 | step in the root model."""
484 | if step == 1: # and as_start_idx:
485 | return 0
486 | if step < 0:
487 | return len(self.unwrapped) + step
488 | if step < 1:
489 | return 0
490 | if as_start_idx:
491 | if self.continuous_step_numbers:
492 | for s in self.unwrapped:
493 | if s["step"] == step - 1 and s["callout"] == 0:
494 | return s["idx"] + 1
495 | else:
496 | for s in self.unwrapped:
497 | if s["step"] == step - 1 and s["level"] == 0:
498 | return s["idx"] + 1
499 | else:
500 | if self.continuous_step_numbers:
501 | for s in self.unwrapped:
502 | if s["step"] == step and s["callout"] == 0:
503 | return s["idx"]
504 | else:
505 | for s in self.unwrapped:
506 | if s["step"] == step and s["level"] == 0:
507 | return s["idx"]
508 |
509 | return len(self.unwrapped) - 1
510 |
511 | def print_callouts(self):
512 | for k, v in self.callouts.items():
513 | print(
514 | "Callout: level %d: %d -> %d parent: %d"
515 | % (v["level"], k, v["end"], v["parent"])
516 | )
517 |
518 | def is_callout_start(self, idx):
519 | """Returns True if the index to the unwrapped model points to a
520 | the beginning of a callout sequence."""
521 | return idx in self.callouts
522 |
523 | def prev_step_was_callout(self, idx):
524 | """Returns True if the previous step was in a callout."""
525 | callout_before = self[idx - 1]["callout"] > 0 if idx > 0 else False
526 | return callout_before
527 |
528 | def prev_step_was_callout_end(self, idx):
529 | """Returns True if the previous step ended a callout."""
530 | did_end = self.is_callout_end(idx - 1) if idx > 0 else False
531 | return did_end
532 |
533 | def is_callout_end(self, idx):
534 | """Returns True if the index to the unwrapped model points to a
535 | step at the end of a callout sequence."""
536 | for k, v in self.callouts.items():
537 | if v["end"] == idx:
538 | return True
539 | return False
540 |
541 | def callout_has_meta(self, idx, tag):
542 | """Returns True if the index to the unwrapped model points to a
543 | a callout whose model has a specified meta tag."""
544 | for k, v in self.callouts.items():
545 | if idx >= k and idx <= v["end"]:
546 | for i in range(k, v["end"] + 1):
547 | if self.step_has_meta(i, tag):
548 | return True
549 | return False
550 |
551 | def callout_meta_values(self, idx, tag):
552 | """Returns the values of a callout whose model has a specified meta tag."""
553 | for k, v in self.callouts.items():
554 | if idx >= k and idx <= v["end"]:
555 | for i in range(k, v["end"] + 1):
556 | if self.step_has_meta(i, tag):
557 | return self.step_meta_values(idx, tag)
558 | return None
559 |
560 | def callout_parent(self, idx):
561 | """Returns the level into the model hierarchy of a callout's
562 | parent level at the specified index into the unwrapped model."""
563 | level = 0
564 | for k, v in self.callouts.items():
565 | if idx >= k and idx <= v["end"]:
566 | level = max(level, v["parent"])
567 | return level
568 |
569 | def is_parent_a_callout(self, idx):
570 | """Returns True if at the index into the unwrapped model
571 | a callout step is contained in another callout."""
572 | my_parent = self.callout_parent(idx)
573 | if my_parent > 0:
574 | for k, v in self.callouts.items():
575 | if v["level"] == my_parent and idx >= k and idx <= v["end"]:
576 | return True
577 | return False
578 |
579 | def has_assembly_arrows(self, idx):
580 | meta = self.unwrapped[idx]["meta"]
581 | for m in meta:
582 | if "arrow_begin" in m:
583 | return True
584 | return False
585 |
586 | def has_meta_tag(self, meta, tag):
587 | for m in meta:
588 | if tag in m:
589 | return True
590 | return False
591 |
592 | def step_meta_values(self, idx, tag):
593 | meta = self.unwrapped[idx]["meta"]
594 | for m in meta:
595 | if tag in m:
596 | return m[tag]["values"]
597 | return None
598 |
599 | def count_meta_keys(self, idx, tag):
600 | meta = self.unwrapped[idx]["meta"]
601 | count = 0
602 | for m in meta:
603 | if tag in m:
604 | count += 1
605 | return count
606 |
607 | def step_has_meta(self, idx, tag):
608 | meta = self.unwrapped[idx]["meta"]
609 | for m in meta:
610 | if tag in m:
611 | return True
612 | return False
613 |
614 | def has_no_preview_meta(self, idx):
615 | return self.step_has_meta(idx, "no_preview")
616 |
617 | def has_model_scale_meta(self, idx):
618 | return self.step_has_meta(idx, "model_scale")
619 |
620 | def has_fixed_scale_meta(self, idx):
621 | return self.step_has_meta(idx, "scale")
622 |
623 | def print_parts_at_idx(self, idx):
624 | parts = self.unwrapped[idx]["step_parts"]
625 | for p in parts:
626 | print(str(p).rstrip())
627 |
628 | def print_model_at_idx(self, idx):
629 | parts = self.unwrapped[idx]["parts"]
630 | for p in parts:
631 | print(str(p).rstrip())
632 |
633 | def print_pli_at_idx(self, idx):
634 | parts = self.unwrapped[idx]["pli_bom"]
635 | print(parts)
636 |
637 | def print_bom_at_idx(self, idx):
638 | print(self.unwrapped[idx]["pli_bom"])
639 |
640 | def get_sub_model_assem(self, submodel):
641 | """Returns the index into the unwrapped model of a specified
642 | sub-model assembly."""
643 | last_idx = 0
644 | submodel = submodel if ".ldr" in submodel else submodel + ".ldr"
645 | for s in self.unwrapped:
646 | if s["model"] == submodel:
647 | last_idx = max(last_idx, s["idx"])
648 | return last_idx
649 |
650 | def model_has_meta(self, submodel, tag):
651 | submodel = submodel if ".ldr" in submodel else submodel + ".ldr"
652 | for s in self.unwrapped:
653 | if s["model"] == submodel:
654 | if self.step_has_meta(s["idx"], tag):
655 | return True
656 | return False
657 |
658 | def model_meta_values(self, submodel, tag):
659 | submodel = submodel if ".ldr" in submodel else submodel + ".ldr"
660 | for s in self.unwrapped:
661 | if s["model"] == submodel:
662 | if self.step_has_meta(s["idx"], tag):
663 | return self.step_meta_values(s["idx"], tag)
664 | return None
665 |
666 | def unwrap(self):
667 | self.unwrapped = self.unwrap_model()
668 |
669 | def unwrap_model(
670 | self,
671 | root=None,
672 | idx=None,
673 | level=None,
674 | model_name=None,
675 | model_qty=None,
676 | unwrapped=None,
677 | ):
678 | """This recursive function unwraps the entire sequence of building steps
679 | for a LDraw model. This sequence unwraps nested building steps implied in
680 | the hierarchy of a model consisting of sub-models and the sub-model's
681 | children. Unwrapping the model also allows pre-computation of transitions
682 | to nested sub-steps which can either be represented as callouts or inline
683 | build instructions. The unwrapped model is represented as a list of
684 | dictionaries with a rich representation of the model at each step.
685 | The unwrapped model can then be traversed easily in a linear fashion
686 | by an iterator."""
687 |
688 | if root is None:
689 | idx = 0
690 | level = 0
691 | unwrapped = []
692 | model = self.steps
693 | model_name = "root"
694 | model_qty = 0
695 | self.continuous_step_count = 0
696 | else:
697 | idx = idx
698 | level = level
699 | model = root
700 | unwrapped = unwrapped
701 | model_name = model_name
702 | model_qty = model_qty
703 | for k, v in model.items():
704 | if len(v["sub_models"]) > 0:
705 | subs = unique_set(v["sub_models"])
706 | for name, qty in subs.items():
707 | pli, steps = self.parse_model(name, is_top_level=False)
708 | _, newidx = self.unwrap_model(
709 | root=steps,
710 | idx=idx,
711 | level=level + 1,
712 | model_name=name,
713 | model_qty=qty,
714 | unwrapped=unwrapped,
715 | )
716 | idx = newidx
717 | sd = {
718 | "idx": idx,
719 | "level": level,
720 | "step": k,
721 | "next_step": k + 1 if k < len(model.items()) else k,
722 | "num_steps": len(model.items()),
723 | "model": model_name,
724 | "qty": model_qty,
725 | "scale": v["scale"],
726 | "model_scale": v["model_scale"],
727 | "aspect": v["aspect"],
728 | "parts": v["parts"],
729 | "step_parts": v["step_parts"],
730 | "pli_bom": v["pli_bom"],
731 | "meta": v["meta"],
732 | "aspect_change": v["aspect_change"],
733 | "raw_ldraw": v["raw_ldraw"],
734 | "sub_parts": v["sub_parts"],
735 | }
736 | unwrapped.append(sd)
737 | idx += 1
738 | if level == 0:
739 | # when finished unwrapping the model, loop through the model
740 | # to add new keys for next and prev level changes and automatic
741 | # callout detection and page breaks across changes in level which
742 | # do not result in a callout. Also apply step numbering scheme.
743 | umodel = []
744 | step_num = 1
745 | prev_level = 0
746 | next_level = 0
747 | prev_break = False
748 | callout = []
749 | callout_start = []
750 | callout_end = []
751 | callout_parent = []
752 | dont_callout_models = []
753 | for i, e in enumerate(unwrapped):
754 | level = e["level"]
755 | next_level = (
756 | unwrapped[i + 1]["level"] if i < len(unwrapped) - 1 else level
757 | )
758 | level_up = True if next_level > level else False
759 | level_down = True if next_level < level else False
760 | levelled_up = True if level > prev_level else False
761 | levelled_down = True if level < prev_level else False
762 | next_steps = (
763 | unwrapped[i + 1]["num_steps"]
764 | if i < len(unwrapped) - 1
765 | else e["num_steps"]
766 | )
767 | prev_steps = e["num_steps"] if i == 0 else unwrapped[i - 1]["num_steps"]
768 | dont_callout = (
769 | self.has_meta_tag(unwrapped[i]["meta"], "no_callout")
770 | if i < len(unwrapped)
771 | else False
772 | )
773 | dont_callout_next = (
774 | self.has_meta_tag(unwrapped[i + 1]["meta"], "no_callout")
775 | if i < len(unwrapped) - 1
776 | else False
777 | )
778 | if dont_callout:
779 | dont_callout_models.append(e["model"])
780 | page_break = (
781 | True if level_up and next_steps >= self.callout_step_thr else False
782 | )
783 | page_break = (
784 | True
785 | if level_down and e["num_steps"] >= self.callout_step_thr
786 | else page_break
787 | )
788 | page_break = True if level_up and dont_callout_next else page_break
789 | page_break = (
790 | True
791 | if level_down and e["model"] in dont_callout_models
792 | else page_break
793 | )
794 | pb = False
795 | for x in unwrapped[i]["meta"]:
796 | if "page_break" in x:
797 | pb = True
798 | elif "pli_proxy" in x:
799 | for item in x["pli_proxy"]["values"]:
800 | if "_" in item:
801 | sp = item.split("_")
802 | pname = sp[0]
803 | pcolour = int(sp[1])
804 | else:
805 | pname = item
806 | pcolour = LDR_DEF_COLOUR
807 | proxy_part = BOMPart(1, pname, pcolour)
808 | e["pli_bom"].add_part(proxy_part)
809 | self.bom.add_part(proxy_part)
810 |
811 | page_break = True if pb else page_break
812 | no_pli = True if levelled_down and prev_break else False
813 | if (
814 | levelled_up
815 | and (e["num_steps"] < self.callout_step_thr)
816 | and not dont_callout
817 | ):
818 | callout.append(level)
819 | callout_start.append(i)
820 | callout_parent.append(prev_level)
821 |
822 | elif levelled_down:
823 | if len(callout) > 0:
824 | callout.pop()
825 | callout_end.append(i - 1)
826 | if len(callout) > 0:
827 | callout_level = callout[len(callout) - 1]
828 | else:
829 | callout_level = 0
830 | if self.continuous_step_numbers:
831 | if callout_level == 0:
832 | e["step"] = step_num
833 | step_num += 1
834 | self.continuous_step_count += 1
835 | x = {
836 | **e,
837 | "prev_level": prev_level,
838 | "next_level": next_level,
839 | "page_break": page_break,
840 | "no_pli": no_pli,
841 | "callout": callout_level,
842 | }
843 | umodel.append(x)
844 | prev_level = level
845 | prev_break = page_break
846 | if self.continuous_step_numbers:
847 | for e in umodel:
848 | if e["callout"] == 0:
849 | e["num_steps"] = self.continuous_step_count
850 | # pair up the callout boundaries in dictionary with the start
851 | # index as the key and the end index and parent level as values
852 | self.callouts = {}
853 | for x0, p in zip(callout_start, callout_parent):
854 | level = umodel[x0]["callout"]
855 | for x1 in callout_end:
856 | endlevel = umodel[x1]["callout"]
857 | if x1 >= x0 and level == endlevel:
858 | self.callouts[x0] = {"level": level, "end": x1, "parent": p}
859 | # set custom scale for the callout if configured
860 | if self.has_meta_tag(umodel[x0]["meta"], "model_scale"):
861 | self.callouts[x0]["scale"] = umodel[x0]["model_scale"]
862 | for ix in range(x0, x1 + 1):
863 | umodel[ix]["scale"] = umodel[ix]["model_scale"]
864 | break
865 |
866 | return umodel
867 | return unwrapped, idx
868 |
869 | def transform_parts_to(self, parts, origin=None, aspect=None, use_exceptions=False):
870 | """Transforms the location and/or aspect angle of all the parts in
871 | a list to a fixed position and/or aspect angle."""
872 | aspect = aspect if aspect is not None else self.global_aspect
873 | if origin is not None and not isinstance(origin, Vector):
874 | origin = Vector(origin[0], origin[1], origin[2])
875 | tparts = []
876 | for p in parts:
877 | np = p.copy()
878 | angle = aspect
879 | # override the aspect angle for any parts which need a special
880 | # orientation for clarity
881 | if use_exceptions:
882 | if p.name in self.pli_exceptions:
883 | angle = self.pli_exceptions[p.name]
884 | np.set_rotation(angle)
885 | if origin is not None:
886 | np.move_to(origin)
887 | tparts.append(np)
888 | return tparts
889 |
890 | def transform_parts(self, parts, offset=None, aspect=None):
891 | """Transforms the geometry (location and or aspect angle) of all
892 | the parts in a list. The transform is applied as an offset to
893 | the existing part geometry."""
894 | aspect = aspect if aspect is not None else self.global_aspect
895 | if offset is not None and not isinstance(offset, Vector):
896 | offset = Vector(offset[0], offset[1], offset[2])
897 | tparts = []
898 | if len(parts) < 1:
899 | return []
900 | if isinstance(parts[0], LDRPart):
901 | for p in parts:
902 | np = p.copy()
903 | np.rotate_by(aspect)
904 | if offset is not None:
905 | np.move_by(offset)
906 | tparts.append(np)
907 | return tparts
908 | else:
909 | for p in parts:
910 | np = LDRPart()
911 | if np.from_str(p) is not None:
912 | np.rotate_by(aspect)
913 | if offset is not None:
914 | np.move_by(offset)
915 | tparts.append(str(np))
916 | else:
917 | # print("part is None ", p)
918 | tparts.append(p)
919 | return tparts
920 |
921 | def transform_colour(self, parts, to_colour, from_colour=None, as_str=False):
922 | """Transforms the colour of a provided list of parts."""
923 | tparts = []
924 | if len(parts) < 1:
925 | return
926 | if isinstance(parts[0], LDRPart):
927 | for p in parts:
928 | np = p.copy()
929 | np.change_colour(to_colour)
930 | tparts.append(np)
931 | else:
932 | for p in parts:
933 | np = LDRPart()
934 | if np.from_str(p) is not None:
935 | np.change_colour(to_colour)
936 | tparts.append(np)
937 | if as_str:
938 | return [str(p) for p in tparts]
939 | return tparts
940 |
941 | def parse_file(self):
942 | """Parses an LDraw file and determines the root model and any included
943 | submodels."""
944 | self.sub_models = {}
945 | with open(self.filename, "rt") as fp:
946 | files = fp.read().split("0 FILE")
947 | root = None
948 | if len(files) == 1:
949 | root = files[0]
950 | else:
951 | root = "0 FILE " + files[1]
952 | for sub_file in files[2:]:
953 | sub_name = sub_file.splitlines()[0].lower().strip()
954 | sub_str = "0 FILE" + sub_file
955 | self.sub_model_str[sub_name] = sub_str
956 | self.sub_models[sub_name] = get_parts_from_model(sub_str)
957 | self.pli, self.steps = self.parse_model(root, is_top_level=True)
958 | self.unwrap()
959 |
960 | def ad_hoc_parse(self, ldrstring, only_submodel=None):
961 | """Performs an adhoc parsing operation on a provided LDraw formatted text
962 | string. If any references are made to submodels, then it recursively un packs
963 | the parts for the submodels based on a previous call to parse_model.
964 | Optionally, the parsing can be confined to only one submodel identified
965 | by only_submodel."""
966 | model_parts = []
967 | step_parts = get_parts_from_model(ldrstring)
968 | recursive_parse_model(
969 | step_parts,
970 | self.sub_models,
971 | model_parts,
972 | reset_parts=False,
973 | only_submodel=only_submodel,
974 | )
975 | return model_parts
976 |
977 | def parse_model(self, root, is_top_level=True, mask_submodels=False):
978 | """Generic parser for LDraw text. It parses a model provided either as a string
979 | of the entire LDR file at root level or as a key to a submodel in the LDR file.
980 | In either case, it recursively traverses the LDraw tree including all the
981 | children of the desired model and returns two lists: one for the parts
982 | at each step and one representing the model at each step.
983 |
984 | To parse at the root:
985 | self.pli, self.steps = self.parse_model(root, is_top_level=True)
986 | To parse a submodel:
987 | pli, steps = self.parse_model("submodel.ldr", is_top_level=False)
988 |
989 | The PLI list is a list of LDRPart objects for each step.
990 | The steps list is list of dictionaries with the following data for each step:
991 | parts - the aggregate parts that form the model at the step
992 | step_parts - only the parts that have been added at the step
993 | sub_models - a list of submodels referred to in this step
994 | scale - the current model scale
995 | aspect - the current euler viewing aspect angle
996 | pli_bom - a BOM object containing the parts in this step
997 | meta - a list of dictionaries representing any meta commands
998 | found in this step
999 | raw_ldraw - the raw LDraw text in the step
1000 | aspect_change - a flag indicating the aspect angle has changed
1001 | sub_parts - parts added to this step that come from sub-models
1002 | indexed by submodel name in a dictionary
1003 | """
1004 | is_masked = False
1005 | if not is_top_level:
1006 | if root in self.sub_model_str:
1007 | root = self.sub_model_str[root]
1008 | else:
1009 | key = root + ".ldr"
1010 | if key in self.sub_model_str:
1011 | root = self.sub_model_str[key]
1012 | is_masked = True if mask_submodels else False
1013 |
1014 | model_pli = {}
1015 | model_steps = {}
1016 | steps = root.split("0 STEP")
1017 | model_parts = []
1018 |
1019 | current_aspect = self.global_aspect
1020 | current_scale = self.global_scale
1021 | model_scale = self.global_scale
1022 | callout_style = "top"
1023 |
1024 | step_num = 1
1025 | progress_bar(0, len(steps), "Parsing:", length=50)
1026 | for i, step in enumerate(steps):
1027 | aspect_change = False
1028 | proxy_parts = []
1029 | step_parts = get_parts_from_model(step)
1030 | meta_cmd = get_meta_commands(step)
1031 | for cmd in meta_cmd:
1032 | if "scale" in cmd:
1033 | current_scale = float(cmd["scale"]["values"][0])
1034 | elif "model_scale" in cmd:
1035 | model_scale = float(cmd["model_scale"]["values"][0])
1036 | elif "callout" in cmd:
1037 | callout_style = cmd["callout"]["values"][0].lower()
1038 | elif "rotation_abs" in cmd:
1039 | current_aspect = [float(x) for x in cmd["rotation_abs"]["values"]]
1040 | current_aspect[0] = -current_aspect[0]
1041 | current_aspect = tuple(current_aspect)
1042 | aspect_change = True if step_num > 1 else False
1043 | elif "rotation_rel" in cmd:
1044 | aspect_change = True if step_num > 1 else False
1045 | ar = tuple([float(x) for x in cmd["rotation_rel"]["values"]])
1046 | current_aspect = (
1047 | (current_aspect[0] + ar[0]),
1048 | (current_aspect[1] + ar[1]),
1049 | (current_aspect[2] + ar[2]),
1050 | )
1051 | current_aspect = norm_aspect(current_aspect)
1052 | elif "rotation_pre" in cmd:
1053 | current_aspect = preset_aspect(
1054 | current_aspect, cmd["rotation_pre"]["values"]
1055 | )
1056 | aspect_change = True if step_num > 1 else False
1057 | elif "pli_proxy" in cmd:
1058 | for item in cmd["pli_proxy"]["values"]:
1059 | if "_" in item:
1060 | sp = item.split("_")
1061 | pname = sp[0]
1062 | pcolour = sp[1]
1063 | else:
1064 | pname = item
1065 | pcolour = LDR_DEF_COLOUR
1066 | proxy_part = LDRPart(colour=pcolour, name=pname)
1067 | proxy_parts.append(proxy_part)
1068 |
1069 | # capture submodel references in this step
1070 | subs = []
1071 | for p in step_parts:
1072 | if p["partname"] in self.sub_models:
1073 | subs.append(p["partname"])
1074 | # capture the parts that have been added in this step
1075 | # and store a transformed/normalized version for a PLI
1076 | parts_in_step = []
1077 | recursive_parse_model(
1078 | step_parts, self.sub_models, parts_in_step, reset_parts=True
1079 | )
1080 | pli = self.transform_parts_to(
1081 | parts_in_step,
1082 | origin=(0, 0, 0),
1083 | aspect=self.pli_aspect,
1084 | use_exceptions=True,
1085 | )
1086 | # check for proxy parts added to step for PLI
1087 | if len(proxy_parts) > 0:
1088 | proxy_parts = self.transform_parts_to(
1089 | proxy_parts,
1090 | origin=(0, 0, 0),
1091 | aspect=self.pli_aspect,
1092 | use_exceptions=True,
1093 | )
1094 | pli.extend(proxy_parts)
1095 | # submodel parts stored in separate dictionaries for convenient
1096 | # access if required
1097 | sub_dict = {}
1098 | for sub in subs:
1099 | sub_parts = []
1100 | recursive_parse_model(
1101 | step_parts,
1102 | self.sub_models,
1103 | sub_parts,
1104 | reset_parts=True,
1105 | only_submodel=sub,
1106 | )
1107 | pn = self.transform_parts(sub_parts, aspect=current_aspect)
1108 | sub_dict[sub] = pn
1109 |
1110 | step_dict = {}
1111 | if len(pli) > 0:
1112 | model_pli[step_num] = pli
1113 | # Store a BOM object representation of the parts for convenience
1114 | pli_bom = BOM()
1115 | pli_bom.ignore_parts = self.bom.ignore_parts
1116 | for p in pli:
1117 | pli_bom.add_part(BOMPart(1, p.name, p.attrib.colour))
1118 | if is_top_level:
1119 | self.bom.add_part(BOMPart(1, p.name, p.attrib.colour))
1120 | # store the model representation
1121 | recursive_parse_model(
1122 | step_parts, self.sub_models, model_parts, reset_parts=False
1123 | )
1124 | p = self.transform_parts(model_parts, aspect=current_aspect)
1125 | # store only the parts added in this step
1126 | pn = self.transform_parts(parts_in_step, aspect=current_aspect)
1127 | # put all the collection info into a dictionary
1128 | step_dict["parts"] = p
1129 | step_dict["sub_models"] = subs
1130 | step_dict["aspect"] = current_aspect
1131 | step_dict["scale"] = current_scale
1132 | step_dict["model_scale"] = model_scale
1133 | step_dict["raw_ldraw"] = step
1134 | step_dict["step_parts"] = pn
1135 | step_dict["pli_bom"] = pli_bom
1136 | step_dict["meta"] = meta_cmd
1137 | step_dict["aspect_change"] = aspect_change
1138 | step_dict["sub_parts"] = sub_dict
1139 | model_steps[step_num] = step_dict
1140 | step_num += 1
1141 |
1142 | progress_bar(i, len(steps), "Parsing:", length=50)
1143 | return model_pli, model_steps
1144 |
--------------------------------------------------------------------------------
/ldrawpy/ldrpprint.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #
3 | # Copyright (C) 2020 Michael Gale
4 | # This file is part of the legocad python module.
5 | # Permission is hereby granted, free of charge, to any person
6 | # obtaining a copy of this software and associated documentation
7 | # files (the "Software"), to deal in the Software without restriction,
8 | # including without limitation the rights to use, copy, modify, merge,
9 | # publish, distribute, sublicense, and/or sell copies of the Software,
10 | # and to permit persons to whom the Software is furnished to do so,
11 | # subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be
14 | # included in all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | #
24 | # LDraw pretty printer helper functions
25 |
26 | import string
27 |
28 | from rich import print
29 | from ldrawpy import *
30 |
31 |
32 | def is_hex_colour(text):
33 | text = text.strip('"')
34 | if not len(text) == 7:
35 | return False
36 | if not text[0] == "#":
37 | return False
38 | hs = text.lstrip("#")
39 | if not all(c in string.hexdigits for c in hs):
40 | return False
41 | return True
42 |
43 |
44 | def pprint_ldr_colour(code):
45 | if code == "16" or code == "24":
46 | return "[bold navajo_white1]%s" % (code)
47 | if code == "0":
48 | return "[bold]0"
49 | # return "[bold yellow reverse]%s[not reverse]" % (code)
50 | colour = LDRColour(int(code))
51 | return "[#%s reverse]%s[not reverse]" % (colour.as_hex(), code)
52 |
53 |
54 | def pprint_coord_str(v, colour="[white]"):
55 | return "[not bold]%s%s %s %s" % (colour, v[0], v[1], v[2])
56 |
57 |
58 | def pprint_line1(line):
59 | s = []
60 | line = line.rstrip()
61 | ls = line.split()
62 | s.append("[bold white]%s" % (ls[0]))
63 | s.append("%s" % (pprint_ldr_colour(ls[1])))
64 | s.append(pprint_coord_str(ls[2:5]))
65 | s.append(pprint_coord_str(ls[5:8], colour="[#91E3FF]"))
66 | s.append(pprint_coord_str(ls[8:11], colour="[#FFF3AF]"))
67 | s.append(pprint_coord_str(ls[11:14], colour="[#91E3FF]"))
68 | if line.lower().endswith(".ldr"):
69 | s.append("[bold #B7E67A]%s" % (" ".join(ls[14:])))
70 | else:
71 | s.append("[bold #F27759]%s" % (" ".join(ls[14:])))
72 | return " ".join(s)
73 |
74 |
75 | def pprint_line2345(line):
76 | s = []
77 | line = line.rstrip()
78 | ls = line.split()
79 | s.append("[bold white]%s" % (ls[0]))
80 | s.append("%s" % (pprint_ldr_colour(ls[1])))
81 | s.append(pprint_coord_str(ls[2:5]))
82 | s.append(pprint_coord_str(ls[5:8], colour="[#91E3FF]"))
83 | if len(ls) > 8:
84 | s.append(pprint_coord_str(ls[8:11], colour="[#FFF3AF]"))
85 | if len(ls) > 11:
86 | s.append(pprint_coord_str(ls[8:11], colour="[#91E3FF]"))
87 | return " ".join(s)
88 |
89 |
90 | def pprint_line0(line):
91 | s = []
92 | line = line.rstrip()
93 | ls = line.split()
94 | if ls[1] in LDRAW_TOKENS or ls[1] in META_TOKENS or ls[1].startswith("!"):
95 | s.append("[bold white]%s[not bold]" % (ls[0]))
96 | if "FILE" in ls[1]:
97 | s.append("[bold #B7E67A]%s[not bold]" % (ls[1]))
98 | elif ls[1].startswith("!"):
99 | s.append("[bold #78D4FE]%s[not bold]" % (ls[1]))
100 | elif ls[1] in META_TOKENS:
101 | s.append("[bold #BA7AE4]%s[not bold]" % (ls[1]))
102 | else:
103 | s.append("[bold #7096FF]%s[not bold]" % (ls[1]))
104 | if len(ls) > 2:
105 | if line.lower().endswith(".ldr"):
106 | for e in ls[2:]:
107 | s.append("[bold #B7E67A]%s" % (e))
108 | elif line.lower().endswith(".dat"):
109 | for e in ls[2:]:
110 | s.append("[bold #F27759]%s" % (e))
111 | else:
112 | for e in ls[2:]:
113 | if e in META_TOKENS:
114 | s.append("[bold #BA7AE4]%s[not bold]" % (e))
115 | elif is_hex_colour(e):
116 | s.append("[%s reverse]%s[not reverse]" % (e.strip('"'), e))
117 | else:
118 | s.append("[white]%s" % (e))
119 | else:
120 | s.append("[bold black]%s" % (line.rstrip()))
121 | return " ".join(s)
122 |
123 |
124 | def pprint_line(line, lineno=None, nocolour=False):
125 | ls = line.split()
126 | s = []
127 | if lineno is not None:
128 | s.append("[#404040]%4d | " % (lineno))
129 | if len(ls) > 1 and not nocolour:
130 | if ls[0] == "1":
131 | s.append(pprint_line1(line))
132 | elif ls[0] == "0":
133 | s.append(pprint_line0(line))
134 | elif any([ls[0] in "2345"]):
135 | s.append(pprint_line2345(line))
136 | else:
137 | s.append("[white]%s" % (line.rstrip()))
138 | else:
139 | s.append("[white][not bold]%s" % (line.rstrip()))
140 | print("".join(s))
141 |
--------------------------------------------------------------------------------
/ldrawpy/ldrprimitives.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #
3 | # Copyright (C) 2020 Michael Gale
4 | # This file is part of the legocad python module.
5 | # Permission is hereby granted, free of charge, to any person
6 | # obtaining a copy of this software and associated documentation
7 | # files (the "Software"), to deal in the Software without restriction,
8 | # including without limitation the rights to use, copy, modify, merge,
9 | # publish, distribute, sublicense, and/or sell copies of the Software,
10 | # and to permit persons to whom the Software is furnished to do so,
11 | # subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be
14 | # included in all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | #
24 | # LDraw primitives
25 |
26 | import hashlib
27 |
28 | from functools import reduce
29 |
30 | from toolbox import *
31 | from ldrawpy import *
32 | from .ldrhelpers import vector_str, mat_str, quantize
33 |
34 |
35 | class LDRAttrib:
36 | __slots__ = ["colour", "units", "loc", "matrix"]
37 |
38 | def __init__(self, colour=LDR_DEF_COLOUR, units="ldu"):
39 | self.colour = int(colour)
40 | self.units = units
41 | self.loc = Vector(0, 0, 0)
42 | self.matrix = Identity()
43 |
44 | def __eq__(self, other):
45 | if not isinstance(other, self.__class__):
46 | return False
47 | if not self.colour == other.colour:
48 | return False
49 | if not self.loc.almost_same_as(other.loc):
50 | return False
51 | if not self.matrix.is_almost_same_as(other.matrix):
52 | return False
53 | return True
54 |
55 | def copy(self):
56 | a = LDRAttrib()
57 | a.colour = self.colour
58 | a.units = self.units
59 | a.loc = self.loc.copy()
60 | a.matrix = self.matrix.copy()
61 | return a
62 |
63 |
64 | class LDRLine:
65 | __slots__ = ["attrib", "p1", "p2"]
66 |
67 | def __init__(self, colour=LDR_DEF_COLOUR, units="ldu"):
68 | self.attrib = LDRAttrib(colour, units)
69 | self.p1 = Vector(0, 0, 0)
70 | self.p2 = Vector(0, 0, 0)
71 |
72 | def __str__(self):
73 | return (
74 | ("2 %d " % self.attrib.colour)
75 | + vector_str(self.p1, self.attrib)
76 | + vector_str(self.p2, self.attrib)
77 | + "\n"
78 | )
79 |
80 | def translate(self, offset):
81 | self.p1 += offset
82 | self.p2 += offset
83 |
84 | def transform(self, matrix):
85 | self.p1 = self.p1 * matrix
86 | self.p2 = self.p2 * matrix
87 |
88 |
89 | class LDRTriangle:
90 | __slots__ = ["attrib", "p1", "p2", "p3"]
91 |
92 | def __init__(self, colour=LDR_DEF_COLOUR, units="ldu"):
93 | self.attrib = LDRAttrib(colour, units)
94 | self.p1 = Vector(0, 0, 0)
95 | self.p2 = Vector(0, 0, 0)
96 | self.p3 = Vector(0, 0, 0)
97 |
98 | def __str__(self):
99 | return (
100 | ("3 %d " % self.attrib.colour)
101 | + vector_str(self.p1, self.attrib)
102 | + vector_str(self.p2, self.attrib)
103 | + vector_str(self.p3, self.attrib)
104 | + "\n"
105 | )
106 |
107 | def translate(self, offset):
108 | self.p1 += offset
109 | self.p2 += offset
110 | self.p3 += offset
111 |
112 | def transform(self, matrix):
113 | self.p1 *= matrix
114 | self.p2 *= matrix
115 | self.p3 *= matrix
116 |
117 |
118 | class LDRQuad:
119 | __slots__ = ["attrib", "p1", "p2", "p3", "p4"]
120 |
121 | def __init__(self, colour=LDR_DEF_COLOUR, units="ldu"):
122 | self.attrib = LDRAttrib(colour, units)
123 | self.p1 = Vector(0, 0, 0)
124 | self.p2 = Vector(0, 0, 0)
125 | self.p3 = Vector(0, 0, 0)
126 | self.p4 = Vector(0, 0, 0)
127 |
128 | def __str__(self):
129 | return (
130 | ("4 %d " % self.attrib.colour)
131 | + vector_str(self.p1, self.attrib)
132 | + vector_str(self.p2, self.attrib)
133 | + vector_str(self.p3, self.attrib)
134 | + vector_str(self.p4, self.attrib)
135 | + "\n"
136 | )
137 |
138 | def translate(self, offset):
139 | self.p1 += offset
140 | self.p2 += offset
141 | self.p3 += offset
142 | self.p4 += offset
143 |
144 | def transform(self, matrix):
145 | self.p1 *= matrix
146 | self.p2 *= matrix
147 | self.p3 *= matrix
148 | self.p4 *= matrix
149 |
150 |
151 | class LDRPart:
152 | __slots__ = ["attrib", "name", "wrapcallout"]
153 |
154 | def __init__(self, colour=LDR_DEF_COLOUR, name=None, units="ldu"):
155 | self.attrib = LDRAttrib(colour, units)
156 | self.name = name if name is not None else ""
157 | self.wrapcallout = False
158 |
159 | def __str__(self):
160 | tup = tuple(reduce(lambda row1, row2: row1 + row2, self.attrib.matrix.rows))
161 | name = self.name
162 | ext = name[-4:].lower()
163 | name = self.name
164 | if len(name) > 4:
165 | if not (ext == ".ldr" or ext == ".dat"):
166 | name += ".dat"
167 | else:
168 | name += ".dat"
169 | s = (
170 | ("1 %i " % self.attrib.colour)
171 | + vector_str(self.attrib.loc, self.attrib)
172 | + mat_str(tup)
173 | + ("%s\n" % name)
174 | )
175 | if self.wrapcallout and ext == ".ldr":
176 | return "0 !LPUB CALLOUT BEGIN\n" + s + "0 !LPUB CALLOUT END\n"
177 | return s
178 |
179 | def __eq__(self, other):
180 | if not isinstance(other, self.__class__):
181 | return False
182 | if not self.name == other.name:
183 | return False
184 | if not self.attrib.colour == other.attrib.colour:
185 | return False
186 | return True
187 |
188 | def copy(self):
189 | p = LDRPart()
190 | p.name = self.name
191 | p.wrapcallout = self.wrapcallout
192 | p.attrib = self.attrib.copy()
193 | return p
194 |
195 | def sha1hash(self):
196 | shash = hashlib.sha1()
197 | shash.update(bytes(str(self), encoding="utf8"))
198 | return shash.hexdigest()
199 |
200 | def is_identical(self, other):
201 | if not self.name == other.name:
202 | return False
203 | if not self.attrib == other.attrib:
204 | return False
205 | return True
206 |
207 | def is_same(self, other, ignore_location=False, ignore_colour=False, exact=False):
208 | if not self.name == other.name:
209 | return False
210 | if exact:
211 | if not self.sha1hash() == other.sha1hash():
212 | return False
213 | if not ignore_colour:
214 | if not self.attrib.colour == other.attrib.colour:
215 | return False
216 | if not ignore_location:
217 | if not self.attrib.loc.almost_same_as(other.attrib.loc):
218 | return False
219 | if not ignore_colour and not ignore_location:
220 | if not self.sha1hash() == other.sha1hash():
221 | return False
222 | return True
223 |
224 | def is_coaligned(self, other):
225 | v1 = self.attrib.loc * self.attrib.matrix
226 | v2 = other.attrib.loc * other.attrib.matrix
227 | naxis = v1.is_colinear_with(v2)
228 | if naxis == 2:
229 | return True
230 | return False
231 |
232 | def change_colour(self, to_colour):
233 | self.attrib.colour = to_colour
234 |
235 | def set_rotation(self, angle):
236 | rm = euler_to_rot_matrix(angle)
237 | self.attrib.matrix = rm
238 |
239 | def move_to(self, pos):
240 | o = safe_vector(pos)
241 | self.attrib.loc = o
242 |
243 | def move_by(self, offset):
244 | o = safe_vector(offset)
245 | self.attrib.loc += o
246 |
247 | def rotate_by(self, angle):
248 | rm = euler_to_rot_matrix(angle)
249 | rt = rm.transpose()
250 | self.attrib.matrix = rm * self.attrib.matrix
251 | self.attrib.loc *= rt
252 |
253 | def transform(self, matrix=Identity(), offset=Vector(0, 0, 0)):
254 | mt = matrix.transpose()
255 | self.attrib.matrix = matrix * self.attrib.matrix
256 | self.attrib.loc *= mt
257 | self.attrib.loc += offset
258 |
259 | def from_str(self, s):
260 | split_line = s.lower().split()
261 | if not len(split_line) >= 15:
262 | return None
263 | line_type = int(split_line[0].lstrip())
264 | if not line_type == 1:
265 | return None
266 | self.attrib.colour = int(split_line[1])
267 | self.attrib.loc.x = quantize(split_line[2])
268 | self.attrib.loc.y = quantize(split_line[3])
269 | self.attrib.loc.z = quantize(split_line[4])
270 | self.attrib.matrix = Matrix(
271 | [
272 | [
273 | quantize(split_line[5]),
274 | quantize(split_line[6]),
275 | quantize(split_line[7]),
276 | ],
277 | [
278 | quantize(split_line[8]),
279 | quantize(split_line[9]),
280 | quantize(split_line[10]),
281 | ],
282 | [
283 | quantize(split_line[11]),
284 | quantize(split_line[12]),
285 | quantize(split_line[13]),
286 | ],
287 | ]
288 | )
289 | pname = " ".join(split_line[14:])
290 | self.name = pname.replace(".dat", "")
291 | return self
292 |
293 | @staticmethod
294 | def translate_from_str(s, offset):
295 | offset = safe_vector(offset)
296 | p = LDRPart()
297 | p.from_str(s)
298 | p.attrib.loc += offset
299 | p.wrapcallout = False
300 | return str(p)
301 |
302 | @staticmethod
303 | def transform_from_str(s, matrix=Identity(), offset=Vector(0, 0, 0), colour=None):
304 | offset = safe_vector(offset)
305 | p = LDRPart()
306 | p.from_str(s)
307 | mt = matrix.transpose()
308 | p.attrib.matrix = matrix * p.attrib.matrix
309 | p.attrib.loc *= mt
310 | p.attrib.loc += offset
311 | p.wrapcallout = False
312 | if colour is not None:
313 | p.attrib.colour = colour
314 | return str(p)
315 |
316 |
317 | class LDRHeader:
318 | def __init__(self):
319 | self.title = ""
320 | self.file = ""
321 | self.name = ""
322 | self.author = ""
323 |
324 | def __str__(self):
325 | return (
326 | ("0 %s\n" % self.title)
327 | + ("0 FILE %s\n" % self.file)
328 | + ("0 Name: %s\n" % self.name)
329 | + ("0 Author: %s\n" % self.author)
330 | )
331 |
--------------------------------------------------------------------------------
/ldrawpy/ldrshapes.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #
3 | # Copyright (C) 2020 Michael Gale
4 | # This file is part of the legocad python module.
5 | # Permission is hereby granted, free of charge, to any person
6 | # obtaining a copy of this software and associated documentation
7 | # files (the "Software"), to deal in the Software without restriction,
8 | # including without limitation the rights to use, copy, modify, merge,
9 | # publish, distribute, sublicense, and/or sell copies of the Software,
10 | # and to permit persons to whom the Software is furnished to do so,
11 | # subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be
14 | # included in all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | #
24 | # LDraw helper functions to generate complicated shapes and solids
25 | # from LDraw primitives
26 |
27 | import os
28 | import copy
29 |
30 | from toolbox import *
31 | from ldrawpy import *
32 |
33 |
34 | class LDRPolyWall:
35 | def __init__(self, colour=LDR_DEF_COLOUR, units="ldu"):
36 | self.attrib = LDRAttrib(colour, units)
37 | self.height = 1
38 |
39 | def __str__(self):
40 |
41 | s = []
42 | nPoints = len(self.points)
43 | for i in range(nPoints):
44 | q = LDRQuad(self.attrib.colour, self.attrib.units)
45 | q.p1.x = self.points[i].x
46 | q.p1.y = self.height
47 | q.p1.z = self.points[i].z
48 | thePoint = self.points[0]
49 | if i < nPoints - 1:
50 | thePoint = self.points[i + 1]
51 | q.p2.x = thePoint.x
52 | q.p2.y = self.height
53 | q.p2.z = thePoint.z
54 | q.p3.x = thePoint.x
55 | q.p3.y = 0
56 | q.p3.z = thePoint.z
57 | q.p4.x = self.points[i].x
58 | q.p4.y = 0
59 | q.p4.z = self.points[i].z
60 | q.transform(self.attrib.matrix)
61 | q.translate(self.attrib.loc)
62 | s.append(str(q))
63 | return "".join(s)
64 |
65 |
66 | class LDRRect:
67 | def __init__(self, colour=LDR_DEF_COLOUR, units="ldu"):
68 | self.attrib = LDRAttrib(colour, units)
69 | self.length = 1
70 | self.width = 1
71 |
72 | def __str__(self):
73 | s = []
74 | q = LDRQuad(self.attrib.colour, self.attrib.units)
75 | q.p1.x
76 | q.p1.x = -self.length / 2
77 | q.p1.y = 0
78 | q.p1.z = self.width / 2
79 | q.p2.x = -self.length / 2
80 | q.p2.y = 0
81 | q.p2.z = -self.width / 2
82 | q.p3.x = self.length / 2
83 | q.p3.y = 0
84 | q.p3.z = -self.width / 2
85 | q.p4.x = self.length / 2
86 | q.p4.y = 0
87 | q.p4.z = self.width / 2
88 | q.transform(self.attrib.matrix)
89 | q.translate(self.attrib.loc)
90 | l = LDRLine(self.attrib.colour, self.attrib.units)
91 | l.p1 = q.p1
92 | l.p2 = q.p2
93 | s.append(str(l))
94 | l.p1 = q.p2
95 | l.p2 = q.p3
96 | s.append(str(l))
97 | l.p1 = q.p3
98 | l.p2 = q.p4
99 | s.append(str(l))
100 | l.p1 = q.p4
101 | l.p2 = q.p1
102 | s.append(str(l))
103 | s.append(str(q))
104 | return "".join(s)
105 |
106 |
107 | class LDRCircle:
108 | def __init__(self, colour=LDR_DEF_COLOUR, units="ldu"):
109 | self.attrib = LDRAttrib(colour, units)
110 | self.radius = 1
111 | self.segments = 24
112 | self.fill = False
113 |
114 | def __str__(self):
115 | s = []
116 | lines = GetCircleSegments(self.radius, self.segments, self.attrib)
117 | for line in lines:
118 | l = LDRLine(self.attrib.colour, self.attrib.units)
119 | l.transform(self.attrib.matrix)
120 | l.translate(self.attrib.loc)
121 | s.append(str(l))
122 | if self.fill == True:
123 | for line in lines:
124 | t = LDRTriangle(self.attrib.colour, self.attrib.units)
125 | t.p2 = line.p1
126 | t.p3 = line.p2
127 | t.transform(self.attrib.matrix)
128 | t.translate(self.attrib.loc)
129 | s.append(str(t))
130 | return "".join(s)
131 |
132 |
133 | class LDRDisc:
134 | def __init__(self, colour=LDR_DEF_COLOUR, units="ldu"):
135 | self.attrib = LDRAttrib(colour, units)
136 | self.radius = 1
137 | self.border = 2
138 | self.segments = 24
139 |
140 | def __str__(self):
141 | s = []
142 | lines = GetCircleSegments(self.radius, self.segments, self.attrib)
143 | for line in lines:
144 | l = LDRLine(self.attrib.colour, self.attrib.units)
145 | l = copy.deepcopy(line)
146 | l.transform(self.attrib.matrix)
147 | l.translate(self.attrib.loc)
148 | s.append(str(l))
149 |
150 | olines = GetCircleSegments(
151 | self.radius + self.border, self.segments, self.attrib
152 | )
153 |
154 | for i, line in enumerate(lines):
155 | q = LDRQuad(self.attrib.colour, self.attrib.units)
156 | q.p1.x = line.p1.x
157 | q.p1.z = line.p1.z
158 | q.p2.x = line.p2.x
159 | q.p2.z = line.p2.z
160 | q.p3.x = olines[i].p2.x
161 | q.p3.z = olines[i].p2.z
162 | q.p4.x = olines[i].p1.x
163 | q.p4.z = olines[i].p1.z
164 | q.transform(self.attrib.matrix)
165 | q.translate(self.attrib.loc)
166 | s.append(str(q))
167 | return "".join(s)
168 |
169 |
170 | class LDRHole:
171 | def __init__(self, colour=LDR_DEF_COLOUR, units="ldu"):
172 | self.attrib = LDRAttrib(colour, units)
173 | self.radius = 1
174 | self.segments = 24
175 |
176 | def __str__(self):
177 | s = []
178 | for seg in range(self.segments):
179 | t = LDRTriangle(self.attrib.colour, self.attrib.units)
180 | a1 = seg / self.segments * 2.0 * pi
181 | a2 = (seg + 1) / self.segments * 2.0 * pi
182 | t.p1.x = self.radius * cos(a1)
183 | t.p1.z = self.radius * sin(a1)
184 | t.p2.x = self.radius * cos(a2)
185 | t.p2.z = self.radius * sin(a2)
186 | if (a1 >= 0.0) and (a1 < pi / 2.0):
187 | t.p3.x = self.radius
188 | t.p3.z = self.radius
189 | elif (a1 >= pi / 2.0) and (a1 < pi):
190 | t.p3.x = -self.radius
191 | t.p3.z = self.radius
192 | elif (a1 >= pi) and (a1 < 3 * pi / 2):
193 | t.p3.x = -self.radius
194 | t.p3.z = -self.radius
195 | else:
196 | t.p3.x = self.radius
197 | t.p3.z = -self.radius
198 | t.transform(self.attrib.matrix)
199 | t.translate(self.attrib.loc)
200 | l = LDRLine(OPT_COLOUR, self.attrib.units)
201 | l.p1 = t.p1
202 | l.p2 = t.p2
203 | s.append(str(t))
204 | s.append(str(l))
205 | return "".join(s)
206 |
207 |
208 | class LDRCylinder:
209 | def __init__(self, colour=LDR_DEF_COLOUR, units="ldu"):
210 | self.attrib = LDRAttrib(colour, units)
211 | self.radius = 1
212 | self.height = 1
213 | self.segments = 24
214 |
215 | def __str__(self):
216 | s = []
217 | lines = GetCircleSegments(self.radius, self.segments, self.attrib)
218 | for line in lines:
219 | l = LDRLine(self.attrib.colour, self.attrib.units)
220 | l = copy.deepcopy(line)
221 | l.transform(self.attrib.matrix)
222 | l.translate(self.attrib.loc)
223 | s.append(str(l))
224 | for line in lines:
225 | l = LDRLine(self.attrib.colour, self.attrib.units)
226 | l = copy.deepcopy(line)
227 | l.translate(Vector(0, self.height, 0))
228 | l.transform(self.attrib.matrix)
229 | l.translate(self.attrib.loc)
230 | s.append(str(l))
231 | for line in lines:
232 | q = LDRQuad(self.attrib.colour, self.attrib.units)
233 | q.p1.x = line.p1.x
234 | q.p1.z = line.p1.z
235 | q.p2.x = line.p2.x
236 | q.p2.z = line.p2.z
237 | q.p3.x = line.p2.x
238 | q.p3.z = line.p2.z
239 | q.p4.x = line.p1.x
240 | q.p4.z = line.p1.z
241 | q.p1.y = self.height
242 | q.p2.y = self.height
243 | q.p3.y = 0
244 | q.p4.y = 0
245 | q.transform(self.attrib.matrix)
246 | q.translate(self.attrib.loc)
247 | s.append(str(q))
248 | return "".join(s)
249 |
250 |
251 | class LDRBox:
252 | def __init__(self, colour=LDR_DEF_COLOUR, units="ldu"):
253 | self.attrib = LDRAttrib(colour, units)
254 | self.length = 1
255 | self.width = 1
256 | self.height = 1
257 |
258 | def __str__(self):
259 | s = []
260 | l = LDRLine(OPT_COLOUR, self.attrib.units)
261 | p = []
262 | p.append(Vector(self.length / 2, self.height / 2, self.width / 2))
263 | p.append(Vector(-self.length / 2, self.height / 2, self.width / 2))
264 | p.append(Vector(-self.length / 2, self.height / 2, -self.width / 2))
265 | p.append(Vector(self.length / 2, self.height / 2, -self.width / 2))
266 | p.append(Vector(self.length / 2, -self.height / 2, self.width / 2))
267 | p.append(Vector(-self.length / 2, -self.height / 2, self.width / 2))
268 | p.append(Vector(-self.length / 2, -self.height / 2, -self.width / 2))
269 | p.append(Vector(self.length / 2, -self.height / 2, -self.width / 2))
270 | coords = [
271 | [0, 1],
272 | [1, 2],
273 | [2, 3],
274 | [3, 0],
275 | [4, 5],
276 | [5, 6],
277 | [6, 7],
278 | [7, 4],
279 | [0, 4],
280 | [1, 5],
281 | [2, 6],
282 | [3, 7],
283 | ]
284 | for coord in coords:
285 | l.p1 = p[coord[0]]
286 | l.p2 = p[coord[1]]
287 | l.transform(self.attrib.matrix)
288 | l.translate(self.attrib.loc)
289 | s.append(str(l))
290 | q = LDRQuad(self.attrib.colour, self.attrib.units)
291 | coords = [
292 | [0, 3, 2, 1],
293 | [6, 7, 4, 5],
294 | [5, 4, 0, 1],
295 | [6, 5, 1, 2],
296 | [7, 6, 2, 3],
297 | [4, 7, 3, 0],
298 | ]
299 | for coord in coords:
300 | q.p1 = p[coord[0]]
301 | q.p2 = p[coord[1]]
302 | q.p3 = p[coord[2]]
303 | q.p4 = p[coord[3]]
304 | q.transform(self.attrib.matrix)
305 | q.translate(self.attrib.loc)
306 | s.append(str(q))
307 | return "".join(s)
308 |
--------------------------------------------------------------------------------
/ldrawpy/ldvrender.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #
3 | # Copyright (C) 2020 Michael Gale
4 | # This file is part of the legocad python module.
5 | # Permission is hereby granted, free of charge, to any person
6 | # obtaining a copy of this software and associated documentation
7 | # files (the "Software"), to deal in the Software without restriction,
8 | # including without limitation the rights to use, copy, modify, merge,
9 | # publish, distribute, sublicense, and/or sell copies of the Software,
10 | # and to permit persons to whom the Software is furnished to do so,
11 | # subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be
14 | # included in all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | #
24 | # LDView render class and helper functions
25 |
26 | import os, tempfile
27 | import datetime
28 | import subprocess, shlex
29 | import crayons
30 | from datetime import datetime
31 | from collections import defaultdict
32 | from PIL import Image, ImageOps, ImageChops, ImageFilter, ImageEnhance
33 |
34 | from toolbox import *
35 | from ldrawpy import *
36 |
37 | LDVIEW_BIN = "/Applications/LDView.app/Contents/MacOS/LDView"
38 | LDVIEW_DICT = {
39 | "DefaultMatrix": "1,0,0,0,1,0,0,0,1",
40 | "SnapshotSuffix": ".png",
41 | "BlackHighlights": 0,
42 | "ProcessLDConfig": 1,
43 | "EdgeThickness": 3,
44 | "EdgesOnly": 0,
45 | "ShowHighlightLines": 1,
46 | "ConditionalHighlights": 1,
47 | "SaveZoomToFit": 0,
48 | "SubduedLighting": 1,
49 | "UseSpecular": 1,
50 | "UseFlatShading": 0,
51 | "LightVector": "0,1,1",
52 | "AllowPrimitiveSubstitution": 0,
53 | "HiResPrimitives": 1,
54 | "UseQualityLighting": 0,
55 | "ShowAxes": 0,
56 | "UseQualityStuds": 1,
57 | "TextureStuds": 0,
58 | "SaveActualSize": 0,
59 | "SaveAlpha": 1,
60 | "AutoCrop": 0,
61 | "LineSmoothing": 1,
62 | "Texmaps": 1,
63 | "MemoryUsage": 2,
64 | "MultiThreaded": 1,
65 | }
66 | # 10.0 / tan(0.005 deg)
67 | LDU_DISTANCE = 114591
68 |
69 |
70 | def camera_distance(scale=1.0, dpi=300, page_width=8.5):
71 | one = 20 * 1 / 64 * dpi * scale
72 | sz = page_width * dpi / one * LDU_DISTANCE * 0.775
73 | sz *= 1700 / 1000
74 | return sz
75 |
76 |
77 | def _coord_str(x, y=None, sep=", "):
78 | if isinstance(x, (tuple, list)):
79 | a, b = float(x[0]), float(x[1])
80 | else:
81 | a, b = float(x), float(y)
82 | sa = ("%f" % (a)).rstrip("0").rstrip(".")
83 | sb = ("%f" % (b)).rstrip("0").rstrip(".")
84 | s = []
85 | s.append(str(crayons.yellow("%s" % (sa))))
86 | s.append(sep)
87 | s.append(str(crayons.yellow("%s" % (sb))))
88 | return "".join(s)
89 |
90 |
91 | class LDViewRender:
92 | """LDView render session helper class."""
93 |
94 | PARAMS = {
95 | "dpi": 300,
96 | "page_width": 8.5,
97 | "page_height": 11.0,
98 | "auto_crop": True,
99 | "image_smooth": False,
100 | "no_lines": False,
101 | "wireframe": False,
102 | "quality_lighting": True,
103 | "flat_shading": False,
104 | "specular": True,
105 | "line_thickness": 3,
106 | "texmaps": True,
107 | "scale": 1.0,
108 | "output_path": None,
109 | "log_output": True,
110 | "log_level": 0,
111 | "overwrite": False,
112 | }
113 |
114 | def __init__(self, **kwargs):
115 | self.ldr_temp_path = tempfile.gettempdir() + os.sep + "temp.ldr"
116 | apply_params(self, kwargs)
117 | self.set_page_size(self.page_width, self.page_height)
118 | self.set_scale(self.scale)
119 | self.settings_snapshot = None
120 |
121 | def __str__(self):
122 | s = []
123 | s.append("LDViewRender: ")
124 | s.append(" DPI: %d Scale: %.2f" % (self.dpi, self.scale))
125 | s.append(
126 | " Page size: %s in (%s pixels)"
127 | % (
128 | _coord_str(self.page_width, self.page_height, " x "),
129 | _coord_str(self.pix_width, self.pix_height, " x "),
130 | )
131 | )
132 | s.append(
133 | " Auto crop: %s Image smooth: %s" % (self.auto_crop, self.image_smooth)
134 | )
135 | s.append(" Camera distance: %d" % (self.cam_dist))
136 | return "\n".join(s)
137 |
138 | def snapshot_settings(self):
139 | self.settings_snapshot = {}
140 | for key in ["page_width", "page_height", "auto_crop", "image_smooth", "no_lines", "wireframe", "quality_lighting", "scale",
141 | "log_output", "log_level", "overwrite", "texmaps", "flat_shading", "specular"]:
142 | self.settings_snapshot[key] = self.__dict__[key]
143 |
144 | def restore_settings(self):
145 | if self.settings_snapshot is None:
146 | return
147 | for key in ["page_width", "page_height", "auto_crop", "image_smooth", "no_lines", "wireframe", "quality_lighting", "scale",
148 | "log_output", "log_level", "overwrite", "texmaps", "flat_shading", "specular"]:
149 | self.__dict__[key] = self.settings_snapshot[key]
150 |
151 | def set_page_size(self, width, height):
152 | self.page_width = width
153 | self.page_height = height
154 | self.pix_width = self.page_width * self.dpi
155 | self.pix_height = self.page_height * self.dpi
156 | self.args_size = "-SaveWidth=%d -SaveHeight=%d" % (
157 | self.pix_width,
158 | self.pix_height,
159 | )
160 |
161 | def set_dpi(self, dpi):
162 | self.dpi = dpi
163 | self.set_page_size(width=self.page_width, height=self.page_height)
164 | self.set_scale(scale=self.scale)
165 |
166 | def set_scale(self, scale):
167 | self.scale = scale
168 | self.cam_dist = int(camera_distance(self.scale, self.dpi, self.page_width))
169 | self.args_cam = "-ca0.01 -cg0.0,0.0,%d" % (self.cam_dist)
170 |
171 | def _logoutput(self, msg, tstart=None, level=2):
172 | logmsg(msg, level=level, prefix="LDR", log_level=self.log_level)
173 |
174 | def render_from_str(self, ldrstr, outfile):
175 | """Render from a LDraw text string."""
176 | if self.log_output:
177 | s = ldrstr.splitlines()[0]
178 | self._logoutput(
179 | "rendering string (%s)..." % (crayons.green(s[: min(len(s), 80)]))
180 | )
181 | with open(self.ldr_temp_path, "w") as f:
182 | f.write(ldrstr)
183 | self.render_from_file(self.ldr_temp_path, outfile)
184 |
185 | def render_from_parts(self, parts, outfile):
186 | """Render using a list of LDRPart objects."""
187 | if self.log_output:
188 | self._logoutput("rendering parts (%s)..." % (crayons.green(len(parts))))
189 | ldrstr = []
190 | for p in parts:
191 | ldrstr.append(str(p))
192 | ldrstr = "".join(ldrstr)
193 | self.render_from_str(ldrstr, outfile)
194 |
195 | def render_from_file(self, ldrfile, outfile):
196 | """Render from an LDraw file."""
197 | tstart = datetime.datetime.now()
198 | if self.output_path is not None:
199 | path, name = split_path(outfile)
200 | oppath = full_path(self.output_path)
201 | if not oppath in path:
202 | filename = os.path.normpath(self.output_path + os.sep + outfile)
203 | else:
204 | filename = outfile
205 | else:
206 | filename = full_path(outfile)
207 | _, fno = split_path(filename)
208 | if not self.overwrite and os.path.isfile(full_path(filename)):
209 | if self.log_output:
210 | _, fno = split_path(filename)
211 | fno = colour_path_str(fno)
212 | self._logoutput("rendered file %s already exists, skipping" % fno)
213 | return
214 | ldv = []
215 | ldv.append(LDVIEW_BIN)
216 | ldv.append("-SaveSnapShot=%s" % filename)
217 | ldv.append(self.args_size)
218 | ldv.append(self.args_cam)
219 | for key, value in LDVIEW_DICT.items():
220 | if key == "EdgeThickness":
221 | value = self.line_thickness
222 | elif key == "UseQualityLighting":
223 | value = 1 if self.quality_lighting else 0
224 | elif key == "Texmaps":
225 | value = 1 if self.texmaps else 0
226 | elif key == "UseFlatShading":
227 | value = 1 if self.flat_shading else 0
228 | elif key == "UseSpecular":
229 | value = 1 if self.specular else 0
230 | if self.no_lines:
231 | if key == "EdgeThickness":
232 | value = 0
233 | elif key == "ShowHighlightLines":
234 | value = 0
235 | elif key == "ConditionalHighlights":
236 | value = 0
237 | elif key == "UseQualityStuds":
238 | value = 0
239 | if self.wireframe:
240 | if key == "EdgesOnly":
241 | value = 1
242 | ldv.append("-%s=%s" % (key, value))
243 | ldv.append(ldrfile)
244 | s = " ".join(ldv)
245 | args = shlex.split(s)
246 | subprocess.Popen(args).wait()
247 | if self.log_output:
248 | _, fni = split_path(ldrfile)
249 | _, fno = split_path(filename)
250 | fni = colour_path_str(fni)
251 | fno = colour_path_str(fno)
252 | self._logoutput("rendered file %s to %s..." % (fni, fno), tstart, level=0)
253 |
254 | if self.auto_crop:
255 | self.crop(filename)
256 | if self.image_smooth:
257 | self.smooth(filename)
258 |
259 | def crop(self, filename):
260 | """Crop image file."""
261 | tstart = datetime.datetime.now()
262 | im = Image.open(filename)
263 | bg = Image.new(im.mode, im.size, im.getpixel((1, 1)))
264 | diff = ImageChops.difference(im, bg)
265 | diff = ImageChops.add(diff, diff, 2.0, 0)
266 | bbox = diff.getbbox()
267 | if bbox:
268 | im2 = im.crop(bbox)
269 | else:
270 | im2 = im
271 | im2.save(filename)
272 | if self.log_output:
273 | _, fn = split_path(filename)
274 | fn = colour_path_str(fn)
275 | self._logoutput(
276 | "> cropped %s from (%s) to (%s)"
277 | % (fn, _coord_str(im.size), _coord_str(im2.size)),
278 | tstart,
279 | )
280 |
281 | def smooth(self, filename):
282 | """Apply a smoothing filter to image file."""
283 | tstart = datetime.datetime.now()
284 | im = Image.open(filename)
285 | im = im.filter(ImageFilter.SMOOTH)
286 | im.save(filename)
287 | if self.log_output:
288 | _, fn = split_path(filename)
289 | fn = colour_path_str(fn)
290 | self._logoutput("> smoothed %s (%s)" % (fn, _coord_str(im.size)), tstart)
291 |
--------------------------------------------------------------------------------
/ldrawpy/scripts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelgale/ldraw-py/d650ab0a9c2a774e1ba6f206aa2cd3b1925478e3/ldrawpy/scripts/__init__.py
--------------------------------------------------------------------------------
/ldrawpy/scripts/ldrcat.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import math, os.path
4 | import sys
5 | import argparse
6 |
7 | from ldrawpy import *
8 |
9 |
10 | def main():
11 | parser = argparse.ArgumentParser(
12 | description="Display the contents of a LDraw file.",
13 | )
14 | parser.add_argument(
15 | "filename", metavar="filename", type=str, nargs="?", help="LDraw filename"
16 | )
17 | parser.add_argument(
18 | "-c",
19 | "--clean",
20 | action="store_true",
21 | default=False,
22 | help="Clean up coordinate values",
23 | )
24 | parser.add_argument(
25 | "-n",
26 | "--nocolour",
27 | action="store_true",
28 | default=False,
29 | help="Do not show file with colour syntax highlighting",
30 | )
31 | parser.add_argument(
32 | "-l",
33 | "--lineno",
34 | action="store_true",
35 | default=False,
36 | help="Show line numbers",
37 | )
38 | args = parser.parse_args()
39 | argsd = vars(args)
40 |
41 | if len(argsd) < 1 or "filename" not in argsd or argsd["filename"] is None:
42 | parser.print_help()
43 | exit()
44 | if argsd["clean"]:
45 | lines = clean_file(argsd["filename"], as_str=True)
46 | else:
47 | with open(argsd["filename"], "r") as f:
48 | lines = f.readlines()
49 | for i, line in enumerate(lines):
50 | lineno = (i + 1) if argsd["lineno"] else None
51 | pprint_line(line, lineno, nocolour=argsd["nocolour"])
52 |
53 |
54 | if __name__ == "__main__":
55 | main()
56 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Learn more: https://github.com/michaelgale/ldraw-py
5 |
6 | import os
7 | import sys
8 | import setuptools
9 |
10 | PACKAGE_NAME = "ldrawpy"
11 | MINIMUM_PYTHON_VERSION = "3.7"
12 |
13 |
14 | def check_python_version():
15 | """Exit when the Python version is too low."""
16 | if sys.version < MINIMUM_PYTHON_VERSION:
17 | sys.exit("Python {0}+ is required.".format(MINIMUM_PYTHON_VERSION))
18 |
19 |
20 | def read_package_variable(key, filename="__init__.py"):
21 | """Read the value of a variable from the package without importing."""
22 | module_path = os.path.join(PACKAGE_NAME, filename)
23 | with open(module_path) as module:
24 | for line in module:
25 | parts = line.strip().split(" ", 2)
26 | if parts[:-1] == [key, "="]:
27 | return parts[-1].strip("'")
28 | sys.exit("'{0}' not found in '{1}'".format(key, module_path))
29 |
30 |
31 | def build_description():
32 | """Build a description for the project from documentation files."""
33 | try:
34 | readme = open("README.rst").read()
35 | changelog = open("CHANGELOG.rst").read()
36 | except IOError:
37 | return ""
38 | else:
39 | return readme + "\n" + changelog
40 |
41 |
42 | check_python_version()
43 |
44 | setuptools.setup(
45 | name=read_package_variable("__project__"),
46 | version=read_package_variable("__version__"),
47 | description="A python utility package for creating, modifying, and reading LDraw files and data structures.",
48 | url="https://github.com/michaelgale/ldraw-py",
49 | author="Michael Gale",
50 | author_email="michael@fxbricks.com",
51 | packages=setuptools.find_packages(),
52 | long_description=build_description(),
53 | license="MIT",
54 | classifiers=[
55 | "Development Status :: 3 - Alpha",
56 | "Natural Language :: English",
57 | "Operating System :: OS Independent",
58 | "Programming Language :: Python :: 3.6",
59 | "Intended Audience :: Developers",
60 | "License :: OSI Approved :: MIT License",
61 | ],
62 | install_requires=[
63 | "pillow",
64 | "pytest",
65 | "rich",
66 | ],
67 | entry_points={
68 | "console_scripts": [
69 | "ldrcat=ldrawpy.scripts.ldrcat:main",
70 | ]
71 | }
72 | )
73 |
--------------------------------------------------------------------------------
/tests/rerun.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # rm ~/Dropbox/Lego/PartCache/Thumbnails/*
3 | cd ..
4 | python setup.py install
5 | cd tests
6 | pytest -s
7 |
--------------------------------------------------------------------------------
/tests/test_files/stylesheet.yml:
--------------------------------------------------------------------------------
1 | #
2 | # Colour definitions
3 | #
4 | L1: "#FFFFBE"
5 | L2: "#FEC6C7"
6 | L3: "#C8C6FF"
7 | BLACK: "#000000"
8 | WHITE: "#FFFFFF"
9 | PLI: "#D4F9FE"
10 | BLUE: "#204080"
11 |
12 | #
13 | # Font definitions
14 | #
15 | NUM_FONT: British-Rail-Light
16 | QTY_FONT: IKEA-Sans-Regular
17 |
18 | #
19 | # Style dictionary containing all the style sheet styles
20 | #
21 | styles:
22 | COLUMN_STYLE:
23 | gutter-width: 0.2 in
24 | top-margin: 0.0 in
25 | bottom-margin: 0.0 in
26 | line-width: 0.025 in
27 | line-colour: ${BLACK}
28 |
29 | PAGE_NUM_STYLE:
30 | font: ${NUM_FONT}
31 | font-size: 18
32 | font-colour: ${BLACK}
33 | horz-align: right
34 | vert-align: top
35 |
36 | STEP_NUM_STYLE:
37 | font: ${NUM_FONT}
38 | font-size: 32
39 | font-colour: ${BLACK}
40 | horz-align: right
41 | vert-align: top
42 | top-padding: 0.05 in
43 | right-padding: 0.15 in
44 |
45 | STEP_L0_STYLE:
46 | bottom-padding: 0.35 in
47 | right-padding: 0.025 in
48 | background-fill: True
49 | background-colour: ${WHITE}
50 |
51 | STEP_L1_STYLE:
52 | bottom-padding: 0.25 in
53 | right-padding: 0.025 in
54 | background-fill: True
55 | background-colour: ${L1}
56 |
57 | STEP_L2_STYLE:
58 | bottom-padding: 0.15 in
59 | right-padding: 0.025 in
60 | background-fill: True
61 | background-colour: ${L2}
62 |
63 | STEP_L3_STYLE:
64 | bottom-padding: 0.15 in
65 | right-padding: 0.025 in
66 | background-fill: True
67 | background-colour: ${L3}
68 |
69 | STEP_ASSEM_SYTLE:
70 | bottom-padding: 0.5 in
71 |
72 | STEP_ASSEM_IMAGE_STYLE:
73 | top-padding: 0
74 | bottom-padding: 0
75 | left-padding: 0
76 | right-padding: 0
77 |
78 | CALLOUT_STYLE_L1:
79 | font: ${NUM_FONT}
80 | font-size: 20
81 | font-colour: ${BLACK}
82 | background-fill: True
83 | background-colour: ${L1}
84 | border-outline: True
85 | border-width: 0.015 inch
86 | border-radius: 0.08 inch
87 | left-padding: 0.05 inch
88 | right-padding: 0.1 inch
89 | top-padding: 0.1 inch
90 | bottom-padding: 0.1 inch
91 | vert-align: top
92 | horz-align: left
93 |
94 | CALLOUT_STYLE_L2:
95 | font: ${NUM_FONT}
96 | font-size: 20
97 | font-colour: ${BLACK}
98 | background-fill: True
99 | background-colour: ${L2}
100 | border-outline: True
101 | border-width: 0.015 inch
102 | border-radius: 0.08 inch
103 | left-padding: 0.05 inch
104 | right-padding: 0.1 inch
105 | top-padding: 0.1 inch
106 | bottom-padding: 0.1 inch
107 | vert-align: top
108 | horz-align: left
109 |
110 | CALLOUT_STYLE_L3:
111 | font: ${NUM_FONT}
112 | font-size: 20
113 | font-colour: ${BLACK}
114 | background-fill: True
115 | background-colour: ${L3}
116 | border-outline: True
117 | border-width: 0.015 inch
118 | border-radius: 0.08 inch
119 | left-padding: 0.05 inch
120 | right-padding: 0.1 inch
121 | top-padding: 0.1 inch
122 | bottom-padding: 0.1 inch
123 | vert-align: top
124 | horz-align: left
125 |
126 | CALLOUT_STEP_NUM_STYLE:
127 | font: ${NUM_FONT}
128 | font-size: 20
129 | font-colour: ${BLACK}
130 | horz-align: centre
131 | vert-align: top
132 | top-padding: 0.05 inch
133 | right-padding: 0.05 inch
134 | left-padding: 0.05 inch
135 |
136 | CALLOUT_ASSEM_IMAGE_STYLE:
137 | top-padding: 0.0 in
138 | bottom-padding: 0.0 in
139 | left-padding: 0.05 in
140 | right-padding: 0.05 in
141 | top-margin: 0.1 inch
142 |
143 | CALLOUT_STEP_ASSEM_STYLE:
144 | horz-align: left
145 | vert-align: top
146 | bottom-padding: 0.0
147 | right-padding: 0.0
148 | left-padding: 0.0
149 |
150 | CALLOUT_QTY_STYLE:
151 | font: ${QTY_FONT}
152 | font-size: 24
153 | font-colour: ${BLACK}
154 | horz-align: left
155 | vert-align: bottom
156 | right-padding: 0.0 inch
157 | left-padding: 0.15 inch
158 |
159 | CALLOUT_ARROW_STYLE:
160 | line-colour: ${BLACK}
161 | border-colour: ${WHITE}
162 | border-outline: True
163 | border-width: 0.015 inch
164 | line-width: 0.015 inch
165 | length: 28
166 | width: 12
167 | arrow-style: taper
168 |
169 | STEP_PLI_STYLE :
170 | font: ${NUM_FONT}
171 | font-size: 10
172 | font-colour: ${BLACK}
173 | background-fill: True
174 | background-colour: ${PLI}
175 | border-outline: True
176 | border-width: 0.015 inch
177 | border-radius: 0.08 inch
178 | left-padding: 0.1 inch
179 | right-padding: 0.1 inch
180 | top-padding: 0.1 inch
181 | vert-align: bottom
182 |
183 |
184 | STEP_PLI_ITEM_STYLE :
185 | top-padding: 0.0 inch
186 | bottom-padding: 0.0 inch
187 | left-padding: 0.08 inch
188 | right-padding: 0.08 inch
189 |
190 |
191 | STEP_PLI_IMAGE_STYLE :
192 | vert-align: bottom
193 | horz-align: center
194 | top-padding: 0.0 inch
195 | bottom-padding: 0.0
196 | left-padding: 0.0 inch
197 | right-padding: 0.0 inch
198 |
199 |
200 | STEP_PLI_TEXT_STYLE :
201 | font: ${NUM_FONT}
202 | font-size: 9.5
203 | vert-align: top
204 | horz-align: left
205 | top-padding: 0.05 inch
206 | bottom-padding: 0.0 inch
207 | left-padding: 0.0 inch
208 | right-padding: 0.0 inch
209 |
210 |
211 | PREVIEW_ASSEM_STYLE :
212 | background-fill: True
213 | background-colour: ${WHITE}
214 | border-outline: True
215 | border-width: 0.015 inch
216 | border-radius: 0.08 inch
217 | left-padding: 0.25 inch
218 | right-padding: 0.25 inch
219 | top-padding: 0.1 inch
220 | bottom-padding: 0.05 inch
221 | bottom-margin: 0.25 inch
222 | vert-align: centre
223 | horz-align: centre
224 |
225 |
226 | PREVIEW_IMAGE_STYLE :
227 | vert-align: centre
228 | horz-align: centre
229 | top-padding: 0.0 inch
230 | bottom-padding: 0.0 inch
231 | left-padding: 0.0 inch
232 | right-padding: 0.0 inch
233 |
234 |
235 | PREVIEW_QTY_STYLE :
236 | font: ${QTY_FONT}
237 | font-size: 24
238 | font-colour: ${BLACK}
239 | horz-align: centre
240 | vert-align: centre
241 | right-padding: 0.0 inch
242 | left-padding: 0.0 inch
243 | top-padding: 0.0 inch
244 | bottom-padding: 0.0 inch
245 |
246 | ROTATE_ICON_STYLE:
247 | background-fill: True
248 | background-colour: ${WHITE}
249 | border-outline: True
250 | border-colour: ${BLACK}
251 | border-width: 0.015 inch
252 | border-radius: 0.08 inch
253 |
254 | ROTATE_ARROW_STYLE:
255 | line-colour: ${BLUE}
256 | border-colour: ${WHITE}
257 | border-outline: False
258 | line-width: 0.02 inch
259 | length: 10
260 | width: 8
261 | arrow-style: taper
262 |
--------------------------------------------------------------------------------
/tests/test_files/stylesheet_dark.yml:
--------------------------------------------------------------------------------
1 | #
2 | # Colour definitions
3 | #
4 | L1: "#1A1E24"
5 | L2: "#1A1E24"
6 | L3: "#1A1E24"
7 | DARK: "#1A1E24"
8 | LIGHT: "#F6FDFF"
9 | BLACK: "#000000"
10 | WHITE: "#FFFFFF"
11 | PLI: "#D4F9FE"
12 | BLUE: "#204080"
13 |
14 | #
15 | # Font definitions
16 | #
17 | NUM_FONT: British-Rail-Light
18 | QTY_FONT: IKEA-Sans-Regular
19 |
20 | #
21 | # Style dictionary containing all the style sheet styles
22 | #
23 | styles:
24 |
25 | COLUMN_STYLE:
26 | gutter-width: 0.2 in
27 | top-margin: 0.0 in
28 | bottom-margin: 0.0 in
29 | line-width: 0.025 in
30 | line-colour: ${WHITE}
31 |
32 | PAGE_NUM_STYLE:
33 | font: ${NUM_FONT}
34 | font-size: 18
35 | font-colour: ${WHITE}
36 | horz-align: right
37 | vert-align: top
38 |
39 | STEP_NUM_STYLE:
40 | font: ${NUM_FONT}
41 | font-size: 32
42 | font-colour: ${WHITE}
43 | horz-align: right
44 | vert-align: top
45 | top-padding: 0.05 in
46 | right-padding: 0.15 in
47 |
48 | STEP_L0_STYLE:
49 | bottom-padding: 0.35 in
50 | right-padding: 0.025 in
51 | background-fill: True
52 | background-colour: ${DARK}
53 |
54 | STEP_L1_STYLE:
55 | bottom-padding: 0.25 in
56 | right-padding: 0.025 in
57 | background-fill: True
58 | background-colour: ${L1}
59 |
60 | STEP_L2_STYLE:
61 | bottom-padding: 0.15 in
62 | right-padding: 0.025 in
63 | background-fill: True
64 | background-colour: ${L2}
65 |
66 | STEP_L3_STYLE:
67 | bottom-padding: 0.15 in
68 | right-padding: 0.025 in
69 | background-fill: True
70 | background-colour: ${L3}
71 |
72 | STEP_ASSEM_SYTLE:
73 | bottom-padding: 0.5 in
74 |
75 | STEP_ASSEM_IMAGE_STYLE:
76 | top-padding: 0
77 | bottom-padding: 0
78 | left-padding: 0
79 | right-padding: 0
80 |
81 | CALLOUT_STYLE_L1:
82 | font: ${NUM_FONT}
83 | font-size: 20
84 | font-colour: ${WHITE}
85 | background-fill: True
86 | background-colour: ${DARK}
87 | border-colour: ${WHITE}
88 | border-outline: True
89 | border-width: 0.015 inch
90 | border-radius: 0.00 inch
91 | left-padding: 0.05 inch
92 | right-padding: 0.1 inch
93 | top-padding: 0.1 inch
94 | bottom-padding: 0.1 inch
95 | vert-align: top
96 | horz-align: left
97 |
98 | CALLOUT_STYLE_L2:
99 | font: ${NUM_FONT}
100 | font-size: 20
101 | font-colour: ${WHITE}
102 | background-fill: True
103 | background-colour: ${DARK}
104 | border-colour: ${WHITE}
105 | border-outline: True
106 | border-width: 0.015 inch
107 | border-radius: 0.00 inch
108 | left-padding: 0.05 inch
109 | right-padding: 0.1 inch
110 | top-padding: 0.1 inch
111 | bottom-padding: 0.1 inch
112 | vert-align: top
113 | horz-align: left
114 |
115 | CALLOUT_STYLE_L3:
116 | font: ${NUM_FONT}
117 | font-size: 20
118 | font-colour: ${WHITE}
119 | background-fill: True
120 | background-colour: ${DARK}
121 | border-colour: ${WHITE}
122 | border-outline: True
123 | border-width: 0.015 inch
124 | border-radius: 0.0 inch
125 | left-padding: 0.05 inch
126 | right-padding: 0.1 inch
127 | top-padding: 0.1 inch
128 | bottom-padding: 0.1 inch
129 | vert-align: top
130 | horz-align: left
131 |
132 | CALLOUT_STEP_NUM_STYLE:
133 | font: ${NUM_FONT}
134 | font-size: 20
135 | font-colour: ${WHITE}
136 | horz-align: centre
137 | vert-align: top
138 | top-padding: 0.05 inch
139 | right-padding: 0.05 inch
140 | left-padding: 0.05 inch
141 |
142 | CALLOUT_ASSEM_IMAGE_STYLE:
143 | top-padding: 0 in
144 | bottom-padding: 0
145 | left-padding: 0.05 in
146 | right-padding: 0.05 in
147 | top-margin: 0.1 inch
148 |
149 | CALLOUT_STEP_ASSEM_STYLE:
150 | horz-align: left
151 | vert-align: top
152 | bottom-padding: 0.0
153 | right-padding: 0.0
154 | left-padding: 0.0
155 |
156 | CALLOUT_QTY_STYLE:
157 | font: ${QTY_FONT}
158 | font-size: 24
159 | font-colour: ${WHITE}
160 | horz-align: left
161 | vert-align: bottom
162 | right-padding: 0.0 inch
163 | left-padding: 0.15 inch
164 |
165 | CALLOUT_ARROW_STYLE:
166 | line-colour: ${WHITE}
167 | border-colour: ${DARK}
168 | border-outline: True
169 | border-width: 0.015 inch
170 | line-width: 0.015 inch
171 | length: 28
172 | width: 12
173 | arrow-style: taper
174 |
175 | STEP_PLI_STYLE :
176 | font: ${NUM_FONT}
177 | font-size: 10
178 | font-colour: ${WHITE}
179 | background-fill: True
180 | background-colour: ${DARK}
181 | border-colour: ${WHITE}
182 | border-outline: True
183 | border-width: 0.015 inch
184 | border-radius: 0.0 inch
185 | left-padding: 0.1 inch
186 | right-padding: 0.1 inch
187 | top-padding: 0.1 inch
188 | vert-align: bottom
189 |
190 | STEP_PLI_ITEM_STYLE :
191 | top-padding: 0.0 inch
192 | bottom-padding: 0.0 inch
193 | left-padding: 0.08 inch
194 | right-padding: 0.08 inch
195 |
196 | STEP_PLI_IMAGE_STYLE :
197 | vert-align: bottom
198 | horz-align: center
199 | top-padding: 0.0 inch
200 | bottom-padding: 0.0
201 | left-padding: 0.0 inch
202 | right-padding: 0.0 inch
203 |
204 | STEP_PLI_TEXT_STYLE :
205 | font: ${NUM_FONT}
206 | font-colour: ${WHITE}
207 | font-size: 9.5
208 | vert-align: top
209 | horz-align: left
210 | top-padding: 0.05 inch
211 | bottom-padding: 0.0 inch
212 | left-padding: 0.0 inch
213 | right-padding: 0.0 inch
214 |
215 |
216 | PREVIEW_ASSEM_STYLE :
217 | background-fill: True
218 | background-colour: ${WHITE}
219 | border-outline: True
220 | border-width: 0.015 inch
221 | border-radius: 0.00 inch
222 | left-padding: 0.25 inch
223 | right-padding: 0.25 inch
224 | top-padding: 0.1 inch
225 | bottom-padding: 0.05 inch
226 | bottom-margin: 0.25 inch
227 | vert-align: centre
228 | horz-align: centre
229 |
230 |
231 | PREVIEW_IMAGE_STYLE :
232 | vert-align: centre
233 | horz-align: centre
234 | top-padding: 0.0 inch
235 | bottom-padding: 0.0 inch
236 | left-padding: 0.0 inch
237 | right-padding: 0.0 inch
238 |
239 |
240 | PREVIEW_QTY_STYLE :
241 | font: ${QTY_FONT}
242 | font-size: 24
243 | font-colour: ${WHITE}
244 | horz-align: centre
245 | vert-align: centre
246 | right-padding: 0.0 inch
247 | left-padding: 0.0 inch
248 | top-padding: 0.0 inch
249 | bottom-padding: 0.0 inch
250 |
251 | ROTATE_ICON_STYLE:
252 | background-fill: True
253 | background-colour: ${DARK}
254 | border-outline: True
255 | border-colour: ${WHITE}
256 | border-width: 0.015 inch
257 | border-radius: 0.08 inch
258 |
259 | ROTATE_ARROW_STYLE:
260 | line-colour: ${WHITE}
261 | border-colour: ${DARK}
262 | border-outline: False
263 | line-width: 0.02 inch
264 | length: 10
265 | width: 8
266 | arrow-style: taper
267 |
--------------------------------------------------------------------------------
/tests/test_files/test_model.ldr:
--------------------------------------------------------------------------------
1 | 0 FILE rootmodel.ldr
2 | 0 untitled model
3 | 0 Name: rootmodel.ldr
4 | 0 Author: Michael Gale
5 | 1 2 0 0 0 1 0 0 0 1 0 -0 0 1 3010.dat
6 | 1 1 0 24 0 1 0 0 0 1 0 -0 0 1 3008.dat
7 | 0 STEP
8 | 1 4 -60 24 50 0 0 -1 0 1 0 1 0 0 3001.dat
9 | 1 4 60 24 50 0 0 -1 0 1 0 1 0 0 3001.dat
10 | 0 STEP
11 | 0 !PY SCALE 0.9
12 | 0 ROTSTEP 0 60 0 REL
13 | 1 14 50 16 50 -0 0 -1 -0 1 0 1 0 -0 3666.dat
14 | 1 14 -50 16 50 -0 0 -1 -0 1 0 1 0 -0 3666.dat
15 | 0 STEP
16 | 0 !PY PAGE_BREAK
17 | 0 !PY ARROW BEGIN 0 -100 0
18 | 1 27 20 -8 0 0 0 1 0 1 0 -1 0 0 submodel2.ldr
19 | 0 !PY ARROW END
20 | 1 27 88 44 0 0 -1 0 -0 0 1 -1 0 -0 submodel2.ldr
21 | 1 27 40 32 100 -0 0 1 0 -1 0 1 0 0 submodel2.ldr
22 | 0 STEP
23 | 0 !PY SCALE 0.5
24 | 1 27 -20 -8 0 1 0 0 0 1 0 -0 0 1 submodel1.ldr
25 | 0 STEP
26 | 1 70 0 -8 40 -1 -0 0 -0 1 -0 0 -0 -1 92950.dat
27 | 0 STEP
28 | 1 15 70 16 50 1 0 0 0 1 0 -0 0 1 submodel2.ldr
29 | 1 15 -70 16 90 -1 0 0 0 1 0 0 0 -1 submodel2.ldr
30 | 0 STEP
31 | 1 25 0 -8 60 -1 -0 0 -0 1 -0 0 -0 -1 92950.dat
32 | 0 STEP
33 | 1 19 -10 -32 50 0 0 -1 0 1 0 1 0 0 submodel3.ldr
34 | 0 STEP
35 | 1 10 0 48 50 1 0 0 0 1 0 -0 0 1 92438.dat
36 | 0 STEP
37 | 0 NOFILE
38 | 0 FILE submodel1.ldr
39 | 0 untitled model
40 | 0 Name: submodel1.ldr
41 | 0 Author: Michael Gale
42 | 1 72 0 0 0 1 0 0 0 1 0 -0 0 1 3023.dat
43 | 0 !PY ARROW BEGIN 0 -50 0
44 | 1 72 0 -8 0 1 0 0 0 1 0 -0 0 1 3023.dat
45 | 0 !PY ARROW END
46 | 0 STEP
47 | 1 27 0 -16 0 1 0 0 0 1 0 -0 0 1 3069b.dat
48 | 0 STEP
49 | 0 NOFILE
50 | 0 FILE submodel2.ldr
51 | 0 untitled model
52 | 0 Name: submodel2.ldr
53 | 0 Author: Michael Gale
54 | 0 !PY SCALE 0.75
55 | 1 0 0 0 0 0 0 -1 0 1 0 1 0 0 99781.dat
56 | 0 STEP
57 | 1 15 22 10 0 0 -1 0 -0 0 -1 1 0 -0 2431.dat
58 | 0 STEP
59 | 1 10 0 -8 10 -0 0 -1 -0 1 0 1 0 -0 3024.dat
60 | 0 STEP
61 | 0 NOFILE
62 | 0 FILE submodel3.ldr
63 | 0 untitled model
64 | 0 Name: submodel3.ldr
65 | 0 Author: Michael Gale
66 | 0 !CALLOUT BOTTOM
67 | 1 19 0 0 0 1 0 0 0 1 0 -0 0 1 3297.dat
68 | 0 STEP
69 | 1 19 0 -24 0 1 0 0 0 1 0 -0 0 1 subsubmodel3.ldr
70 | 0 STEP
71 | 1 1 0 24 0 1 0 0 0 1 0 -0 0 1 3297.dat
72 | 0 STEP
73 | 0 NOFILE
74 | 0 FILE subsubmodel3.ldr
75 | 0 untitled model
76 | 0 Name: subsubmodel3.ldr
77 | 0 Author: Michael Gale
78 | 1 19 0 0 0 1 0 0 0 1 0 -0 0 1 3010.dat
79 | 0 STEP
80 | 1 19 20 0 0 1 0 0 0 1 0 -0 0 1 85984.dat
81 | 1 19 -20 0 0 1 0 0 0 1 0 -0 0 1 85984.dat
82 | 0 STEP
83 | 0 NOFILE
--------------------------------------------------------------------------------
/tests/test_files/testfile2.ldr:
--------------------------------------------------------------------------------
1 | 0 FILE test2.ldr
2 | 0 untitled model
3 | 0 Name: test2.ldr
4 | 0 Author: Michael Gale
5 | 1 71 -49.999969 8 160 -0 0 1 0 1 0 -1 0 -0 30414.dat
6 | 1 71 -49.999969 8 240 -0 0 1 0 1 0 -1 0 -0 30414.dat
7 | 0 STEP
8 | 1 72 50 0 120 -0 0 1 -0 1 0 -1 -0 -0 3710.dat
9 | 1 72 50 0 280 -0 0 1 -0 1 0 -1 -0 -0 3710.dat
10 | 1 72 -50 0 120 -0 0 1 -0 1 0 -1 -0 -0 3710.dat
11 | 1 72 -50 0 280 -0 0 1 -0 1 0 -1 -0 -0 3710.dat
12 | 1 72 -49.999989 -72 280 -0 0 1 -0 1 0 -1 -0 -0 3701.dat
13 | 1 71 -49.999992 -72 360 -0 0 1 0 1 0 -1 0 -0 30414.dat
14 | 1 71 -50 -72 460 -0 0 1 0 1 0 -1 0 -0 30414.dat
15 | 1 71 -49.999985 -72 40 -0 0 1 0 1 0 -1 0 -0 30414.dat
16 | 1 71 -49.999989 -72 200 -0 0 1 0 1 0 -1 0 -0 30414.dat
17 | 1 71 -50.000015 -72 -59.999975 -0 0 1 0 1 0 -1 0 -0 30414.dat
18 | 1 72 -49.999989 -72 120 -0 0 1 -0 1 0 -1 -0 -0 3701.dat
19 | 0 STEP
20 | 1 71 -49.999992 -112 480 -0 0 1 0 1 0 -1 0 -0 30414.dat
21 | 1 71 -49.999996 -112 200 -0 0 1 0 1 0 -1 0 -0 30414.dat
22 | 1 71 -50 -112 380 -0 0 1 0 1 0 -1 0 -0 30414.dat
23 | 1 71 -49.999992 -112 -80 -0 0 1 0 1 0 -1 0 -0 30414.dat
24 | 1 71 -50 -112 20 -0 0 1 0 1 0 -1 0 -0 30414.dat
25 | 1 71 -49.999996 -112 120 -0 0 1 0 1 0 -1 0 -0 30414.dat
26 | 1 71 -49.999996 -112 280 -0 0 1 0 1 0 -1 0 -0 30414.dat
27 | 0 STEP
28 | 1 71 0 -94 -156 -0.999999 0 0 0 -0 -0.999999 0 -1 -0 85984.dat
29 | 1 71 0 -94 -156 1 0 0 0 -0 0.999999 0 -0.999999 -0 99780.dat
30 | 0 NOFILE
31 |
--------------------------------------------------------------------------------
/tests/test_ldrcolour.py:
--------------------------------------------------------------------------------
1 | # Sample Test passing with nose and pytest
2 |
3 | import os
4 | import sys
5 | import pytest
6 |
7 | from ldrawpy import *
8 | from ldrawpy.ldrcolour import FillColoursFromLDRCode, FillTitlesFromLDRCode
9 |
10 |
11 | def test_init_colour():
12 | c1 = LDRColour(15)
13 | c2 = LDRColour("White")
14 | c3 = LDRColour([1.0, 1.0, 1.0])
15 | c4 = LDRColour([255, 255, 255])
16 | c5 = LDRColour("#FFFFFF")
17 | assert c1 == c2
18 | assert c1 == c3
19 | assert c1 == c4
20 | assert c1 == c5
21 |
22 |
23 | def test_equality():
24 | c1 = LDRColour([0.4, 0.2, 0.6])
25 | c2 = LDRColour("#663399")
26 | assert c1 == c2
27 | c3 = LDRColour([102, 51, 153])
28 | assert c2 == c3
29 |
30 |
31 | def test_dict_lookup():
32 | c1 = LDRColour(LDR_ORGYLW_COLOUR)
33 | assert c1.name() == "Orange/Yellow"
34 | c2 = LDRColour.SafeLDRColourName(LDR_ORGYLW_COLOUR)
35 | assert c2 == "Orange/Yellow"
36 | c3 = FillColoursFromLDRCode(LDR_BLKWHT_COLOUR)
37 | assert (1.0, 1.0, 1.0) in c3
38 | c4 = FillTitlesFromLDRCode(LDR_BLKWHT_COLOUR)
39 | assert "Black" in c4
40 | assert "White" in c4
41 | assert len(c4) == 2
42 |
--------------------------------------------------------------------------------
/tests/test_ldrprimitives.py:
--------------------------------------------------------------------------------
1 | # Sample Test passing with nose and pytest
2 |
3 | import os
4 | import sys
5 | import pytest
6 |
7 | from toolbox import *
8 | from ldrawpy import *
9 |
10 |
11 | def test_ldrline():
12 | l1 = LDRLine(0)
13 | l1.p2 = Vector(1, 2, 3)
14 | l1.translate(Vector(5, 10, 20))
15 | assert l1.p2.x == 6
16 | assert l1.p2.y == 12
17 | assert l1.p2.z == 23
18 | ls = str(l1).rstrip()
19 | assert ls == "2 0 5 10 20 6 12 23"
20 |
21 |
22 | def test_ldrtriangle():
23 | t1 = LDRTriangle(0)
24 | t1.p2 = Vector(5, 10, 0)
25 | t1.p3 = Vector(10, 0, 0)
26 | t1.translate(Vector(2, 3, -7))
27 | assert t1.p3.x == 12
28 | assert t1.p3.y == 3
29 | assert t1.p3.z == -7
30 | ts = str(t1).rstrip()
31 | assert ts == "3 0 2 3 -7 7 13 -7 12 3 -7"
32 |
33 |
34 | def test_ldrquad():
35 | q1 = LDRQuad(0)
36 | q1.p2 = Vector(0, 5, 0)
37 | q1.p3 = Vector(20, 5, 0)
38 | q1.p4 = Vector(20, 0, 0)
39 | q1.translate(Vector(7, 3, 8))
40 | assert q1.p4.x == 27
41 | assert q1.p4.y == 3
42 | assert q1.p4.z == 8
43 | qs = str(q1).rstrip()
44 | assert qs == "4 0 7 3 8 7 8 8 27 8 8 27 3 8"
45 |
46 |
47 | def test_ldrpart():
48 | p1 = LDRPart(0)
49 | p1.attrib.loc = Vector(5, 7, 8)
50 | p1.name = "3002"
51 | assert p1.attrib.loc.x == 5
52 | assert p1.attrib.loc.y == 7
53 | assert p1.attrib.loc.z == 8
54 | ps = str(p1).rstrip()
55 | assert ps == "1 0 5 7 8 1 0 0 0 1 0 0 0 1 3002.dat"
56 |
57 |
58 | def test_ldrpart_translate():
59 | p1 = LDRPart(0)
60 | p1.attrib.loc = Vector(5, 7, 8)
61 | p1.name = "3002"
62 | assert p1.attrib.loc.x == 5
63 | assert p1.attrib.loc.y == 7
64 | assert p1.attrib.loc.z == 8
65 | ps = str(p1).rstrip()
66 | assert ps == "1 0 5 7 8 1 0 0 0 1 0 0 0 1 3002.dat"
67 | p2 = LDRPart.translate_from_str(ps, Vector(0, -2, -4))
68 | assert str(p2).rstrip() == "1 0 5 5 4 1 0 0 0 1 0 0 0 1 3002.dat"
69 |
70 |
71 | def test_ldrpart_equality():
72 | p1 = LDRPart(0, name="3001")
73 | p2 = LDRPart(0, name="3001")
74 | assert p1 == p2
75 | p3 = LDRPart(0, name="3002")
76 | assert p1 != p3
77 | assert p1.is_identical(p2)
78 | p2.move_to((0, 8, 20))
79 | assert p1 == p2
80 | assert not p1.is_identical(p2)
81 |
82 |
83 | def test_ldrpart_sort():
84 | p1 = LDRPart(0, name="3001")
85 | p2 = LDRPart(1, name="3666")
86 | p3 = LDRPart(14, name="3070b")
87 | sp = sort_parts([p1, p2, p3], key="sha1")
88 | assert sp[0].name == "3070b"
89 | assert sp[1].name == "3001"
90 | assert sp[2].name == "3666"
91 | sp = sort_parts([p1, p2, p3], key="name", order="descending")
92 | assert sp[2].name == "3001"
93 | assert sp[1].name == "3070b"
94 | assert sp[0].name == "3666"
95 |
--------------------------------------------------------------------------------
/tests/test_misc.py:
--------------------------------------------------------------------------------
1 | # Sample Test passing with nose and pytest
2 |
3 | import os
4 | import sys
5 | import pytest
6 |
7 | from toolbox import *
8 | from ldrawpy import *
9 |
10 | fin = "./test_files/testfile2.ldr"
11 |
12 |
13 | def test_cleanup():
14 | fno = fin.replace(".ldr", "_clean.ldr")
15 | clean_file(fin, fno)
16 | with open(fin, "r") as f:
17 | fl = f.read()
18 | assert len(fl) == 1284
19 | assert "-59.999975" in fl
20 | with open(fno, "r") as f:
21 | fl = f.read()
22 | assert len(fl) == 1101
23 | assert "-60" in fl
24 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py27,py34,py35,py36,py37
3 |
4 | [testenv]
5 | commands = py.test ldraw-py
6 | deps = pytest
7 |
--------------------------------------------------------------------------------