├── .gitignore
├── LICENCE
├── README.md
├── freecad
└── fcscript
│ ├── __init__.py
│ ├── demo
│ ├── __init__.py
│ ├── v_0_0_1.py
│ └── v_0_0_2.py
│ ├── init_gui.py
│ ├── v_0_0_1.py
│ └── v_0_0_2.py
├── package.xml
└── screenshot.jpg
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FCScript
2 |
3 | Simple DSL for Macro writers. In other words, a simplified API for macro coders.
4 |
5 | Work In Progress ....
6 |
7 | 
8 |
9 |
10 | Demo video: https://www.youtube.com/watch?v=vs939L5Ku2Q
11 |
12 |
13 | ## Usage
14 |
15 | This AddOn is for people interested in writing macros for FreeCAD. The intention is
16 | to provide a simplified API for common operations.
17 |
18 | ## Estimated Progress/TODO:
19 |
20 | -  Sketcher basic API
21 | -  Basic GUI API
22 | -  Dynamic/Expressions API
23 | -  Draft basic API
24 | -  PartDesign API
25 | -  Part API
26 |
27 | ## Install
28 |
29 | - Use FreeCAD Builtin Addon Manager to install this package.
30 | - Or Download this repo as a zip file and unzip it in `$HOME/.FreeCAD/Mod`, then restart FreeCAD.
31 |
32 | ## Documentation
33 |
34 | Once installed and restarted, you can find a demo Macro called: `FCScript_Demo.FCMacro`. This contains examples.
35 |
36 | See: https://github.com/mnesarco/fcscript/blob/main/freecad/fcscript/demo/v_0_0_1.py
37 |
38 | Many of the demos require **LinkStage3** because mainstream FreeCAD does not support multiple solids inside a body.
39 |
40 | ## License
41 |
42 | GPL 3.0
43 |
44 |
--------------------------------------------------------------------------------
/freecad/fcscript/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2022 Frank David Martinez M.
4 | #
5 | # This file is part of FCScript.
6 | #
7 | # FCScript is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # Utils is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with FCScript. If not, see .
19 | #
20 |
21 | """
22 | Small DSL for FreeCAD Macro development.
23 | """
24 |
25 | __author__ = "Frank David Martinez M"
26 | __copyright__ = "Copyright (c) 2022, Frank David Martinez M. "
27 | __license__ = "GPL"
28 | __version__ = "0.0.1"
29 | __maintainer__ = "Frank David Martinez M. "
30 | __git__ = "https://github.com/mnesarco/fcscript.git"
31 | __status__ = "Development"
32 |
--------------------------------------------------------------------------------
/freecad/fcscript/demo/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mnesarco/fcscript/bbdf566249578f27e95845cef9b752900e88ddf9/freecad/fcscript/demo/__init__.py
--------------------------------------------------------------------------------
/freecad/fcscript/demo/v_0_0_1.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2022 Frank David Martinez M.
4 | #
5 | # This file is part of FCScript.
6 | #
7 | # FCScript is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # Utils is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with FCScript. If not, see .
19 | #
20 |
21 | from math import cos, radians, sin, sqrt
22 | from freecad.fcscript.v_0_0_1 import (
23 | InputOptions, InputText, InputVector, XBody, XSketch, Vec, Pnt, Quantity, recompute, Dx, Dy, Dz, Expr,
24 | Dialog, InputFloat, InputInt, InputSelectMany, InputSelectOne,
25 | InputBoolean, Icon, Row, Col, TabContainer, Tab,
26 | button, on_event, gq, progress_indicator, selection,
27 | show_error, show_info, show_msgbox, show_warning,
28 | DataObject, App, Gui
29 | )
30 |
31 | def test1():
32 | s = XSketch("test1")
33 |
34 | # Triangle
35 | grp = s.create_group()
36 | grp.line_to((30, 30))
37 | grp.line_to((100, 0))
38 | grp.close()
39 |
40 | # Triangle
41 | grp.move_to((200,0))
42 | grp.line_to((250,250))
43 | grp.line_to((0,250))
44 | grp.close()
45 |
46 | # SpLine
47 | g2 = s.create_group()
48 | g2.move_to((300,0))
49 | g2.line_to((0, 50))
50 | g2.bspline_to((50,50), (50,60), (60,50), (50,70), (70,50), (50,80), (80,50))
51 | g2.close()
52 |
53 | # SpLine
54 | g3 = s.create_group()
55 | g3.move_to((400,0))
56 | g3.bspline_to((200,100), (300,200), (300,300))
57 | g3.move_to((0,0))
58 | g3.circle(45)
59 | g3.rect_to((20,20))
60 | g3.move_to((-100,-100))
61 | g3.rect(50, 100, angle=Quantity('30 deg'))
62 | g3.rect(50, 100)
63 |
64 | recompute()
65 |
66 | def test2_partial(body, plane):
67 | rows, cols = 5, 3
68 | w, h = 25, 10
69 | sx, sy = 15, 5
70 | s = body.sketch(plane=plane, name=plane)
71 | g = s.create_group()
72 |
73 | x, y = g.pos[0], g.pos[1] + 10
74 | x0 = x
75 | for row in range(rows):
76 | for col in range(cols):
77 | g.move_to((x,y))
78 | g.rect(w, h)
79 | x = x + w + sx
80 | x = x0
81 | y = y + h + sy
82 |
83 | s.pad(10, name=f'Pad{plane}')
84 | recompute()
85 |
86 |
87 | def test2():
88 | with progress_indicator():
89 | body = XBody(name="test2")
90 | test2_partial(body, 'XY')
91 | test2_partial(body, 'XZ')
92 |
93 |
94 | def test3():
95 | with progress_indicator():
96 | hole = 5
97 | side = 100
98 |
99 | body = XBody(name='test3')
100 | sketch = body.sketch(plane='XY', name='s1')
101 |
102 | path = sketch.create_group()
103 | path.rect_to(Pnt(side, side))
104 | h1 = Pnt(hole, hole) * 2
105 | hole_dist = side - 4*hole
106 | path.move_to(h1)
107 | path.circle(hole)
108 | path.move_to(h1 + Vec(hole_dist, 0, 0))
109 | path.circle(hole)
110 | path.move_to(h1 + Vec(0, hole_dist, 0))
111 | path.circle(hole)
112 | path.move_to(h1 + Vec(hole_dist, hole_dist, 0))
113 | path.circle(hole)
114 |
115 | sketch.pad(10)
116 | recompute()
117 |
118 | def test4():
119 | with progress_indicator():
120 | body = XBody(name='test4')
121 | sketch = body.sketch(plane='XY', name='s2')
122 | path = sketch.create_group()
123 |
124 | path.line_to(Pnt(10,0))
125 | path.bspline_to(Pnt(15, 0), Pnt(15, 5))
126 | path.line_to(Pnt(15,20))
127 | path.bspline_to(Pnt(15, 25), Pnt(10, 25))
128 | path.line_to(Pnt(0,25))
129 | path.close()
130 |
131 | path = sketch.create_group()
132 | path.move_to(Pnt(7.5, 7.5))
133 | path.rect(7, 7, Quantity('45 deg'))
134 |
135 | sketch.pad(30, direction=Vec(0,0,0) + Dy(-1) + Dz(-1))
136 | recompute()
137 |
138 |
139 | def test5():
140 | with progress_indicator():
141 | body = XBody(name='test5')
142 | sketch = body.sketch(plane='XY', name='s1')
143 | path = sketch.create_group()
144 | path.rect_rounded(w=7, h=14, r=2, angle=Quantity('45 deg'))
145 | path.move_to((50,0))
146 | path.rect_rounded(w=7, h=14, r=2, angle=Expr("0.5 rad"))
147 | sketch.pad(3)
148 | recompute()
149 |
150 |
151 | def test6():
152 | with progress_indicator():
153 | body = XBody(name='test6')
154 | sketch = body.sketch(plane='XY', name='s1')
155 | path = sketch.create_group()
156 | path.rect_rounded(w=10, h=10, r=(1,2,1,2), angle=Expr("30 deg"))
157 | path.move_to((50,0))
158 | path.rect_rounded(w=10, h=10, r=(1,2,1,2), angle=Expr("30 deg"))
159 | sketch.pad(2)
160 | recompute()
161 |
162 |
163 | def test_7_polygons():
164 | with progress_indicator():
165 | body = XBody(name='test7')
166 | sketch = body.sketch(plane='XY', name='poly')
167 | path = sketch.create_group()
168 |
169 | def auto(fn, *args, **kwargs):
170 | fn(*args, **kwargs)
171 |
172 | def constrain_pos(fn, constrain, *args, **kwargs):
173 | fn(*args, **kwargs, constrain_pos=constrain)
174 |
175 | auto(path.regular_polygon, 50, 3)
176 | path.move(dx=50)
177 | constrain_pos(path.regular_polygon, True, 50, 4)
178 | path.move(dx=50)
179 | constrain_pos(path.regular_polygon, False, 50, 5)
180 | path.move(dx=50)
181 | constrain_pos(path.regular_polygon, True, 50, 6)
182 |
183 | path.move(dx=50)
184 | constrain_pos(path.regular_polygon, True, 50, 6, angle=Quantity('15 deg'))
185 | recompute()
186 |
187 |
188 | def test_8_hive():
189 | with progress_indicator():
190 | body = XBody(name='test8')
191 | sketch = body.sketch(plane='XY', name='s1')
192 | path = sketch.create_group()
193 | size = 10
194 | sep = 1,-1
195 | rows = 6
196 | cols = 4
197 | for row in range(rows):
198 | shift = -0.5 if row % 2 else 0.5
199 | for col in range(cols):
200 | path.move(dx=size+sep[0])
201 | path.regular_polygon(gq.Radius(size/2.0), 6, constrain_pos=True)
202 | path.move(dx=-(cols - shift)*(size+sep[0]), dy=size+sep[1])
203 | path.move_to((0, -size/2 -sep[1] -5))
204 | path.rect_rounded(w=(size+sep[0])*cols+30, h=(size+sep[1])*rows+10, r=3)
205 | sketch.pad(2)
206 | recompute()
207 |
208 |
209 | def test9_diag1():
210 | with Dialog(title="1st Dialog"):
211 | with Row():
212 | with Col():
213 | rad = InputFloat(label="Radius:")
214 | sides = InputInt(label="Sides:")
215 | with Col():
216 | it = InputInt(label="Iteration:")
217 | conv = InputFloat(label="Convolution:")
218 |
219 | def test10_diag2():
220 | with Dialog(title="2nd Dialog"):
221 | with Col():
222 | rad = InputFloat(label="Radius:")
223 | sides = InputInt(label="Sides:")
224 | it = InputInt(label="Iteration:")
225 | conv = InputFloat(label="Convolution:")
226 | active = InputBoolean(label="Active:")
227 | pos = InputVector(label="Pos (Vector):", value=(10,20,30))
228 | @button(text="Dump")
229 | def btn():
230 | print(f"rad={rad.value()}, sides={sides.value()}, it={it.value()}, conv={conv.value()}, active={active.value()}")
231 | print(f"Pos={pos.value()}")
232 |
233 |
234 | def test11_diag3():
235 | with Dialog(title="3rd Dialog"):
236 | rad = InputFloat(label="Radius:")
237 | sides = InputInt(label="Sides:")
238 | it = InputInt(label="Iteration:")
239 | conv = InputFloat(label="Convolution:")
240 | active = InputBoolean(label="Active:")
241 |
242 |
243 | def test12_diag4():
244 | with Dialog(title="4th Dialog") as dialog:
245 | with Col():
246 | count = InputInt(label="Count:", value=2)
247 | width = InputFloat(label="Width:", value=4)
248 | height = InputFloat(label="Height:", value=6)
249 | radius = InputFloat(label="Radius:", value=1)
250 | @button(text="Execute")
251 | def execute():
252 | with progress_indicator("Working..."):
253 | body = XBody(name='test9')
254 | sketch = body.sketch(plane='XY', name='main9')
255 | path = sketch.create_group()
256 | for _ in range(count.value()):
257 | path.rect_rounded(width.value(), height.value(), radius.value())
258 | path.move(dx=width.value()*1.2)
259 | sketch.pad(3)
260 | recompute()
261 | dialog.close()
262 |
263 |
264 | def test13_diag5():
265 | with Dialog(title="5th Dialog / Single Select") as dialog:
266 | with Col():
267 | a = InputSelectOne(label="Part A:")
268 | b = InputSelectOne(label="Part B:")
269 |
270 | @on_event(a.selected)
271 | def example_listener(sel):
272 | print(f"Part A was selected: {sel}")
273 |
274 | @on_event(b.selected)
275 | def example_listener2(sel):
276 | print(f"Part B was selected: {sel}")
277 |
278 | @button(text="Do Something", icon=Icon(':/icons/edit_OK.svg'))
279 | def do_something():
280 | with selection(a.value(), b.value()):
281 | print(f"Parts: {a.value()} and {b.value()}")
282 |
283 |
284 | def test14_diag6():
285 | with Dialog(title="6th Dialog / Multi Select") as dialog:
286 | with Col():
287 | c = InputSelectMany(label="Many Parts:")
288 |
289 | @on_event(c.selected)
290 | def example_listener(sel):
291 | print(f"One Part was selected: {sel}")
292 |
293 | @button(text="Do Something", icon=Icon(':/icons/edit_OK.svg'))
294 | def do_something():
295 | print("-" * 80)
296 | for sel in c.value():
297 | print(f"Selected: {sel}")
298 |
299 |
300 | def test15_diag7():
301 | with Dialog(title="7th Dialog / Tabs") as dialog:
302 | with Col():
303 | with TabContainer():
304 |
305 | with Tab('First Tab'):
306 | with Col():
307 | InputFloat(label="Sample input1:")
308 | InputFloat(label="Sample input2:")
309 | InputFloat(label="Sample input3:")
310 | InputFloat(label="Sample input4:")
311 |
312 | with Tab('Second Tab'):
313 | with Col():
314 | InputSelectMany(label="Sample select many")
315 |
316 | with Tab('3th Tab'):
317 | with Row():
318 | @button(text="Btn1")
319 | def do_nothing():
320 | pass
321 | @button(text="Btn2")
322 | def do_nothing():
323 | pass
324 |
325 |
326 | def test16_msgboxes():
327 | show_info("test info")
328 | show_warning("test warn")
329 | show_error("test err")
330 |
331 | show_info("test info", title="aaaa")
332 | show_warning("test warn", title="bbbb")
333 | show_error("test err", title="cccc")
334 |
335 |
336 | def test17_parametric():
337 | dataObject = DataObject("test17")
338 | dataObject.add_property("App::PropertyFloat", "Radius")
339 | dataObject.add_property("App::PropertyFloat", "Extent")
340 | dataObject.add_property("App::PropertyFloat", "Whatever")
341 | dataObject.Radius = 55
342 | dataObject.Extent = Expr(".Radius*2")
343 | dataObject.Whatever = Expr(".Extent*2.5 + 10")
344 |
345 |
346 | def test18_rounded_rect():
347 | with Dialog("Test18: Simple Rounded Rect"):
348 | with Col():
349 | width = InputFloat(label="Width:", value=50)
350 | length = InputFloat(label="Length:", value=50)
351 | height = InputFloat(label="Height:", value=5)
352 | radius = InputFloat(label="Border radius:", value=3)
353 | @button(text="Create")
354 | def create():
355 | body = XBody(name='test18')
356 | sketch = body.sketch(plane='XY', name='sketch')
357 | path = sketch.create_group()
358 | path.rect_rounded(w=width.value(), h=length.value(), r=radius.value())
359 | sketch.pad(height.value())
360 | recompute()
361 |
362 | def test19_options():
363 | with Dialog("Test19: Options"):
364 | with Col():
365 | # Key is the option label, Value can be anything
366 | options = {
367 | "Test 18": test18_rounded_rect,
368 | "Test 12": test12_diag4,
369 | "Test 15": test15_diag7
370 | }
371 | sel = InputOptions(options, value=test12_diag4, label="Select One Demo:")
372 | @button(text="Run")
373 | def run():
374 | data_fn = sel.value()
375 | data_fn()
376 |
377 |
378 | def test20():
379 | ''''Spiral'''
380 | with progress_indicator("Working..."):
381 | body = XBody(name='test20')
382 | sketch = body.sketch(plane='XY', name='sketch')
383 | path = sketch.create_group()
384 | radius = 100
385 | angle = 0
386 | for t in range(60):
387 | angle = radians(12*t)
388 | x, y = radius * cos(angle), radius * sin(angle)
389 | path.move_to(Pnt(x, y))
390 | path.circle(radius/12)
391 | radius *= 1.06
392 | sketch.pad(5000)
393 | recompute()
394 | Gui.SendMsgToActiveView("ViewFit")
395 |
396 |
397 | def test21():
398 | """Generate Part Demo"""
399 | # GUI
400 | with Dialog("Part Demo Changed"):
401 | # Get Input parameters
402 | base_name = InputText(label="Name:", value="test21")
403 | main_radius_input = InputFloat(label="Main Hole Radius:", value=100)
404 | small_radius_input = InputFloat(label="Small Holes Radius:", value=10)
405 | margin_input = InputFloat(label="Distance between Main Hole and small Holes:", value=10)
406 | corner_input = InputFloat(label="Corner Angle Offset:", value=5)
407 | thickness_input = InputFloat(label="Thickness:", value=5)
408 |
409 | # Polar to Cartesian converter
410 | def coords(radius, angle):
411 | angle = radians(angle)
412 | return radius * cos(angle), radius * sin(angle)
413 |
414 | @button(text="Create")
415 | def execute():
416 |
417 | # Read inputs
418 | main_radius = main_radius_input.value()
419 | small_radius = small_radius_input.value()
420 | margin = margin_input.value()
421 | thickness = thickness_input.value()
422 | dev = corner_input.value()
423 |
424 | # Create base objects
425 | body = XBody(name=base_name.value() or 'test21')
426 | sketch = body.sketch(plane='XY', name='sketch')
427 | path = sketch.create_group()
428 |
429 | # Main hole
430 | path.circle(main_radius)
431 |
432 | # Small Holes
433 | for angle in (90, 210, 330):
434 | path.move_to(coords(main_radius + margin + small_radius, angle))
435 | path.circle(small_radius)
436 |
437 | # Perimeter bspline
438 | points = []
439 | outer = main_radius + 2*margin + 2*small_radius
440 | points.append(coords(outer, 90))
441 | angle = 90
442 | for t in range(3):
443 | points.append(coords(outer, angle + dev))
444 | points.append(coords(outer-2*margin, angle + 60 - dev))
445 | points.append(coords(outer-2*margin, angle + 60))
446 | points.append(coords(outer-2*margin, angle + 60 + dev))
447 | points.append(coords(outer, angle + 120 - dev))
448 | angle += 120
449 | points.append(coords(outer, 90))
450 | points.append(coords(outer, 90))
451 | path.move_to(coords(outer, 90))
452 | path.bspline_to(*points)
453 |
454 | sketch.pad(thickness)
455 | recompute()
456 | Gui.SendMsgToActiveView("ViewFit")
457 |
458 |
459 | with Dialog(title="FCScript Demo") as dialog:
460 | if not App.ActiveDocument:
461 | App.newDocument()
462 | with Row():
463 | with Col():
464 | @button(text="Test1: Basic Sketch")
465 | def run_test1(): test1()
466 |
467 | @button(text="Test2: Planes (LinkStage3)")
468 | def run_test2(): test2()
469 |
470 | @button(text="Test3: Plate-Holes")
471 | def run_test3(): test3()
472 |
473 | @button(text="Test4: Extrusion")
474 | def run_test4(): test4()
475 |
476 | @button(text="Test5: Rounded Rect")
477 | def run_test5(): test5()
478 |
479 | with Col():
480 |
481 | @button(text="Test6: Asymmetric Rounded Rect")
482 | def run_test6(): test6()
483 |
484 | @button(text="Test7: Polygons")
485 | def run_test7(): test_7_polygons()
486 |
487 | @button(text="Test8: Hive")
488 | def run_test8(): test_8_hive()
489 |
490 | @button(text="Test11: Dialogs Basic")
491 | def run_test11(): test11_diag3()
492 |
493 | @button(text="Test10: Dialogs Col")
494 | def run_test10(): test10_diag2()
495 |
496 | with Col():
497 |
498 | @button(text="Test9: Dialogs Row/Col")
499 | def run_test9(): test9_diag1()
500 |
501 | @button(text="Test12: Dialogs Button")
502 | def run_test12(): test12_diag4()
503 |
504 | @button(text="Test13: Dialogs Single Select")
505 | def run_test13(): test13_diag5()
506 |
507 | @button(text="Test14: Dialogs Multi Select")
508 | def run_test14(): test14_diag6()
509 |
510 | @button(text="Test15: Dialogs Tabs")
511 | def run_test15(): test15_diag7()
512 |
513 | with Col():
514 |
515 | @button(text="Test16: Message Boxes")
516 | def run_test16(): test16_msgboxes()
517 |
518 | @button(text="Test17: Data Object")
519 | def run_test17(): test17_parametric()
520 |
521 | @button(text="Test18: Rounded Rect")
522 | def run_test18(): test18_rounded_rect()
523 |
524 | @button(text="Test19: Options")
525 | def run_test19(): test19_options()
526 |
527 | @button(text="Test20: Spiral (LinkStage3)")
528 | def run_test20(): test20()
529 |
530 | with Col():
531 |
532 | @button(text="Test21: Part")
533 | def run_test21(): test21()
534 |
535 |
--------------------------------------------------------------------------------
/freecad/fcscript/demo/v_0_0_2.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2022 Frank David Martinez M.
4 | #
5 | # This file is part of FCScript.
6 | #
7 | # FCScript is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # Utils is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with FCScript. If not, see .
19 | #
20 |
21 | from freecad.fcscript.v_0_0_2 import *
22 |
23 | def test1_wire3D():
24 | radius = 5
25 | start = 0,0,radius
26 | w = Wire3D(start)
27 | w.add_segment((0,0,100-radius))
28 | w.add_round_corner((0,100,100), radius)
29 | w.add_round_corner((100,100,100), radius)
30 | w.add_round_corner((0,0,radius), radius)
31 | w.set_to('test1_wire_3d')
32 |
33 |
34 | with Dialog(title="FCScript Demo 0.0.2") as dialog:
35 | if not App.ActiveDocument:
36 | App.newDocument()
37 | with Row():
38 | with Col():
39 | @button(text="Test1: Basic Wire in 3D Space")
40 | def run_test1(): test1_wire3D()
41 |
--------------------------------------------------------------------------------
/freecad/fcscript/init_gui.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2022 Frank David Martinez M.
4 | #
5 | # This file is part of FCScript.
6 | #
7 | # FCScript is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # Utils is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with FCScript. If not, see .
19 | #
20 |
21 | from freecad.fcscript import __version__ as fs_version
22 | import pathlib, shutil, os, re
23 | import FreeCAD as App
24 |
25 | auto_update_macro = True
26 |
27 | if auto_update_macro:
28 | user_macro_dir = pathlib.Path(App.getUserMacroDir())
29 | demo_macro_dir = pathlib.Path(pathlib.Path(__file__).parent, "demo")
30 | pattern = re.compile(r'^v_(\d+)_(\d+)_(\d+).py$')
31 | for file in os.listdir(demo_macro_dir):
32 | if pattern.match(file):
33 | src = pathlib.Path(demo_macro_dir, file)
34 | target = pathlib.Path(user_macro_dir, f"FCScript_Demo_{file}")
35 | print(f"FCScript Demo Macro init: {src} -> {target}")
36 | shutil.copy2(src, target)
37 |
--------------------------------------------------------------------------------
/freecad/fcscript/v_0_0_1.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2022 Frank David Martinez M.
4 | #
5 | # This file is part of FCScript.
6 | #
7 | # FCScript is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # Utils is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with FCScript. If not, see .
19 | #
20 |
21 | # └────────────────────────────────────────────────────────────────────────────┘
22 | # [SECTION] Common Builtin Imports
23 | # ┌────────────────────────────────────────────────────────────────────────────┐
24 |
25 | from cmath import exp
26 | from contextlib import contextmanager
27 | from enum import Enum
28 | from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
29 | import math
30 | import sys
31 | import threading
32 |
33 | try:
34 | from typing import Protocol, runtime_checkable
35 | except:
36 | from typing_extensions import Protocol, runtime_checkable
37 |
38 |
39 | # └────────────────────────────────────────────────────────────────────────────┘
40 | # [SECTION] FreeCAD Imports
41 | # ┌────────────────────────────────────────────────────────────────────────────┐
42 |
43 | from FreeCAD import Base
44 | import FreeCAD as App
45 | import Part
46 | import Sketcher
47 | from ProfileLib import RegularPolygon
48 | import FreeCADGui as Gui
49 |
50 |
51 | # └────────────────────────────────────────────────────────────────────────────┘
52 | # [SECTION] [Lang] Typing
53 | # ┌────────────────────────────────────────────────────────────────────────────┐
54 |
55 | @runtime_checkable
56 | class ObjectWithOrigin(Protocol):
57 | @property
58 | def Origin(self) -> App.DocumentObject:
59 | ...
60 |
61 |
62 | # └────────────────────────────────────────────────────────────────────────────┘
63 | # [SECTION] [FreeCAD] Aliases
64 | # ┌────────────────────────────────────────────────────────────────────────────┐
65 |
66 | #: Point/Vector alias
67 | Pnt = Base.Vector
68 |
69 | #: Vector alias
70 | Vec = Base.Vector
71 |
72 | #: App.Rotation alias
73 | Rotation = App.Rotation
74 |
75 | #: Quantity converter from string
76 | Quantity = App.Units.Quantity
77 |
78 | #: Commands
79 | command = Gui.runCommand
80 |
81 | # └────────────────────────────────────────────────────────────────────────────┘
82 | # [SECTION] [Constants] Common constants
83 | # ┌────────────────────────────────────────────────────────────────────────────┐
84 |
85 | #: Geometry start point (except circles or ellipses)
86 | GeomStart = 1
87 |
88 | #: Geometry end point (except circles or ellipses)
89 | GeomEnd = 2
90 |
91 | #: Geometry center point (only circles and ellipses)
92 | GeomCenter = 3
93 |
94 | #: X Axis Geometry index in Sketch
95 | XAxisIndex = -1
96 |
97 | #: Y Axis Geometry index in Sketch
98 | YAxisIndex = -2
99 |
100 |
101 | # └────────────────────────────────────────────────────────────────────────────┘
102 | # [SECTION] [Util] Common utilities
103 | # ┌────────────────────────────────────────────────────────────────────────────┐
104 |
105 | def to_vec(input : Any) -> Vec:
106 | """Convert tuple/list/vector to Vec."""
107 | if isinstance(input, Vec):
108 | return input
109 | if isinstance(input, (tuple, list)):
110 | if len(input) == 3:
111 | return Vec(*input)
112 | if len(input) == 2:
113 | return Vec(*input, 0)
114 | if len(input) == 1:
115 | return Vec(*input, 0, 0)
116 | if hasattr(input, "X"):
117 | if hasattr(input, "Y"):
118 | if hasattr(input, "Z"):
119 | return Vec(input.X, input.Y, input.Z)
120 | else:
121 | return Vec(input.X, input.Y, 0)
122 | else:
123 | return Vec(input.X, 0, 0)
124 | if isinstance(input, (float, int)):
125 | return Vec(input, 0, 0)
126 | raise RuntimeError(f"Invalid input, {type(input)} is not convertible to Vec")
127 |
128 |
129 | def to_vecs(*input : Any) -> Tuple[Vec, ...]:
130 | """Convert arguments into Vectors. See to_vec."""
131 | return tuple(to_vec(i) for i in input)
132 |
133 |
134 | def find_obj_origin_geo_feature(obj: ObjectWithOrigin, prefix: str) -> App.GeoFeature:
135 | """Extract Axes or Planes from object's Origin"""
136 | for geo in obj.Origin.OutList:
137 | if geo.Name.startswith(prefix):
138 | return geo
139 |
140 |
141 | def find_obj_origin_axis(obj: ObjectWithOrigin, prefix: str) -> App.GeoFeature:
142 | """Extract Axes from object's Origin"""
143 | return find_obj_origin_geo_feature(obj, f'{prefix.upper()}_Axis')
144 |
145 |
146 | def find_obj_origin_plane(obj: ObjectWithOrigin, prefix: str) -> App.GeoFeature:
147 | """Extract Planes from object's Origin"""
148 | return find_obj_origin_geo_feature(obj, f'{prefix.upper()}_Plane')
149 |
150 |
151 | def recompute():
152 | """Recompute the whole active document"""
153 | App.ActiveDocument.recompute()
154 |
155 |
156 | def Dx(v: Union[float, Vec]):
157 | """Vector in x direction"""
158 | if isinstance(v, (float, int)):
159 | return Vec(v, 0, 0)
160 | return Vec(v.X, 0, 0)
161 |
162 |
163 | def Dy(v: Union[float, Vec]):
164 | """Vector in y direction"""
165 | if isinstance(v, (float, int)):
166 | return Vec(0, v, 0)
167 | return Vec(0, v.Y, 0)
168 |
169 |
170 | def Dz(v: Union[float, Vec]):
171 | """Vector in z direction"""
172 | if isinstance(v, (float, int)):
173 | return Vec(0, 0, v)
174 | return Vec(0, 0, v.Z)
175 |
176 | #: Decorator
177 | def set_function(target, attribute):
178 | """Decorator: Set function to an attribute of target."""
179 | def deco(fn):
180 | setattr(target, attribute, fn)
181 | return fn
182 | return deco
183 |
184 |
185 | # └────────────────────────────────────────────────────────────────────────────┘
186 | # [SECTION] [Expression] Expression Engine Utilities
187 | # ┌────────────────────────────────────────────────────────────────────────────┐
188 | #
189 | # Example 1 (Eval):
190 | # e1 = Expr(".Length * 3 + <>.Radius")
191 | # result = e1(App.ActiveDocument.Box)
192 | #
193 | # Example 2 (Set):
194 | # e1.set_to(App.ActiveDocument.Pad002, 'Length')
195 | #
196 | # Example 3 (Set in place)
197 | # bind_expr(App.ActiveDocument.Pad002, 'Length', ".Length * 3 + <>.Radius")
198 | #
199 |
200 | class Expr:
201 | """Callable expression (FreeCAD's Expression Engine)"""
202 |
203 | def __init__(self, value: str, context: Any = None):
204 | self.value = value
205 | self.context = context
206 |
207 | def __call__(self, context: Any = None) -> Any:
208 | return (context or self.context).evalExpression(self.value)
209 |
210 | def set_to(self, obj, property: str, auto_recompute=False):
211 | obj.setExpression(property, self.value)
212 | if auto_recompute:
213 | recompute()
214 |
215 | def bind_expr(obj: Any, property: str, expr: Union[str, Expr], auto_recompute: bool = False):
216 | if not isinstance(expr, Expr):
217 | expr = Expr(expr, obj)
218 | expr.set_to(obj, property, auto_recompute)
219 |
220 |
221 | # └────────────────────────────────────────────────────────────────────────────┘
222 | # [SECTION] [Sketch] Geometry
223 | # ┌────────────────────────────────────────────────────────────────────────────┐
224 |
225 | class SketchGeom:
226 | """Geometry info inside a sketch"""
227 | def __init__(self, index, obj, name=None):
228 | self.index = index
229 | self.obj = obj
230 | self.name = name
231 |
232 |
233 | # └────────────────────────────────────────────────────────────────────────────┘
234 | # [SECTION] [Sketch] Solver
235 | # ┌────────────────────────────────────────────────────────────────────────────┐
236 |
237 | class Solver(Enum):
238 | BFGSSolver = 0
239 | LevenbergMarquardtSolver = 1
240 | DogLegSolver = 2
241 |
242 |
243 | # └────────────────────────────────────────────────────────────────────────────┘
244 | # [SECTION] [Sketch] Geometric Quantities
245 | # ┌────────────────────────────────────────────────────────────────────────────┐
246 |
247 | class gq:
248 |
249 | class Base:
250 | value: float
251 | def __init__(self, value: Union[float,str]):
252 | if isinstance(value, str):
253 | self.value = Quantity(str)
254 | else:
255 | self.value = value
256 |
257 | class Radius(Base):
258 | def __init__(self, value: Union[float,str]):
259 | super().__init__(value)
260 |
261 | class Diameter(Base):
262 | def __init__(self, value: Union[float,str]):
263 | super().__init__(value)
264 |
265 | class Length(Base):
266 | def __init__(self, value: Union[float,str]):
267 | super().__init__(value)
268 |
269 | class Angle(Base):
270 | def __init__(self, value: Union[float,str]):
271 | super().__init__(value)
272 |
273 | class DeltaVec(Base):
274 | def __init__(self, value: any):
275 | self.value = to_vec(value)
276 |
277 | # └────────────────────────────────────────────────────────────────────────────┘
278 | # [SECTION] [Sketch] Sketch
279 | # ┌────────────────────────────────────────────────────────────────────────────┐
280 |
281 | class XSketch:
282 | """Sketch Wrapper"""
283 |
284 | DEFAULT_SOLVER : Solver = Solver.DogLegSolver
285 |
286 | @staticmethod
287 | def select_default_solver(solver: Solver):
288 | App.ParamGet('User parameter:BaseApp/Preferences/Mod/Sketcher').SetBool('ShowSolverAdvancedWidget', True)
289 | App.ParamGet('User parameter:BaseApp/Preferences/Mod/Sketcher/SolverAdvanced').SetInt('DefaultSolver', solver.value)
290 |
291 |
292 | def __init__(self, name: str = 'XSketch', parent: Any = None, clean: bool = True):
293 | XSketch.select_default_solver(XSketch.DEFAULT_SOLVER)
294 | if parent:
295 | name = f"{parent.Name}_{name}"
296 | self.obj = App.ActiveDocument.getObject(name)
297 | if self.obj:
298 | if clean:
299 | self.obj.deleteAllGeometry()
300 | else:
301 | if parent:
302 | self.obj = parent.newObject("Sketcher::SketchObject", name)
303 | else:
304 | self.obj = App.ActiveDocument.addObject("Sketcher::SketchObject", name)
305 |
306 | self.named_geom = {}
307 | self._ref_mode = False
308 | self.parent = parent
309 |
310 |
311 | @contextmanager
312 | def ref_mode(self, mode : bool = True):
313 | saved = self._ref_mode
314 | if saved == mode:
315 | yield self
316 | else:
317 | self._ref_mode = mode
318 | try:
319 | yield self
320 | finally:
321 | self._ref_mode = saved
322 |
323 |
324 | def set_geom_mode(self):
325 | self._ref_mode = False
326 |
327 | # └────────────────────────────────────────────────────────────────────────────┘
328 | # [SECTION] [Sketch/Geometry]
329 | # ┌────────────────────────────────────────────────────────────────────────────┐
330 |
331 | def g_line(self, start, end):
332 | """Draw a line segment from start to end"""
333 | p0, p1 = to_vec(start), to_vec(end)
334 | line = Part.LineSegment(p0, p1)
335 | index = self.obj.addGeometry(line, self._ref_mode)
336 | return SketchGeom(index, line)
337 |
338 |
339 | def g_regular_polygon(self, p1, p2, edges):
340 | """Draw a regular polygon (center, vertex, edges)."""
341 | RegularPolygon.makeRegularPolygon(self.obj, edges, to_vec(p1), to_vec(p2), self._ref_mode)
342 | index = len(self.obj.Geometry) - 1
343 | return SketchGeom(index, self.obj.Geometry[index])
344 |
345 |
346 | def g_bspline(self, poles, mults=None, knots=None, periodic=False, degree=3, weights=None, check_rational=False):
347 | """Draw a bspline"""
348 | vec_poles = [to_vec(p) for p in poles]
349 | bspline = Part.BSplineCurve(vec_poles, mults, knots, periodic, degree, weights, check_rational)
350 | index = self.obj.addGeometry(bspline, self._ref_mode)
351 | return SketchGeom(index, bspline)
352 |
353 |
354 | def g_circle_center_radius(self, cnt, rad):
355 | c = to_vec(cnt)
356 | circle = Part.Circle(c, Vec(0,0,1), rad)
357 | index = self.obj.addGeometry(circle, self._ref_mode)
358 | return SketchGeom(index, circle)
359 |
360 |
361 | def g_circle_3points(self, p1, p2, p3):
362 | v1, v2, v3 = to_vecs(p1, p2, p3)
363 | circle = Part.Circle(v1, v2, v3)
364 | index = self.obj.addGeometry(circle, self._ref_mode)
365 | return SketchGeom(index, circle)
366 |
367 |
368 | def g_arc_3points(self, p1, p2, p3):
369 | v1, v2, v3 = to_vecs(p1, p2, p3)
370 | arc = Part.Arc(v1, v2, v3)
371 | index = self.obj.addGeometry(arc, self._ref_mode)
372 | return SketchGeom(index, arc)
373 |
374 |
375 | def g_arc_center_radius(self, cnt, rad, start=0, end=math.radians(180)):
376 | c = to_vec(cnt)
377 | circle = Part.Circle(c, Vec(0,0,1), rad)
378 | arc = Part.ArcOfCircle(circle, start, end)
379 | index = self.obj.addGeometry(arc, self._ref_mode)
380 | return SketchGeom(index, arc)
381 |
382 |
383 | def g_point(self, p):
384 | pnt = Part.Point(p)
385 | index = self.obj.addGeometry(pnt, self._ref_mode)
386 | return SketchGeom(index, pnt)
387 |
388 | # └────────────────────────────────────────────────────────────────────────────┘
389 | # [SECTION] [Sketch/Constraints]
390 | # ┌────────────────────────────────────────────────────────────────────────────┐
391 |
392 | def rename_constraint(self, constraint, name):
393 | if name:
394 | self.obj.renameConstraint(constraint, name)
395 |
396 |
397 | def c_coincident(self, g1, g1c, g2, g2c, name=None):
398 | c = self.obj.addConstraint(Sketcher.Constraint("Coincident", g1, g1c, g2, g2c))
399 | self.rename_constraint(c, name)
400 | return c
401 |
402 |
403 | def c_vertical(self, index, name=None):
404 | c = self.obj.addConstraint(Sketcher.Constraint("Vertical", index))
405 | self.rename_constraint(c, name)
406 | return c
407 |
408 |
409 | def c_horizontal(self, index, name=None):
410 | c = self.obj.addConstraint(Sketcher.Constraint("Horizontal", index))
411 | self.rename_constraint(c, name)
412 | return c
413 |
414 |
415 | def c_coincident_end_start(self, g1, g2, name=None):
416 | g_prev = self.obj.Geometry[g1]
417 | g_current = self.obj.Geometry[g2]
418 | c_end = GeomEnd if hasattr(g_prev, "EndPoint") else GeomCenter
419 | c_start = GeomStart if hasattr(g_current, "StartPoint") else GeomCenter
420 | c = self.c_coincident(g1, c_end, g2, c_start)
421 | self.rename_constraint(c, name)
422 | return c
423 |
424 |
425 | def c_x_angle(self, index: int, angle: Union[float, Expr], name=None):
426 | return self.c_angle(XAxisIndex, GeomStart, index, GeomStart, angle, name=name)
427 |
428 |
429 | def c_y_angle(self, index, angle: Union[float, Expr], name=None):
430 | return self.c_angle(YAxisIndex, GeomStart, index, GeomStart, angle, name=name)
431 |
432 |
433 | def c_angle(self, g1, g1c, g2, g2c, angle: Union[float, Expr], name=None):
434 | if isinstance(angle, Expr):
435 | value = angle(self.obj)
436 | c = self.obj.addConstraint(Sketcher.Constraint("Angle", g1, g1c, g2, g2c, value))
437 | angle.set_to(self.obj, f'.Constraints[{c}]')
438 | else:
439 | c = self.obj.addConstraint(Sketcher.Constraint("Angle", g1, g1c, g2, g2c, angle))
440 | self.rename_constraint(c, name)
441 | return c
442 |
443 |
444 | def c_length(self, index, length: Union[float, Expr], name=None):
445 | if isinstance(length, Expr):
446 | value = length(self.obj)
447 | c = self.obj.addConstraint(Sketcher.Constraint("Distance", index, value))
448 | length.set_to(self.obj, f'.Constraints[{c}]')
449 | else:
450 | c = self.obj.addConstraint(Sketcher.Constraint("Distance", index, length))
451 | self.rename_constraint(c, name)
452 | return c
453 |
454 |
455 | def c_distance(self, g1, g1p, g2, g2p, dist: Union[float, Expr], name=None):
456 | if isinstance(dist, Expr):
457 | value = dist(self.obj)
458 | c = self.obj.addConstraint(Sketcher.Constraint("Distance", g1, g1p, g2, g2p, value))
459 | dist.set_to(self.obj, f'.Constraints[{c}]')
460 | else:
461 | c = self.obj.addConstraint(Sketcher.Constraint("Distance", g1, g1p, g2, g2p, dist))
462 | self.rename_constraint(c, name)
463 | return c
464 |
465 |
466 | def c_perpendicular(self, g1, g2, name=None):
467 | c = self.obj.addConstraint(Sketcher.Constraint("Perpendicular", g1, g2))
468 | self.rename_constraint(c, name)
469 | return c
470 |
471 |
472 | def c_parallel(self, g1, g2, name=None):
473 | c = self.obj.addConstraint(Sketcher.Constraint("Parallel", g1, g2))
474 | self.rename_constraint(c, name)
475 | return c
476 |
477 |
478 | def _c_xy(self, g, distance, type, anchor=GeomStart, name=None):
479 | if isinstance(distance, Expr):
480 | value = distance(self.obj)
481 | c = self.obj.addConstraint(Sketcher.Constraint(f"Distance{type}", g, anchor, value))
482 | distance.set_to(self.obj, f'.Constraints[{c}]')
483 | else:
484 | c = self.obj.addConstraint(Sketcher.Constraint(f"Distance{type}", g, anchor, distance))
485 | self.rename_constraint(c, name)
486 | return c
487 |
488 |
489 | def c_x(self, g, distance, anchor=GeomStart, name=None):
490 | return self._c_xy(g, distance, 'X', anchor, name)
491 |
492 |
493 | def c_y(self, g, distance, anchor=GeomStart, name=None):
494 | return self._c_xy(g, distance, 'Y', anchor, name)
495 |
496 |
497 | def c_xy(self, g, x, y, anchor=GeomStart, name=None):
498 | c1 = self.c_x(g, x, anchor=anchor, name=None if name is None else f'{name}_x')
499 | c2 = self.c_y(g, y, anchor=anchor, name=None if name is None else f'{name}_y')
500 | return c1, c2
501 |
502 |
503 | def c_point_on_object(self, pnt, obj, name=None):
504 | c = self.obj.addConstraint(Sketcher.Constraint("PointOnObject", pnt, GeomStart, obj))
505 | self.rename_constraint(c, name)
506 | return c
507 |
508 |
509 | def c_equal(self, g1, g2, name=None):
510 | c = self.obj.addConstraint(Sketcher.Constraint("Equal", g1, g2))
511 | self.rename_constraint(c, name)
512 | return c
513 |
514 |
515 | def c_diameter(self, g, diameter, name=None):
516 | if isinstance(diameter, Expr):
517 | value = diameter(self.obj)
518 | c = self.obj.addConstraint(Sketcher.Constraint("Diameter", g, value))
519 | diameter.set_to(self.obj, f'.Constraints[{c}]')
520 | else:
521 | c = self.obj.addConstraint(Sketcher.Constraint("Diameter", g, diameter))
522 | self.rename_constraint(c, name)
523 | return c
524 |
525 |
526 | def c_radius(self, g, radius, name=None):
527 | if isinstance(radius, Expr):
528 | value = radius(self.obj)
529 | c = self.obj.addConstraint(Sketcher.Constraint("Radius", g, value))
530 | radius.set_to(self.obj, f'.Constraints[{c}]')
531 | else:
532 | c = self.obj.addConstraint(Sketcher.Constraint("Radius", g, radius))
533 | self.rename_constraint(c, name)
534 | return c
535 |
536 |
537 | def c_auto_coincident(self, name=None):
538 | g_len = len(self.obj.Geometry)
539 | if g_len > 1:
540 | return self.c_coincident_end_start(g_len-2, g_len-1, name=name)
541 |
542 |
543 | def c_bspline_control_point(self, g, g_c, bspl, bspl_c, weight=1.0):
544 | with self.ref_mode():
545 | geom = self.obj.Geometry[g]
546 | pnt = None
547 | if g_c == GeomStart:
548 | if hasattr(geom, "StartPoint"):
549 | pnt = geom.StartPoint
550 | elif hasattr(geom, "X"):
551 | pnt = Pnt(geom.X, geom.Y, geom.Z)
552 | elif g_c == GeomEnd:
553 | if hasattr(geom, "EndPoint"):
554 | pnt = geom.EndPoint
555 | elif hasattr(geom, "X"):
556 | pnt = Pnt(geom.X, geom.Y, geom.Z)
557 | elif g_c == GeomCenter:
558 | if hasattr(geom, "Center"):
559 | pnt = geom.Center
560 | elif hasattr(geom, "X"):
561 | pnt = Pnt(geom.X, geom.Y, geom.Z)
562 | if pnt is None:
563 | raise RuntimeError(f"Unsupported constraint {g}.{g_c}")
564 |
565 | cpc = self.g_circle_center_radius(pnt, 10)
566 | self.obj.addConstraint(Sketcher.Constraint('Weight', cpc.index, weight))
567 | self.obj.addConstraint(Sketcher.Constraint('Coincident', cpc.index, GeomCenter, g, g_c))
568 | self.obj.addConstraint(Sketcher.Constraint('InternalAlignment:Sketcher::BSplineControlPoint', cpc.index, GeomCenter, bspl, bspl_c))
569 |
570 | # └────────────────────────────────────────────────────────────────────────────┘
571 | # [SECTION] [Sketch/Lookup]
572 | # ┌────────────────────────────────────────────────────────────────────────────┐
573 |
574 | def g_by_name(self, name):
575 | index = self.named_geom.get(name, None)
576 | if index:
577 | return SketchGeom(index, self.obj.Geometry[index], name)
578 |
579 | def g_by_index(self, index):
580 | return SketchGeom(index, self.obj.Geometry[index])
581 |
582 | def set_name(self, name, geom):
583 | if name in self.named_geom:
584 | raise RuntimeError("Duplicated name")
585 | if isinstance(geom, SketchGeom):
586 | self.named_geom[name] = geom.index
587 | else:
588 | self.named_geom[name] = geom
589 |
590 | # └────────────────────────────────────────────────────────────────────────────┘
591 | # [SECTION] [Sketch/Builders]
592 | # ┌────────────────────────────────────────────────────────────────────────────┐
593 |
594 | def create_group(self, auto_coincident=True):
595 | return XSketchGroup(self, auto_coincident)
596 |
597 | # └────────────────────────────────────────────────────────────────────────────┘
598 | # [SECTION] [Sketch/Placement]
599 | # ┌────────────────────────────────────────────────────────────────────────────┐
600 |
601 | def rotate(self, angle):
602 | self.obj.AttachmentOffset = App.Placement(
603 | self.obj.AttachmentOffset.Base,
604 | App.Rotation(self.obj.AttachmentOffset.Rotation.Axis, angle)
605 | )
606 |
607 | def move(self, pos):
608 | self.obj.AttachmentOffset = App.Placement(
609 | to_vec(pos),
610 | self.obj.AttachmentOffset.Rotation
611 | )
612 |
613 | def attach(self, pos, axis, angle):
614 | self.obj.AttachmentOffset = App.Placement(
615 | to_vec(pos),
616 | App.Rotation(to_vec(axis), angle)
617 | )
618 |
619 | # └────────────────────────────────────────────────────────────────────────────┘
620 | # [SECTION] [Sketch/PartDesign]
621 | # ┌────────────────────────────────────────────────────────────────────────────┐
622 |
623 | def pad(self, value, name='Pad', direction=None):
624 | name = f"{self.obj.Name}_{name}"
625 | if self.parent:
626 | feature = self.parent.getObject(name)
627 | if not feature:
628 | feature = self.parent.newObject('PartDesign::Pad', name)
629 | else:
630 | if feature.TypeId != 'PartDesign::Pad':
631 | raise RuntimeError(f'The named object "{name}" already exists but it is not a PartDesign::Pad')
632 | feature.Profile = self.obj
633 | feature.Length = value
634 | if direction:
635 | feature.UseCustomVector = True
636 | feature.Direction = direction
637 | return feature
638 |
639 |
640 | # └────────────────────────────────────────────────────────────────────────────┘
641 | # [SECTION] [Sketch] Group
642 | # ┌────────────────────────────────────────────────────────────────────────────┐
643 |
644 | class XSketchGroup:
645 |
646 | def __init__(self, sketch: XSketch, auto_coincident: bool=True):
647 | self.sketch = sketch
648 | self.begin = None
649 | self.auto_coincident = auto_coincident
650 | self.pos = Pnt(0,0,0)
651 |
652 | def _concat(self, pnt, g, name):
653 | if self.begin is not None and g.index > 0 and self.auto_coincident:
654 | self.sketch.c_auto_coincident()
655 | self.pos = pnt
656 | if self.begin is None:
657 | self.begin = g.index
658 | if name:
659 | self.sketch.set_name(name, g)
660 |
661 | def line_to(self, pnt_, name=None):
662 | pnt = to_vec(pnt_)
663 | g = self.sketch.g_line(self.pos, pnt)
664 | self._concat(pnt, g, name)
665 | return g
666 |
667 | def line(self, dx=0, dy=0, name=None):
668 | return self.line_to(self.pos + Vec(dx, dy, 0), name=name)
669 |
670 | def close(self, keep=False):
671 | if self.begin is not None:
672 | begin = self.sketch.g_by_index(self.begin)
673 | g = self.line_to(begin.obj.StartPoint)
674 | if self.auto_coincident:
675 | self.sketch.c_coincident_end_start(g.index, self.begin)
676 | if not keep:
677 | self.begin = None
678 | return g
679 |
680 | def by_name(self, name):
681 | return self.sketch.g_by_name(name)
682 |
683 | def move_to(self, pnt):
684 | self.pos = to_vec(pnt)
685 | self.begin = None
686 |
687 | def move(self, dx=0, dy=0):
688 | self.pos = self.pos + to_vec((dx, dy))
689 | self.begin = None
690 |
691 | def bspline_to(self, *pnts, name=None):
692 | pnt = to_vec(pnts[-1])
693 | g = self.sketch.g_bspline([self.pos, *pnts])
694 | self._concat(pnt, g, name)
695 | return g
696 |
697 | def bspline(self, *pnts, name=None):
698 | start = self.pos
699 | apnts = []
700 | for pnt in pnts:
701 | start = start + pnt
702 | apnts.append(start)
703 | pnt = to_vec(apnts[-1])
704 | g = self.sketch.g_bspline([self.pos, *apnts])
705 | self._concat(pnt, g, name)
706 | return g
707 |
708 | def circle(self, radius, name=None):
709 | g = self.sketch.g_circle_center_radius(self.pos, radius)
710 | self._concat(self.pos, g, name)
711 | return g
712 |
713 | def point(self, name=None):
714 | g = self.sketch.g_point(self.pos)
715 | self.pos = to_vec(g.obj)
716 | if name:
717 | self.sketch.set_name(name, g)
718 | return g
719 |
720 | def circle_3points(self, p2, p3, name=None):
721 | g = self.sketch.g_circle_3points(self.pos, p2, p3)
722 | self._concat(to_vec(p3), g, name)
723 | return g
724 |
725 | def rect_to(self, pnt):
726 | orig = self.pos
727 | self.move_to(orig)
728 | g = self.line_to((orig[0], pnt[1]))
729 | self.sketch.c_vertical(g.index)
730 | g = self.line_to(pnt)
731 | self.sketch.c_horizontal(g.index)
732 | g = self.line_to((pnt[0], orig[1]))
733 | self.sketch.c_vertical(g.index)
734 | g = self.close()
735 | self.sketch.c_horizontal(g.index)
736 | return g
737 |
738 | def rect(self, w, h, angle=Quantity('0.0 deg')):
739 | if w <= 0:
740 | raise(RuntimeError(f'Invalid w={w} too small'))
741 | if h <= 0:
742 | raise(RuntimeError(f'Invalid h={h} too small'))
743 | orig = self.pos
744 | self.move_to(orig)
745 | g1 = self.line(dx=w)
746 | self.sketch.c_length(g1.index, w)
747 | g2 = self.line(dy=h)
748 | self.sketch.c_length(g2.index, h)
749 | self.sketch.c_angle(g1.index, GeomEnd, g2.index, GeomStart, Quantity('270 deg'))
750 | g3 = self.line(dx=-w)
751 | self.sketch.c_length(g3.index, w)
752 | self.sketch.c_angle(g2.index, GeomEnd, g3.index, GeomStart, Quantity('270 deg'))
753 | g4 = self.close()
754 | self.sketch.c_x_angle(g1.index, angle)
755 | self.sketch.c_xy(g1.index, orig[0], orig[1])
756 | return g4
757 |
758 | def regular_polygon(self, param: any, edges: int, angle=None, constrain_pos: Optional[bool]=None, constrain_size: bool=True):
759 | orig = self.pos
760 | prev_index = len(self.sketch.obj.Geometry) - 1
761 | if isinstance(param, gq.Length):
762 | r = param.value / (2 * math.sin(math.pi/edges))
763 | p2 = orig + Vec(0, r, 0)
764 | elif isinstance(param, gq.Radius):
765 | r = param.value
766 | p2 = orig + Vec(0, r, 0)
767 | elif isinstance(param, gq.Diameter):
768 | r = param.value/2.0
769 | p2 = orig + Vec(0, r, 0)
770 | elif isinstance(param, gq.DeltaVec):
771 | r = param.value.Length
772 | p2 = orig + param.value
773 | elif isinstance(param, (float,int)): # Implies Diameter
774 | r = param/2.0
775 | p2 = orig + Vec(0, r, 0)
776 | else: # Absolute point
777 | p2 = to_vec(param)
778 | r = (orig - p2).Length
779 | g = self.sketch.g_regular_polygon(orig, p2, edges)
780 |
781 | if constrain_pos is True:
782 | self.sketch.c_xy(g.index, orig[0], orig[1], anchor=GeomCenter)
783 | elif constrain_pos is None and self.auto_coincident and prev_index > -1 and self.begin:
784 | self.sketch.c_coincident_end_start(prev_index, g.index)
785 |
786 | if constrain_size:
787 | if isinstance(param, gq.Length):
788 | self.sketch.c_length(g.index-1, param.value)
789 | elif isinstance(param, gq.Radius):
790 | self.sketch.c_radius(g.index, param.value)
791 | elif isinstance(param, gq.Diameter):
792 | self.sketch.c_diameter(g.index, param.value)
793 | elif isinstance(param, gq.DeltaVec):
794 | self.sketch.c_radius(g.index, r)
795 | else:
796 | length = r * (2 * math.sin(math.pi/edges))
797 | self.sketch.c_length(g.index-1, length)
798 |
799 | if angle is not None:
800 | with self.sketch.ref_mode():
801 | seg = self.sketch.g_line(orig, p2)
802 | self.sketch.c_coincident(g.index, GeomCenter, seg.index, GeomStart)
803 | self.sketch.c_coincident(prev_index+1, GeomStart, seg.index, GeomEnd)
804 | self.sketch.c_y_angle(seg.index, angle)
805 |
806 | return g
807 |
808 |
809 | def line_with_length(self, dx=0, dy=0, ref=False, constraint_name=None):
810 | with self.sketch.ref_mode(ref):
811 | if dx == 0 and dy != 0:
812 | length = abs(dy)
813 | elif dx != 0 and dy == 0:
814 | length = abs(dx)
815 | elif dx != 0 and dy != 0:
816 | length = math.sqrt(dx*dx + dy*dy)
817 | else:
818 | raise(RuntimeError(f"Invalid (dx, dy) = ({dx}, {dy})"))
819 | seg = self.line(dx, dy)
820 | self.sketch.c_length(seg.index, length, name=constraint_name)
821 | return seg
822 |
823 |
824 | def rect_rounded(self, w, h, r, angle=Quantity('0.0 deg')):
825 | """
826 | r: int | float => for all corners
827 | (rw, rh) => width and height radious
828 | (tl, tr, br, bl) => custom radius for each corner
829 | """
830 | if w <= 0:
831 | raise(RuntimeError(f'Invalid w={w} too small'))
832 | if h <= 0:
833 | raise(RuntimeError(f'Invalid h={h} too small'))
834 |
835 | BL, BR, TR, TL = 0, 1, 2, 3 # Corners
836 | RW, RH = 0, 1 # Radiuses
837 |
838 | # transform r into ( (blw, blh), (brw, brh), (trw, trh), (tlw, tlh) )
839 | if isinstance(r, (float, int)):
840 | r = ((r,r),)*4
841 | elif len(r) == 1:
842 | r = ((r,r),)*4
843 | elif len(r) == 2:
844 | r = ((r[0],r[1]),)*4
845 | elif len(r) == 4:
846 | r = ((r[0],r[0]),(r[1],r[1]),(r[2],r[2]),(r[3],r[3]))
847 | else:
848 | raise(RuntimeError(f'Invalid r={r}'))
849 |
850 | if any((x[RW] <= 0 or x[RH] <= 0 for x in r)):
851 | raise(RuntimeError(f'Invalid r={r} too small values'))
852 |
853 | if ( (r[BL][RW] + r[BR][RW] > w)
854 | or (r[TR][RW] + r[TL][RW] > w)
855 | or (r[BL][RH] + r[TL][RH] > h)
856 | or (r[BR][RH] + r[TR][RH] > h)):
857 | raise(RuntimeError(f'Invalid r={r} too large values'))
858 |
859 | orig = self.pos
860 |
861 | self.move_to(orig)
862 |
863 | angle180 = Quantity('180 deg')
864 | angle270 = Quantity('270 deg')
865 |
866 | # Bottom
867 | bl = self.line_with_length(dx=r[BL][RW], ref=True)
868 | bottom = self.line_with_length(dx=w - r[BL][RW] - r[BR][RW])
869 | br = self.line_with_length(dx=r[BR][RW], ref=True)
870 |
871 | self.sketch.c_angle(bl.index, GeomEnd, bottom.index, GeomStart, angle180)
872 | self.sketch.c_angle(bottom.index, GeomEnd, br.index, GeomStart, angle180)
873 |
874 | # Right
875 | rb = self.line_with_length(dy=r[BR][RH], ref=True)
876 | right = self.line_with_length(dy=h - r[BR][RH] - r[TR][RH])
877 | rt = self.line_with_length(dy=-r[TR][RH], ref=True)
878 |
879 | self.sketch.c_angle(br.index, GeomEnd, rb.index, GeomStart, angle270)
880 | self.sketch.c_angle(rb.index, GeomEnd, right.index, GeomStart, angle180)
881 | self.sketch.c_angle(right.index, GeomEnd, rt.index, GeomStart, angle180)
882 |
883 | # Top
884 | tr = self.line_with_length(dx=-r[TR][RW], ref=True)
885 | top = self.line_with_length(dx=-w + r[TR][RW] + r[TL][RW])
886 | tl = self.line_with_length(dx=-r[TL][RW], ref=True)
887 |
888 | self.sketch.c_angle(rt.index, GeomEnd, tr.index, GeomStart, angle270)
889 | self.sketch.c_angle(tr.index, GeomEnd, top.index, GeomStart, angle180)
890 | self.sketch.c_angle(top.index, GeomEnd, tl.index, GeomStart, angle180)
891 |
892 | # Left
893 | lt = self.line_with_length(dy=-r[TL][RH], ref=True)
894 | left = self.line_with_length(dy=-h + r[BL][RH] + r[TL][RH])
895 | lb = self.line_with_length(dy=r[BL][RH], ref=True)
896 |
897 | self.sketch.c_angle(tl.index, GeomEnd, lt.index, GeomStart, angle270)
898 | self.sketch.c_angle(lt.index, GeomEnd, left.index, GeomStart, angle180)
899 | self.sketch.c_angle(left.index, GeomEnd, lb.index, GeomStart, angle180)
900 |
901 | # Corner: BL
902 | self.move_to(left.obj.EndPoint)
903 | blc = self.bspline_to(lb.obj.EndPoint, bottom.obj.StartPoint)
904 | self.sketch.c_bspline_control_point(left.index, GeomEnd, blc.index, 0)
905 | self.sketch.c_bspline_control_point(lb.index, GeomEnd, blc.index, 1)
906 | self.sketch.c_bspline_control_point(bottom.index, GeomStart, blc.index, 2)
907 |
908 | # Corner: BR
909 | self.move_to(bottom.obj.EndPoint)
910 | blc = self.bspline_to(br.obj.EndPoint, rb.obj.StartPoint)
911 | self.sketch.c_bspline_control_point(bottom.index, GeomEnd, blc.index, 0)
912 | self.sketch.c_bspline_control_point(br.index, GeomEnd, blc.index, 1)
913 | self.sketch.c_bspline_control_point(right.index, GeomStart, blc.index, 2)
914 |
915 | # Corner: TR
916 | self.move_to(right.obj.EndPoint)
917 | blc = self.bspline_to(rt.obj.EndPoint, top.obj.StartPoint)
918 | self.sketch.c_bspline_control_point(right.index, GeomEnd, blc.index, 0)
919 | self.sketch.c_bspline_control_point(rt.index, GeomEnd, blc.index, 1)
920 | self.sketch.c_bspline_control_point(top.index, GeomStart, blc.index, 2)
921 |
922 | # Corner: TL
923 | self.move_to(top.obj.EndPoint)
924 | blc = self.bspline_to(tl.obj.EndPoint, left.obj.StartPoint)
925 | self.sketch.c_bspline_control_point(top.index, GeomEnd, blc.index, 0)
926 | self.sketch.c_bspline_control_point(tl.index, GeomEnd, blc.index, 1)
927 | self.sketch.c_bspline_control_point(left.index, GeomStart, blc.index, 2)
928 |
929 | # Pos
930 | self.sketch.c_xy(bl.index, orig[0], orig[1])
931 |
932 | # Angle
933 | self.sketch.c_x_angle(bottom.index, angle)
934 |
935 | return blc
936 |
937 |
938 | # └────────────────────────────────────────────────────────────────────────────┘
939 | # [SECTION] [PartDesign] Body
940 | # ┌────────────────────────────────────────────────────────────────────────────┐
941 |
942 |
943 | class XBody:
944 | """PartDesign Body builder"""
945 |
946 | def __init__(self, name='XBody'):
947 | """Create or reuse a PartDesign Body"""
948 | self.obj = App.ActiveDocument.getObject(name)
949 | if not self.obj:
950 | self.obj = App.ActiveDocument.addObject('PartDesign::Body', name)
951 | else:
952 | if self.obj.TypeId != 'PartDesign::Body':
953 | raise RuntimeError(f'The named object "{name}" already exists but it is not a PartDesign::Body')
954 |
955 |
956 | @property
957 | def name(self):
958 | return self.obj.Name
959 |
960 |
961 | def sketch(self, name:str='XSketch', plane:str='XY', reversed:bool=False, pos:Vec=Vec(0,0,0), rot:Rotation=Rotation(0,0,0)):
962 | """Sketch builder."""
963 | support = find_obj_origin_plane(self.obj, plane)
964 | return self.sketch_on([(support, '')], name=name, reversed=reversed, pos=pos, rot=rot)
965 |
966 |
967 | def sketch_on(self, support, mode='FlatFace', parameter=0.0, name:str='XSketch', reversed:bool=False, pos:Vec=Vec(0,0,0), rot:Rotation=Rotation(0,0,0)):
968 | """Sketch builder."""
969 | xsketch = XSketch(name, parent=self.obj)
970 | sketch = xsketch.obj
971 | sketch.AttachmentOffset = App.Placement(pos, rot)
972 | sketch.MapReversed = reversed
973 | sketch.Support = support
974 | sketch.MapPathParameter = parameter
975 | sketch.MapMode = mode
976 | return xsketch
977 |
978 |
979 | def fillet(self, edges: list, name='Fillet'):
980 | name = f"{self.name}_{name}"
981 | fillet = App.ActiveDocument.getObject(name)
982 | if not fillet:
983 | fillet = App.ActiveDocument.addObject('Part::Fillet', name)
984 | fillet.Base = self.obj
985 | fillet.Edges = [*edges]
986 | Gui.ActiveDocument.getObject(self.name).Visibility = False
987 |
988 |
989 | # └────────────────────────────────────────────────────────────────────────────┘
990 | # [SECTION] [FeaturePython] Data Object
991 | # ┌────────────────────────────────────────────────────────────────────────────┐
992 |
993 | class DataObject:
994 | """Document Object with properties"""
995 |
996 | def __init__(self, name="Data"):
997 | obj = App.ActiveDocument.getObject(name)
998 | if not obj:
999 | obj = App.ActiveDocument.addObject('App::FeaturePython', name)
1000 | else:
1001 | if obj.TypeId != 'App::FeaturePython':
1002 | raise RuntimeError(f'The named object "{name}" already exists but it is not of type App::FeaturePython')
1003 | super().__setattr__('obj', obj)
1004 |
1005 | def add_property(self, property_type, name, section="Properties", docs="", mode=0):
1006 | try:
1007 | self.obj.addProperty(property_type, name, section, docs, mode)
1008 | except:
1009 | pass # Ignore if already exists
1010 |
1011 | def __setattr__(self, __name: str, __value: Any) -> None:
1012 | if hasattr(self, __name):
1013 | super().__setattr__(__name, __value)
1014 | else:
1015 | if isinstance(__value, Expr):
1016 | self.obj.__setattr__(__name, __value(self.obj))
1017 | __value.set_to(self.obj, __name)
1018 | else:
1019 | self.obj.setExpression(__name, None)
1020 | self.obj.__setattr__(__name, __value)
1021 |
1022 | def __getattr__(self, __name: str) -> Any:
1023 | return self.obj.__getattr(__name)
1024 |
1025 | # └────────────────────────────────────────────────────────────────────────────┘
1026 | # [SECTION] [GUI] Imports
1027 | # ┌────────────────────────────────────────────────────────────────────────────┐
1028 |
1029 | from PySide import QtCore, QtGui
1030 |
1031 | # └────────────────────────────────────────────────────────────────────────────┘
1032 | # [SECTION] [GUI] Aliases
1033 | # ┌────────────────────────────────────────────────────────────────────────────┐
1034 |
1035 | Icon = QtGui.QIcon
1036 | Image = QtGui.QPixmap
1037 |
1038 | # └────────────────────────────────────────────────────────────────────────────┘
1039 | # [SECTION] [GUI] Globals
1040 | # ┌────────────────────────────────────────────────────────────────────────────┐
1041 |
1042 | ThreadLocalGuiVars = threading.local() # Store GUI State per thread
1043 |
1044 |
1045 | # └────────────────────────────────────────────────────────────────────────────┘
1046 | # [SECTION] [GUI] Utils
1047 | # ┌────────────────────────────────────────────────────────────────────────────┐
1048 |
1049 | def set_qt_attrs(qt_object, **kwargs):
1050 | """Call setters on QT objects by argument names."""
1051 | for name, value in kwargs.items():
1052 | if value is not None:
1053 | setter = getattr(qt_object, f'set{name[0].upper()}{name[1:]}', None)
1054 | if setter:
1055 | if isinstance(value, tuple):
1056 | setter(*value)
1057 | else:
1058 | setter(value)
1059 |
1060 |
1061 | def setup_layout(layout, add=True, **kwargs):
1062 | """Setup layouts adding wrapper widget if required."""
1063 | set_qt_attrs(layout, **kwargs)
1064 | parent = build_context().current()
1065 | if parent.layout() is not None or add is False:
1066 | w = QtGui.QWidget()
1067 | w.setLayout(layout)
1068 | if add:
1069 | parent.layout().addWidget(w)
1070 | with build_context().stack(w):
1071 | yield w
1072 | else:
1073 | parent.setLayout(layout)
1074 | yield parent
1075 |
1076 |
1077 | def place_widget(ed, label=None, stretch=0, alignment=QtCore.Qt.Alignment()):
1078 | """Place widget in layout."""
1079 | layout = build_context().current().layout()
1080 | if layout is None:
1081 | layout = QtGui.QVBoxLayout()
1082 | build_context().current().setLayout(layout)
1083 | if label is None:
1084 | build_context().current().layout().addWidget(ed, stretch, alignment)
1085 | else:
1086 | w = QtGui.QWidget()
1087 | parent = QtGui.QHBoxLayout()
1088 | parent.addWidget(QtGui.QLabel(label))
1089 | parent.addWidget(ed)
1090 | w.setLayout(parent)
1091 | build_context().current().layout().addWidget(w, stretch, alignment)
1092 |
1093 |
1094 | class PySignal:
1095 | """Imitate Qt Signals for non QObject objects"""
1096 |
1097 | def __init__(self):
1098 | self._listeners = []
1099 |
1100 | def connect(self, listener):
1101 | self._listeners.append(listener)
1102 |
1103 | def trigger(self, *args, **kwargs):
1104 | for listener in self._listeners:
1105 | listener(*args, **kwargs)
1106 |
1107 |
1108 | #: Decorator
1109 | def on_event(target, event=None):
1110 | """Decorator: Event binder"""
1111 | if event is None:
1112 | def deco(fn):
1113 | target.connect(fn)
1114 | return fn
1115 | else:
1116 | def deco(fn):
1117 | getattr(target, event).connect(fn)
1118 | return fn
1119 | return deco
1120 |
1121 |
1122 | # └────────────────────────────────────────────────────────────────────────────┘
1123 | # [SECTION] [GUI] Selection
1124 | # ┌────────────────────────────────────────────────────────────────────────────┐
1125 |
1126 | class SelectedObject:
1127 | """Store Selection information"""
1128 |
1129 | def __init__(self, doc, obj, sub=None, pnt=None):
1130 | self.doc = doc
1131 | self.obj = obj
1132 | self.sub = sub
1133 | self.pnt = pnt
1134 |
1135 | def __iter__(self):
1136 | yield App.getDocument(self.doc).getObject(self.obj)
1137 | yield self.sub
1138 | yield self.pnt
1139 |
1140 | def __repr__(self) -> str:
1141 | return f"{self.doc}#{self.obj}.{self.sub}"
1142 |
1143 | def __hash__(self) -> int:
1144 | return hash((self.doc, self.obj, self.sub))
1145 |
1146 | def __eq__(self, __o: object) -> bool:
1147 | return hash(self) == hash(__o)
1148 |
1149 | def __ne__(self, __o: object) -> bool:
1150 | return not self.__eq__(__o)
1151 |
1152 |
1153 | def register_select_observer(owner: QtGui.QWidget, observer):
1154 | """Add observer with auto remove on owner destroyed"""
1155 | Gui.Selection.addObserver(observer)
1156 | def destroyed(_):
1157 | Gui.Selection.removeObserver(observer)
1158 | owner.destroyed.connect(destroyed)
1159 |
1160 |
1161 | @contextmanager
1162 | def selection(*names, clean=True):
1163 | sel = Gui.Selection
1164 | try:
1165 | doc = App.ActiveDocument.Name
1166 | if len(names) == 0:
1167 | yield sel.getSelection(doc)
1168 | else:
1169 | sel.clearSelection()
1170 | for name in names:
1171 | if isinstance(name, (tuple, list)):
1172 | sel.addSelection(doc, *name)
1173 | elif isinstance(name, SelectedObject):
1174 | sel.addSelection(name.doc, name.obj, name.sub)
1175 | else:
1176 | sel.addSelection(doc, name)
1177 | yield sel.getSelection(doc)
1178 | finally:
1179 | if clean:
1180 | sel.clearSelection()
1181 |
1182 |
1183 | # └────────────────────────────────────────────────────────────────────────────┘
1184 | # [SECTION] [GUI] Build Context
1185 | # ┌────────────────────────────────────────────────────────────────────────────┐
1186 |
1187 | def build_context():
1188 | bc = getattr(ThreadLocalGuiVars, 'BuildContext', None)
1189 | if bc is None:
1190 | ThreadLocalGuiVars.BuildContext = _BuildContext()
1191 | return ThreadLocalGuiVars.BuildContext
1192 | else:
1193 | return bc
1194 |
1195 | class _BuildContext:
1196 | def __init__(self):
1197 | self._stack = []
1198 |
1199 | def push(self, widget):
1200 | self._stack.append(widget)
1201 |
1202 | def pop(self):
1203 | self._stack.pop()
1204 |
1205 | @contextmanager
1206 | def stack(self, widget):
1207 | self.push(widget)
1208 | try:
1209 | yield widget
1210 | finally:
1211 | self.pop()
1212 |
1213 | @contextmanager
1214 | def parent(self):
1215 | if len(self._stack) > 1:
1216 | current = self._stack[-1]
1217 | self._stack.pop()
1218 | parent = self._stack[-1]
1219 | try:
1220 | yield parent
1221 | finally:
1222 | self._stack.append(current)
1223 |
1224 | def current(self):
1225 | return self._stack[-1]
1226 |
1227 | def dump(self):
1228 | print(f"BuildContext: {self._stack}")
1229 |
1230 | @contextmanager
1231 | def Parent():
1232 | """Put parent on top of BuildContext"""
1233 | with build_context().parent() as p:
1234 | yield p
1235 |
1236 |
1237 | # └────────────────────────────────────────────────────────────────────────────┘
1238 | # [SECTION] [GUI] [Widget] Dialog
1239 | # ┌────────────────────────────────────────────────────────────────────────────┐
1240 |
1241 | class Dialogs:
1242 | _list = []
1243 |
1244 | @classmethod
1245 | def dump(cls):
1246 | print(f"Dialogs: {cls._list}")
1247 |
1248 | @classmethod
1249 | def register(cls, dialog):
1250 | cls._list.append(dialog)
1251 | dialog.closeEvent = lambda e: cls.destroy_dialog(dialog)
1252 |
1253 | @classmethod
1254 | def destroy_dialog(cls, dlg):
1255 | cls._list.remove(dlg)
1256 | dlg.deleteLater()
1257 |
1258 |
1259 | @contextmanager
1260 | def Dialog(title=None, size=None, show=True, parent=None):
1261 | if parent is None:
1262 | w = QtGui.QDialog(parent=Gui.getMainWindow())
1263 | else:
1264 | w = QtGui.QWidget(parent=parent)
1265 | if title is not None:
1266 | w.setWindowTitle(title)
1267 | with build_context().stack(w):
1268 | yield w
1269 | if show:
1270 | Dialogs.register(w)
1271 | w.show()
1272 | if isinstance(size, (tuple,list)):
1273 | w.resize(size[0], size[1])
1274 | else:
1275 | w.adjustSize()
1276 |
1277 |
1278 | # └────────────────────────────────────────────────────────────────────────────┘
1279 | # [SECTION] [GUI] [Widget] GroupBox
1280 | # ┌────────────────────────────────────────────────────────────────────────────┐
1281 |
1282 | @contextmanager
1283 | def GroupBox(title=None):
1284 | w = QtGui.QGroupBox()
1285 | if title:
1286 | w.setTitle(title)
1287 | place_widget(w)
1288 | with build_context().stack(w):
1289 | yield w
1290 |
1291 |
1292 | # └────────────────────────────────────────────────────────────────────────────┘
1293 | # [SECTION] [GUI] [Widget] Stretch
1294 | # ┌────────────────────────────────────────────────────────────────────────────┐
1295 |
1296 | def Stretch(stretch=0):
1297 | """Add Layout spacer"""
1298 | layout = build_context().current().layout()
1299 | if layout:
1300 | layout.addStretch(stretch)
1301 |
1302 |
1303 | # └────────────────────────────────────────────────────────────────────────────┘
1304 | # [SECTION] [GUI] [Widget] TabContainer
1305 | # ┌────────────────────────────────────────────────────────────────────────────┐
1306 |
1307 | @contextmanager
1308 | def TabContainer(**kwargs):
1309 | w = QtGui.QTabWidget()
1310 | set_qt_attrs(w, **kwargs)
1311 | place_widget(w)
1312 | with build_context().stack(w):
1313 | yield w
1314 |
1315 |
1316 | # └────────────────────────────────────────────────────────────────────────────┘
1317 | # [SECTION] [GUI] [Widget] Tab
1318 | # ┌────────────────────────────────────────────────────────────────────────────┐
1319 |
1320 | @contextmanager
1321 | def Tab(title:str, icon=None):
1322 | w = QtGui.QWidget()
1323 | with build_context().stack(w):
1324 | yield w
1325 | if icon:
1326 | build_context().current().addTab(w, title, icon)
1327 | else:
1328 | build_context().current().addTab(w, title)
1329 |
1330 |
1331 | # └────────────────────────────────────────────────────────────────────────────┘
1332 | # [SECTION] [GUI] [Layout] Col
1333 | # ┌────────────────────────────────────────────────────────────────────────────┐
1334 |
1335 | @contextmanager
1336 | def Col(add=True, **kwargs):
1337 | """Vertical Layout"""
1338 | yield from setup_layout(QtGui.QVBoxLayout(), add=add, **kwargs)
1339 |
1340 |
1341 | # └────────────────────────────────────────────────────────────────────────────┘
1342 | # [SECTION] [GUI] [Layout] Row
1343 | # ┌────────────────────────────────────────────────────────────────────────────┐
1344 |
1345 | @contextmanager
1346 | def Row(add=True, **kwargs):
1347 | """Horizontal Layout"""
1348 | yield from setup_layout(QtGui.QHBoxLayout(), add=add, **kwargs)
1349 |
1350 |
1351 | # └────────────────────────────────────────────────────────────────────────────┘
1352 | # [SECTION] [GUI] [Widget] TextLabel
1353 | # ┌────────────────────────────────────────────────────────────────────────────┐
1354 |
1355 | def TextLabel(text="", stretch=0, alignment=QtCore.Qt.Alignment(), **kwargs):
1356 | label = QtGui.QLabel(text)
1357 | set_qt_attrs(label, **kwargs)
1358 | place_widget(label, stretch=stretch, alignment=alignment)
1359 | return label
1360 |
1361 |
1362 | # └────────────────────────────────────────────────────────────────────────────┘
1363 | # [SECTION] [GUI] [Widget] InputFloat
1364 | # ┌────────────────────────────────────────────────────────────────────────────┐
1365 |
1366 | def InputFloat(name=None, min=0.0, max=sys.float_info.max, decimals=6,
1367 | step=0.01, label=None, value=0.0, stretch=0,
1368 | alignment=QtCore.Qt.Alignment(), **kwargs):
1369 | editor = QtGui.QDoubleSpinBox()
1370 | editor.setMinimum(min)
1371 | editor.setMaximum(max)
1372 | editor.setSingleStep(step)
1373 | editor.setDecimals(decimals)
1374 | editor.setValue(value)
1375 | set_qt_attrs(editor, **kwargs)
1376 | if name:
1377 | editor.setObjectName(name)
1378 | place_widget(editor, label=label, stretch=stretch, alignment=alignment)
1379 | return editor
1380 |
1381 |
1382 | # └────────────────────────────────────────────────────────────────────────────┘
1383 | # [SECTION] [GUI] [Widget] InputInt
1384 | # ┌────────────────────────────────────────────────────────────────────────────┐
1385 |
1386 | class InputTextWidget(QtGui.QLineEdit):
1387 | def __init__(self, *args, **kwargs):
1388 | super().__init__(*args, **kwargs)
1389 | def value(self):
1390 | return self.text()
1391 | def setValue(self, value):
1392 | self.setText(str(value))
1393 |
1394 | def InputText(name=None, label=None, value="",
1395 | stretch=0, alignment=QtCore.Qt.Alignment(), **kwargs):
1396 | editor = InputTextWidget()
1397 | editor.setText(value)
1398 | set_qt_attrs(editor, **kwargs)
1399 | if name:
1400 | editor.setObjectName(name)
1401 | place_widget(editor, label=label, stretch=stretch, alignment=alignment)
1402 | return editor
1403 |
1404 |
1405 | # └────────────────────────────────────────────────────────────────────────────┘
1406 | # [SECTION] [GUI] [Widget] InputInt
1407 | # ┌────────────────────────────────────────────────────────────────────────────┐
1408 |
1409 | def InputInt(name=None, min=0, max=2^31, step=1, label=None, value=0,
1410 | stretch=0, alignment=QtCore.Qt.Alignment(), **kwargs):
1411 | editor = QtGui.QSpinBox()
1412 | editor.setMinimum(min)
1413 | editor.setMaximum(max)
1414 | editor.setSingleStep(step)
1415 | editor.setValue(value)
1416 | set_qt_attrs(editor, **kwargs)
1417 | if name:
1418 | editor.setObjectName(name)
1419 | place_widget(editor, label=label, stretch=stretch, alignment=alignment)
1420 | return editor
1421 |
1422 |
1423 | # └────────────────────────────────────────────────────────────────────────────┘
1424 | # [SECTION] [GUI] [Widget] InputBoolean
1425 | # ┌────────────────────────────────────────────────────────────────────────────┐
1426 |
1427 | class QCheckBoxExt(QtGui.QCheckBox):
1428 | def __init__(self, *args, **kwargs):
1429 | super().__init__(*args, **kwargs)
1430 |
1431 | def value(self) -> bool:
1432 | return self.checkState() == QtCore.Qt.Checked
1433 |
1434 | def setValue(self, value : bool):
1435 | self.setCheckState(QtCore.Qt.Checked if value else QtCore.Qt.Unchecked)
1436 |
1437 |
1438 | def InputBoolean(name=None, label=None, value=False, stretch=0,
1439 | alignment=QtCore.Qt.Alignment(), **kwargs):
1440 | editor = QCheckBoxExt()
1441 | editor.setValue(value)
1442 | set_qt_attrs(editor, **kwargs)
1443 | if name:
1444 | editor.setObjectName(name)
1445 | place_widget(editor, label=label, stretch=stretch, alignment=alignment)
1446 | return editor
1447 |
1448 | # └────────────────────────────────────────────────────────────────────────────┘
1449 | # [SECTION] [GUI] [Widget] InputVector
1450 | # ┌────────────────────────────────────────────────────────────────────────────┐
1451 |
1452 | class InputVectorWrapper:
1453 | def __init__(self, g, x, y, z):
1454 | self.group = g
1455 | self.x = x
1456 | self.y = y
1457 | self.z = z
1458 |
1459 | def value(self) -> Vec:
1460 | return Vec(self.x.value(), self.y.value(), self.z.value())
1461 |
1462 | def setValue(self, value):
1463 | v = to_vec(value)
1464 | self.x.setValue(v.x)
1465 | self.y.setValue(v.y)
1466 | self.z.setValue(v.z)
1467 |
1468 | def InputVector(label=None, value=(0.0,0.0,0.0)):
1469 | with GroupBox(title=label) as g:
1470 | with Col():
1471 | x = InputFloat(label="X:")
1472 | y = InputFloat(label="Y:")
1473 | z = InputFloat(label="Z:")
1474 | widget = InputVectorWrapper(g, x, y, z)
1475 | widget.setValue(value)
1476 | return widget
1477 |
1478 |
1479 | # └────────────────────────────────────────────────────────────────────────────┘
1480 | # [SECTION] [GUI] [Widget] InputOptions
1481 | # ┌────────────────────────────────────────────────────────────────────────────┐
1482 |
1483 | class InputOptionsWrapper:
1484 | def __init__(self, combobox:QtGui.QComboBox, data: Dict[str,Any]):
1485 | self.combobox = combobox
1486 | self.index = dict()
1487 | self.lookup = dict()
1488 | i = 0
1489 | for label, value in data.items():
1490 | self.index[i] = value
1491 | self.lookup[value] = i
1492 | i += 1
1493 | combobox.addItem(label)
1494 |
1495 | def value(self):
1496 | return self.index.get(self.combobox.currentIndex(), None)
1497 |
1498 | def setValue(self, value):
1499 | index = self.lookup.get(value, None)
1500 | if index is not None:
1501 | self.combobox.setCurrentIndex(index)
1502 |
1503 | def InputOptions(options, value=None, label=None, name=None, stretch=0,
1504 | alignment=QtCore.Qt.Alignment(), **kwargs):
1505 | widget = QtGui.QComboBox()
1506 | set_qt_attrs(widget, **kwargs)
1507 | editor = InputOptionsWrapper(widget, options)
1508 | if value is not None:
1509 | editor.setValue(value)
1510 | if name:
1511 | editor.combobox.setObjectName(name)
1512 | place_widget(editor.combobox, label=label, stretch=stretch, alignment=alignment)
1513 | return editor
1514 |
1515 |
1516 | # └────────────────────────────────────────────────────────────────────────────┘
1517 | # [SECTION] [GUI] [Widget] InputSelectOne
1518 | # ┌────────────────────────────────────────────────────────────────────────────┐
1519 |
1520 | class InputSelectOne:
1521 |
1522 | def __init__(self, label=None, name=None, active=False, auto_deactivate=True):
1523 | self._value = None
1524 | self._pre = None
1525 | self._auto_deactivate = auto_deactivate
1526 | self.selected = PySignal()
1527 | with Row(add=False, spacing=0, margin=0, contentsMargins=(0,0,0,0)) as ctl:
1528 |
1529 | @button(
1530 | text="Select...",
1531 | tool=True,
1532 | checkable=True,
1533 | styleSheet="QToolButton:checked{background-color: #FF0000; color:#FFFFFF;}",
1534 | focusPolicy=QtCore.Qt.FocusPolicy.NoFocus,
1535 | objectName=name,
1536 | checked=active)
1537 | def select(): pass
1538 |
1539 | @button(
1540 | tool=True,
1541 | focusPolicy=QtCore.Qt.FocusPolicy.NoFocus,
1542 | icon=Icon(':icons/edit-cleartext.svg'))
1543 | def clear(): self.setValue(None)
1544 |
1545 | display = QtGui.QLineEdit()
1546 | display.setReadOnly(True)
1547 | place_widget(display)
1548 |
1549 | self.display = display
1550 | self.button = select
1551 | register_select_observer(select, self)
1552 |
1553 | with Parent():
1554 | place_widget(ctl, label=label)
1555 |
1556 | @property
1557 | def active(self) -> bool:
1558 | return self.button.isChecked()
1559 |
1560 | def value(self) -> Optional[SelectedObject]:
1561 | return self._value
1562 |
1563 | def pre(self) -> Optional[SelectedObject]:
1564 | return self._pre
1565 |
1566 | def setValue(self, value: Optional[SelectedObject]) -> None:
1567 | self._value = value
1568 | if value:
1569 | self.display.setText(f"{value.doc}#{value.obj}.{value.sub}")
1570 | if self._auto_deactivate:
1571 | self.button.setChecked(False)
1572 | self.selected.trigger(self._value)
1573 | else:
1574 | self.display.setText(f"")
1575 |
1576 | def setPreselection(self, doc, obj, sub):
1577 | if self.button.isChecked():
1578 | self._pre = SelectedObject(doc, obj, sub)
1579 |
1580 | def addSelection(self, doc, obj, sub, pnt):
1581 | if self.button.isChecked():
1582 | self.setValue(SelectedObject(doc, obj, sub, pnt))
1583 |
1584 | def removeSelection(self, doc, obj, sub):
1585 | if self.button.isChecked():
1586 | if self._value:
1587 | v = self._value
1588 | if (v.doc, v.obj) == (doc, obj):
1589 | self.setValue(None)
1590 |
1591 | def setSelection(self, doc):
1592 | if self.button.isChecked():
1593 | self.setValue(SelectedObject(doc, Gui.Selection.getSelection()[-1].Name))
1594 |
1595 | def clearSelection(self, doc):
1596 | pass
1597 |
1598 |
1599 | # └────────────────────────────────────────────────────────────────────────────┘
1600 | # [SECTION] [GUI] [Widget] InputSelectMany
1601 | # ┌────────────────────────────────────────────────────────────────────────────┐
1602 |
1603 | class InputSelectMany:
1604 |
1605 | ValueDataRole = QtCore.Qt.UserRole
1606 |
1607 | def __init__(self, label=None, name=None, active=False):
1608 | self._value = set()
1609 | self.selected = PySignal()
1610 | with Col(add=False, spacing=0, margin=0, contentsMargins=(0,0,0,0)) as ctl:
1611 | with Row(spacing=0, margin=0, contentsMargins=(0,0,0,0)):
1612 | @button(
1613 | text="Add",
1614 | alignment=QtCore.Qt.AlignLeft,
1615 | tool=True,
1616 | checkable=True,
1617 | styleSheet="QToolButton:checked{background-color: #FF0000; color:#FFFFFF;}",
1618 | focusPolicy=QtCore.Qt.FocusPolicy.NoFocus,
1619 | objectName=name,
1620 | checked=active)
1621 | def select(): pass
1622 |
1623 | @button(
1624 | text="Remove",
1625 | tool=True,
1626 | alignment=QtCore.Qt.AlignLeft,
1627 | focusPolicy=QtCore.Qt.FocusPolicy.NoFocus)
1628 | def remove():
1629 | selected = self.display.selectedItems()
1630 | for item in selected:
1631 | value = item.data(0, InputSelectMany.ValueDataRole)
1632 | self._value.remove(value)
1633 | self.display.takeTopLevelItem(self.display.indexOfTopLevelItem(item))
1634 |
1635 | @button(
1636 | text="Clean",
1637 | tool=True,
1638 | alignment=QtCore.Qt.AlignLeft,
1639 | focusPolicy=QtCore.Qt.FocusPolicy.NoFocus,
1640 | icon=Icon(':icons/edit-cleartext.svg'))
1641 | def clear():
1642 | self._value.clear()
1643 | self.display.clear()
1644 |
1645 | Stretch()
1646 |
1647 | display = QtGui.QTreeWidget()
1648 | display.setColumnCount(2)
1649 | display.setHeaderLabels(['Object', 'SubObject'])
1650 | place_widget(display)
1651 |
1652 | self.display = display
1653 | self.button = select
1654 | register_select_observer(select, self)
1655 |
1656 | with Parent():
1657 | with GroupBox(title=label):
1658 | place_widget(ctl)
1659 |
1660 | @property
1661 | def active(self) -> bool:
1662 | return self.button.isChecked()
1663 |
1664 | def value(self) -> List[SelectedObject]:
1665 | return self._value
1666 |
1667 | def addValue(self, value: SelectedObject) -> None:
1668 | if value not in self._value:
1669 | item = QtGui.QTreeWidgetItem([value.obj, value.sub])
1670 | item.setData(0, InputSelectMany.ValueDataRole, value)
1671 | self.display.addTopLevelItem(item)
1672 | self._value.add(value)
1673 | self.selected.trigger(value)
1674 |
1675 | def setPreselection(self, doc, obj, sub):
1676 | pass
1677 |
1678 | def addSelection(self, doc, obj, sub, pnt):
1679 | if self.button.isChecked():
1680 | self.addValue(SelectedObject(doc, obj, sub, pnt))
1681 |
1682 | def removeSelection(self, doc, obj, sub):
1683 | pass
1684 |
1685 | def setSelection(self, doc):
1686 | if self.button.isChecked():
1687 | self.addValue(SelectedObject(doc, Gui.Selection.getSelection()[-1].Name))
1688 |
1689 | def clearSelection(self, doc):
1690 | pass
1691 |
1692 |
1693 | # └────────────────────────────────────────────────────────────────────────────┘
1694 | # [SECTION] [GUI] [Widget] button
1695 | # ┌────────────────────────────────────────────────────────────────────────────┐
1696 |
1697 | def button(label=None, add:bool=True, tool:bool=False, stretch=0, alignment=QtCore.Qt.Alignment(), **kwargs):
1698 | if tool:
1699 | btn = QtGui.QToolButton()
1700 | else:
1701 | btn = QtGui.QPushButton()
1702 | set_qt_attrs(btn, **kwargs)
1703 | if label:
1704 | btn.setText(label)
1705 | elif 'text' not in kwargs:
1706 | btn.setText("Button")
1707 | if add:
1708 | place_widget(btn, stretch=stretch, alignment=alignment)
1709 | def wrapper(handler):
1710 | btn.clicked.connect(handler)
1711 | return btn
1712 | return wrapper
1713 |
1714 |
1715 | # └────────────────────────────────────────────────────────────────────────────┘
1716 | # [SECTION] [GUI] progress_indicator
1717 | # ┌────────────────────────────────────────────────────────────────────────────┐
1718 |
1719 | class ProgressIndicator:
1720 | def __init__(self, *args, **kwargs) -> None:
1721 | try:
1722 | self.control = Base.ProgressIndicator(*args, **kwargs)
1723 | except:
1724 | self.control = None
1725 | def start(self, *args, **kwargs):
1726 | if self.control:
1727 | self.control.start(*args, **kwargs)
1728 | def next(self, *args, **kwargs):
1729 | if self.control:
1730 | self.control.next(*args, **kwargs)
1731 | def stop(self, *args, **kwargs):
1732 | if self.control:
1733 | self.control.stop(*args, **kwargs)
1734 |
1735 | @contextmanager
1736 | def progress_indicator(message: str = "Working...", steps: int = 0):
1737 | bar = ProgressIndicator()
1738 | bar.start(message, steps)
1739 | try:
1740 | yield bar
1741 | finally:
1742 | bar.stop()
1743 | del bar
1744 |
1745 |
1746 | # └────────────────────────────────────────────────────────────────────────────┘
1747 | # [SECTION] [GUI] Message Boxes
1748 | # ┌────────────────────────────────────────────────────────────────────────────┐
1749 |
1750 | def show_msgbox(message, title="Information", std_icon=QtGui.QMessageBox.Information, std_buttons=QtGui.QMessageBox.NoButton, parent=None):
1751 | diag = QtGui.QMessageBox(std_icon, title, message, std_buttons, parent)
1752 | diag.setWindowModality(QtCore.Qt.ApplicationModal)
1753 | diag.exec_()
1754 |
1755 |
1756 | def show_warning(message, title="Warning", std_icon=QtGui.QMessageBox.Warning, std_buttons=QtGui.QMessageBox.NoButton, parent=None):
1757 | show_msgbox(message, title, std_icon, std_buttons, parent)
1758 |
1759 |
1760 | def show_error(message, title="Error", std_icon=QtGui.QMessageBox.Critical, std_buttons=QtGui.QMessageBox.NoButton, parent=None):
1761 | show_msgbox(message, title, std_icon, std_buttons, parent)
1762 |
1763 |
1764 | show_info = show_msgbox
1765 |
1766 |
1767 | # └────────────────────────────────────────────────────────────────────────────┘
1768 | # [SECTION] [TEST]
1769 | # ┌────────────────────────────────────────────────────────────────────────────┐
1770 |
1771 |
--------------------------------------------------------------------------------
/freecad/fcscript/v_0_0_2.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2022 Frank David Martinez M.
4 | #
5 | # This file is part of FCScript.
6 | #
7 | # FCScript is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # Utils is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with FCScript. If not, see .
19 | #
20 |
21 | # └────────────────────────────────────────────────────────────────────────────┘
22 | # [SECTION] Common Builtin Imports
23 | # ┌────────────────────────────────────────────────────────────────────────────┐
24 |
25 | from cmath import exp
26 | from contextlib import contextmanager
27 | from enum import Enum
28 | from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
29 | from pathlib import Path
30 | import math
31 | import sys
32 | import threading
33 | import json
34 |
35 | try:
36 | from typing import Protocol, runtime_checkable
37 | except:
38 | from typing_extensions import Protocol, runtime_checkable
39 |
40 |
41 | # └────────────────────────────────────────────────────────────────────────────┘
42 | # [SECTION] FreeCAD Imports
43 | # ┌────────────────────────────────────────────────────────────────────────────┐
44 |
45 | from FreeCAD import Base
46 | import FreeCAD as App
47 | import Part
48 | import Sketcher
49 | from ProfileLib import RegularPolygon
50 | import FreeCADGui as Gui
51 |
52 |
53 | # └────────────────────────────────────────────────────────────────────────────┘
54 | # [SECTION] [Lang] Typing
55 | # ┌────────────────────────────────────────────────────────────────────────────┐
56 |
57 | @runtime_checkable
58 | class ObjectWithOrigin(Protocol):
59 | @property
60 | def Origin(self) -> App.DocumentObject:
61 | ...
62 |
63 |
64 | # └────────────────────────────────────────────────────────────────────────────┘
65 | # [SECTION] [FreeCAD] Aliases
66 | # ┌────────────────────────────────────────────────────────────────────────────┐
67 |
68 | #: Point/Vector alias
69 | Pnt = Base.Vector
70 |
71 | #: Vector alias
72 | Vec = Base.Vector
73 |
74 | #: App.Rotation alias
75 | Rotation = App.Rotation
76 |
77 | #: Quantity converter from string
78 | Quantity = App.Units.Quantity
79 |
80 | #: Commands
81 | command = Gui.runCommand
82 |
83 | # └────────────────────────────────────────────────────────────────────────────┘
84 | # [SECTION] [Constants] Common constants
85 | # ┌────────────────────────────────────────────────────────────────────────────┐
86 |
87 | #: Geometry start point (except circles or ellipses)
88 | GeomStart = 1
89 |
90 | #: Geometry end point (except circles or ellipses)
91 | GeomEnd = 2
92 |
93 | #: Geometry center point (only circles and ellipses)
94 | GeomCenter = 3
95 |
96 | #: X Axis Geometry index in Sketch
97 | XAxisIndex = -1
98 |
99 | #: Y Axis Geometry index in Sketch
100 | YAxisIndex = -2
101 |
102 |
103 | # └────────────────────────────────────────────────────────────────────────────┘
104 | # [SECTION] [Util] Common utilities
105 | # ┌────────────────────────────────────────────────────────────────────────────┐
106 |
107 | def to_vec(input : Any) -> Vec:
108 | """Convert tuple/list/vector to Vec."""
109 | if isinstance(input, Vec):
110 | return input
111 | if isinstance(input, (tuple, list)):
112 | if len(input) == 3:
113 | return Vec(*input)
114 | if len(input) == 2:
115 | return Vec(*input, 0)
116 | if len(input) == 1:
117 | return Vec(*input, 0, 0)
118 | if hasattr(input, "X"):
119 | if hasattr(input, "Y"):
120 | if hasattr(input, "Z"):
121 | return Vec(input.X, input.Y, input.Z)
122 | else:
123 | return Vec(input.X, input.Y, 0)
124 | else:
125 | return Vec(input.X, 0, 0)
126 | if isinstance(input, (float, int)):
127 | return Vec(input, 0, 0)
128 | raise RuntimeError(f"Invalid input, {type(input)} is not convertible to Vec")
129 |
130 |
131 | def to_vecs(*input : Any) -> Tuple[Vec, ...]:
132 | """Convert arguments into Vectors. See to_vec."""
133 | return tuple(to_vec(i) for i in input)
134 |
135 |
136 | def find_obj_origin_geo_feature(obj: ObjectWithOrigin, prefix: str) -> App.GeoFeature:
137 | """Extract Axes or Planes from object's Origin"""
138 | for geo in obj.Origin.OutList:
139 | if geo.Name.startswith(prefix):
140 | return geo
141 |
142 |
143 | def find_obj_origin_axis(obj: ObjectWithOrigin, prefix: str) -> App.GeoFeature:
144 | """Extract Axes from object's Origin"""
145 | return find_obj_origin_geo_feature(obj, f'{prefix.upper()}_Axis')
146 |
147 |
148 | def find_obj_origin_plane(obj: ObjectWithOrigin, prefix: str) -> App.GeoFeature:
149 | """Extract Planes from object's Origin"""
150 | return find_obj_origin_geo_feature(obj, f'{prefix.upper()}_Plane')
151 |
152 |
153 | def recompute():
154 | """Recompute the whole active document"""
155 | App.ActiveDocument.recompute()
156 |
157 |
158 | def Dx(v: Union[float, Vec]):
159 | """Vector in x direction"""
160 | if isinstance(v, (float, int)):
161 | return Vec(v, 0, 0)
162 | return Vec(v.X, 0, 0)
163 |
164 |
165 | def Dy(v: Union[float, Vec]):
166 | """Vector in y direction"""
167 | if isinstance(v, (float, int)):
168 | return Vec(0, v, 0)
169 | return Vec(0, v.Y, 0)
170 |
171 |
172 | def Dz(v: Union[float, Vec]):
173 | """Vector in z direction"""
174 | if isinstance(v, (float, int)):
175 | return Vec(0, 0, v)
176 | return Vec(0, 0, v.Z)
177 |
178 | #: Decorator
179 | def set_function(target, attribute):
180 | """Decorator: Set function to an attribute of target."""
181 | def deco(fn):
182 | setattr(target, attribute, fn)
183 | return fn
184 | return deco
185 |
186 |
187 | def clamp(value, min_, max_):
188 | return max(min_, min(max_, value))
189 |
190 |
191 | def dir_of_2points(p1, p2):
192 | return (p2 - p1).normalize()
193 |
194 |
195 | def get_macros_dir():
196 | Standard, UserDefined = False, True
197 | root = Path(App.getUserMacroDir(UserDefined))
198 | if not root.exists():
199 | root = Path(App.getUserMacroDir(Standard))
200 | return root
201 |
202 |
203 | # └────────────────────────────────────────────────────────────────────────────┘
204 | # [SECTION] [Expression] Expression Engine Utilities
205 | # ┌────────────────────────────────────────────────────────────────────────────┐
206 | #
207 | # Example 1 (Eval):
208 | # e1 = Expr(".Length * 3 + <>.Radius")
209 | # result = e1(App.ActiveDocument.Box)
210 | #
211 | # Example 2 (Set):
212 | # e1.set_to(App.ActiveDocument.Pad002, 'Length')
213 | #
214 | # Example 3 (Set in place)
215 | # bind_expr(App.ActiveDocument.Pad002, 'Length', ".Length * 3 + <>.Radius")
216 | #
217 |
218 | class Expr:
219 | """Callable expression (FreeCAD's Expression Engine)"""
220 |
221 | def __init__(self, value: str, context: Any = None):
222 | self.value = value
223 | self.context = context
224 |
225 | def __call__(self, context: Any = None) -> Any:
226 | return (context or self.context).evalExpression(self.value)
227 |
228 | def set_to(self, obj, property: str, auto_recompute=False):
229 | obj.setExpression(property, self.value)
230 | if auto_recompute:
231 | recompute()
232 |
233 | def bind_expr(obj: Any, property: str, expr: Union[str, Expr], auto_recompute: bool = False):
234 | if not isinstance(expr, Expr):
235 | expr = Expr(expr, obj)
236 | expr.set_to(obj, property, auto_recompute)
237 |
238 |
239 | # └────────────────────────────────────────────────────────────────────────────┘
240 | # [SECTION] [Sketch] Geometry
241 | # ┌────────────────────────────────────────────────────────────────────────────┐
242 |
243 | class SketchGeom:
244 | """Geometry info inside a sketch"""
245 | def __init__(self, index, obj, name=None):
246 | self.index = index
247 | self.obj = obj
248 | self.name = name
249 |
250 |
251 | # └────────────────────────────────────────────────────────────────────────────┘
252 | # [SECTION] [Sketch] Solver
253 | # ┌────────────────────────────────────────────────────────────────────────────┐
254 |
255 | class Solver(Enum):
256 | BFGSSolver = 0
257 | LevenbergMarquardtSolver = 1
258 | DogLegSolver = 2
259 |
260 |
261 | # └────────────────────────────────────────────────────────────────────────────┘
262 | # [SECTION] [Sketch] Geometric Quantities
263 | # ┌────────────────────────────────────────────────────────────────────────────┐
264 |
265 | class gq:
266 |
267 | class Base:
268 | value: float
269 | def __init__(self, value: Union[float,str]):
270 | if isinstance(value, str):
271 | self.value = Quantity(str)
272 | else:
273 | self.value = value
274 |
275 | class Radius(Base):
276 | def __init__(self, value: Union[float,str]):
277 | super().__init__(value)
278 |
279 | class Diameter(Base):
280 | def __init__(self, value: Union[float,str]):
281 | super().__init__(value)
282 |
283 | class Length(Base):
284 | def __init__(self, value: Union[float,str]):
285 | super().__init__(value)
286 |
287 | class Angle(Base):
288 | def __init__(self, value: Union[float,str]):
289 | super().__init__(value)
290 |
291 | class DeltaVec(Base):
292 | def __init__(self, value: any):
293 | self.value = to_vec(value)
294 |
295 | # └────────────────────────────────────────────────────────────────────────────┘
296 | # [SECTION] [Sketch] Sketch
297 | # ┌────────────────────────────────────────────────────────────────────────────┐
298 |
299 | class XSketch:
300 | """Sketch Wrapper"""
301 |
302 | DEFAULT_SOLVER : Solver = Solver.DogLegSolver
303 |
304 | @staticmethod
305 | def select_default_solver(solver: Solver):
306 | App.ParamGet('User parameter:BaseApp/Preferences/Mod/Sketcher').SetBool('ShowSolverAdvancedWidget', True)
307 | App.ParamGet('User parameter:BaseApp/Preferences/Mod/Sketcher/SolverAdvanced').SetInt('DefaultSolver', solver.value)
308 |
309 |
310 | def __init__(self, name: str = 'XSketch', parent: Any = None, clean: bool = True):
311 | XSketch.select_default_solver(XSketch.DEFAULT_SOLVER)
312 | if parent:
313 | name = f"{parent.Name}_{name}"
314 | self.obj = App.ActiveDocument.getObject(name)
315 | if self.obj:
316 | if clean:
317 | self.obj.deleteAllGeometry()
318 | else:
319 | if parent:
320 | self.obj = parent.newObject("Sketcher::SketchObject", name)
321 | else:
322 | self.obj = App.ActiveDocument.addObject("Sketcher::SketchObject", name)
323 |
324 | self.named_geom = {}
325 | self._ref_mode = False
326 | self.parent = parent
327 |
328 |
329 | @contextmanager
330 | def ref_mode(self, mode : bool = True):
331 | saved = self._ref_mode
332 | if saved == mode:
333 | yield self
334 | else:
335 | self._ref_mode = mode
336 | try:
337 | yield self
338 | finally:
339 | self._ref_mode = saved
340 |
341 |
342 | def set_geom_mode(self):
343 | self._ref_mode = False
344 |
345 | # └────────────────────────────────────────────────────────────────────────────┘
346 | # [SECTION] [Sketch/Geometry]
347 | # ┌────────────────────────────────────────────────────────────────────────────┐
348 |
349 | def g_line(self, start, end):
350 | """Draw a line segment from start to end"""
351 | p0, p1 = to_vec(start), to_vec(end)
352 | line = Part.LineSegment(p0, p1)
353 | index = self.obj.addGeometry(line, self._ref_mode)
354 | return SketchGeom(index, line)
355 |
356 |
357 | def g_regular_polygon(self, p1, p2, edges):
358 | """Draw a regular polygon (center, vertex, edges)."""
359 | RegularPolygon.makeRegularPolygon(self.obj, edges, to_vec(p1), to_vec(p2), self._ref_mode)
360 | index = len(self.obj.Geometry) - 1
361 | return SketchGeom(index, self.obj.Geometry[index])
362 |
363 |
364 | def g_bspline(self, poles, mults=None, knots=None, periodic=False, degree=3, weights=None, check_rational=False):
365 | """Draw a bspline"""
366 | vec_poles = [to_vec(p) for p in poles]
367 | bspline = Part.BSplineCurve(vec_poles, mults, knots, periodic, degree, weights, check_rational)
368 | index = self.obj.addGeometry(bspline, self._ref_mode)
369 | return SketchGeom(index, bspline)
370 |
371 |
372 | def g_circle_center_radius(self, cnt, rad):
373 | c = to_vec(cnt)
374 | circle = Part.Circle(c, Vec(0,0,1), rad)
375 | index = self.obj.addGeometry(circle, self._ref_mode)
376 | return SketchGeom(index, circle)
377 |
378 |
379 | def g_circle_3points(self, p1, p2, p3):
380 | v1, v2, v3 = to_vecs(p1, p2, p3)
381 | circle = Part.Circle(v1, v2, v3)
382 | index = self.obj.addGeometry(circle, self._ref_mode)
383 | return SketchGeom(index, circle)
384 |
385 |
386 | def g_arc_3points(self, p1, p2, p3):
387 | v1, v2, v3 = to_vecs(p1, p2, p3)
388 | arc = Part.Arc(v1, v2, v3)
389 | index = self.obj.addGeometry(arc, self._ref_mode)
390 | return SketchGeom(index, arc)
391 |
392 |
393 | def g_arc_center_radius(self, cnt, rad, start=0, end=math.radians(180)):
394 | c = to_vec(cnt)
395 | circle = Part.Circle(c, Vec(0,0,1), rad)
396 | arc = Part.ArcOfCircle(circle, start, end)
397 | index = self.obj.addGeometry(arc, self._ref_mode)
398 | return SketchGeom(index, arc)
399 |
400 |
401 | def g_point(self, p):
402 | pnt = Part.Point(p)
403 | index = self.obj.addGeometry(pnt, self._ref_mode)
404 | return SketchGeom(index, pnt)
405 |
406 | # └────────────────────────────────────────────────────────────────────────────┘
407 | # [SECTION] [Sketch/Constraints]
408 | # ┌────────────────────────────────────────────────────────────────────────────┐
409 |
410 | def rename_constraint(self, constraint, name):
411 | if name:
412 | self.obj.renameConstraint(constraint, name)
413 |
414 |
415 | def c_coincident(self, g1, g1c, g2, g2c, name=None):
416 | c = self.obj.addConstraint(Sketcher.Constraint("Coincident", g1, g1c, g2, g2c))
417 | self.rename_constraint(c, name)
418 | return c
419 |
420 |
421 | def c_vertical(self, index, name=None):
422 | c = self.obj.addConstraint(Sketcher.Constraint("Vertical", index))
423 | self.rename_constraint(c, name)
424 | return c
425 |
426 |
427 | def c_horizontal(self, index, name=None):
428 | c = self.obj.addConstraint(Sketcher.Constraint("Horizontal", index))
429 | self.rename_constraint(c, name)
430 | return c
431 |
432 |
433 | def c_coincident_end_start(self, g1, g2, name=None):
434 | g_prev = self.obj.Geometry[g1]
435 | g_current = self.obj.Geometry[g2]
436 | c_end = GeomEnd if hasattr(g_prev, "EndPoint") else GeomCenter
437 | c_start = GeomStart if hasattr(g_current, "StartPoint") else GeomCenter
438 | c = self.c_coincident(g1, c_end, g2, c_start)
439 | self.rename_constraint(c, name)
440 | return c
441 |
442 |
443 | def c_x_angle(self, index: int, angle: Union[float, Expr], name=None):
444 | return self.c_angle(XAxisIndex, GeomStart, index, GeomStart, angle, name=name)
445 |
446 |
447 | def c_y_angle(self, index, angle: Union[float, Expr], name=None):
448 | return self.c_angle(YAxisIndex, GeomStart, index, GeomStart, angle, name=name)
449 |
450 |
451 | def c_angle(self, g1, g1c, g2, g2c, angle: Union[float, Expr], name=None):
452 | if isinstance(angle, Expr):
453 | value = angle(self.obj)
454 | c = self.obj.addConstraint(Sketcher.Constraint("Angle", g1, g1c, g2, g2c, value))
455 | angle.set_to(self.obj, f'.Constraints[{c}]')
456 | else:
457 | c = self.obj.addConstraint(Sketcher.Constraint("Angle", g1, g1c, g2, g2c, angle))
458 | self.rename_constraint(c, name)
459 | return c
460 |
461 |
462 | def c_length(self, index, length: Union[float, Expr], name=None):
463 | if isinstance(length, Expr):
464 | value = length(self.obj)
465 | c = self.obj.addConstraint(Sketcher.Constraint("Distance", index, value))
466 | length.set_to(self.obj, f'.Constraints[{c}]')
467 | else:
468 | c = self.obj.addConstraint(Sketcher.Constraint("Distance", index, length))
469 | self.rename_constraint(c, name)
470 | return c
471 |
472 |
473 | def c_distance(self, g1, g1p, g2, g2p, dist: Union[float, Expr], name=None):
474 | if isinstance(dist, Expr):
475 | value = dist(self.obj)
476 | c = self.obj.addConstraint(Sketcher.Constraint("Distance", g1, g1p, g2, g2p, value))
477 | dist.set_to(self.obj, f'.Constraints[{c}]')
478 | else:
479 | c = self.obj.addConstraint(Sketcher.Constraint("Distance", g1, g1p, g2, g2p, dist))
480 | self.rename_constraint(c, name)
481 | return c
482 |
483 |
484 | def c_perpendicular(self, g1, g2, name=None):
485 | c = self.obj.addConstraint(Sketcher.Constraint("Perpendicular", g1, g2))
486 | self.rename_constraint(c, name)
487 | return c
488 |
489 |
490 | def c_parallel(self, g1, g2, name=None):
491 | c = self.obj.addConstraint(Sketcher.Constraint("Parallel", g1, g2))
492 | self.rename_constraint(c, name)
493 | return c
494 |
495 |
496 | def _c_xy(self, g, distance, type, anchor=GeomStart, name=None):
497 | if isinstance(distance, Expr):
498 | value = distance(self.obj)
499 | c = self.obj.addConstraint(Sketcher.Constraint(f"Distance{type}", g, anchor, value))
500 | distance.set_to(self.obj, f'.Constraints[{c}]')
501 | else:
502 | c = self.obj.addConstraint(Sketcher.Constraint(f"Distance{type}", g, anchor, distance))
503 | self.rename_constraint(c, name)
504 | return c
505 |
506 |
507 | def c_x(self, g, distance, anchor=GeomStart, name=None):
508 | return self._c_xy(g, distance, 'X', anchor, name)
509 |
510 |
511 | def c_y(self, g, distance, anchor=GeomStart, name=None):
512 | return self._c_xy(g, distance, 'Y', anchor, name)
513 |
514 |
515 | def c_xy(self, g, x, y, anchor=GeomStart, name=None):
516 | c1 = self.c_x(g, x, anchor=anchor, name=None if name is None else f'{name}_x')
517 | c2 = self.c_y(g, y, anchor=anchor, name=None if name is None else f'{name}_y')
518 | return c1, c2
519 |
520 |
521 | def c_point_on_object(self, pnt, obj, name=None):
522 | c = self.obj.addConstraint(Sketcher.Constraint("PointOnObject", pnt, GeomStart, obj))
523 | self.rename_constraint(c, name)
524 | return c
525 |
526 |
527 | def c_equal(self, g1, g2, name=None):
528 | c = self.obj.addConstraint(Sketcher.Constraint("Equal", g1, g2))
529 | self.rename_constraint(c, name)
530 | return c
531 |
532 |
533 | def c_diameter(self, g, diameter, name=None):
534 | if isinstance(diameter, Expr):
535 | value = diameter(self.obj)
536 | c = self.obj.addConstraint(Sketcher.Constraint("Diameter", g, value))
537 | diameter.set_to(self.obj, f'.Constraints[{c}]')
538 | else:
539 | c = self.obj.addConstraint(Sketcher.Constraint("Diameter", g, diameter))
540 | self.rename_constraint(c, name)
541 | return c
542 |
543 |
544 | def c_radius(self, g, radius, name=None):
545 | if isinstance(radius, Expr):
546 | value = radius(self.obj)
547 | c = self.obj.addConstraint(Sketcher.Constraint("Radius", g, value))
548 | radius.set_to(self.obj, f'.Constraints[{c}]')
549 | else:
550 | c = self.obj.addConstraint(Sketcher.Constraint("Radius", g, radius))
551 | self.rename_constraint(c, name)
552 | return c
553 |
554 |
555 | def c_auto_coincident(self, name=None):
556 | g_len = len(self.obj.Geometry)
557 | if g_len > 1:
558 | return self.c_coincident_end_start(g_len-2, g_len-1, name=name)
559 |
560 |
561 | def c_bspline_control_point(self, g, g_c, bspl, bspl_c, weight=1.0):
562 | with self.ref_mode():
563 | geom = self.obj.Geometry[g]
564 | pnt = None
565 | if g_c == GeomStart:
566 | if hasattr(geom, "StartPoint"):
567 | pnt = geom.StartPoint
568 | elif hasattr(geom, "X"):
569 | pnt = Pnt(geom.X, geom.Y, geom.Z)
570 | elif g_c == GeomEnd:
571 | if hasattr(geom, "EndPoint"):
572 | pnt = geom.EndPoint
573 | elif hasattr(geom, "X"):
574 | pnt = Pnt(geom.X, geom.Y, geom.Z)
575 | elif g_c == GeomCenter:
576 | if hasattr(geom, "Center"):
577 | pnt = geom.Center
578 | elif hasattr(geom, "X"):
579 | pnt = Pnt(geom.X, geom.Y, geom.Z)
580 | if pnt is None:
581 | raise RuntimeError(f"Unsupported constraint {g}.{g_c}")
582 |
583 | cpc = self.g_circle_center_radius(pnt, 10)
584 | self.obj.addConstraint(Sketcher.Constraint('Weight', cpc.index, weight))
585 | self.obj.addConstraint(Sketcher.Constraint('Coincident', cpc.index, GeomCenter, g, g_c))
586 | self.obj.addConstraint(Sketcher.Constraint('InternalAlignment:Sketcher::BSplineControlPoint', cpc.index, GeomCenter, bspl, bspl_c))
587 |
588 | # └────────────────────────────────────────────────────────────────────────────┘
589 | # [SECTION] [Sketch/Lookup]
590 | # ┌────────────────────────────────────────────────────────────────────────────┐
591 |
592 | def g_by_name(self, name):
593 | index = self.named_geom.get(name, None)
594 | if index:
595 | return SketchGeom(index, self.obj.Geometry[index], name)
596 |
597 | def g_by_index(self, index):
598 | return SketchGeom(index, self.obj.Geometry[index])
599 |
600 | def set_name(self, name, geom):
601 | if name in self.named_geom:
602 | raise RuntimeError("Duplicated name")
603 | if isinstance(geom, SketchGeom):
604 | self.named_geom[name] = geom.index
605 | else:
606 | self.named_geom[name] = geom
607 |
608 | # └────────────────────────────────────────────────────────────────────────────┘
609 | # [SECTION] [Sketch/Builders]
610 | # ┌────────────────────────────────────────────────────────────────────────────┐
611 |
612 | def create_group(self, auto_coincident=True):
613 | return XSketchGroup(self, auto_coincident)
614 |
615 | # └────────────────────────────────────────────────────────────────────────────┘
616 | # [SECTION] [Sketch/Placement]
617 | # ┌────────────────────────────────────────────────────────────────────────────┐
618 |
619 | def rotate(self, angle):
620 | self.obj.AttachmentOffset = App.Placement(
621 | self.obj.AttachmentOffset.Base,
622 | App.Rotation(self.obj.AttachmentOffset.Rotation.Axis, angle)
623 | )
624 |
625 | def move(self, pos):
626 | self.obj.AttachmentOffset = App.Placement(
627 | to_vec(pos),
628 | self.obj.AttachmentOffset.Rotation
629 | )
630 |
631 | def attach(self, pos, axis, angle):
632 | self.obj.AttachmentOffset = App.Placement(
633 | to_vec(pos),
634 | App.Rotation(to_vec(axis), angle)
635 | )
636 |
637 | # └────────────────────────────────────────────────────────────────────────────┘
638 | # [SECTION] [Sketch/PartDesign]
639 | # ┌────────────────────────────────────────────────────────────────────────────┐
640 |
641 | def pad(self, value, name='Pad', direction=None):
642 | name = f"{self.obj.Name}_{name}"
643 | if self.parent:
644 | feature = self.parent.getObject(name)
645 | if not feature:
646 | feature = self.parent.newObject('PartDesign::Pad', name)
647 | else:
648 | if feature.TypeId != 'PartDesign::Pad':
649 | raise RuntimeError(f'The named object "{name}" already exists but it is not a PartDesign::Pad')
650 | feature.Profile = self.obj
651 | feature.Length = value
652 | if direction:
653 | feature.UseCustomVector = True
654 | feature.Direction = direction
655 | return feature
656 |
657 |
658 | # └────────────────────────────────────────────────────────────────────────────┘
659 | # [SECTION] [Sketch] Group
660 | # ┌────────────────────────────────────────────────────────────────────────────┐
661 |
662 | class XSketchGroup:
663 |
664 | def __init__(self, sketch: XSketch, auto_coincident: bool=True):
665 | self.sketch = sketch
666 | self.begin = None
667 | self.auto_coincident = auto_coincident
668 | self.pos = Pnt(0,0,0)
669 |
670 | def _concat(self, pnt, g, name):
671 | if self.begin is not None and g.index > 0 and self.auto_coincident:
672 | self.sketch.c_auto_coincident()
673 | self.pos = pnt
674 | if self.begin is None:
675 | self.begin = g.index
676 | if name:
677 | self.sketch.set_name(name, g)
678 |
679 | def line_to(self, pnt_, name=None):
680 | pnt = to_vec(pnt_)
681 | g = self.sketch.g_line(self.pos, pnt)
682 | self._concat(pnt, g, name)
683 | return g
684 |
685 | def line(self, dx=0, dy=0, name=None):
686 | return self.line_to(self.pos + Vec(dx, dy, 0), name=name)
687 |
688 | def close(self, keep=False):
689 | if self.begin is not None:
690 | begin = self.sketch.g_by_index(self.begin)
691 | g = self.line_to(begin.obj.StartPoint)
692 | if self.auto_coincident:
693 | self.sketch.c_coincident_end_start(g.index, self.begin)
694 | if not keep:
695 | self.begin = None
696 | return g
697 |
698 | def by_name(self, name):
699 | return self.sketch.g_by_name(name)
700 |
701 | def move_to(self, pnt):
702 | self.pos = to_vec(pnt)
703 | self.begin = None
704 |
705 | def move(self, dx=0, dy=0):
706 | self.pos = self.pos + to_vec((dx, dy))
707 | self.begin = None
708 |
709 | def bspline_to(self, *pnts, name=None):
710 | pnt = to_vec(pnts[-1])
711 | g = self.sketch.g_bspline([self.pos, *pnts])
712 | self._concat(pnt, g, name)
713 | return g
714 |
715 | def bspline(self, *pnts, name=None):
716 | start = self.pos
717 | apnts = []
718 | for pnt in pnts:
719 | start = start + pnt
720 | apnts.append(start)
721 | pnt = to_vec(apnts[-1])
722 | g = self.sketch.g_bspline([self.pos, *apnts])
723 | self._concat(pnt, g, name)
724 | return g
725 |
726 | def circle(self, radius, name=None):
727 | g = self.sketch.g_circle_center_radius(self.pos, radius)
728 | self._concat(self.pos, g, name)
729 | return g
730 |
731 | def point(self, name=None):
732 | g = self.sketch.g_point(self.pos)
733 | self.pos = to_vec(g.obj)
734 | if name:
735 | self.sketch.set_name(name, g)
736 | return g
737 |
738 | def circle_3points(self, p2, p3, name=None):
739 | g = self.sketch.g_circle_3points(self.pos, p2, p3)
740 | self._concat(to_vec(p3), g, name)
741 | return g
742 |
743 | def rect_to(self, pnt):
744 | orig = self.pos
745 | self.move_to(orig)
746 | g = self.line_to((orig[0], pnt[1]))
747 | self.sketch.c_vertical(g.index)
748 | g = self.line_to(pnt)
749 | self.sketch.c_horizontal(g.index)
750 | g = self.line_to((pnt[0], orig[1]))
751 | self.sketch.c_vertical(g.index)
752 | g = self.close()
753 | self.sketch.c_horizontal(g.index)
754 | return g
755 |
756 | def rect(self, w, h, angle=Quantity('0.0 deg')):
757 | if w <= 0:
758 | raise(RuntimeError(f'Invalid w={w} too small'))
759 | if h <= 0:
760 | raise(RuntimeError(f'Invalid h={h} too small'))
761 | orig = self.pos
762 | self.move_to(orig)
763 | g1 = self.line(dx=w)
764 | self.sketch.c_length(g1.index, w)
765 | g2 = self.line(dy=h)
766 | self.sketch.c_length(g2.index, h)
767 | self.sketch.c_angle(g1.index, GeomEnd, g2.index, GeomStart, Quantity('270 deg'))
768 | g3 = self.line(dx=-w)
769 | self.sketch.c_length(g3.index, w)
770 | self.sketch.c_angle(g2.index, GeomEnd, g3.index, GeomStart, Quantity('270 deg'))
771 | g4 = self.close()
772 | self.sketch.c_x_angle(g1.index, angle)
773 | self.sketch.c_xy(g1.index, orig[0], orig[1])
774 | return g4
775 |
776 | def regular_polygon(self, param: any, edges: int, angle=None, constrain_pos: Optional[bool]=None, constrain_size: bool=True):
777 | orig = self.pos
778 | prev_index = len(self.sketch.obj.Geometry) - 1
779 | if isinstance(param, gq.Length):
780 | r = param.value / (2 * math.sin(math.pi/edges))
781 | p2 = orig + Vec(0, r, 0)
782 | elif isinstance(param, gq.Radius):
783 | r = param.value
784 | p2 = orig + Vec(0, r, 0)
785 | elif isinstance(param, gq.Diameter):
786 | r = param.value/2.0
787 | p2 = orig + Vec(0, r, 0)
788 | elif isinstance(param, gq.DeltaVec):
789 | r = param.value.Length
790 | p2 = orig + param.value
791 | elif isinstance(param, (float,int)): # Implies Diameter
792 | r = param/2.0
793 | p2 = orig + Vec(0, r, 0)
794 | else: # Absolute point
795 | p2 = to_vec(param)
796 | r = (orig - p2).Length
797 | g = self.sketch.g_regular_polygon(orig, p2, edges)
798 |
799 | if constrain_pos is True:
800 | self.sketch.c_xy(g.index, orig[0], orig[1], anchor=GeomCenter)
801 | elif constrain_pos is None and self.auto_coincident and prev_index > -1 and self.begin:
802 | self.sketch.c_coincident_end_start(prev_index, g.index)
803 |
804 | if constrain_size:
805 | if isinstance(param, gq.Length):
806 | self.sketch.c_length(g.index-1, param.value)
807 | elif isinstance(param, gq.Radius):
808 | self.sketch.c_radius(g.index, param.value)
809 | elif isinstance(param, gq.Diameter):
810 | self.sketch.c_diameter(g.index, param.value)
811 | elif isinstance(param, gq.DeltaVec):
812 | self.sketch.c_radius(g.index, r)
813 | else:
814 | length = r * (2 * math.sin(math.pi/edges))
815 | self.sketch.c_length(g.index-1, length)
816 |
817 | if angle is not None:
818 | with self.sketch.ref_mode():
819 | seg = self.sketch.g_line(orig, p2)
820 | self.sketch.c_coincident(g.index, GeomCenter, seg.index, GeomStart)
821 | self.sketch.c_coincident(prev_index+1, GeomStart, seg.index, GeomEnd)
822 | self.sketch.c_y_angle(seg.index, angle)
823 |
824 | return g
825 |
826 |
827 | def line_with_length(self, dx=0, dy=0, ref=False, constraint_name=None):
828 | with self.sketch.ref_mode(ref):
829 | if dx == 0 and dy != 0:
830 | length = abs(dy)
831 | elif dx != 0 and dy == 0:
832 | length = abs(dx)
833 | elif dx != 0 and dy != 0:
834 | length = math.sqrt(dx*dx + dy*dy)
835 | else:
836 | raise(RuntimeError(f"Invalid (dx, dy) = ({dx}, {dy})"))
837 | seg = self.line(dx, dy)
838 | self.sketch.c_length(seg.index, length, name=constraint_name)
839 | return seg
840 |
841 |
842 | def rect_rounded(self, w, h, r, angle=Quantity('0.0 deg')):
843 | """
844 | r: int | float => for all corners
845 | (rw, rh) => width and height radious
846 | (tl, tr, br, bl) => custom radius for each corner
847 | """
848 | if w <= 0:
849 | raise(RuntimeError(f'Invalid w={w} too small'))
850 | if h <= 0:
851 | raise(RuntimeError(f'Invalid h={h} too small'))
852 |
853 | BL, BR, TR, TL = 0, 1, 2, 3 # Corners
854 | RW, RH = 0, 1 # Radiuses
855 |
856 | # transform r into ( (blw, blh), (brw, brh), (trw, trh), (tlw, tlh) )
857 | if isinstance(r, (float, int)):
858 | r = ((r,r),)*4
859 | elif len(r) == 1:
860 | r = ((r,r),)*4
861 | elif len(r) == 2:
862 | r = ((r[0],r[1]),)*4
863 | elif len(r) == 4:
864 | r = ((r[0],r[0]),(r[1],r[1]),(r[2],r[2]),(r[3],r[3]))
865 | else:
866 | raise(RuntimeError(f'Invalid r={r}'))
867 |
868 | if any((x[RW] <= 0 or x[RH] <= 0 for x in r)):
869 | raise(RuntimeError(f'Invalid r={r} too small values'))
870 |
871 | if ( (r[BL][RW] + r[BR][RW] > w)
872 | or (r[TR][RW] + r[TL][RW] > w)
873 | or (r[BL][RH] + r[TL][RH] > h)
874 | or (r[BR][RH] + r[TR][RH] > h)):
875 | raise(RuntimeError(f'Invalid r={r} too large values'))
876 |
877 | orig = self.pos
878 |
879 | self.move_to(orig)
880 |
881 | angle180 = Quantity('180 deg')
882 | angle270 = Quantity('270 deg')
883 |
884 | # Bottom
885 | bl = self.line_with_length(dx=r[BL][RW], ref=True)
886 | bottom = self.line_with_length(dx=w - r[BL][RW] - r[BR][RW])
887 | br = self.line_with_length(dx=r[BR][RW], ref=True)
888 |
889 | self.sketch.c_angle(bl.index, GeomEnd, bottom.index, GeomStart, angle180)
890 | self.sketch.c_angle(bottom.index, GeomEnd, br.index, GeomStart, angle180)
891 |
892 | # Right
893 | rb = self.line_with_length(dy=r[BR][RH], ref=True)
894 | right = self.line_with_length(dy=h - r[BR][RH] - r[TR][RH])
895 | rt = self.line_with_length(dy=-r[TR][RH], ref=True)
896 |
897 | self.sketch.c_angle(br.index, GeomEnd, rb.index, GeomStart, angle270)
898 | self.sketch.c_angle(rb.index, GeomEnd, right.index, GeomStart, angle180)
899 | self.sketch.c_angle(right.index, GeomEnd, rt.index, GeomStart, angle180)
900 |
901 | # Top
902 | tr = self.line_with_length(dx=-r[TR][RW], ref=True)
903 | top = self.line_with_length(dx=-w + r[TR][RW] + r[TL][RW])
904 | tl = self.line_with_length(dx=-r[TL][RW], ref=True)
905 |
906 | self.sketch.c_angle(rt.index, GeomEnd, tr.index, GeomStart, angle270)
907 | self.sketch.c_angle(tr.index, GeomEnd, top.index, GeomStart, angle180)
908 | self.sketch.c_angle(top.index, GeomEnd, tl.index, GeomStart, angle180)
909 |
910 | # Left
911 | lt = self.line_with_length(dy=-r[TL][RH], ref=True)
912 | left = self.line_with_length(dy=-h + r[BL][RH] + r[TL][RH])
913 | lb = self.line_with_length(dy=r[BL][RH], ref=True)
914 |
915 | self.sketch.c_angle(tl.index, GeomEnd, lt.index, GeomStart, angle270)
916 | self.sketch.c_angle(lt.index, GeomEnd, left.index, GeomStart, angle180)
917 | self.sketch.c_angle(left.index, GeomEnd, lb.index, GeomStart, angle180)
918 |
919 | # Corner: BL
920 | self.move_to(left.obj.EndPoint)
921 | blc = self.bspline_to(lb.obj.EndPoint, bottom.obj.StartPoint)
922 | self.sketch.c_bspline_control_point(left.index, GeomEnd, blc.index, 0)
923 | self.sketch.c_bspline_control_point(lb.index, GeomEnd, blc.index, 1)
924 | self.sketch.c_bspline_control_point(bottom.index, GeomStart, blc.index, 2)
925 |
926 | # Corner: BR
927 | self.move_to(bottom.obj.EndPoint)
928 | blc = self.bspline_to(br.obj.EndPoint, rb.obj.StartPoint)
929 | self.sketch.c_bspline_control_point(bottom.index, GeomEnd, blc.index, 0)
930 | self.sketch.c_bspline_control_point(br.index, GeomEnd, blc.index, 1)
931 | self.sketch.c_bspline_control_point(right.index, GeomStart, blc.index, 2)
932 |
933 | # Corner: TR
934 | self.move_to(right.obj.EndPoint)
935 | blc = self.bspline_to(rt.obj.EndPoint, top.obj.StartPoint)
936 | self.sketch.c_bspline_control_point(right.index, GeomEnd, blc.index, 0)
937 | self.sketch.c_bspline_control_point(rt.index, GeomEnd, blc.index, 1)
938 | self.sketch.c_bspline_control_point(top.index, GeomStart, blc.index, 2)
939 |
940 | # Corner: TL
941 | self.move_to(top.obj.EndPoint)
942 | blc = self.bspline_to(tl.obj.EndPoint, left.obj.StartPoint)
943 | self.sketch.c_bspline_control_point(top.index, GeomEnd, blc.index, 0)
944 | self.sketch.c_bspline_control_point(tl.index, GeomEnd, blc.index, 1)
945 | self.sketch.c_bspline_control_point(left.index, GeomStart, blc.index, 2)
946 |
947 | # Pos
948 | self.sketch.c_xy(bl.index, orig[0], orig[1])
949 |
950 | # Angle
951 | self.sketch.c_x_angle(bottom.index, angle)
952 |
953 | return blc
954 |
955 |
956 | # └────────────────────────────────────────────────────────────────────────────┘
957 | # [SECTION] [PartDesign] Body
958 | # ┌────────────────────────────────────────────────────────────────────────────┐
959 |
960 |
961 | class XBody:
962 | """PartDesign Body builder"""
963 |
964 | def __init__(self, name='XBody'):
965 | """Create or reuse a PartDesign Body"""
966 | self.obj = App.ActiveDocument.getObject(name)
967 | if not self.obj:
968 | self.obj = App.ActiveDocument.addObject('PartDesign::Body', name)
969 | else:
970 | if self.obj.TypeId != 'PartDesign::Body':
971 | raise RuntimeError(f'The named object "{name}" already exists but it is not a PartDesign::Body')
972 |
973 |
974 | @property
975 | def name(self):
976 | return self.obj.Name
977 |
978 |
979 | def sketch(self, name:str='XSketch', plane:str='XY', reversed:bool=False, pos:Vec=Vec(0,0,0), rot:Rotation=Rotation(0,0,0)):
980 | """Sketch builder."""
981 | support = find_obj_origin_plane(self.obj, plane)
982 | return self.sketch_on([(support, '')], name=name, reversed=reversed, pos=pos, rot=rot)
983 |
984 |
985 | def sketch_on(self, support, mode='FlatFace', parameter=0.0, name:str='XSketch', reversed:bool=False, pos:Vec=Vec(0,0,0), rot:Rotation=Rotation(0,0,0)):
986 | """Sketch builder."""
987 | xsketch = XSketch(name, parent=self.obj)
988 | sketch = xsketch.obj
989 | sketch.AttachmentOffset = App.Placement(pos, rot)
990 | sketch.MapReversed = reversed
991 | sketch.Support = support
992 | sketch.MapPathParameter = parameter
993 | sketch.MapMode = mode
994 | return xsketch
995 |
996 |
997 | def fillet(self, edges: list, name='Fillet'):
998 | name = f"{self.name}_{name}"
999 | fillet = App.ActiveDocument.getObject(name)
1000 | if not fillet:
1001 | fillet = App.ActiveDocument.addObject('Part::Fillet', name)
1002 | fillet.Base = self.obj
1003 | fillet.Edges = [*edges]
1004 | Gui.ActiveDocument.getObject(self.name).Visibility = False
1005 |
1006 |
1007 | # └────────────────────────────────────────────────────────────────────────────┘
1008 | # [SECTION] [FeaturePython] Data Object
1009 | # ┌────────────────────────────────────────────────────────────────────────────┐
1010 |
1011 | class DataObject:
1012 | """Document Object with properties"""
1013 |
1014 | def __init__(self, name="Data"):
1015 | obj = App.ActiveDocument.getObject(name)
1016 | if not obj:
1017 | obj = App.ActiveDocument.addObject('App::FeaturePython', name)
1018 | else:
1019 | if obj.TypeId != 'App::FeaturePython':
1020 | raise RuntimeError(f'The named object "{name}" already exists but it is not of type App::FeaturePython')
1021 | super().__setattr__('obj', obj)
1022 |
1023 | def add_property(self, property_type, name, section="Properties", docs="", mode=0):
1024 | try:
1025 | self.obj.addProperty(property_type, name, section, docs, mode)
1026 | except:
1027 | pass # Ignore if already exists
1028 |
1029 | def __setattr__(self, __name: str, __value: Any) -> None:
1030 | if hasattr(self, __name):
1031 | super().__setattr__(__name, __value)
1032 | else:
1033 | if isinstance(__value, Expr):
1034 | self.obj.__setattr__(__name, __value(self.obj))
1035 | __value.set_to(self.obj, __name)
1036 | else:
1037 | self.obj.setExpression(__name, None)
1038 | self.obj.__setattr__(__name, __value)
1039 |
1040 | def __getattr__(self, __name: str) -> Any:
1041 | return self.obj.__getattr(__name)
1042 |
1043 |
1044 | # └────────────────────────────────────────────────────────────────────────────┘
1045 | # [SECTION] [Wire] Free Wire in 3D
1046 | # ┌────────────────────────────────────────────────────────────────────────────┐
1047 |
1048 | class Wire3D:
1049 | def __init__(self, origin=(0,0,0)):
1050 | self.origin = to_vec(origin)
1051 | self.pos = self.origin
1052 | self.edges = []
1053 |
1054 | def add_bspline(self, poles):
1055 | vec_poles = [to_vec(p) for p in poles]
1056 | bspline = Part.BSplineCurve([self.pos, *vec_poles])
1057 | self.edges.append(Part.Edge(bspline))
1058 | self.pos = vec_poles[-1]
1059 |
1060 | def add_segment(self, point):
1061 | point = to_vec(point)
1062 | if self.pos != point:
1063 | segment = Part.Edge(Part.LineSegment(self.pos, point))
1064 | self.edges.append(segment)
1065 | self.pos = point
1066 |
1067 | def add_round_corner(self, point, radius, tangency=0.618033):
1068 | point = to_vec(point)
1069 | tangency = clamp(tangency, 0.2, 0.8)
1070 | point_dist = radius * tangency
1071 | point_dist2 = radius - point_dist
1072 | tangent1 = self.tangent * point_dist
1073 | p0 = self.pos
1074 | p1 = self.pos + tangent1
1075 | tangent1 = self.tangent * point_dist2
1076 | p2 = p1 + tangent1
1077 | v_tangent2 = dir_of_2points(p2, point)
1078 | tangent2 = v_tangent2 * point_dist2
1079 | p3 = p2 + tangent2
1080 | tangent2 = v_tangent2 * point_dist
1081 | p4 = p3 + tangent2
1082 | self.add_bspline((p1, p3, p4))
1083 | self.add_segment(point)
1084 |
1085 | def build(self):
1086 | return Part.Wire(self.edges)
1087 |
1088 | def set_to(self, name):
1089 | obj = App.ActiveDocument.getObject(name)
1090 | if obj:
1091 | obj.Shape = self.build()
1092 | else:
1093 | Part.show(self.build(), name)
1094 |
1095 | @property
1096 | def tangent(self):
1097 | last_edge = self.edges[-1]
1098 | return last_edge.Curve.tangent(last_edge.LastParameter)[0].normalize()
1099 |
1100 |
1101 | # └────────────────────────────────────────────────────────────────────────────┘
1102 | # [SECTION] [GUI] Imports
1103 | # ┌────────────────────────────────────────────────────────────────────────────┐
1104 |
1105 | from PySide import QtCore, QtGui
1106 |
1107 | # └────────────────────────────────────────────────────────────────────────────┘
1108 | # [SECTION] [GUI] Aliases
1109 | # ┌────────────────────────────────────────────────────────────────────────────┐
1110 |
1111 | Icon = QtGui.QIcon
1112 | Image = QtGui.QPixmap
1113 |
1114 | # └────────────────────────────────────────────────────────────────────────────┘
1115 | # [SECTION] [GUI] Globals
1116 | # ┌────────────────────────────────────────────────────────────────────────────┐
1117 |
1118 | ThreadLocalGuiVars = threading.local() # Store GUI State per thread
1119 |
1120 |
1121 | # └────────────────────────────────────────────────────────────────────────────┘
1122 | # [SECTION] [GUI] Utils
1123 | # ┌────────────────────────────────────────────────────────────────────────────┐
1124 |
1125 | def set_qt_attrs(qt_object, **kwargs):
1126 | """Call setters on QT objects by argument names."""
1127 | for name, value in kwargs.items():
1128 | if value is not None:
1129 | setter = getattr(qt_object, f'set{name[0].upper()}{name[1:]}', None)
1130 | if setter:
1131 | if isinstance(value, tuple):
1132 | setter(*value)
1133 | else:
1134 | setter(value)
1135 |
1136 |
1137 | def setup_layout(layout, add=True, **kwargs):
1138 | """Setup layouts adding wrapper widget if required."""
1139 | set_qt_attrs(layout, **kwargs)
1140 | parent = build_context().current()
1141 | if parent.layout() is not None or add is False:
1142 | w = QtGui.QWidget()
1143 | w.setLayout(layout)
1144 | if add:
1145 | parent.layout().addWidget(w)
1146 | with build_context().stack(w):
1147 | yield w
1148 | else:
1149 | parent.setLayout(layout)
1150 | yield parent
1151 |
1152 |
1153 | def place_widget(ed, label=None, stretch=0, alignment=QtCore.Qt.Alignment()):
1154 | """Place widget in layout."""
1155 | layout = build_context().current().layout()
1156 | if layout is None:
1157 | layout = QtGui.QVBoxLayout()
1158 | build_context().current().setLayout(layout)
1159 | if label is None:
1160 | build_context().current().layout().addWidget(ed, stretch, alignment)
1161 | else:
1162 | w = QtGui.QWidget()
1163 | parent = QtGui.QHBoxLayout()
1164 | parent.addWidget(QtGui.QLabel(label))
1165 | parent.addWidget(ed)
1166 | w.setLayout(parent)
1167 | build_context().current().layout().addWidget(w, stretch, alignment)
1168 |
1169 |
1170 | class PySignal:
1171 | """Imitate Qt Signals for non QObject objects"""
1172 |
1173 | def __init__(self):
1174 | self._listeners = []
1175 |
1176 | def connect(self, listener):
1177 | self._listeners.append(listener)
1178 |
1179 | def trigger(self, *args, **kwargs):
1180 | for listener in self._listeners:
1181 | listener(*args, **kwargs)
1182 |
1183 |
1184 | #: Decorator
1185 | def on_event(target, event=None):
1186 | """Decorator: Event binder"""
1187 | if event is None:
1188 | def deco(fn):
1189 | target.connect(fn)
1190 | return fn
1191 | else:
1192 | def deco(fn):
1193 | getattr(target, event).connect(fn)
1194 | return fn
1195 | return deco
1196 |
1197 |
1198 | # └────────────────────────────────────────────────────────────────────────────┘
1199 | # [SECTION] [GUI] Selection
1200 | # ┌────────────────────────────────────────────────────────────────────────────┐
1201 |
1202 | class SelectedObject:
1203 | """Store Selection information"""
1204 |
1205 | def __init__(self, doc, obj, sub=None, pnt=None):
1206 | self.doc = doc
1207 | self.obj = obj
1208 | self.sub = sub
1209 | self.pnt = pnt
1210 |
1211 | def __iter__(self):
1212 | yield App.getDocument(self.doc).getObject(self.obj)
1213 | yield self.sub
1214 | yield self.pnt
1215 |
1216 | def __repr__(self) -> str:
1217 | return f"{self.doc}#{self.obj}.{self.sub}"
1218 |
1219 | def __hash__(self) -> int:
1220 | return hash((self.doc, self.obj, self.sub))
1221 |
1222 | def __eq__(self, __o: object) -> bool:
1223 | return hash(self) == hash(__o)
1224 |
1225 | def __ne__(self, __o: object) -> bool:
1226 | return not self.__eq__(__o)
1227 |
1228 |
1229 | def register_select_observer(owner: QtGui.QWidget, observer):
1230 | """Add observer with auto remove on owner destroyed"""
1231 | Gui.Selection.addObserver(observer)
1232 | def destroyed(_):
1233 | Gui.Selection.removeObserver(observer)
1234 | owner.destroyed.connect(destroyed)
1235 |
1236 |
1237 | @contextmanager
1238 | def selection(*names, clean=True):
1239 | sel = Gui.Selection
1240 | try:
1241 | doc = App.ActiveDocument.Name
1242 | if len(names) == 0:
1243 | yield sel.getSelection(doc)
1244 | else:
1245 | sel.clearSelection()
1246 | for name in names:
1247 | if isinstance(name, (tuple, list)):
1248 | sel.addSelection(doc, *name)
1249 | elif isinstance(name, SelectedObject):
1250 | sel.addSelection(name.doc, name.obj, name.sub)
1251 | else:
1252 | sel.addSelection(doc, name)
1253 | yield sel.getSelection(doc)
1254 | finally:
1255 | if clean:
1256 | sel.clearSelection()
1257 |
1258 |
1259 | # └────────────────────────────────────────────────────────────────────────────┘
1260 | # [SECTION] [GUI] Build Context
1261 | # ┌────────────────────────────────────────────────────────────────────────────┐
1262 |
1263 | def build_context():
1264 | bc = getattr(ThreadLocalGuiVars, 'BuildContext', None)
1265 | if bc is None:
1266 | ThreadLocalGuiVars.BuildContext = _BuildContext()
1267 | return ThreadLocalGuiVars.BuildContext
1268 | else:
1269 | return bc
1270 |
1271 | class _BuildContext:
1272 | def __init__(self):
1273 | self._stack = []
1274 |
1275 | def push(self, widget):
1276 | self._stack.append(widget)
1277 |
1278 | def pop(self):
1279 | self._stack.pop()
1280 |
1281 | @contextmanager
1282 | def stack(self, widget):
1283 | self.push(widget)
1284 | try:
1285 | yield widget
1286 | finally:
1287 | self.pop()
1288 |
1289 | @contextmanager
1290 | def parent(self):
1291 | if len(self._stack) > 1:
1292 | current = self._stack[-1]
1293 | self._stack.pop()
1294 | parent = self._stack[-1]
1295 | try:
1296 | yield parent
1297 | finally:
1298 | self._stack.append(current)
1299 |
1300 | def current(self):
1301 | return self._stack[-1]
1302 |
1303 | def dump(self):
1304 | print(f"BuildContext: {self._stack}")
1305 |
1306 | @contextmanager
1307 | def Parent():
1308 | """Put parent on top of BuildContext"""
1309 | with build_context().parent() as p:
1310 | yield p
1311 |
1312 |
1313 | # └────────────────────────────────────────────────────────────────────────────┘
1314 | # [SECTION] [GUI] [Widget] Dialog
1315 | # ┌────────────────────────────────────────────────────────────────────────────┐
1316 |
1317 | class Dialogs:
1318 | _list = []
1319 |
1320 | @classmethod
1321 | def dump(cls):
1322 | print(f"Dialogs: {cls._list}")
1323 |
1324 | @classmethod
1325 | def register(cls, dialog):
1326 | cls._list.append(dialog)
1327 | dialog.closeEvent = lambda e: cls.destroy_dialog(dialog)
1328 |
1329 | @classmethod
1330 | def destroy_dialog(cls, dlg):
1331 | cls._list.remove(dlg)
1332 | dlg.deleteLater()
1333 |
1334 |
1335 | @contextmanager
1336 | def Dialog(title=None, size=None, show=True, parent=None):
1337 | if parent is None:
1338 | w = QtGui.QDialog(parent=Gui.getMainWindow())
1339 | else:
1340 | w = QtGui.QWidget(parent=parent)
1341 | if title is not None:
1342 | w.setWindowTitle(title)
1343 | with build_context().stack(w):
1344 | yield w
1345 | if show:
1346 | Dialogs.register(w)
1347 | w.show()
1348 | if isinstance(size, (tuple,list)):
1349 | w.resize(size[0], size[1])
1350 | else:
1351 | w.adjustSize()
1352 |
1353 |
1354 | # └────────────────────────────────────────────────────────────────────────────┘
1355 | # [SECTION] [GUI] [Widget] GroupBox
1356 | # ┌────────────────────────────────────────────────────────────────────────────┐
1357 |
1358 | @contextmanager
1359 | def GroupBox(title=None):
1360 | w = QtGui.QGroupBox()
1361 | if title:
1362 | w.setTitle(title)
1363 | place_widget(w)
1364 | with build_context().stack(w):
1365 | yield w
1366 |
1367 |
1368 | # └────────────────────────────────────────────────────────────────────────────┘
1369 | # [SECTION] [GUI] [Widget] Stretch
1370 | # ┌────────────────────────────────────────────────────────────────────────────┐
1371 |
1372 | def Stretch(stretch=0):
1373 | """Add Layout spacer"""
1374 | layout = build_context().current().layout()
1375 | if layout:
1376 | layout.addStretch(stretch)
1377 |
1378 |
1379 | # └────────────────────────────────────────────────────────────────────────────┘
1380 | # [SECTION] [GUI] [Widget] TabContainer
1381 | # ┌────────────────────────────────────────────────────────────────────────────┐
1382 |
1383 | @contextmanager
1384 | def TabContainer(**kwargs):
1385 | w = QtGui.QTabWidget()
1386 | set_qt_attrs(w, **kwargs)
1387 | place_widget(w)
1388 | with build_context().stack(w):
1389 | yield w
1390 |
1391 |
1392 | # └────────────────────────────────────────────────────────────────────────────┘
1393 | # [SECTION] [GUI] [Widget] Tab
1394 | # ┌────────────────────────────────────────────────────────────────────────────┐
1395 |
1396 | @contextmanager
1397 | def Tab(title:str, icon=None):
1398 | w = QtGui.QWidget()
1399 | with build_context().stack(w):
1400 | yield w
1401 | if icon:
1402 | build_context().current().addTab(w, title, icon)
1403 | else:
1404 | build_context().current().addTab(w, title)
1405 |
1406 |
1407 | # └────────────────────────────────────────────────────────────────────────────┘
1408 | # [SECTION] [GUI] [Layout] Col
1409 | # ┌────────────────────────────────────────────────────────────────────────────┐
1410 |
1411 | @contextmanager
1412 | def Col(add=True, **kwargs):
1413 | """Vertical Layout"""
1414 | yield from setup_layout(QtGui.QVBoxLayout(), add=add, **kwargs)
1415 |
1416 |
1417 | # └────────────────────────────────────────────────────────────────────────────┘
1418 | # [SECTION] [GUI] [Layout] Row
1419 | # ┌────────────────────────────────────────────────────────────────────────────┐
1420 |
1421 | @contextmanager
1422 | def Row(add=True, **kwargs):
1423 | """Horizontal Layout"""
1424 | yield from setup_layout(QtGui.QHBoxLayout(), add=add, **kwargs)
1425 |
1426 |
1427 | # └────────────────────────────────────────────────────────────────────────────┘
1428 | # [SECTION] [GUI] [Widget] TextLabel
1429 | # ┌────────────────────────────────────────────────────────────────────────────┐
1430 |
1431 | def TextLabel(text="", stretch=0, alignment=QtCore.Qt.Alignment(), **kwargs):
1432 | label = QtGui.QLabel(text)
1433 | set_qt_attrs(label, **kwargs)
1434 | place_widget(label, stretch=stretch, alignment=alignment)
1435 | return label
1436 |
1437 |
1438 | # └────────────────────────────────────────────────────────────────────────────┘
1439 | # [SECTION] [GUI] [Widget] InputFloat
1440 | # ┌────────────────────────────────────────────────────────────────────────────┐
1441 |
1442 | def InputFloat(name=None, min=0.0, max=sys.float_info.max, decimals=6,
1443 | step=0.01, label=None, value=0.0, stretch=0,
1444 | alignment=QtCore.Qt.Alignment(), **kwargs):
1445 | editor = QtGui.QDoubleSpinBox()
1446 | editor.setMinimum(min)
1447 | editor.setMaximum(max)
1448 | editor.setSingleStep(step)
1449 | editor.setDecimals(decimals)
1450 | editor.setValue(value)
1451 | set_qt_attrs(editor, **kwargs)
1452 | if name:
1453 | editor.setObjectName(name)
1454 | place_widget(editor, label=label, stretch=stretch, alignment=alignment)
1455 | return editor
1456 |
1457 |
1458 | # └────────────────────────────────────────────────────────────────────────────┘
1459 | # [SECTION] [GUI] [Widget] InputText
1460 | # ┌────────────────────────────────────────────────────────────────────────────┐
1461 |
1462 | class InputTextWidget(QtGui.QLineEdit):
1463 | def __init__(self, *args, **kwargs):
1464 | super().__init__(*args, **kwargs)
1465 | def value(self):
1466 | return self.text()
1467 | def setValue(self, value):
1468 | self.setText(str(value))
1469 |
1470 | def InputText(name=None, label=None, value="",
1471 | stretch=0, alignment=QtCore.Qt.Alignment(), **kwargs):
1472 | editor = InputTextWidget()
1473 | editor.setText(value)
1474 | set_qt_attrs(editor, **kwargs)
1475 | if name:
1476 | editor.setObjectName(name)
1477 | place_widget(editor, label=label, stretch=stretch, alignment=alignment)
1478 | return editor
1479 |
1480 |
1481 | # └────────────────────────────────────────────────────────────────────────────┘
1482 | # [SECTION] [GUI] [Widget] InputInt
1483 | # ┌────────────────────────────────────────────────────────────────────────────┐
1484 |
1485 | def InputInt(name=None, min=0, max=2^31, step=1, label=None, value=0,
1486 | stretch=0, alignment=QtCore.Qt.Alignment(), **kwargs):
1487 | editor = QtGui.QSpinBox()
1488 | editor.setMinimum(min)
1489 | editor.setMaximum(max)
1490 | editor.setSingleStep(step)
1491 | editor.setValue(value)
1492 | set_qt_attrs(editor, **kwargs)
1493 | if name:
1494 | editor.setObjectName(name)
1495 | place_widget(editor, label=label, stretch=stretch, alignment=alignment)
1496 | return editor
1497 |
1498 |
1499 | # └────────────────────────────────────────────────────────────────────────────┘
1500 | # [SECTION] [GUI] [Widget] InputBoolean
1501 | # ┌────────────────────────────────────────────────────────────────────────────┐
1502 |
1503 | class QCheckBoxExt(QtGui.QCheckBox):
1504 | def __init__(self, *args, **kwargs):
1505 | super().__init__(*args, **kwargs)
1506 |
1507 | def value(self) -> bool:
1508 | return self.checkState() == QtCore.Qt.Checked
1509 |
1510 | def setValue(self, value : bool):
1511 | self.setCheckState(QtCore.Qt.Checked if value else QtCore.Qt.Unchecked)
1512 |
1513 |
1514 | def InputBoolean(name=None, label=None, value=False, stretch=0,
1515 | alignment=QtCore.Qt.Alignment(), **kwargs):
1516 | editor = QCheckBoxExt()
1517 | editor.setValue(value)
1518 | set_qt_attrs(editor, **kwargs)
1519 | if name:
1520 | editor.setObjectName(name)
1521 | place_widget(editor, label=label, stretch=stretch, alignment=alignment)
1522 | return editor
1523 |
1524 | # └────────────────────────────────────────────────────────────────────────────┘
1525 | # [SECTION] [GUI] [Widget] InputVector
1526 | # ┌────────────────────────────────────────────────────────────────────────────┐
1527 |
1528 | class InputVectorWrapper:
1529 | def __init__(self, g, x, y, z):
1530 | self.group = g
1531 | self.x = x
1532 | self.y = y
1533 | self.z = z
1534 |
1535 | def value(self) -> Vec:
1536 | return Vec(self.x.value(), self.y.value(), self.z.value())
1537 |
1538 | def setValue(self, value):
1539 | v = to_vec(value)
1540 | self.x.setValue(v.x)
1541 | self.y.setValue(v.y)
1542 | self.z.setValue(v.z)
1543 |
1544 | def InputVector(label=None, value=(0.0,0.0,0.0)):
1545 | with GroupBox(title=label) as g:
1546 | with Col():
1547 | x = InputFloat(label="X:")
1548 | y = InputFloat(label="Y:")
1549 | z = InputFloat(label="Z:")
1550 | widget = InputVectorWrapper(g, x, y, z)
1551 | widget.setValue(value)
1552 | return widget
1553 |
1554 |
1555 | # └────────────────────────────────────────────────────────────────────────────┘
1556 | # [SECTION] [GUI] [Widget] InputOptions
1557 | # ┌────────────────────────────────────────────────────────────────────────────┐
1558 |
1559 | class InputOptionsWrapper:
1560 | def __init__(self, combobox:QtGui.QComboBox, data: Dict[str,Any]):
1561 | self.combobox = combobox
1562 | self.index = dict()
1563 | self.lookup = dict()
1564 | i = 0
1565 | for label, value in data.items():
1566 | self.index[i] = value
1567 | self.lookup[value] = i
1568 | i += 1
1569 | combobox.addItem(label)
1570 |
1571 | def value(self):
1572 | return self.index.get(self.combobox.currentIndex(), None)
1573 |
1574 | def setValue(self, value):
1575 | index = self.lookup.get(value, None)
1576 | if index is not None:
1577 | self.combobox.setCurrentIndex(index)
1578 |
1579 | def InputOptions(options, value=None, label=None, name=None, stretch=0,
1580 | alignment=QtCore.Qt.Alignment(), **kwargs):
1581 | widget = QtGui.QComboBox()
1582 | set_qt_attrs(widget, **kwargs)
1583 | editor = InputOptionsWrapper(widget, options)
1584 | if value is not None:
1585 | editor.setValue(value)
1586 | if name:
1587 | editor.combobox.setObjectName(name)
1588 | place_widget(editor.combobox, label=label, stretch=stretch, alignment=alignment)
1589 | return editor
1590 |
1591 |
1592 | # └────────────────────────────────────────────────────────────────────────────┘
1593 | # [SECTION] [GUI] [Widget] InputSelectOne
1594 | # ┌────────────────────────────────────────────────────────────────────────────┐
1595 |
1596 | class InputSelectOne:
1597 |
1598 | def __init__(self, label=None, name=None, active=False, auto_deactivate=True):
1599 | self._value = None
1600 | self._pre = None
1601 | self._auto_deactivate = auto_deactivate
1602 | self.selected = PySignal()
1603 | with Row(add=False, spacing=0, margin=0, contentsMargins=(0,0,0,0)) as ctl:
1604 |
1605 | @button(
1606 | text="Select...",
1607 | tool=True,
1608 | checkable=True,
1609 | styleSheet="QToolButton:checked{background-color: #FF0000; color:#FFFFFF;}",
1610 | focusPolicy=QtCore.Qt.FocusPolicy.NoFocus,
1611 | objectName=name,
1612 | checked=active)
1613 | def select(): pass
1614 |
1615 | @button(
1616 | tool=True,
1617 | focusPolicy=QtCore.Qt.FocusPolicy.NoFocus,
1618 | icon=Icon(':icons/edit-cleartext.svg'))
1619 | def clear(): self.setValue(None)
1620 |
1621 | display = QtGui.QLineEdit()
1622 | display.setReadOnly(True)
1623 | place_widget(display)
1624 |
1625 | self.display = display
1626 | self.button = select
1627 | register_select_observer(select, self)
1628 |
1629 | with Parent():
1630 | place_widget(ctl, label=label)
1631 |
1632 | @property
1633 | def active(self) -> bool:
1634 | return self.button.isChecked()
1635 |
1636 | def value(self) -> Optional[SelectedObject]:
1637 | return self._value
1638 |
1639 | def pre(self) -> Optional[SelectedObject]:
1640 | return self._pre
1641 |
1642 | def setValue(self, value: Optional[SelectedObject]) -> None:
1643 | self._value = value
1644 | if value:
1645 | self.display.setText(f"{value.doc}#{value.obj}.{value.sub}")
1646 | if self._auto_deactivate:
1647 | self.button.setChecked(False)
1648 | self.selected.trigger(self._value)
1649 | else:
1650 | self.display.setText(f"")
1651 |
1652 | def setPreselection(self, doc, obj, sub):
1653 | if self.button.isChecked():
1654 | self._pre = SelectedObject(doc, obj, sub)
1655 |
1656 | def addSelection(self, doc, obj, sub, pnt):
1657 | if self.button.isChecked():
1658 | self.setValue(SelectedObject(doc, obj, sub, pnt))
1659 |
1660 | def removeSelection(self, doc, obj, sub):
1661 | if self.button.isChecked():
1662 | if self._value:
1663 | v = self._value
1664 | if (v.doc, v.obj) == (doc, obj):
1665 | self.setValue(None)
1666 |
1667 | def setSelection(self, doc):
1668 | if self.button.isChecked():
1669 | self.setValue(SelectedObject(doc, Gui.Selection.getSelection()[-1].Name))
1670 |
1671 | def clearSelection(self, doc):
1672 | pass
1673 |
1674 |
1675 | # └────────────────────────────────────────────────────────────────────────────┘
1676 | # [SECTION] [GUI] [Widget] InputSelectMany
1677 | # ┌────────────────────────────────────────────────────────────────────────────┐
1678 |
1679 | class InputSelectMany:
1680 |
1681 | ValueDataRole = QtCore.Qt.UserRole
1682 |
1683 | def __init__(self, label=None, name=None, active=False):
1684 | self._value = set()
1685 | self.selected = PySignal()
1686 | with Col(add=False, spacing=0, margin=0, contentsMargins=(0,0,0,0)) as ctl:
1687 | with Row(spacing=0, margin=0, contentsMargins=(0,0,0,0)):
1688 | @button(
1689 | text="Add",
1690 | alignment=QtCore.Qt.AlignLeft,
1691 | tool=True,
1692 | checkable=True,
1693 | styleSheet="QToolButton:checked{background-color: #FF0000; color:#FFFFFF;}",
1694 | focusPolicy=QtCore.Qt.FocusPolicy.NoFocus,
1695 | objectName=name,
1696 | checked=active)
1697 | def select(): pass
1698 |
1699 | @button(
1700 | text="Remove",
1701 | tool=True,
1702 | alignment=QtCore.Qt.AlignLeft,
1703 | focusPolicy=QtCore.Qt.FocusPolicy.NoFocus)
1704 | def remove():
1705 | selected = self.display.selectedItems()
1706 | for item in selected:
1707 | value = item.data(0, InputSelectMany.ValueDataRole)
1708 | self._value.remove(value)
1709 | self.display.takeTopLevelItem(self.display.indexOfTopLevelItem(item))
1710 |
1711 | @button(
1712 | text="Clean",
1713 | tool=True,
1714 | alignment=QtCore.Qt.AlignLeft,
1715 | focusPolicy=QtCore.Qt.FocusPolicy.NoFocus,
1716 | icon=Icon(':icons/edit-cleartext.svg'))
1717 | def clear():
1718 | self._value.clear()
1719 | self.display.clear()
1720 |
1721 | Stretch()
1722 |
1723 | display = QtGui.QTreeWidget()
1724 | display.setColumnCount(2)
1725 | display.setHeaderLabels(['Object', 'SubObject'])
1726 | place_widget(display)
1727 |
1728 | self.display = display
1729 | self.button = select
1730 | register_select_observer(select, self)
1731 |
1732 | with Parent():
1733 | with GroupBox(title=label):
1734 | place_widget(ctl)
1735 |
1736 | @property
1737 | def active(self) -> bool:
1738 | return self.button.isChecked()
1739 |
1740 | def value(self) -> List[SelectedObject]:
1741 | return self._value
1742 |
1743 | def addValue(self, value: SelectedObject) -> None:
1744 | if value not in self._value:
1745 | item = QtGui.QTreeWidgetItem([value.obj, value.sub])
1746 | item.setData(0, InputSelectMany.ValueDataRole, value)
1747 | self.display.addTopLevelItem(item)
1748 | self._value.add(value)
1749 | self.selected.trigger(value)
1750 |
1751 | def setPreselection(self, doc, obj, sub):
1752 | pass
1753 |
1754 | def addSelection(self, doc, obj, sub, pnt):
1755 | if self.button.isChecked():
1756 | self.addValue(SelectedObject(doc, obj, sub, pnt))
1757 |
1758 | def removeSelection(self, doc, obj, sub):
1759 | pass
1760 |
1761 | def setSelection(self, doc):
1762 | if self.button.isChecked():
1763 | self.addValue(SelectedObject(doc, Gui.Selection.getSelection()[-1].Name))
1764 |
1765 | def clearSelection(self, doc):
1766 | pass
1767 |
1768 |
1769 | # └────────────────────────────────────────────────────────────────────────────┘
1770 | # [SECTION] [GUI] [Widget] button
1771 | # ┌────────────────────────────────────────────────────────────────────────────┐
1772 |
1773 | def button(label=None, add:bool=True, tool:bool=False, stretch=0, alignment=QtCore.Qt.Alignment(), **kwargs):
1774 | if tool:
1775 | btn = QtGui.QToolButton()
1776 | else:
1777 | btn = QtGui.QPushButton()
1778 | set_qt_attrs(btn, **kwargs)
1779 | if label:
1780 | btn.setText(label)
1781 | elif 'text' not in kwargs:
1782 | btn.setText("Button")
1783 | if add:
1784 | place_widget(btn, stretch=stretch, alignment=alignment)
1785 | def wrapper(handler):
1786 | btn.clicked.connect(handler)
1787 | return btn
1788 | return wrapper
1789 |
1790 |
1791 | # └────────────────────────────────────────────────────────────────────────────┘
1792 | # [SECTION] [GUI] progress_indicator
1793 | # ┌────────────────────────────────────────────────────────────────────────────┐
1794 |
1795 | class ProgressIndicator:
1796 | def __init__(self, *args, **kwargs) -> None:
1797 | try:
1798 | self.control = Base.ProgressIndicator(*args, **kwargs)
1799 | except:
1800 | self.control = None
1801 | def start(self, *args, **kwargs):
1802 | if self.control:
1803 | self.control.start(*args, **kwargs)
1804 | def next(self, *args, **kwargs):
1805 | if self.control:
1806 | self.control.next(*args, **kwargs)
1807 | def stop(self, *args, **kwargs):
1808 | if self.control:
1809 | self.control.stop(*args, **kwargs)
1810 |
1811 | @contextmanager
1812 | def progress_indicator(message: str = "Working...", steps: int = 0):
1813 | bar = ProgressIndicator()
1814 | bar.start(message, steps)
1815 | try:
1816 | yield bar
1817 | finally:
1818 | bar.stop()
1819 | del bar
1820 |
1821 |
1822 | # └────────────────────────────────────────────────────────────────────────────┘
1823 | # [SECTION] [GUI] State
1824 | # ┌────────────────────────────────────────────────────────────────────────────┐
1825 |
1826 | def qt_get_widget_path(widget, index):
1827 | name = widget.objectName()
1828 | if not name:
1829 | name = f"{widget.__class__.__name__}_{index}"
1830 | path = [name]
1831 | parent = widget.parent()
1832 | while parent:
1833 | path.append(parent.objectName() or parent.__class__.__name__)
1834 | parent = parent.parent()
1835 | return "/".join(reversed(path))
1836 |
1837 |
1838 | def save_widget_state(widget, name):
1839 | root = get_macros_dir()
1840 | data = {}
1841 | index = 0
1842 | for child in widget.findChildren(QtGui.QWidget):
1843 | if hasattr(child, 'value'):
1844 | path = qt_get_widget_path(child, index)
1845 | index += 1
1846 | try:
1847 | data[path] = child.value()
1848 | except:
1849 | print(f"Ignoring value of {path}")
1850 | with open(Path(root, f"fcscript_{name}.json"), 'w') as f:
1851 | f.write(json.dumps(data))
1852 |
1853 |
1854 | def load_widget_state(widget, name):
1855 | file = Path(get_macros_dir(), f"fcscript_{name}.json")
1856 | if file.exists():
1857 | with open(file, 'r') as f:
1858 | data = json.load(f)
1859 | index = 0
1860 | for child in widget.findChildren(QtGui.QWidget):
1861 | if hasattr(child, 'value'):
1862 | path = qt_get_widget_path(child, index)
1863 | index += 1
1864 | if path in data:
1865 | try:
1866 | child.setValue(data[path])
1867 | except:
1868 | print(f"Ignoring value of {path} because it was not found")
1869 |
1870 |
1871 | # └────────────────────────────────────────────────────────────────────────────┘
1872 | # [SECTION] [GUI] Message Boxes
1873 | # ┌────────────────────────────────────────────────────────────────────────────┐
1874 |
1875 | def show_msgbox(message, title="Information", std_icon=QtGui.QMessageBox.Information, std_buttons=QtGui.QMessageBox.NoButton, parent=None):
1876 | diag = QtGui.QMessageBox(std_icon, title, message, std_buttons, parent)
1877 | diag.setWindowModality(QtCore.Qt.ApplicationModal)
1878 | diag.exec_()
1879 |
1880 |
1881 | def show_warning(message, title="Warning", std_icon=QtGui.QMessageBox.Warning, std_buttons=QtGui.QMessageBox.NoButton, parent=None):
1882 | show_msgbox(message, title, std_icon, std_buttons, parent)
1883 |
1884 |
1885 | def show_error(message, title="Error", std_icon=QtGui.QMessageBox.Critical, std_buttons=QtGui.QMessageBox.NoButton, parent=None):
1886 | show_msgbox(message, title, std_icon, std_buttons, parent)
1887 |
1888 |
1889 | show_info = show_msgbox
1890 |
1891 |
1892 | # └────────────────────────────────────────────────────────────────────────────┘
1893 | # [SECTION] [TEST]
1894 | # ┌────────────────────────────────────────────────────────────────────────────┐
1895 |
1896 |
1897 |
--------------------------------------------------------------------------------
/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | FCScript
4 | DSL For Macros
5 | 0.0.1
6 | Frank Martinez
7 | GPL-3.0
8 | https://github.com/mnesarco/fcscript
9 | https://github.com/mnesarco/fcscript/wiki
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mnesarco/fcscript/bbdf566249578f27e95845cef9b752900e88ddf9/screenshot.jpg
--------------------------------------------------------------------------------