remove git repo status from boxes

This commit is contained in:
Mario Voigt 2022-11-30 15:57:58 +01:00
parent 803f35fd41
commit 7e149e5d9c
382 changed files with 28618 additions and 0 deletions

View File

@ -0,0 +1 @@
This page is intentionally left blank.

View File

@ -0,0 +1,165 @@
Contributing to Boxes.py
========================
You are thinking about contributing to Boxes.py? That's great!
Boxes.py is designed to be re-used and extended.
This document gives you some guidelines how your contribution is most
likely to impact the development and your changes are most likely to
be merged into the upstream repository.
Most of them should be just general best practises and not be
surprising. Don't worry if you find them too complicated. It is OK
leave the final touch to someone else.
Writing code for Boxes.py
-------------------------
You will often be compelled to just do a quick thing that will solve
your immediate needs. That's fine. But nevertheless it is often worth
doing things the right way and be able to submit your changes
upstream. For one to give something back to the community. But also
for purely selfish reasons like getting the code maintained. Also
Boxes.py is designed to make doing things properly the easy way.
Here are some guidelines that make this easier. Depending on what you
are up to they may apply to a varying degree. It's ok to submit
patches that are not quite ready yet. But please state in the pull
request message what you think the status is and whether you want help
or are going to finish it on your own.
* Please fork the repository at GitHub before getting started
* Start with creating separate branches for each of your new generators or features
* You can merge them into your master branch to have them all in one place
* Please continue your work in the branches and repeatedly merge them to master
* Before submitting a pull request intended to go upstream have clean patches that are self contained and error free
* Re-order and squash patches with *git rebase -i*
* The patches should containing meaningful changes and not (necessarily) reflect how the code was created
* Rebase your branch to the current master branch
* Be prepared that your code may get reworked before being merged upstream
* Submit a pull request in GitHub based on your feature branch
* Describe the status of the patch set and your intentions with it in the pull request message
If you want to discuss your idea open a ticket describing it and ask
questions there. This is encouraged even if you think you know what
you want to do. There are many short cuts in Boxes.py and pointing you
in the right direction may save you a lot of work.
If you want feed back on you code feel free to open a PR. State that
this is work in progress in the PR message. It's OK if it does not
follow the guidelines (yet).
Writing new Generators
......................
Writing new generators is the most straight forward thing to do with
Boxes.py. Here are some guidelines that make it easier to get them added:
* Start with a copy of another generator or *boxes/generators/_template.py*
* Commit changes to the library in separate patches
* Use parameters with sane defaults instead of hard coding dimensions
* Simple generators can end up as one single commit
* For more complicated generators there can be multiple patches -
each adding another feature
Adding new Dependencies
.......................
Adding new dependencies should be considered thoroughly. If a new
depencendcy is added it needs to be added in all these places:
* *documentation/src/install.rst*
* RST files in *documentation/src/install/*
* *scripts/Dockerfile*
* *.travis.yml*
If it is a Python module it also needs to be added:
* *requirements.txt*
* *setup.py*
Improving the Documentation
---------------------------
Boxes.py comes with Sphinx based documentation that is in large parts
generated from the doc strings in the code. Nevertheless documentation
has a tendency to get outdated. If you encounter outdated pieces of
documentation feel free to submit a pull request or open a ticket
pointing out what should be changed or even suggesting a better text.
To check your changes docs need to be build with *make html* in
*documentation/src*. This places the compiled documentation in
*documentation/build/html*. You need to have *sphinx* installed for
this to work.
The online documentation gets build and updated automatically by the Travis CI
as soon as the changes makes it into the GitHub master branch.
Provide photos for generators
-----------------------------
Many generators still come without an example photo. If you are
creating such an item consider donating a good picture. You can
simply attach it to `ticket #140
<https://github.com/florianfesti/boxes/issues/140>`_. If you want you can
also create a proper pull request instead:
* Make sure you have sh, convert (ImageMagick), sed and sha256sum installed
* The picture needs to be an jpg file with the name of the generator
(This is case sensitive. Use CamelCase.)
* The picture should be 1200 pixels wide and square or not too far
from square (3:4 is fine).
* Place the file in *static/samples/*
* Check if the picture shows up at the bottom of the settings page of
the generator when running *scripts/boxesserver*
* Change dir to *./scripts* and there execute *./gen_thumbnails.sh*
* Check if the thumbnail is seen in the main page when hovering over
the generator entry
* Create a commit including *static/samples/$GeneratorName\*.jpg* and
*static/samples/samples.sha256*
* Create a pull request from that
Improving the User Interface
----------------------------
Coming up with good names and good descriptions is hard. Often writing
a new generator is much easier than coming up with a good name for it
and its arguments. If you think something deserves a better name or
description and you can come up with one please don't hesitate to open
a ticket. It is this small things that make something like Boxes.py
easy or hard to use.
There is also an - often empty - space for a longer text for each
generator that could house assembling instructions, instructions for
use or just more detailed descriptions. If you are interested in
writing some please open a ticket. Your text does not have to be
perfect. We can work on it together.
Running the Code
----------------------------
To serve website, run `scripts/boxesserver` script
Reporting bugs
--------------
If you encounter issues with Boxes.py, please open a ticket at
GitHub. Please provide all information necessary to reproduce the
bug. Often this can be the URL of the broken result. If the issue is
easy to spot it may be sufficient to just give a brief
description. Otherwise it can be helpful to attach the resulting SVG,
a screen shot or the error message. Add a "bug" tag to draw additional
attention.
Suggesting new generators or features
-------------------------------------
If you have an idea for a new generator or feature please open a
ticket. Give some short rational how or where you would use such a
thing. Try to give a precise description how it should look like and
which features and details are important. The less is left open the
easier it is to implement. You can add an "enhancement" tag.

View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@ -0,0 +1,62 @@
About Boxes.py
==============
+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+
| .. image:: static/samples/NotesHolder.jpg | .. image:: static/samples/OttoBody.jpg | .. image:: static/samples/PaintStorage.jpg | .. image:: static/samples/ShutterBox.jpg | .. image:: static/samples/TwoPiece.jpg |
+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+
* Boxes.py is an online box generator
* https://www.festi.info/boxes.py/index.html
* Boxes.py is an Inkscape plug-in
* Boxes.py is library to write your own
* Boxes.py is free software licensed under GPL v3+
* Boxes.py is written in Python and runs with Python 3
Boxes.py comes with a growing set of ready-to-use, fully parametrized
generators. See https://florianfesti.github.io/boxes/html/generators.html for the full list.
+----------------------------------------------+----------------------------------------------+----------------------------------------------+
| .. image:: static/samples/AngledBox.jpg | .. image:: static/samples/FlexBox2.jpg | .. image:: static/samples/HingeBox.jpg |
+----------------------------------------------+----------------------------------------------+----------------------------------------------+
Features
--------
Boxes.py generates SVG images that can be viewed directly in a web browser but also
postscript and - with pstoedit as external helper - other vector formats
including dxf, plt (aka hpgl) and gcode.
Of course the library and the generators allow selecting the "thickness"
of the material used and automatically adjusts lengths and width of
joining fingers and other elements.
The "burn" parameter compensates for the material removed by the laser. This
allows fine tuning the gaps between joins up to the point where plywood
can be press fitted even without any glue.
Finger Joints are the work horse of the library. They allow 90° edges
and T connections. Their size is scaled up with the material
"thickness" to maintain the same appearance. The library also allows
putting holes and slots for screws (bed bolts) into finger joints,
although this is currently not supported for the included generators.
Dovetail joints can be used to join pieces in the same plane.
Flex cuts allows bending and stretching the material in one direction. This
is used for rounded edges and living hinges.
+----------------------------------------------+----------------------------------------------+----------------------------------------------+
| .. image:: static/samples/TypeTray.jpg | .. image:: static/samples/BinTray.jpg | .. image:: static/samples/DisplayShelf.jpg |
+----------------------------------------------+----------------------------------------------+----------------------------------------------+
| .. image:: static/samples/AgricolaInsert.jpg | .. image:: static/samples/HeartBox.jpg | .. image:: static/samples/Atreus21.jpg |
+----------------------------------------------+----------------------------------------------+----------------------------------------------+
Documentation
-------------
Boxes.py comes with Sphinx based documentation for usage, installation
and development.
The rendered version can be viewed at <https://florianfesti.github.io/boxes/html/index.html>.

View File

@ -0,0 +1,16 @@
class Color:
BLACK = [ 0.0, 0.0, 0.0 ]
BLUE = [ 0.0, 0.0, 1.0 ]
GREEN = [ 0.0, 1.0, 0.0 ]
RED = [ 1.0, 0.0, 0.0 ]
CYAN = [ 0.0, 1.0, 1.0 ]
YELLOW = [ 1.0, 1.0, 0.0 ]
MAGENTA = [ 1.0, 0.0, 1.0 ]
WHITE = [ 1.0, 1.0, 1.0 ]
# TODO: Make this configurable
OUTER_CUT = BLACK
INNER_CUT = BLUE
ANNOTATIONS = RED
ETCHING = GREEN
ETCHING_DEEP = CYAN

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
class Extents:
__slots__ = "xmin ymin xmax ymax".split()
def __init__(self,xmin=float('inf'),ymin=float('inf'),xmax=float('-inf'),ymax=float('-inf')):
self.xmin = xmin
self.ymin = ymin
self.xmax = xmax
self.ymax = ymax
def add(self,x,y):
self.xmin = min(self.xmin,x)
self.xmax = max(self.xmax,x)
self.ymin = min(self.ymin,y)
self.ymax = max(self.ymax,y)
def extend(self,l):
for x,y in l:
self.add(x,y)
def __add__(self,extent):
#todo: why can this happen?
if extent == 0:
return Extents(self.xmin,self.ymin,self.xmax,self.ymax)
return Extents(
min(self.xmin,extent.xmin),min(self.ymin,extent.ymin),
max(self.xmax,extent.xmax),max(self.ymax,extent.ymax)
)
def __radd__(self,extent):
if extent == 0:
return Extents(self.xmin,self.ymin,self.xmax,self.ymax)
return self.__add__(extent)
def get_width(self):
return self.xmax-self.xmin
def get_height(self):
return self.ymax-self.ymin
width = property(get_width)
height = property(get_height)
def __repr__(self):
return f'Extents ({self.xmin},{self.ymin})-({self.xmax},{self.ymax})'

View File

@ -0,0 +1,91 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import subprocess
import tempfile
import os
import shutil
from boxes.drawing import SVGSurface, PSSurface, LBRN2Surface, Context
class Formats:
pstoedit_candidates = ["/usr/bin/pstoedit", "pstoedit", "pstoedit.exe"]
_BASE_FORMATS = ['svg', 'svg_Ponoko', 'ps', 'lbrn2']
formats = {
"svg": None,
"svg_Ponoko": None,
"ps": None,
"lbrn2": None,
"dxf": "-flat 0.1 -f dxf:-mm".split(),
"gcode": "-f gcode".split(),
"plt": "-f plot-hpgl".split(),
"ai": "-f ps2ai".split(),
"pdf": "-f pdf".split(),
}
http_headers = {
"svg": [('Content-type', 'image/svg+xml; charset=utf-8')],
"svg_Ponoko": [('Content-type', 'image/svg+xml; charset=utf-8')],
"ps": [('Content-type', 'application/postscript')],
"lbrn2": [('Content-type', 'application/lbrn2')],
"dxf": [('Content-type', 'image/vnd.dxf')],
"plt": [('Content-type', ' application/vnd.hp-hpgl')],
"gcode": [('Content-type', 'text/plain; charset=utf-8')],
# "" : [('Content-type', '')],
}
def __init__(self):
for cmd in self.pstoedit_candidates:
self.pstoedit = shutil.which(cmd)
if self.pstoedit:
break
def getFormats(self):
if self.pstoedit:
return sorted(self.formats.keys())
return self._BASE_FORMATS
def getSurface(self, fmt, filename):
if fmt in ("svg", "svg_Ponoko"):
surface = SVGSurface(filename)
elif fmt == "lbrn2":
surface = LBRN2Surface(filename)
else:
surface = PSSurface(filename)
ctx = Context(surface)
return surface, ctx
def convert(self, filename, fmt, metadata=None):
if fmt not in self._BASE_FORMATS:
fd, tmpfile = tempfile.mkstemp(dir=os.path.dirname(filename))
cmd = [self.pstoedit] + self.formats[fmt] + [filename, tmpfile]
err = subprocess.call(cmd)
if err:
# XXX show stderr output
try:
os.unlink(tmpfile)
except:
pass
raise ValueError("Conversion failed. pstoedit returned %i" % err)
os.rename(tmpfile, filename)

View File

@ -0,0 +1,740 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
""
'''
Copyright (C) 2007 Aaron Spike (aaron @ ekips.org)
Copyright (C) 2007 Tavmjong Bah (tavmjong @ free.fr)
Copyright (C) http://cnc-club.ru/forum/viewtopic.php?f=33&t=434&p=2594#p2500
Copyright (C) 2014 Jürgen Weigert (juewei@fabmail.org)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2014-03-20 jw@suse.de 0.2 Option --accuracy=0 for automatic added.
2014-03-21 sent upstream: https://bugs.launchpad.net/inkscape/+bug/1295641
2014-03-21 jw@suse.de 0.3 Fixed center of rotation for gears with odd number of teeth.
2014-04-04 juewei 0.7 Revamped calc_unit_factor().
2014-04-05 juewei 0.7a Correctly positioned rack gear.
The geometry above the meshing line is wrong.
2014-04-06 juewei 0.7b Undercut detection added. Reference:
http://nptel.ac.in/courses/IIT-MADRAS/Machine_Design_II/pdf/2_2.pdf
Manually merged https://github.com/jnweiger/inkscape-gears-dev/pull/15
2014-04-07 juewei 0.7c Manually merged https://github.com/jnweiger/inkscape-gears-dev/pull/17
2014-04-09 juewei 0.8 Fixed https://github.com/jnweiger/inkscape-gears-dev/issues/19
Ring gears are ready for production now. Thanks neon22 for driving this.
Profile shift implemented (Advanced Tab), fixing
https://github.com/jnweiger/inkscape-gears-dev/issues/9
2015-05-29 juewei 0.9 ported to inkscape 0.91
AttributeError: 'module' object inkex has no attribute 'uutounit
Fixed https://github.com/jnweiger/inkscape-gears-dev
'''
from os import devnull # for debugging
from math import pi, cos, sin, tan, radians, degrees, ceil, asin, acos, sqrt
two_pi = 2 * pi
import argparse
from boxes.vectors import kerf, vdiff, vlength
__version__ = '0.9'
def linspace(a,b,n):
""" return list of linear interp of a to b in n steps
- if a and b are ints - you'll get an int result.
- n must be an integer
"""
return [a+x*(b-a)/(n-1) for x in range(0,n)]
def involute_intersect_angle(Rb, R):
" "
Rb, R = float(Rb), float(R)
return (sqrt(R**2 - Rb**2) / (Rb)) - (acos(Rb / R))
def point_on_circle(radius, angle):
" return xy coord of the point at distance radius from origin at angle "
x = radius * cos(angle)
y = radius * sin(angle)
return (x, y)
### Undercut support functions
def undercut_min_teeth(pitch_angle, k=1.0):
"""
computes the minimum tooth count for a
spur gear so that no undercut with the given pitch_angle (in deg)
and an addendum = k * metric_module, where 0 < k < 1
Note:
The return value should be rounded upwards for perfect safety. E.g.
min_teeth = int(math.ceil(undercut_min_teeth(20.0))) # 18, not 17
"""
x = max(sin(radians(pitch_angle)), 0.01)
return 2*k /(x*x)
def undercut_max_k(teeth, pitch_angle=20.0):
""" computes the maximum k value for a given teeth count and pitch_angle
so that no undercut occurs.
"""
x = max(sin(radians(pitch_angle)), 0.01)
return 0.5 * teeth * x * x
def undercut_min_angle(teeth, k=1.0):
""" computes the minimum pitch angle, to that the given teeth count (and
profile shift) cause no undercut.
"""
return degrees(asin(min(0.856, sqrt(2.0*k/teeth)))) # max 59.9 deg
def have_undercut(teeth, pitch_angle=20.0, k=1.0):
""" returns true if the specified number of teeth would
cause an undercut.
"""
return (teeth < undercut_min_teeth(pitch_angle, k))
## gather all basic gear calculations in one place
def gear_calculations(num_teeth, circular_pitch, pressure_angle, clearance=0, ring_gear=False, profile_shift=0.):
""" Put base calcs for spur/ring gears in one place.
- negative profile shifting helps against undercut.
"""
diametral_pitch = pi / circular_pitch
pitch_diameter = num_teeth / diametral_pitch
pitch_radius = pitch_diameter / 2.0
addendum = 1 / diametral_pitch
dedendum = addendum
dedendum *= 1+profile_shift
addendum *= 1-profile_shift
if ring_gear:
addendum = addendum + clearance # our method
else:
dedendum = dedendum + clearance # our method
base_radius = pitch_diameter * cos(radians(pressure_angle)) / 2.0
outer_radius = pitch_radius + addendum
root_radius = pitch_radius - dedendum
# Tooth thickness: Tooth width along pitch circle.
tooth_thickness = ( pi * pitch_diameter ) / ( 2.0 * num_teeth )
return (pitch_radius, base_radius,
addendum, dedendum, outer_radius, root_radius,
tooth_thickness
)
def generate_rack_points(tooth_count, pitch, addendum, pressure_angle,
base_height, tab_length, clearance=0, draw_guides=False):
""" Return path (suitable for svg) of the Rack gear.
- rack gear uses straight sides
- involute on a circle of infinite radius is a simple linear ramp
- the meshing circle touches at y = 0,
- the highest elevation of the teeth is at y = +addendum
- the lowest elevation of the teeth is at y = -addendum-clearance
- the base_height extends downwards from the lowest elevation.
- we generate this middle tooth exactly centered on the y=0 line.
(one extra tooth on the right hand side, if number of teeth is even)
"""
spacing = 0.5 * pitch # rolling one pitch distance on the spur gear pitch_diameter.
# roughly center rack in drawing, exact position is so that it meshes
# nicely with the spur gear.
# -0.5*spacing has a gap in the center.
# +0.5*spacing has a tooth in the center.
if tab_length <= 0.0:
tab_length = 1E-8
tas = tan(radians(pressure_angle)) * addendum
tasc = tan(radians(pressure_angle)) * (addendum+clearance)
base_top = addendum+clearance
base_bot = addendum+clearance+base_height
x_lhs = -pitch * 0.5*tooth_count - tab_length
# Start with base tab on LHS
points = [] # make list of points
points.append((x_lhs, base_bot))
points.append((x_lhs, base_top))
x = x_lhs + tab_length+tasc
# An involute on a circle of infinite radius is a simple linear ramp.
# We need to add curve at bottom and use clearance.
for i in range(tooth_count):
# move along path, generating the next 'tooth'
# pitch line is at y=0. the left edge hits the pitch line at x
points.append((x-tasc, base_top))
points.append((x+tas, -addendum))
points.append((x+spacing-tas, -addendum))
points.append((x+spacing+tasc, base_top))
x += pitch
# add base on RHS
x_rhs = x - tasc + tab_length
points.append((x_rhs, base_top))
points.append((x_rhs, base_bot))
# We don't close the path here. Caller does it.
# points.append((x_lhs, base_bot))
# Draw line representing the pitch circle of infinite diameter
guide_path = None
p = []
if draw_guides:
p.append( (x_lhs + 0.5 * tab_length, 0) )
p.append( (x_rhs - 0.5 * tab_length, 0) )
return (points, p)
def generate_spur_points(teeth, base_radius, pitch_radius, outer_radius, root_radius, accuracy_involute, accuracy_circular):
""" given a set of core gear params
- generate the svg path for the gear
"""
half_thick_angle = two_pi / (4.0 * teeth ) #?? = pi / (2.0 * teeth)
pitch_to_base_angle = involute_intersect_angle( base_radius, pitch_radius )
pitch_to_outer_angle = involute_intersect_angle( base_radius, outer_radius ) - pitch_to_base_angle
start_involute_radius = max(base_radius, root_radius)
radii = linspace(start_involute_radius, outer_radius, accuracy_involute)
angles = [involute_intersect_angle(base_radius, r) for r in radii]
centers = [(x * two_pi / float( teeth) ) for x in range( teeth ) ]
points = []
for c in centers:
# Angles
pitch1 = c - half_thick_angle
base1 = pitch1 - pitch_to_base_angle
offsetangles1 = [ base1 + x for x in angles]
points1 = [ point_on_circle( radii[i], offsetangles1[i]) for i in range(0,len(radii)) ]
pitch2 = c + half_thick_angle
base2 = pitch2 + pitch_to_base_angle
offsetangles2 = [ base2 - x for x in angles]
points2 = [ point_on_circle( radii[i], offsetangles2[i]) for i in range(0,len(radii)) ]
points_on_outer_radius = [ point_on_circle(outer_radius, x) for x in linspace(offsetangles1[-1], offsetangles2[-1], accuracy_circular) ]
if root_radius > base_radius:
pitch_to_root_angle = pitch_to_base_angle - involute_intersect_angle(base_radius, root_radius )
root1 = pitch1 - pitch_to_root_angle
root2 = pitch2 + pitch_to_root_angle
points_on_root = [point_on_circle (root_radius, x) for x in linspace(root2, root1+(two_pi/float(teeth)), accuracy_circular) ]
p_tmp = points1 + points_on_outer_radius[1:-1] + points2[::-1] + points_on_root[1:-1] # [::-1] reverses list; [1:-1] removes first and last element
else:
points_on_root = [point_on_circle (root_radius, x) for x in linspace(base2, base1+(two_pi/float(teeth)), accuracy_circular) ]
p_tmp = points1 + points_on_outer_radius[1:-1] + points2[::-1] + points_on_root # [::-1] reverses list
points.extend( p_tmp )
return (points)
def inkbool(val):
return val not in ("False", False, "0", 0, "None", None)
class OptionParser(argparse.ArgumentParser):
types = {
"int" : int,
"float" : float,
"string" : str,
"inkbool" : inkbool,
}
def add_option(self, short, long_, **kw):
kw["type"] = self.types[kw["type"]]
names = []
if short:
names.append("-" + short.replace("-", "_")[1:])
if long_:
names.append("--" + long_.replace("-", "_")[2:])
self.add_argument(*names, **kw)
class Gears():
def __init__(self, boxes, **kw):
# an alternate way to get debug info:
# could use inkex.debug(string) instead...
try:
self.tty = open("/dev/tty", 'w')
except:
self.tty = open(devnull, 'w') # '/dev/null' for POSIX, 'nul' for Windows.
# print >>self.tty, "gears-dev " + __version__
self.boxes = boxes
self.OptionParser = OptionParser()
self.OptionParser.add_option("-t", "--teeth",
action="store", type="int",
dest="teeth", default=24,
help="Number of teeth")
self.OptionParser.add_option("-s", "--system",
action="store", type="string",
dest="system", default='MM',
help="Select system: 'CP' (Cyclic Pitch (default)), 'DP' (Diametral Pitch), 'MM' (Metric Module)")
self.OptionParser.add_option("-d", "--dimension",
action="store", type="float",
dest="dimension", default=1.0,
help="Tooth size, depending on system (which defaults to CP)")
self.OptionParser.add_option("-a", "--angle",
action="store", type="float",
dest="angle", default=20.0,
help="Pressure Angle (common values: 14.5, 20, 25 degrees)")
self.OptionParser.add_option("-p", "--profile-shift",
action="store", type="float",
dest="profile_shift", default=20.0,
help="Profile shift [in percent of the module]. Negative values help against undercut")
self.OptionParser.add_option("-u", "--units",
action="store", type="string",
dest="units", default='mm',
help="Units this dialog is using")
self.OptionParser.add_option("-A", "--accuracy",
action="store", type="int",
dest="accuracy", default=0,
help="Accuracy of involute: automatic: 5..20 (default), best: 20(default), medium 10, low: 5; good acuracy is important with a low tooth count")
# Clearance: Radial distance between top of tooth on one gear to bottom of gap on another.
self.OptionParser.add_option("", "--clearance",
action="store", type="float",
dest="clearance", default=0.0,
help="Clearance between bottom of gap of this gear and top of tooth of another")
self.OptionParser.add_option("", "--annotation",
action="store", type="inkbool",
dest="annotation", default=False,
help="Draw annotation text")
self.OptionParser.add_option("-i", "--internal-ring",
action="store", type="inkbool",
dest="internal_ring", default=False,
help="Ring (or Internal) gear style (default: normal spur gear)")
self.OptionParser.add_option("", "--mount-hole",
action="store", type="float",
dest="mount_hole", default=0.,
help="Mount hole diameter")
self.OptionParser.add_option("", "--mount-diameter",
action="store", type="float",
dest="mount_diameter", default=15,
help="Mount support diameter")
self.OptionParser.add_option("", "--spoke-count",
action="store", type="int",
dest="spoke_count", default=3,
help="Spokes count")
self.OptionParser.add_option("", "--spoke-width",
action="store", type="float",
dest="spoke_width", default=5,
help="Spoke width")
self.OptionParser.add_option("", "--holes-rounding",
action="store", type="float",
dest="holes_rounding", default=5,
help="Holes rounding")
self.OptionParser.add_option("", "--active-tab",
action="store", type="string",
dest="active_tab", default='',
help="Active tab. Not used now.")
self.OptionParser.add_option("-x", "--centercross",
action="store", type="inkbool",
dest="centercross", default=False,
help="Draw cross in center")
self.OptionParser.add_option("-c", "--pitchcircle",
action="store", type="inkbool",
dest="pitchcircle", default=False,
help="Draw pitch circle (for mating)")
self.OptionParser.add_option("-r", "--draw-rack",
action="store", type="inkbool",
dest="drawrack", default=False,
help="Draw rack gear instead of spur gear")
self.OptionParser.add_option("", "--rack-teeth-length",
action="store", type="int",
dest="teeth_length", default=12,
help="Length (in teeth) of rack")
self.OptionParser.add_option("", "--rack-base-height",
action="store", type="float",
dest="base_height", default=8,
help="Height of base of rack")
self.OptionParser.add_option("", "--rack-base-tab",
action="store", type="float",
dest="base_tab", default=14,
help="Length of tabs on ends of rack")
self.OptionParser.add_option("", "--undercut-alert",
action="store", type="inkbool",
dest="undercut_alert", default=False,
help="Let the user confirm a warning dialog if undercut occurs. This dialog also shows helpful hints against undercut")
def drawPoints(self, lines, kerfdir=1, close=True):
if not lines:
return
if kerfdir != 0:
lines = kerf(lines, self.boxes.burn*kerfdir, closed=close)
self.boxes.ctx.save()
self.boxes.ctx.move_to(*lines[0])
for x, y in lines[1:]:
self.boxes.ctx.line_to(x, y)
if close:
self.boxes.ctx.line_to(*lines[0])
self.boxes.ctx.restore()
def calc_circular_pitch(self):
""" We use math based on circular pitch.
"""
dimension = self.options.dimension
if self.options.system == 'CP': # circular pitch
circular_pitch = dimension * 25.4
elif self.options.system == 'DP': # diametral pitch
circular_pitch = pi * 25.4 / dimension
elif self.options.system == 'MM': # module (metric)
circular_pitch = pi * dimension
else:
raise ValueError("unknown system '%s', try CP, DP, MM" % self.options.system)
# circular_pitch defines the size in mm
return circular_pitch
def generate_spokes(self, root_radius, spoke_width, spokes, mount_radius, mount_hole,
unit_factor, unit_label):
""" given a set of constraints
- generate the svg path for the gear spokes
- lies between mount_radius (inner hole) and root_radius (bottom of the teeth)
- spoke width also defines the spacing at the root_radius
- mount_radius is adjusted so that spokes fit if there is room
- if no room (collision) then spokes not drawn
"""
if not spokes:
return []
# Spokes
collision = False # assume we draw spokes
messages = [] # messages to send back about changes.
spoke_holes = []
r_outer = root_radius - spoke_width
try:
spoke_count = spokes
spokes = [i*2*pi/spokes for i in range(spoke_count)]
except TypeError:
spoke_count = len(spokes)
spokes = [radians(a) for a in spokes]
spokes.append(spokes[0]+two_pi)
# checks for collision with spokes
# check for mount hole collision with inner spokes
if mount_radius <= mount_hole/2:
adj_factor = (r_outer - mount_hole/2) / 5
if adj_factor < 0.1:
# not enough reasonable room
collision = True
else:
mount_radius = mount_hole/2 + adj_factor # small fix
messages.append("Mount support too small. Auto increased to %2.2f%s." % (mount_radius/unit_factor*2, unit_label))
# then check to see if cross-over on spoke width
for i in range(spoke_count):
angle = spokes[i]-spokes[i-1]
if spoke_width >= angle * mount_radius:
adj_factor = 1.2 # wrong value. its probably one of the points distances calculated below
mount_radius += adj_factor
messages.append("Too many spokes. Increased Mount support by %2.3f%s" % (adj_factor/unit_factor, unit_label))
# check for collision with outer rim
if r_outer <= mount_radius:
# not enough room to draw spokes so cancel
collision = True
if collision: # don't draw spokes if no room.
messages.append("Not enough room for Spokes. Decrease Spoke width.")
else: # draw spokes
for i in range(spoke_count):
self.boxes.ctx.save()
start_a, end_a = spokes[i], spokes[i+1]
# inner circle around mount
asin_factor = spoke_width/mount_radius/2
# check if need to clamp radius
asin_factor = max(-1.0, min(1.0, asin_factor)) # no longer needed - resized above
a = asin(asin_factor)
# is inner circle too small
asin_factor = spoke_width/r_outer/2
# check if need to clamp radius
asin_factor = max(-1.0, min(1.0, asin_factor)) # no longer needed - resized above
a2 = asin(asin_factor)
l = vlength(vdiff(point_on_circle(mount_radius, start_a + a),
point_on_circle(r_outer, start_a + a2)))
self.boxes.moveTo(*point_on_circle(mount_radius, start_a + a), degrees=degrees(start_a))
self.boxes.polyline(
l,
+90+degrees(a2), 0,
(degrees(end_a-start_a-2*a2), r_outer), 0,
+90+degrees(a2),
l, 90-degrees(a), 0,
(-degrees(end_a-start_a-2*a), mount_radius),
0, 90+degrees(a2), 0
)
self.boxes.ctx.restore()
return messages
def sizes(self, **kw):
self.options = self.OptionParser.parse_args(["--%s=%s" % (name,value) for name, value in kw.items()])
# Pitch (circular pitch): Length of the arc from one tooth to the next)
# Pitch diameter: Diameter of pitch circle.
pitch = self.calc_circular_pitch()
if self.options.drawrack:
base_height = self.options.base_height * unit_factor
tab_width = self.options.base_tab * unit_factor
tooth_count = self.options.teeth_length
width = tooth_count * pitch + 2*tab_width
height = base_height+ 2* addendum
return 0, width, height
teeth = self.options.teeth
# Angle of tangent to tooth at circular pitch wrt radial line.
angle = self.options.angle
# Clearance: Radial distance between top of tooth on one gear to
# bottom of gap on another.
clearance = self.options.clearance # * unit_factor
# Replace section below with this call to get the combined gear_calculations() above
(pitch_radius, base_radius, addendum, dedendum,
outer_radius, root_radius, tooth) = gear_calculations(teeth, pitch, angle, clearance, self.options.internal_ring, self.options.profile_shift*0.01)
if self.options.internal_ring:
outer_radius += self.options.spoke_width
return pitch_radius, 2*outer_radius, 2*outer_radius
def gearCarrier(self, r, spoke_width, positions, mount_radius, mount_hole, circle=True, callback=None, move=None):
width = 2*r+spoke_width
if self.boxes.move(width, width, move, before=True):
return
try:
positions = [i*360/positions for i in range(positions)]
except TypeError:
pass
self.boxes.ctx.save()
self.boxes.moveTo(width/2.0, width/2.0)
if callback:
self.boxes.cc(callback, None)
self.generate_spokes(r+0.5*spoke_width, spoke_width, positions, mount_radius, mount_hole, 1, "")
self.boxes.hole(0, 0, mount_hole)
for angle in positions:
self.boxes.ctx.save()
self.boxes.moveTo(0, 0, angle)
self.boxes.hole(r, 0, mount_hole)
self.boxes.ctx.restore()
self.boxes.moveTo(r+0.5*spoke_width+self.boxes.burn, 0, 90)
self.boxes.corner(360, r+0.5*spoke_width)
self.boxes.ctx.restore()
self.boxes.move(width, width, move)
def __call__(self, teeth_only=False, move="", callback=None, **kw):
""" Calculate Gear factors from inputs.
- Make list of radii, angles, and centers for each tooth and
iterate through them
- Turn on other visual features e.g. cross, rack, annotations, etc
"""
self.options = self.OptionParser.parse_args(["--%s=%s" % (name,value) for name, value in kw.items()])
warnings = [] # list of extra messages to be shown in annotations
# calculate unit factor for units defined in dialog.
unit_factor = 1
# User defined options
teeth = self.options.teeth
# Angle of tangent to tooth at circular pitch wrt radial line.
angle = self.options.angle
# Clearance: Radial distance between top of tooth on one gear to
# bottom of gap on another.
clearance = self.options.clearance * unit_factor
mount_hole = self.options.mount_hole * unit_factor
# for spokes
mount_radius = self.options.mount_diameter * 0.5 * unit_factor
spoke_count = self.options.spoke_count
spoke_width = self.options.spoke_width * unit_factor
holes_rounding = self.options.holes_rounding * unit_factor # unused
# visible guide lines
centercross = self.options.centercross # draw center or not (boolean)
pitchcircle = self.options.pitchcircle # draw pitch circle or not (boolean)
# Accuracy of teeth curves
accuracy_involute = 20 # Number of points of the involute curve
accuracy_circular = 9 # Number of points on circular parts
if self.options.accuracy is not None:
if self.options.accuracy == 0:
# automatic
if teeth < 10: accuracy_involute = 20
elif teeth < 30: accuracy_involute = 12
else: accuracy_involute = 6
else:
accuracy_involute = self.options.accuracy
accuracy_circular = max(3, int(accuracy_involute/2) - 1) # never less than three
# print >>self.tty, "accuracy_circular=%s accuracy_involute=%s" % (accuracy_circular, accuracy_involute)
# Pitch (circular pitch): Length of the arc from one tooth to the next)
# Pitch diameter: Diameter of pitch circle.
pitch = self.calc_circular_pitch()
# Replace section below with this call to get the combined gear_calculations() above
(pitch_radius, base_radius, addendum, dedendum,
outer_radius, root_radius, tooth) = gear_calculations(teeth, pitch, angle, clearance, self.options.internal_ring, self.options.profile_shift*0.01)
b = self.boxes.burn
# Add Rack (instead)
if self.options.drawrack:
base_height = self.options.base_height * unit_factor
tab_width = self.options.base_tab * unit_factor
tooth_count = self.options.teeth_length
(points, guide_points) = generate_rack_points(tooth_count, pitch, addendum, angle,
base_height, tab_width, clearance, pitchcircle)
width = tooth_count * pitch + 2 * tab_width
height = base_height + 2 * addendum
if self.boxes.move(width, height, move, before=True):
return
self.boxes.cc(callback, None)
self.boxes.moveTo(width/2.0, base_height+addendum, -180)
if base_height < 0:
points = points[1:-1]
self.drawPoints(points, close=base_height >= 0)
self.drawPoints(guide_points, kerfdir=0)
self.boxes.move(width, height, move)
return
# Move only
width = height = 2 * outer_radius
if self.options.internal_ring:
width = height = width + 2 * self.options.spoke_width
if not teeth_only and self.boxes.move(width, height, move, before=True):
return
# Detect Undercut of teeth
## undercut = int(ceil(undercut_min_teeth( angle )))
## needs_undercut = teeth < undercut #? no longer needed ?
if have_undercut(teeth, angle, 1.0):
min_teeth = int(ceil(undercut_min_teeth(angle, 1.0)))
min_angle = undercut_min_angle(teeth, 1.0) + .1
max_k = undercut_max_k(teeth, angle)
msg = "Undercut Warning: This gear (%d teeth) will not work well.\nTry tooth count of %d or more,\nor a pressure angle of %.1f [deg] or more,\nor try a profile shift of %d %%.\nOr other decent combinations." % (teeth, min_teeth, min_angle, int(100.*max_k)-100.)
# alas annotation cannot handle the degree symbol. Also it ignore newlines.
# so split and make a list
warnings.extend(msg.split("\n"))
# All base calcs done. Start building gear
points = generate_spur_points(teeth, base_radius, pitch_radius, outer_radius, root_radius, accuracy_involute, accuracy_circular)
if not teeth_only:
self.boxes.moveTo(width/2, height/2)
self.boxes.cc(callback, None, 0, 0)
self.drawPoints(points)
# Spokes
if not teeth_only and not self.options.internal_ring: # only draw internals if spur gear
msg = self.generate_spokes(root_radius, spoke_width, spoke_count, mount_radius, mount_hole,
unit_factor, self.options.units)
warnings.extend(msg)
# Draw mount hole
# A : rx,ry x-axis-rotation, large-arch-flag, sweepflag x,y
r = mount_hole / 2
self.boxes.hole(0, 0, r)
elif not teeth_only:
# its a ring gear
# which only has an outer ring where width = spoke width
r = outer_radius + spoke_width + self.boxes.burn
self.boxes.ctx.save()
self.boxes.moveTo(r, 0)
self.boxes.ctx.arc(-r, 0, r, 0, 2*pi)
self.boxes.ctx.restore()
# Add center
if centercross:
cs = pitch / 3.0 # centercross length
self.boxes.ctx.save()
self.boxes.ctx.move_to(-cs, 0)
self.boxes.ctx.line_to(+cs, 0)
self.boxes.ctx.move_to(0, -cs)
self.boxes.ctx.line_to(0, +cs)
self.boxes.ctx.restore()
# Add pitch circle (for mating)
if pitchcircle:
self.boxes.hole(0, 0, pitch_radius)
# Add Annotations (above)
if self.options.annotation:
outer_dia = outer_radius * 2
if self.options.internal_ring:
outer_dia += 2 * spoke_width
notes = []
notes.extend(warnings)
#notes.append('Document (%s) scale conversion = %2.4f' % (self.document.getroot().find(inkex.addNS('namedview', 'sodipodi')).get(inkex.addNS('document-units', 'inkscape')), unit_factor))
notes.extend(['Teeth: %d CP: %2.4f(%s) ' % (teeth, pitch / unit_factor, self.options.units),
'DP: %2.3f Module: %2.4f(mm)' % (25.4 * pi / pitch, pitch),
'Pressure Angle: %2.2f degrees' % (angle),
'Pitch diameter: %2.3f %s' % (pitch_radius * 2 / unit_factor, self.options.units),
'Outer diameter: %2.3f %s' % (outer_dia / unit_factor, self.options.units),
'Base diameter: %2.3f %s' % (base_radius * 2 / unit_factor, self.options.units)#,
#'Addendum: %2.4f %s' % (addendum / unit_factor, self.options.units),
#'Dedendum: %2.4f %s' % (dedendum / unit_factor, self.options.units)
])
# text height relative to gear size.
# ranges from 10 to 22 over outer radius size 60 to 360
text_height = max(10, min(10+(outer_dia-60)/24, 22))
# position above
y = - outer_radius - (len(notes)+1) * text_height * 1.2
for note in notes:
self.boxes.text(note, -outer_radius, y)
y += text_height * 1.2
if not teeth_only:
self.boxes.move(width, height, move)
if __name__ == '__main__':
e = Gears()
e.affect()
# Notes

View File

@ -0,0 +1,68 @@
import pkgutil
import inspect
import importlib
import boxes
ui_groups_by_name = {}
class UIGroup:
def __init__(self, name, title=None, description="", image=""):
self.name = name
self.title = title or name
self.description = description
self._image = image
self.generators = []
# register
ui_groups_by_name[name] = self
def add(self, box):
self.generators.append(box)
self.generators.sort(key=lambda b:getattr(b, '__name__', None) or b.__class__.__name__)
@property
def thumbnail(self):
return self._image and f"{self._image}-thumb.jpg"
@property
def image(self):
return self._image and f"{self._image}.jpg"
ui_groups = [
UIGroup("Box", "Boxes", image="UniversalBox"),
UIGroup("FlexBox", "Boxes with flex", image="RoundedBox"),
UIGroup("Tray", "Trays and Drawer Inserts", image="TypeTray"),
UIGroup("Shelf", "Shelves", image="DisplayShelf"),
UIGroup("WallMounted", image="WallTypeTray"),
UIGroup("Holes", "Hole patterns", image=""),
UIGroup("Part", "Parts and Samples", image="BurnTest"),
UIGroup("Misc", image="TrafficLight"),
UIGroup("Unstable", description="Generators are still untested or need manual adjustment to be useful."),
]
def getAllBoxGenerators():
generators = {}
for importer, modname, ispkg in pkgutil.walk_packages(
path=__path__,
prefix=__name__+'.'):
module = importlib.import_module(modname)
if module.__name__.split('.')[-1].startswith("_"):
continue
for k, v in module.__dict__.items():
if v is boxes.Boxes:
continue
if (inspect.isclass(v) and issubclass(v, boxes.Boxes) and
v.__name__[0] != '_'):
generators[modname + '.' + v.__name__] = v
return generators
def getAllGeneratorModules():
generators = {}
for importer, modname, ispkg in pkgutil.walk_packages(
path=__path__,
prefix=__name__+'.',
onerror=lambda x: None):
module = importlib.import_module(modname)
generators[modname.split('.')[-1]] = module
return generators

View File

@ -0,0 +1,44 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2019 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class SlatwallXXX(Boxes): # Change class name!
"""DESCRIPTION"""
ui_group = "SlatWall"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.addSettingsArgs(edges.SlatWallSettings)
# remove cli params you do not need
self.buildArgParser(x=100, sx="3*50", y=100, sy="3*50", h=100, hi=0)
# Add non default cli params if needed (see argparse std lib)
self.argparser.add_argument(
"--XX", action="store", type=float, default=0.5,
help="DESCRIPTION")
def render(self):
# Add slat wall edges
s = edges.SlatWallSettings(self.thickness, True,
**self.edgesettings.get("SlatWall", {}))
s.edgeObjects(self)
self.slatWallHolesAt = edges.SlatWallHoles(self, s)
# render your parts here

View File

@ -0,0 +1,58 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class BOX(Boxes): # Change class name!
"""DESCRIPTION"""
ui_group = "Unstable" # see ./__init__.py for names
def __init__(self):
Boxes.__init__(self)
# Uncomment the settings for the edge types you use
# use keyword args to set default values
# self.addSettingsArgs(edges.FingerJointSettings, finger=1.0,space=1.0)
# self.addSettingsArgs(edges.StackableSettings)
# self.addSettingsArgs(edges.HingeSettings)
# self.addSettingsArgs(edges.LidSettings)
# self.addSettingsArgs(edges.ClickSettings)
# self.addSettingsArgs(edges.FlexSettings)
# remove cli params you do not need
self.buildArgParser(x=100, sx="3*50", y=100, sy="3*50", h=100, hi=0)
# Add non default cli params if needed (see argparse std lib)
self.argparser.add_argument(
"--XX", action="store", type=float, default=0.5,
help="DESCRIPTION")
def render(self):
# adjust to the variables you want in the local scope
x, y, h = self.x, self.y, self.h
t = self.thickness
# Create new Edges here if needed E.g.:
s = edges.FingerJointSettings(self.thickness, relative=False,
space = 10, finger=10,
width=self.thickness)
p = edges.FingerJointEdge(self, s)
p.char = "a" # 'a', 'A', 'b' and 'B' is reserved for beeing used within generators
self.addPart(p)
# render your parts here

View File

@ -0,0 +1,65 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class ABox(Boxes):
"""A simple Box"""
description = "This box is kept simple on purpose. If you need more features have a look at the UniversalBox."
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser("x", "y", "h", "outside", "bottom_edge")
#self.argparser.add_argument(
# "--lid", action="store", type=str, default="default (none)",
# choices=("default (none)", "chest", "flat"),
# help="additional lid (for straight top_edge only)")
def render(self):
x, y, h = self.x, self.y, self.h
t = self.thickness
t1, t2, t3, t4 = "eeee"
b = self.edges.get(self.bottom_edge, self.edges["F"])
sideedge = "F" # if self.vertical_edges == "finger joints" else "h"
if self.outside:
self.x = x = self.adjustSize(x, sideedge, sideedge)
self.y = y = self.adjustSize(y)
self.h = h = self.adjustSize(h, b, t1)
with self.saved_context():
self.rectangularWall(x, h, [b, sideedge, t1, sideedge],
ignore_widths=[1, 6], move="up")
self.rectangularWall(x, h, [b, sideedge, t3, sideedge],
ignore_widths=[1, 6], move="up")
if self.bottom_edge != "e":
self.rectangularWall(x, y, "ffff", move="up")
#self.drawAddOnLid(x, y, self.lid)
self.rectangularWall(x, h, [b, sideedge, t3, sideedge],
ignore_widths=[1, 6], move="right only")
self.rectangularWall(y, h, [b, "f", t2, "f"],
ignore_widths=[1, 6], move="up")
self.rectangularWall(y, h, [b, "f", t4, "f"],
ignore_widths=[1, 6], move="up")

View File

@ -0,0 +1,934 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2020 Guillaume Collic
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import math
from functools import partial
from boxes import Boxes, edges
from .dividertray import (
SlotDescriptionsGenerator,
DividerSlotsEdge,
)
class AgricolaInsert(Boxes):
"""
Agricola Revised Edition game box insert, including some expansions.
"""
ui_group = "Misc"
description = """
This insert was designed with 3 mm plywood in mind, and should work fine with
materials around this thickness.
This is an insert for the [Agricola Revised Edition](https://boardgamegeek.com/boardgame/200680/agricola-revised-edition)
board game. It is specifically designed around the [Farmers Of The Moor expansion](https://boardgamegeek.com/boardgameexpansion/257344/agricola-farmers-moor),
and should also store the [5-6 players expansion](https://boardgamegeek.com/boardgameexpansion/210625/agricola-expansion-5-and-6-players)
(not tested, but I tried to take everything into account for it, please inform
us if you tested it).
It can be stored inside the original game box, including the 2 expansions,
with the lid slightly raised.
The parts of a given element are mostly generated next to each other vertically.
It should be straightforward to match them.
Here are the different elements, from left to right in the generated file.
#### Card tray
The cards are all kept in a tray, with paper dividers to sort them easily. When
the tray is not full of cards, wood dividers slides in slots in order to keep
the cards from falling into the empty space.
There should be enough space for the main game, Farmers Of The Moor, and the 5-6
player expansion, but not much more than that.
To keep a lower profile, the cards are at a slight angle, and the paper dividers
tabs are horizontal instead of vertical.
A small wall keeps the card against one side while the tabs protrude on the
other side, above the small wall.
The wall with the big hole is the sloped one. It goes between the two
"comb-like" walls first, with its two small holes at the bottom. Then there is a
low-height long wall with a sloped edge which should go from the sloped wall to
the other side. You can finish the tray with the last wall at the end.
#### Upper level trays
4 trays with movable walls are used to store resources. They were designed to
store them in this order:
* Stone / Vegetable / Pig / Cow
* Reed / Grain / Sheep
* Wood / Clay
* Food / Fire
The wall would probably be better if fixed instead of movable, but I would like
to test with the 5-6 player expansion to be sure their positions are correct
with it too.
The little feet of the movable wall should be glued. The triangles are put
horizontally, with their bases towards the sides.
#### Lower level tray
The lower level tray is used to store the horses.
#### Room/Field tiles
Two boxes are generated to store the room/field tiles. One for the wood/field,
the other for the clay/stone. They are stored with the main opening upside, but
I prefer to use them during play with this face on the side.
#### Moor/Forest and miscellaneous tiles
A box is generated to store the Moor/Forest tiles, and some other tiles such as
the "multiple resources" cardboard tokens.
The Moor/Forest tiles are at the same height as the Room/Field, and the upper
level trays are directly on them. The horse box and player box are slightly
lower. This Moor/Forest box have a lowered corner (the one for the miscellaneous
tiles). Two cardboard pieces can be stored between the smaller boxes and the
upper level trays (as seen on the picture).
Be sure to match the pieces so that the walls with smaller heights are next to
each other.
#### Players bit boxes
Each player has its own box where the bits of his color are stored.
The cardboard bed from Farmers Of The Moor is central to this box.
* The fences are stored inside the bed
* The bed is placed in the box, with holes to keep it there (and to take less
height)
* The stables are stored in the two corners
* The five farmers are stored between the bed and the three walls, alternatively
head up and head down.
During assembly, the small bars are put in the middle holes. The two bigger
holes at the ends are used for the bed feet. The bar keeps the bed from
protruding underneath.
"""
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=1.0)
def render(self):
player_box_height = 34.5
player_box_inner_width = 50.5
bigger_box_inner_height = 36.7
row_width = 37.2
tray_inner_height = 17
box_width = 218
card_tray_height = (
self.thickness * 2 + tray_inner_height + bigger_box_inner_height
)
card_tray_width = (
305.35 - player_box_inner_width * 2 - row_width * 2 - 9 * self.thickness
)
self.render_card_divider_tray(card_tray_height, box_width, card_tray_width)
self.render_upper_token_trays(tray_inner_height, box_width)
wood_room_box_width = 39.8
self.render_room_box(wood_room_box_width, bigger_box_inner_height, row_width)
stone_room_box_width = 26.7
self.render_room_box(stone_room_box_width, bigger_box_inner_height, row_width)
moor_box_length = 84.6
self.render_moor_box(
bigger_box_inner_height, player_box_height, row_width, moor_box_length
)
horse_box_margin = 0.5
horse_box_length = (
box_width
- wood_room_box_width
- stone_room_box_width
- moor_box_length
- 6 * self.thickness
- horse_box_margin
)
self.render_horse_box(player_box_height, row_width, horse_box_length)
for _ in range(6):
self.render_player_box(player_box_height, player_box_inner_width)
def render_card_divider_tray(
self, card_tray_height, card_tray_length, card_tray_width
):
"""
The whole tray which contains the cards, including its dividers.
Cards are at an angle, to save height.
"""
self.ctx.save()
tray_inner_length = card_tray_length - self.thickness
margin_for_score_sheet = 0 # 3 if you want more space for score sheet
sleeved_cards_width = 62 + margin_for_score_sheet
rad = math.acos(card_tray_height / sleeved_cards_width)
angle = math.degrees(rad)
cos = math.cos(rad)
tan = math.tan(rad)
sin = math.sin(rad)
slots_number = 19
slot_depth = 30
slot_descriptions = SlotDescriptionsGenerator().generate_all_same_angles(
[tray_inner_length / slots_number for _ in range(slots_number)],
self.thickness,
0.2,
slot_depth,
card_tray_height,
angle,
)
slot_descriptions.adjust_to_target_length(tray_inner_length)
sloped_wall_height = sleeved_cards_width - self.thickness * (tan + 1 / tan)
sloped_wall_posx_at_y0 = (
tray_inner_length - sloped_wall_height * tan - cos * self.thickness
)
sloped_wall_posx = sloped_wall_posx_at_y0 + cos * self.thickness / 2
sloped_wall_posy = sin * self.thickness / 2
dse = DividerSlotsEdge(self, slot_descriptions.descriptions)
for _ in range(2):
self.rectangularWall(
tray_inner_length,
card_tray_height,
["e", "e", dse, "f"],
move="up",
callback=[
partial(
lambda: self.fingerHolesAt(
sloped_wall_posx,
sloped_wall_posy,
sloped_wall_height,
angle=90 - angle,
)
)
],
)
# generate spacer
spacer_height = card_tray_height / 2
spacer_spacing = card_tray_width-99.8
spacer_upper_width = sloped_wall_posx_at_y0 + spacer_height * tan
self.trapezoidWall(
spacer_height,
spacer_upper_width,
sloped_wall_posx_at_y0,
"fefe",
move="up rotated",
)
self.rectangularWall(
card_tray_width,
card_tray_height,
"eFeF",
move="up",
callback=[
partial(
lambda: self.fingerHolesAt(
spacer_spacing - self.thickness / 2, 0, spacer_height
)
)
],
)
self.rectangularWall(
card_tray_width,
sloped_wall_height,
"efef",
move="up",
callback=[
partial(
self.generate_card_tray_sloped_wall_holes,
card_tray_width,
sloped_wall_height,
spacer_height,
spacer_spacing,
rad,
)
],
)
self.ctx.restore()
self.rectangularWall(card_tray_length, 0, "FFFF", move="right only")
self.ctx.save()
divider_height = sleeved_cards_width - self.thickness * tan
self.generate_divider(
card_tray_width, divider_height, slot_depth, spacer_spacing, "up"
)
self.explain(
[
"Wood divider",
"Hard separation to keep the card",
"from slipping in empty space left.",
"Takes more space, but won't move.",
"Duplicate as much as you want",
"(I use 2).",
]
)
self.ctx.restore()
self.rectangularWall(card_tray_width, 0, "ffff", move="right only")
self.ctx.save()
self.generate_paper_divider(
card_tray_width, sleeved_cards_width, slot_depth, spacer_spacing, "up"
)
self.explain(
[
"Paper divider",
"Soft separation to search easily",
"the card group you need",
"(by expansion, number of player,",
"etc.).",
"Duplicate as much as you want",
"(I use 7).",
]
)
self.ctx.restore()
self.rectangularWall(card_tray_width, 0, "ffff", move="right only")
def explain(self, strings):
self.text(
str.join(
"\n",
strings,
),
fontsize=7,
align="bottom left",
)
def generate_sloped_wall_holes(self, side_wall_length, rad, sloped_wall_height):
cos = math.cos(rad)
tan = math.tan(rad)
sin = math.sin(rad)
posx_at_y0 = side_wall_length - sloped_wall_height * tan
posx = posx_at_y0 - cos * self.thickness / 2
posy = sin * self.thickness / 2
self.fingerHolesAt(posx, posy, sloped_wall_height, angle=90 - math.degrees(rad))
def generate_card_tray_sloped_wall_holes(
self, side_wall_length, sloped_wall_height, spacer_height, spacer_spacing, rad
):
# Spacer finger holes
self.fingerHolesAt(
side_wall_length - (spacer_spacing - self.thickness / 2),
# the sloped wall doesn't exactly touch the bottom of the spacer
-self.thickness * math.tan(rad),
spacer_height / math.cos(rad),
)
# Big hole to access "lost" space behind sloped wall
radius = 5
padding = 8
total_loss = 2 * radius + 2 * padding
self.moveTo(radius + padding, padding)
self.polyline(
side_wall_length - total_loss,
(90, radius),
sloped_wall_height - total_loss,
(90, radius),
side_wall_length - total_loss,
(90, radius),
sloped_wall_height - total_loss,
(90, radius),
)
def generate_paper_divider(self, width, height, slot_depth, spacer_spacing, move):
"""
A card separation made of paper, which moves freely in the card tray.
Takes less space and easy to manipulate, but won't block cards in place.
"""
if self.move(width, height, move, True):
return
margin = 0.5
actual_width = width - margin
self.polyline(
actual_width - spacer_spacing,
90,
height - slot_depth,
-90,
spacer_spacing,
90,
slot_depth,
90,
actual_width,
90,
height,
90,
)
# Move for next piece
self.move(width, height, move)
def generate_divider(self, width, height, slot_depth, spacer_spacing, move):
"""
A card separation made of wood which slides in the side slots.
Can be useful to do hard separations, but takes more space and
less movable than the paper ones.
"""
total_width = width + 2 * self.thickness
if self.move(total_width, height, move, True):
return
radius = 16
padding = 20
divider_notch_depth = 35
self.polyline(
self.thickness + spacer_spacing + padding - radius,
(90, radius),
divider_notch_depth - radius - radius,
(-90, radius),
width - 2 * radius - 2 * padding - spacer_spacing,
(-90, radius),
divider_notch_depth - radius - radius,
(90, radius),
self.thickness + padding - radius,
90,
slot_depth,
90,
self.thickness,
-90,
height - slot_depth,
90,
width - spacer_spacing,
90,
height - slot_depth,
-90,
self.thickness + spacer_spacing,
90,
slot_depth,
)
# Move for next piece
self.move(total_width, height, move)
def render_horse_box(self, player_box_height, row_width, width):
"""
Box for the horses on lower level. Same height as player boxes.
"""
length = 2 * row_width + 3 * self.thickness
self.render_simple_tray(width, length, player_box_height)
def render_moor_box(
self, bigger_box_inner_height, player_box_height, row_width, length
):
"""
Box for the moor/forest tiles, and the cardboard tokens with multiple
units of resources.
A corner is lowered (the one for the tokens) at the same height as player boxes
to store 2 levels of small boards there.
"""
self.ctx.save()
height = bigger_box_inner_height
lowered_height = player_box_height - self.thickness
lowered_corner_height = height - lowered_height
corner_length = 53.5
self.rectangularWall(
length,
2 * row_width + self.thickness,
"FfFf",
move="up",
callback=[
partial(
lambda: self.fingerHolesAt(
0, row_width + 0.5 * self.thickness, length, 0
)
)
],
)
for i in range(2):
self.rectangularWall(
length,
lowered_height,
[
"f",
"f",
MoorBoxSideEdge(
self, corner_length, lowered_corner_height, i % 2 == 0
),
"f",
],
move="up",
)
self.rectangularWall(length, height / 2, "ffef", move="up")
for i in range(2):
self.rectangularWall(
2 * row_width + self.thickness,
lowered_height,
[
"F",
"F",
MoorBoxHoleEdge(self, height, lowered_corner_height, i % 2 == 0),
"F",
],
move="up",
callback=[
partial(self.generate_side_finger_holes, row_width, height / 2)
],
)
self.ctx.restore()
self.rectangularWall(length, 0, "FFFF", move="right only")
def generate_side_finger_holes(self, row_width, height):
self.fingerHolesAt(row_width + 0.5 * self.thickness, 0, height)
def render_room_box(self, width, height, row_width):
"""
A box in which storing room/field tiles.
"""
border_height = 12
room_box_length = row_width * 2 + self.thickness
self.ctx.save()
self.rectangularWall(
room_box_length,
height,
"eFfF",
move="up",
callback=[partial(self.generate_side_finger_holes, row_width, height)],
)
self.rectangularWall(
room_box_length,
width,
"FFfF",
move="up",
callback=[partial(self.generate_side_finger_holes, row_width, width)],
)
self.rectangularWall(
room_box_length,
border_height,
"FFeF",
move="up",
callback=[
partial(self.generate_side_finger_holes, row_width, border_height)
],
)
for _ in range(3):
self.trapezoidWall(width, height, border_height, "ffef", move="up")
self.ctx.restore()
self.rectangularWall(room_box_length, 0, "FFFF", move="right only")
def render_player_box(self, player_box_height, player_box_inner_width):
"""
A box in which storing all the bits of a single player,
including (and designed for) the cardboard bed from Farmers Of The Moor.
"""
self.ctx.save()
bed_inner_height = player_box_height - self.thickness
bed_inner_length = 66.75
bed_inner_width = player_box_inner_width
cardboard_bed_foot_height = 6.5
cardboard_bed_hole_margin = 5
cardboard_bed_hole_length = 12
bed_head_length = 20
bed_foot_height = 18
support_length = 38
bed_edge = Bed2SidesEdge(
self, bed_inner_length, bed_head_length, bed_foot_height
)
noop_edge = NoopEdge(self)
self.ctx.save()
optim_180_x = (
bed_inner_length + self.thickness + bed_head_length + 2 * self.spacing
)
optim_180_y = 2 * bed_foot_height - player_box_height + 2 * self.spacing
for _ in range(2):
self.rectangularWall(
bed_inner_length,
bed_inner_height,
["F", bed_edge, noop_edge, "F"],
move="up",
)
self.moveTo(optim_180_x, optim_180_y, -180)
self.ctx.restore()
self.moveTo(0, bed_inner_height + self.thickness + self.spacing + optim_180_y)
self.rectangularWall(
bed_inner_length,
bed_inner_width,
"feff",
move="up",
callback=[
partial(
self.generate_bed_holes,
bed_inner_width,
cardboard_bed_hole_margin,
cardboard_bed_hole_length,
support_length,
)
],
)
self.ctx.save()
self.rectangularWall(
bed_inner_width,
bed_inner_height,
["F", "f", BedHeadEdge(self, bed_inner_height - 15), "f"],
move="right",
)
for _ in range(2):
self.rectangularWall(
cardboard_bed_foot_height - self.thickness,
support_length,
"efee",
move="right",
)
self.ctx.restore()
self.rectangularWall(
bed_inner_width,
bed_inner_height,
"Ffef",
move="up only",
)
self.ctx.restore()
self.rectangularWall(
bed_inner_length + bed_head_length + self.spacing - self.thickness,
0,
"FFFF",
move="right only",
)
def generate_bed_holes(self, width, margin, hole_length, support_length):
support_start = margin + hole_length
bed_width = 29.5
bed_space_to_wall = (width - bed_width) / 2
bed_feet_width = 3
posy_1 = bed_space_to_wall
posy_2 = width - bed_space_to_wall
for y, direction in [(posy_1, 1), (posy_2, -1)]:
bed_feet_middle_y = y + direction * bed_feet_width / 2
support_middle_y = y + direction * self.thickness / 2
self.rectangularHole(
margin,
bed_feet_middle_y,
hole_length,
bed_feet_width,
center_x=False,
)
self.fingerHolesAt(support_start, support_middle_y, support_length, angle=0)
self.rectangularHole(
support_start + support_length,
bed_feet_middle_y,
hole_length,
bed_feet_width,
center_x=False,
)
def render_upper_token_trays(self, tray_inner_height, box_width):
"""
Upper level : multiple trays for each ressource
(beside horses which are on the lower level)
"""
tray_height = tray_inner_height + self.thickness
upper_level_width = 196
upper_level_length = box_width
row_width = upper_level_width / 3
# Stone / Vegetable / Pig / Cow
self.render_simple_tray(row_width, upper_level_length, tray_height, 3)
# Reed / Grain / Sheep
self.render_simple_tray(row_width, upper_level_length * 2 / 3, tray_height, 2)
# Wood / Clay
self.render_simple_tray(row_width, upper_level_length * 2 / 3, tray_height, 1)
# Food / Fire
self.render_simple_tray(upper_level_length / 3, row_width * 2, tray_height, 1)
def render_simple_tray(self, outer_width, outer_length, outer_height, dividers=0):
"""
One of the upper level trays, with movable dividers.
"""
width = outer_width - 2 * self.thickness
length = outer_length - 2 * self.thickness
height = outer_height - self.thickness
self.ctx.save()
self.rectangularWall(width, length, "FFFF", move="up")
for _ in range(2):
self.rectangularWall(width, height, "ffef", move="up")
self.ctx.restore()
self.rectangularWall(width, length, "FFFF", move="right only")
for _ in range(2):
self.rectangularWall(height, length, "FfFe", move="right")
if dividers:
self.ctx.save()
for _ in range(dividers):
self.render_simple_tray_divider(width, height, "up")
self.ctx.restore()
self.render_simple_tray_divider(width, height, "right only")
def render_simple_tray_divider(self, width, height, move):
"""
Simple movable divider. A wall with small feet for a little more stability.
"""
if self.move(height, width, move, True):
return
t = self.thickness
self.polyline(
height - t,
90,
t,
-90,
t,
90,
width - 2 * t,
90,
t,
-90,
t,
90,
height - t,
90,
width,
90,
)
self.move(height, width, move)
self.render_simple_tray_divider_feet(width, height, move)
def render_simple_tray_divider_feet(self, width, height, move):
sqr2 = math.sqrt(2)
t = self.thickness
divider_foot_width = 2 * t
full_width = t + 2 * divider_foot_width
move_length = self.spacing + full_width / sqr2
move_width = self.spacing + max(full_width, height)
if self.move(move_width, move_length, move, True):
return
self.ctx.save()
self.polyline(
sqr2 * divider_foot_width,
135,
t,
-90,
t,
-90,
t,
135,
sqr2 * divider_foot_width,
135,
full_width,
135,
)
self.ctx.restore()
self.moveTo(-self.burn / sqr2, self.burn * (1 + 1 / sqr2), 45)
self.moveTo(full_width)
self.polyline(
0,
135,
sqr2 * divider_foot_width,
135,
t,
-90,
t,
-90,
t,
135,
sqr2 * divider_foot_width,
135,
)
self.move(move_width, move_length, move)
class MoorBoxSideEdge(edges.BaseEdge):
"""
Edge for the sides of the moor tiles box
"""
def __init__(self, boxes, corner_length, corner_height, lower_corner):
super().__init__(boxes, None)
self.corner_height = corner_height
self.lower_corner = lower_corner
self.corner_length = corner_length
def __call__(self, length, **kw):
radius = self.corner_height / 2
if self.lower_corner:
self.polyline(
length - self.corner_height - self.corner_length,
(90, radius),
0,
(-90, radius),
self.corner_length,
)
else:
self.polyline(length)
def startwidth(self):
return self.corner_height
def endwidth(self):
return 0 if self.lower_corner else self.corner_height
class MoorBoxHoleEdge(edges.BaseEdge):
"""
Edge which does the notches for the moor tiles box
"""
def __init__(self, boxes, height, corner_height, lower_corner):
super().__init__(boxes, None)
self.height = height
self.corner_height = corner_height
self.lower_corner = lower_corner
def __call__(self, length, **kw):
one_side_width = (length - self.thickness) / 2
notch_width = 20
radius = 6
upper_edge = (one_side_width - notch_width - 2 * radius) / 2
hole_start = 10
lowered_hole_start = 2
hole_depth = self.height - 2 * radius
lower_edge = notch_width - 2 * radius
one_side_polyline = lambda margin1, margin2: [
upper_edge,
(90, radius),
hole_depth - margin1,
(-90, radius),
lower_edge,
(-90, radius),
hole_depth - margin2,
(90, radius),
upper_edge,
]
normal_side_polyline = one_side_polyline(hole_start, hole_start)
corner_side_polyline = one_side_polyline(
lowered_hole_start, lowered_hole_start + self.corner_height
)
full_polyline = (
normal_side_polyline
+ [0, self.thickness, 0]
+ (corner_side_polyline if self.lower_corner else normal_side_polyline)
)
self.polyline(*full_polyline)
def startwidth(self):
return self.corner_height
def endwidth(self):
return 0 if self.lower_corner else self.corner_height
class BedHeadEdge(edges.BaseEdge):
"""
Edge which does the head side of the Agricola player box
"""
def __init__(self, boxes, hole_depth):
super().__init__(boxes, None)
self.hole_depth = hole_depth
def __call__(self, length, **kw):
hole_length = 16
upper_corner = 10
lower_corner = 6
depth = self.hole_depth - upper_corner - lower_corner
upper_edge = (length - hole_length - 2 * upper_corner) / 2
lower_edge = hole_length - 2 * lower_corner
self.polyline(
upper_edge,
(90, upper_corner),
depth,
(-90, lower_corner),
lower_edge,
(-90, lower_corner),
depth,
(90, upper_corner),
upper_edge,
)
class Bed2SidesEdge(edges.BaseEdge):
"""
Edge which does a bed like shape, skipping the next corner.
The next edge should be a NoopEdge
"""
def __init__(self, boxes, bed_length, full_head_length, full_foot_height):
super().__init__(boxes, None)
self.bed_length = bed_length
self.full_head_length = full_head_length
self.full_foot_height = full_foot_height
def __call__(self, bed_height, **kw):
foot_corner = 6
middle_corner = 3
head_corner = 10
foot_height = self.full_foot_height - self.thickness - foot_corner
head_length = self.full_head_length - head_corner - self.thickness
corners = foot_corner + middle_corner + head_corner
head_height = bed_height - foot_height - corners
middle_length = self.bed_length - head_length - corners
self.polyline(
foot_height,
(90, foot_corner),
middle_length,
(-90, middle_corner),
head_height,
(90, head_corner),
head_length,
)
class NoopEdge(edges.BaseEdge):
"""
Edge which does nothing, not even turn or move.
"""
def __init__(self, boxes):
super().__init__(boxes, None)
def __call__(self, length, **kw):
# cancel turn
self.corner(-90)

View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2018 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class AllEdges(Boxes):
"""Showing all edge types"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.addSettingsArgs(edges.StackableSettings)
self.addSettingsArgs(edges.HingeSettings)
self.addSettingsArgs(edges.LidSettings)
self.addSettingsArgs(edges.ClickSettings)
self.addSettingsArgs(edges.FlexSettings)
self.addSettingsArgs(edges.HandleEdgeSettings)
self.buildArgParser(x=100)
def render(self):
x = self.x
t = self.thickness
chars = list(self.edges.keys())
chars.sort(key=lambda c: c.lower() + (c if c.isupper() else ''))
chars.reverse()
self.moveTo(0, 10*t)
for c in chars:
with self.saved_context():
self.move(0, 0, "", True)
self.moveTo(x, 0, 90)
self.edge(t+self.edges[c].startwidth())
self.corner(90)
self.edges[c](x, h=4*t)
self.corner(90)
self.edge(t+self.edges[c].endwidth())
self.move(0, 0, "")
self.moveTo(0, 3*t + self.edges[c].spacing())
self.text("%s - %s" % (c, self.edges[c].description))
self.moveTo(0, 12*t)

View File

@ -0,0 +1,147 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import math
import copy
class AngledBox(Boxes):
"""Box with both ends cornered"""
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser("x", "y", "h", "outside", "bottom_edge")
self.argparser.add_argument(
"--n", action="store", type=int, default=5,
help="number of walls at one side (1+)")
self.argparser.add_argument(
"--top", action="store", type=str, default="none",
choices=["none", "angled hole", "angled lid", "angled lid2"],
help="style of the top and lid")
def floor(self, x, y, n, edge='e', hole=None, move=None, callback=None, label=""):
r, h, side = self.regularPolygon(2*n+2, h=y/2.0)
t = self.thickness
if n % 2:
lx = x - 2 * h + side
else:
lx = x - 2 * r + side
edge = self.edges.get(edge, edge)
tx = x + 2 * edge.spacing()
ty = y + 2 * edge.spacing()
if self.move(tx, ty, move, before=True):
return
self.moveTo((tx-lx)/2., edge.margin())
if hole:
with self.saved_context():
hr, hh, hside = self.regularPolygon(2*n+2, h=y/2.0-t)
dx = side - hside
hlx = lx - dx
self.moveTo(dx/2.0, t+edge.spacing())
for i, l in enumerate(([hlx] + ([hside] * n))* 2):
self.edge(l)
self.corner(360.0/(2*n + 2))
for i, l in enumerate(([lx] + ([side] * n))* 2):
self.cc(callback, i, 0, edge.startwidth() + self.burn)
edge(l)
self.edgeCorner(edge, edge, 360.0/(2*n + 2))
self.move(tx, ty, move, label=label)
def render(self):
x, y, h, n = self.x, self.y, self.h, self.n
b = self.bottom_edge
if n < 1:
n = self.n = 1
if x < y:
x, y = y, x
if self.outside:
x = self.adjustSize(x)
y = self.adjustSize(y)
if self.top == "none":
h = self.adjustSize(h, False)
elif "lid" in self.top and self.top != "angled lid":
h = self.adjustSize(h) - self.thickness
else:
h = self.adjustSize(h)
t = self.thickness
r, hp, side = self.regularPolygon(2*n+2, h=y/2.0)
if n % 2:
lx = x - 2 * hp + side
else:
lx = x - 2 * r + side
fingerJointSettings = copy.deepcopy(self.edges["f"].settings)
fingerJointSettings.setValues(self.thickness, angle=360./(2 * (n+1)))
fingerJointSettings.edgeObjects(self, chars="gGH")
with self.saved_context():
if b != "e":
self.floor(x, y , n, edge='f', move="right", label="Bottom")
if self.top == "angled lid":
self.floor(x, y, n, edge='e', move="right", label="Lower Lid")
self.floor(x, y, n, edge='E', move="right", label="Upper Lid")
elif self.top in ("angled hole", "angled lid2"):
self.floor(x, y, n, edge='F', move="right", hole=True, label="Top Rim and Lid")
if self.top == "angled lid2":
self.floor(x, y, n, edge='E', move="right", label="Upper Lid")
self.floor(x, y , n, edge='F', move="up only")
fingers = self.top in ("angled lid2", "angled hole")
cnt = 0
for j in range(2):
cnt += 1
if j == 0 or n % 2:
self.rectangularWall(lx, h, move="right",
edges=b+"GfG" if fingers else b+"GeG",
label="wall {}".format(cnt))
else:
self.rectangularWall(lx, h, move="right",
edges=b+"gfg" if fingers else b+"geg",
label="wall {}".format(cnt))
for i in range(n):
cnt += 1
if (i+j*((n+1)%2)) % 2: # reverse for second half if even n
self.rectangularWall(side, h, move="right",
edges=b+"GfG" if fingers else b+"GeG",
label="wall {}".format(cnt))
else:
self.rectangularWall(side, h, move="right",
edges=b+"gfg" if fingers else b+"geg",
label="wall {}".format(cnt))

View File

@ -0,0 +1,65 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class AngledCutJig(Boxes): # Change class name!
"""Jig for making angled cuts in a laser cutter"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=1.)
# remove cli params you do not need
self.buildArgParser(x=50, y=100)
# Add non default cli params if needed (see argparse std lib)
self.argparser.add_argument(
"--angle", action="store", type=float, default=45.,
help="Angle of the cut")
def bottomCB(self):
t = self.thickness
self.fingerHolesAt(10-t, 4.5*t, 20, 0)
self.fingerHolesAt(30+t, 4.5*t, self.x, 0)
self.fingerHolesAt(10-t, self.y-4.5*t, 20, 0)
self.fingerHolesAt(30+t, self.y-4.5*t, self.x, 0)
def render(self):
# adjust to the variables you want in the local scope
x, y = self.x, self.y
t = self.thickness
th = x * math.tan(math.radians(90-self.angle))
l = (x**2 + th**2)**0.5
th2 = 20 * math.tan(math.radians(self.angle))
l2 = (20**2 + th2**2)**0.5
self.rectangularWall(30+x+2*t, y, callback=[self.bottomCB], move="right")
self.rectangularWall(l, y, callback=[
lambda:self.fingerHolesAt(0, 4.5*t, l, 0), None,
lambda:self.fingerHolesAt(0, 4.5*t, l, 0), None],
move="right")
self.rectangularWall(l2, y, callback=[
lambda:self.fingerHolesAt(0, 4.5*t, l2, 0), None,
lambda:self.fingerHolesAt(0, 4.5*t, l2, 0), None],
move="right")
self.rectangularTriangle(x, th, "fef", num=2, move="up")
self.rectangularTriangle(20, th2, "fef", num=2, move="up")

View File

@ -0,0 +1,118 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class Arcade(Boxes):
"""Desktop Arcade Machine"""
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.argparser.add_argument(
"--width", action="store", type=float, default=450.0,
help="inner width of the console")
self.argparser.add_argument(
"--monitor_height", action="store", type=float, default=350.0,
help="inner width of the console")
self.argparser.add_argument(
"--keyboard_depth", action="store", type=float, default=150.0,
help="inner width of the console")
def side(self, move=None):
# TODO: Add callbacks
y, h = self.y, self.h
t = self.thickness
r = 10
d_30 = 2* r * math.tan(math.radians(15))
tw, th = y+2*r+(self.front+t) * math.sin(math.radians(15)), h+2*r+(self.topback+t)/2**0.5
if self.move(tw, th, move, True):
return
self.moveTo(r+(self.front+t) * math.sin(math.radians(15)), 0)
with self.saved_context():
self.moveTo(0, r)
self.polyline(y, 90, h, 45, self.topback+t, 90, self.top+2*t, 90, 100, -90, self.monitor_height, -30, self.keyboard_depth+2*t, 90, self.front+t, 75)
self.fingerHolesAt(10, r+t/2, self.bottom, 0)
self.polyline(y, (90, r))
self.fingerHolesAt(0.5*t, r+t/2, self.back, 0)
self.fingerHolesAt(h-40-40, r+t/2, self.back, 0)
self.polyline(h, (45, r))
self.fingerHolesAt(0, r+t/2, self.topback, 0)
self.fingerHolesAt(self.topback+t/2, r+t, self.top, 90)
self.fingerHolesAt(self.topback, self.top+r+1.5*t, self.speaker, -180)
self.polyline(self.topback+t, (90, r), self.top+2*t, (90, r), 100-2*r, (-90, r), self.monitor_height-2*r-d_30, (-30, r))
self.fingerHolesAt(-d_30+t, r+.5*t, self.keyboard_depth, 0)
self.fingerHolesAt(-d_30+0.5*t, r+t, self.keyback, 90)
self.fingerHolesAt(self.keyboard_depth-d_30+1.5*t, r+t, self.front, 90)
self.polyline(self.keyboard_depth-d_30+2*t, (90, r), self.front+t, (75, r))
self.move(tw, th, move)
def keyboard(self):
# Add holes for the joystick and buttons here
pass
def speakers(self):
self.hole(self.width/4., 50, 40)
self.hole(self.width*3/4., 50, 40)
def render(self):
width = self.width
t = self.thickness
self.back = 40
self.front = 120
self.keyback = 50
self.speaker = 150
self.top = 100
self.topback = 200
y, h = self.y, self.h = 540, 450
y = self.y = ((self.topback+self.top+3*t-100+self.monitor_height) / 2**0.5
+ (self.keyboard_depth+2*t)*math.cos(math.radians(15))
- (self.front+t) * math.sin(math.radians(15)))
h = self.h = ((self.monitor_height-self.topback+self.top+1*t+100) / 2**0.5 +
+ (self.keyboard_depth+2*t)*math.sin(math.radians(15))
+ (self.front+t) * math.cos(math.radians(15)))
self.bottom = y-40-0.5*t
self.backwall = h-40
# Floor
self.rectangularWall(width, self.bottom, "efff", move="up")
# Back
self.rectangularWall(width, self.back, "Ffef", move="up")
self.rectangularWall(width, self.backwall, move="up")
self.rectangularWall(width, self.back, "efef", move="up")
# Front bottom
self.rectangularWall(width, self.front, "efff", move="up")
self.rectangularWall(width, self.keyboard_depth, "FfFf", callback=[self.keyboard], move="up")
self.rectangularWall(width, self.keyback, "ffef", move="up")
# Top
self.rectangularWall(width, self.speaker, "efff", callback=[None, None, self.speakers], move="up")
self.rectangularWall(width, self.top, "FfFf", move="up")
self.rectangularWall(width, self.topback, "ffef", move="up")
# Sides
self.side(move="up")
self.side(move="up")

View File

@ -0,0 +1,119 @@
"""Generator for a split atreus keyboard."""
from copy import deepcopy
from boxes import Boxes, Color, holeCol, restore, boolarg
from boxes.edges import FingerJointSettings
from .keyboard import Keyboard
class Atreus21(Boxes, Keyboard):
"""Generator for a split atreus keyboard."""
ui_group = 'Misc'
btn_size = 15.6
half_btn = btn_size / 2
border = 6
def __init__(self):
super().__init__()
self.add_common_keyboard_parameters(
# By default, columns from Atreus 21
default_columns_definition='4@3/4@6/4@11/4@5/4@0/1@{}'.format(self.btn_size * 0.5)
)
def render(self):
"""Renders the keyboard."""
self.moveTo(10, 30)
case_x, case_y = self._case_x_y()
margin = 2 * self.border + 1
for reverse in [False, True]:
# keyholder
self.outer()
self.half(reverse=reverse)
self.holes()
self.moveTo(case_x + margin)
# support
self.outer()
self.half(self.support, reverse=reverse)
self.holes()
self.moveTo(-case_x - margin, case_y + margin)
# hotplug
self.outer()
self.half(self.hotplug, reverse=reverse)
self.holes()
self.moveTo(case_x + margin)
# border
self.outer()
self.rim()
self.holes()
self.moveTo(-case_x - margin, case_y + margin)
def holes(self, diameter=3, margin=1.5):
case_x, case_y = self._case_x_y()
for x in [-margin, case_x + margin]:
for y in [-margin, case_y + margin]:
self.hole(x, y, d=diameter)
def micro(self):
x = 17.9
y = 33
b = self.border
case_x, case_y = self._case_x_y()
self.rectangularHole(
x * -.5 + case_x + b * .5,
y * -.5 + case_y + b * .5,
x, y
)
@restore
def rim(self):
x, y = self._case_x_y()
self.moveTo(x * .5, y * .5)
self.rectangularHole(0, 0, x, y, 5)
@restore
def outer(self):
x, y = self._case_x_y()
b = self.border
self.moveTo(0, -b)
corner = [90, b]
self.polyline(*([x, corner, y, corner] * 2))
@restore
def half(self, hole_cb=None, reverse=False):
if hole_cb == None:
hole_cb = self.key
self.moveTo(self.half_btn, self.half_btn)
self.apply_callback_on_columns(
hole_cb,
self.columns_definition,
reverse=reverse,
)
def support(self):
self.configured_plate_cutout(support=True)
def hotplug(self):
self.pcb_holes(
with_hotswap=self.hotswap_enable,
with_pcb_mount=self.pcb_mount_enable,
with_diode=self.diode_enable,
with_led=self.led_enable,
)
def key(self):
self.configured_plate_cutout()
# get case sizes
def _case_x_y(self):
spacing = Keyboard.STANDARD_KEY_SPACING
margin = spacing - self.btn_size
x = len(self.columns_definition) * spacing - margin
y = max(offset + keys * spacing for (offset, keys) in self.columns_definition) - margin
return x, y

View File

@ -0,0 +1,58 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class BasedBox(Boxes):
"""Fully closed box on a base"""
ui_group = "Box"
description = """This box is more of a building block than a finished item.
Use a vector graphics program (like Inkscape) to add holes or adjust the base
plate. The width of the "brim" can also be adjusted with the **edge_width**
parameter in the **Finger Joints Settings**.
See ClosedBox for variant without a base.
"""
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser("x", "y", "h", "outside")
def render(self):
x, y, h = self.x, self.y, self.h
if self.outside:
x = self.adjustSize(x)
y = self.adjustSize(y)
h = self.adjustSize(h)
t = self.thickness
self.rectangularWall(x, h, "fFFF", move="right", label="Wall 1")
self.rectangularWall(y, h, "ffFf", move="up", label="Wall 2")
self.rectangularWall(y, h, "ffFf", label="Wall 4")
self.rectangularWall(x, h, "fFFF", move="left up", label="Wall 3")
self.rectangularWall(x, y, "ffff", move="right", label="Top")
self.rectangularWall(x, y, "hhhh", label="Base")

View File

@ -0,0 +1,128 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2019 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class BayonetBox(Boxes):
"""Round box made from layers with twist on top"""
description = """Glue together - all outside rings to the bottom, all inside rings to the top."""
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.argparser.add_argument(
"--diameter", action="store", type=float, default=50.,
help="Diameter of the box in mm")
self.argparser.add_argument(
"--lugs", action="store", type=int, default=10,
help="number of locking lugs")
self.argparser.add_argument(
"--alignment_pins", action="store", type=float, default=1.0,
help="diameter of the alignment pins")
self.buildArgParser("outside")
def alignmentHoles(self, inner=False, outer=False):
d = self.diameter
r = d / 2
t = self.thickness
p = 0.05*t
l = self.lugs
a = 180 / l
with self.saved_context():
for i in range(3):
if outer:
self.hole(r-t/2, 0, d=self.alignment_pins)
if inner:
self.hole(r-2*t-p, 0, d=self.alignment_pins)
self.moveTo(0, 0, 360/3)
def lowerLayer(self, asPart=False, move=None):
d = self.diameter
r = d / 2
t = self.thickness
p = 0.05*t
l = self.lugs
a = 180 / l
if asPart:
if self.move(d, d, move, True):
return
self.moveTo(d/2, d/2)
self.alignmentHoles(inner=True)
self.hole(0, 0, r=d/2 - 2.5*t)
self.moveTo(d/2 - 1.5*t, 0, -90)
for i in range(l):
self.polyline(0, (-4/3*a, r-1.5*t), 0, 90, 0.5*t, -90, 0, (-2/3*a, r-t), 0, -90, 0.5*t, 90)
if asPart:
self.move(d, d, move)
def lowerCB(self):
d = self.diameter
r = d / 2
t = self.thickness
p = 0.05*t
l = self.lugs
a = 180 / l
self.alignmentHoles(outer=True)
with self.saved_context():
self.lowerLayer()
self.moveTo(d/2 - 1.5*t+p, 0, -90)
for i in range(l):
self.polyline(0, (-2/3*a, r-1.5*t+p), 0, 90, 0.5*t, -90, 0, (-4/3*a, r-t+p), 0, -90, 0.5*t, 90)
def upperCB(self):
d = self.diameter
r = d / 2
t = self.thickness
p = 0.05*t
l = self.lugs
a = 180 / l
self.hole(0, 0, r=d/2 - 2.5*t)
self.hole(0, 0, r=d/2 - 1.5*t)
self.alignmentHoles(inner=True, outer=True)
self.moveTo(d/2 - 1.5*t, 0, -90)
for i in range(l):
self.polyline(0, (-1.3*a, r-1.5*t+p), 0, 90, 0.5*t, -90, 0, (-0.7*a, r-t+p), 0, -90, 0.5*t, 90)
def render(self):
d = self.diameter
t = self.thickness
p = 0.05*t
if not self.outside:
self.diameter = d = d - 3*t
self.parts.disc(d, callback=lambda: self.alignmentHoles(outer=True), move="right")
self.parts.disc(d, callback=lambda: (self.alignmentHoles(outer=True), self.hole(0, 0, d/2-1.5*t)), move="right")
self.parts.disc(d, callback=self.lowerCB, move="right")
self.parts.disc(d, callback=self.upperCB, move="right")
self.parts.disc(d, callback=lambda : self.alignmentHoles(inner=True),move="right")

View File

@ -0,0 +1,159 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import math, copy
class BinFrontEdge(edges.BaseEdge):
char = "B"
def __call__(self, length, **kw):
f = self.settings.front
a1 = math.degrees(math.atan(f/(1-f)))
a2 = 45 + a1
self.corner(-a1)
for i, l in enumerate(self.settings.sy):
self.edges["e"](l* (f**2+(1-f)**2)**0.5)
self.corner(a2)
self.edges["f"](l*f*2**0.5)
if i < len(self.settings.sy)-1:
if self.char == "B":
self.polyline(0, 45, 0.5*self.settings.hi,
-90, self.thickness, -90, 0.5*self.settings.hi, 90-a1)
else:
self.polyline(0, -45, self.thickness, -a1)
else:
self.corner(-45)
def margin(self):
return max(self.settings.sy) * self.settings.front
class BinFrontSideEdge(BinFrontEdge):
char = 'b'
class BinTray(Boxes):
"""A Type tray variant to be used up right with sloped walls in front"""
ui_group = "Shelf"
def __init__(self):
Boxes.__init__(self)
self.buildArgParser("sx", "sy", "h", "outside", "hole_dD")
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=0.5)
self.argparser.add_argument(
"--front", action="store", type=float, default=0.4,
help="fraction of bin height covert with slope")
def xSlots(self):
posx = -0.5 * self.thickness
for x in self.sx[:-1]:
posx += x + self.thickness
posy = 0
for y in self.sy:
self.fingerHolesAt(posx, posy, y)
posy += y + self.thickness
def ySlots(self):
posy = -0.5 * self.thickness
for y in self.sy[:-1]:
posy += y + self.thickness
posx = 0
for x in self.sx:
self.fingerHolesAt(posy, posx, x)
posx += x + self.thickness
def addMount(self):
ds = self.hole_dD[0]
if len(self.hole_dD) < 2: # if no head diameter is given
dh = 0 # only a round hole is generated
y = max (self.thickness * 1.25, self.thickness * 1.0 + ds) # and we assume that a typical screw head diameter is twice the shaft diameter
else:
dh = self.hole_dD[1] # use given head diameter
y = max (self.thickness * 1.25, self.thickness * 1.0 + dh / 2) # and offset the hole to have enough space for the head
dx = sum(self.sx) + self.thickness * (len(self.sx) - 1)
x1 = dx * 0.125
x2 = dx * 0.875
self.mountingHole(x1, y, ds, dh, -90)
self.mountingHole(x2, y, ds, dh, -90)
def xHoles(self):
posx = -0.5 * self.thickness
for x in self.sx[:-1]:
posx += x + self.thickness
self.fingerHolesAt(posx, 0, self.hi)
def frontHoles(self, i):
def CB():
posx = -0.5 * self.thickness
for x in self.sx[:-1]:
posx += x + self.thickness
self.fingerHolesAt(posx, 0, self.sy[i]*self.front*2**0.5)
return CB
def yHoles(self):
posy = -0.5 * self.thickness
for y in reversed(self.sy[1:]):
posy += y + self.thickness
self.fingerHolesAt(posy, 0, self.hi)
def render(self):
if self.outside:
self.sx = self.adjustSize(self.sx)
self.sy = self.adjustSize(self.sy)
self.h = self.adjustSize(self.h, e2=False)
x = sum(self.sx) + self.thickness * (len(self.sx) - 1)
y = sum(self.sy) + self.thickness * (len(self.sy) - 1)
h = self.h
hi = self.hi = h
t = self.thickness
self.front = min(self.front, 0.999)
self.addPart(BinFrontEdge(self, self))
self.addPart(BinFrontSideEdge(self, self))
angledsettings = copy.deepcopy(self.edges["f"].settings)
angledsettings.setValues(self.thickness, True, angle=45)
angledsettings.edgeObjects(self, chars="gGH")
# outer walls
e = ["F", "f", edges.SlottedEdge(self, self.sx[::-1], "G"), "f"]
self.rectangularWall(x, h, e, callback=[self.xHoles], move="right", label="bottom")
self.rectangularWall(y, h, "FFbF", callback=[self.yHoles, ], move="up", label="left")
self.rectangularWall(y, h, "FFbF", callback=[self.yHoles, ], label="right")
self.rectangularWall(x, h, "Ffef", callback=[self.xHoles, ], move="left", label="top")
self.rectangularWall(y, h, "FFBF", move="up only")
# floor
self.rectangularWall(x, y, "ffff", callback=[self.xSlots, self.ySlots, self.addMount], move="right", label="back")
# Inner walls
for i in range(len(self.sx) - 1):
e = [edges.SlottedEdge(self, self.sy, "f"), "f", "B", "f"]
self.rectangularWall(y, hi, e, move="up", label="inner vertical " + str(i+1))
for i in range(len(self.sy) - 1):
e = [edges.SlottedEdge(self, self.sx, "f", slots=0.5 * hi), "f",
edges.SlottedEdge(self, self.sx[::-1], "G"), "f"]
self.rectangularWall(x, hi, e, move="up", label="inner horizontal " + str(i+1))
# Front walls
for i in range(len(self.sy)):
e = [edges.SlottedEdge(self, self.sx, "g"), "F", "e", "F"]
self.rectangularWall(x, self.sy[i]*self.front*2**0.5, e, callback=[self.frontHoles(i)], move="up", label="retainer " + str(i+1))

View File

@ -0,0 +1,75 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2022 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class BirdHouse(Boxes):
"""Simple Bird House"""
ui_group = "Unstable" # "Misc"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, finger=10.0,space=10.0)
# remove cli params you do not need
self.buildArgParser(x=200, y=200, h=200)
def side(self, x, h, edges="hfeffef", callback=None, move=None):
angles = (90, 0, 45, 90, 45, 0, 90)
roof = 2**0.5 * x / 2
t = self.thickness
lengths = (x, h, t, roof, roof, t, h)
edges = [self.edges.get(e, e) for e in edges]
edges.append(edges[0]) # wrap arround
tw = x + edges[1].spacing() + edges[-2].spacing()
th = h + x/2 + t + edges[0].spacing() + max(edges[3].spacing(), edges[4].spacing())
if self.move(tw, th, move, True):
return
self.moveTo(edges[-2].spacing())
for i in range(7):
self.cc(callback, i, y=self.burn+edges[i].startwidth())
edges[i](lengths[i])
self.edgeCorner(edges[i], edges[i+1], angles[i])
self.move(tw, th, move)
def side_hole(self, width):
self.rectangularHole(width/2, self.h/2,
0.75*width, 0.75*self.h,
r=self.thickness)
def render(self):
x, y, h = self.x, self.y, self.h
roof = 2**0.5 * x / 2
cbx = [lambda: self.side_hole(x)]
cby = [lambda: self.side_hole(y)]
self.side(x, h, callback=cbx, move="right")
self.side(x, h, callback=cbx, move="right")
self.rectangularWall(y, h, "hFeF", callback=cby, move="right")
self.rectangularWall(y, h, "hFeF", callback=cby, move="right")
self.rectangularWall(x, y, "ffff", move="right")
self.edges["h"].settings.setValues(self.thickness, relative=False, edge_width=0.1*roof)
self.flangedWall(y, roof, "ehfh", r=0.2*roof, flanges=[0.2*roof], move="right")
self.flangedWall(y, roof, "ehFh", r=0.2*roof, flanges=[0.2*roof], move="right")

View File

@ -0,0 +1,165 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2020 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class BottleStack(Boxes):
"""Stack bottles in a fridge"""
description = """When rendered with the "double" option the parts with the double slots get connected the shorter beams in the asymetrical slots.
Without the "double" option the stand is a bit more narrow.
"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.argparser.add_argument(
"--diameter", action="store", type=float, default=80,
help="diameter of the bottles in mm")
self.argparser.add_argument(
"--number", action="store", type=int, default=3,
help="number of bottles to hold in the bottom row")
self.argparser.add_argument(
"--depth", action="store", type=float, default=80,
help="depth of the stand along the base of the bottles")
self.argparser.add_argument(
"--double", action="store", type=boolarg, default=True,
help="two pieces that can be combined to up to double the width")
def front(self, h_sides, offset=0, move=None):
t = self.thickness
a = 60
nr = self.number
r1 = self.diameter / 2.0 # bottle
r2 = r1 / math.cos(math.radians(90-a)) - r1 # inbetween
if self.double:
r3 = 1.5*t # upper corners
else:
r3 = .5*t
h = (r1+r2) * (1-math.cos(math.radians(a)))
h_extra = 1*t
h_s = h_sides - t
p = 0.05*t # play
tw, th = nr * r1 * 2 + 2*r3, h + 2*t
if self.move(tw, th, move, True):
return
open_sides = r3 <= 0.5*t
if offset == 0:
slot = [0, 90, h_s, -90, t, -90, h_s, 90]
if open_sides:
self.moveTo(0, h_s)
self.polyline(r3-0.5*t)
self.polyline(*slot[4:])
else:
self.polyline(r3-0.5*t)
self.polyline(*slot)
for i in range(nr-open_sides):
self.polyline(2*r1-t)
self.polyline(*slot)
if open_sides:
self.polyline(2*r1-t)
self.polyline(*slot[:-3])
self.polyline(r3-0.5*t)
else:
self.polyline(r3-0.5*t)
else:
slot = [0, 90, h_s, -90, t, -90, h_s, 90]
h_s += t
slot2 = [0, 90, h_s, -90, t+2*p, -90, h_s, 90]
if open_sides:
self.moveTo(0, h_s)
self.polyline(t+p, -90, h_s, 90)
else:
self.polyline(r3-0.5*t-p)
self.polyline(*slot2)
self.polyline(t-p)
self.polyline(*slot)
self.polyline(2*r1-5*t)
self.polyline(*slot)
self.polyline(t-p)
self.polyline(*slot2)
for i in range(1, nr-open_sides):
self.polyline(2*r1-3*t-p)
self.polyline(*slot)
self.polyline(t-p)
self.polyline(*slot2)
if open_sides:
self.polyline(2*r1-3*t-p)
self.polyline(*slot)
self.polyline(t-p)
self.polyline(0, 90, h_s, -90, t+p)
else:
self.polyline(r3-0.5*t-p)
if open_sides:
h_extra -= h_s
self.polyline(0, 90, h_extra+h-r3, (90, r3))
for i in range(nr):
self.polyline(0, (a, r2), 0, (-2*a, r1), 0, (a, r2))
self.polyline(0, (90, r3), h_extra+h-r3, 90)
self.move(tw, th, move)
def side(self, l, h, short=False, move=None):
t = self.thickness
short = bool(short)
tw, th = l + 2*t - 4*t*short, h
if self.move(tw, th, move, True):
return
self.moveTo(t, 0)
self.polyline(l-3*t*short)
if short:
end = [90, h-t, 90, t, -90, t, 90]
else:
end = [(90, t), h-2*t, (90, t), 0, 90, t, -90, t, -90, t, 90]
self.polyline(0, *end)
self.polyline(l-2*t- 3*t*short)
self.polyline(0, *reversed(end))
self.move(tw, th, move)
def render(self):
t = self.thickness
d = self.depth
nr = self.number
h_sides = 2*t
pieces = 2 if self.double else 1
for offset in range(pieces):
self.front(h_sides, offset, move="up")
self.front(h_sides, offset, move="up")
for short in range(pieces):
for i in range(nr+1):
self.side(d, h_sides, short, move="up")

View File

@ -0,0 +1,89 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class BottleTag(Boxes):
"""Paper slip over bottle tag"""
ui_group = "Misc" # see ./__init__.py for names
def __init__(self):
Boxes.__init__(self)
self.buildArgParser()
# Add non default cli params if needed (see argparse std lib)
self.argparser.add_argument(
"--width", action="store", type=float, default=72,
help="width of neck tag")
self.argparser.add_argument(
"--height", action="store", type=float, default=98,
help="height of neck tag")
self.argparser.add_argument(
"--min_diameter", action="store", type=float, default=24,
help="inner diameter of bottle neck hole")
self.argparser.add_argument(
"--max_diameter", action="store", type=float, default=50,
help="outer diameter of bottle neck hole")
self.argparser.add_argument(
"--radius", action="store", type=float, default=15,
help="corner radius of bottom tag")
self.argparser.add_argument(
"--segment_width", action="store", type=int, default=3,
help="inner segment width")
def render(self):
# adjust to the variables you want in the local scope
width = self.width
height = self.height
r_min = self.min_diameter / 2
r_max = self.max_diameter / 2
r = self.radius
segment_width = self.segment_width
# tag outline
self.moveTo(r)
self.edge(width - r - r)
self.corner(90, r)
self.edge(height - width / 2.0 - r)
self.corner(180, width / 2)
self.edge(height - width / 2.0 - r)
self.corner(90, r)
# move to centre of hole and cut the inner circle
self.moveTo(width / 2 - r, height - width / 2)
with self.saved_context():
self.moveTo(0, -r_min)
self.corner(360, r_min)
# draw the radial lines approx 2mm apart on r_min
seg_angle = math.degrees(segment_width / r_min)
# for neatness, we want an integral number of cuts
num = math.floor(360 / seg_angle)
for i in range(num):
with self.saved_context():
self.moveTo(0, 0, i * 360.0 / num)
self.moveTo(r_min)
self.edge(r_max - r_min)
# Add some right angle components to reduce tearing
with self.saved_context():
self.moveTo(0, 0, 90)
self.edge(0.5)
with self.saved_context():
self.moveTo(0, 0, -90)
self.edge(0.5)

View File

@ -0,0 +1,150 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2022 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class BreadBox(Boxes):
"""A BreadBox with a gliding door"""
ui_group = "Unstable" # "FlexBox"
description = """Beware of the rolling shutter effect! Use wax on sliding surfaces.
"""
def side(self, l, h, r, move=None):
t = self.thickness
if self.move(l+2*t, h+2*t, move, True):
return
self.moveTo(t, t)
self.ctx.save()
n = self.n
a = 90. / n
ls = 2*math.sin(math.radians(a/2)) * (r-2.5*t)
self.fingerHolesAt(2*t, 0, h-r, 90)
self.moveTo(2.5*t, h-r, 90-a/2)
for i in range(n):
self.fingerHolesAt(0, 0.5*t, ls, 0)
self.moveTo(ls, 0, -a)
self.moveTo(0, 0, a/2)
self.fingerHolesAt(0, 0.5*t, l / 2 - r, 0)
self.ctx.restore()
self.edges["f"](l)
self.polyline(t, 90, h-r, (90, r+t), l/2-r, 90, t, -90, 0,)
self.edges["f"](l/2)
self.polyline(0, 90)
self.edges["f"](h)
self.move(l+2*t, h+2*t, move)
def cornerRadius(self, r, two=False, move=None):
s = self.spacing
if self.move(r, r+s, move, True):
return
for i in range(2 if two else 1):
self.polyline(r, 90, r, 180, 0, (-90, r), 0 ,-180)
self.moveTo(r, r+s, 180)
self.move(r, r+s, move)
def rails(self, l, h, r, move=None):
t = self.thickness
s = self.spacing
tw, th = l/2+2.5*t+3*s, h+1.5*t+3*s
if self.move(tw, th, move, True):
return
self.moveTo(2.5*t+s, 0)
self.polyline(l/2-r, (90, r+t), h-r, 90, t, 90, h-r, (-90, r), l/2-r, 90, t, 90)
self.moveTo(-t-s, t+s)
self.polyline(l/2-r, (90, r+t), h-r, 90, t, 90, h-r, (-90, r), l/2-r, 90, t, 90)
self.moveTo(+t-s, t+s)
self.polyline(l/2-r, (90, r-1.5*t), h-r, 90, t, 90, h-r, (-90, r-2.5*t), l/2-r, 90, t, 90)
self.moveTo(-t-s, t+s)
self.polyline(l/2-r, (90, r-1.5*t), h-r, 90, t, 90, h-r, (-90, r-2.5*t), l/2-r, 90, t, 90)
self.move(tw, th, move)
def door(self, l, h, move=None):
t = self.thickness
if self.move(l, h, move, True):
return
self.fingerHolesAt(t, t, h-2*t)
self.edge(2*t)
self.edges["X"](l-2*t, h)
self.polyline(0, 90, h, 90, l, 90, h, 90)
self.move(l, h, move)
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=0.5)
self.addSettingsArgs(edges.FlexSettings, distance=.75, connection=2.)
self.buildArgParser(x=150, y=100, h=100)
self.argparser.add_argument(
"--radius", action="store", type=float, default=40.0,
help="radius of the corners")
def render(self):
x, y, h, r = self.x, self.y, self.h, self.radius
self.n = n = 3
if not r:
self.radius = r = h / 2
self.radius = r = min(r, h/2)
t = self.thickness
self.ctx.save()
self.side(x, h, r, move="right")
self.side(x, h, r, move="right")
self.rectangularWall(y, h, "fFfF", move="right")
self.ctx.restore()
self.side(x, h, r, move="up only")
self.rectangularWall(x, y, "FEFF", move="right")
self.rectangularWall(x/2, y, "FeFF", move="right")
self.door(x/2 + h - 2*r + 0.5*math.pi*r + 2*t, y-0.2*t, move="right")
self.rectangularWall(2*t, y-2.2*t, edges="eeef", move="right")
a = 90. / n
ls = 2*math.sin(math.radians(a/2)) * (r-2.5*t)
edges.FingerJointSettings(t, angle=a).edgeObjects(self, chars="aA")
edges.FingerJointSettings(t, angle=a/2).edgeObjects(self, chars="bB")
self.rectangularWall(h-r, y, "fbfe", move="right")
self.rectangularWall(ls, y, "fafB", move="right")
for i in range(n-2):
self.rectangularWall(ls, y, "fafA", move="right")
self.rectangularWall(ls, y, "fbfA", move="right")
self.rectangularWall(x/2 - r, y, "fefB", move="right")
self.rails(x, h, r, move="right mirror")
self.cornerRadius(r, two=True, move="right")

View File

@ -0,0 +1,74 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2019 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class BurnTest(Boxes):
"""Test different burn values"""
description = """This generator will make shapes that you can use to select
optimal value for burn parameter for other generators. After burning try to
attach sides with the same value and use best fitting one on real projects.
In this generator set burn in the Default Settings to the lowest value
to be tested. To get an idea cut a rectangle with known nominal size and
measure the shrinkage due to the width of the laser cut. Now you can
measure the burn value that you should use in other generators. It is half
the difference of the overall size as shrinkage is occurring on both
sides. You can use the reference rectangle as it is rendered without burn
correction.
See also LBeam that can serve as compact BurnTest and FlexTest for testing flex settings.
"""
ui_group = "Part"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser(x=100)
self.argparser.add_argument(
"--step", action="store", type=float, default=0.01,
help="increases in burn value between the sides")
self.argparser.add_argument(
"--pairs", action="store", type=int, default=2,
help="number of pairs (each testing four burn values)")
def render(self):
x, s = self.x, self.step
t = self.thickness
fsize = 12.5 * self.x / 100 if self.x < 81 else 10
self.moveTo(t, t)
for cnt in range(self.pairs):
for i in range(4):
self.text("%.3fmm" % self.burn, x/2, t, fontsize = fsize, align="center", color=Color.ETCHING)
self.edges["f"](x)
self.corner(90)
self.burn += s
self.burn -= 4*s
self.moveTo(x+2*t+self.spacing, -t)
for i in range(4):
self.text("%.3fmm" % self.burn, x/2, t, fontsize = fsize, align="center", color=Color.ETCHING)
self.edges["F"](x)
self.polyline(t, 90, t)
self.burn += s
self.moveTo(x+2*t+self.spacing, t)

View File

@ -0,0 +1,365 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERself.canHightANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import math
class FrontEdge(edges.BaseEdge):
char = "a"
def __call__(self, length, **kw):
x = math.ceil( ((self.canDiameter * 0.5 + 2 * self.thickness) * math.sin(math.radians(self.chuteAngle))) / self.thickness)
if self.top_edge != "e":
self.corner(90, self.thickness)
self.edge(0.5 * self.canDiameter)
self.corner(-90, 0.25 * self.canDiameter)
else:
self.moveTo(-self.burn, self.canDiameter + self.thickness, -90)
self.corner(90, 0.25 * self.canDiameter)
self.edge(self.thickness)
self.edge(0.5 * self.canDiameter - self.thickness)
self.corner(-90, 0.25 * self.canDiameter)
self.edge(0.5 * self.canDiameter)
self.corner(90, self.thickness)
self.edge(x * self.thickness )
self.corner(90, self.thickness)
self.edge(0.5 * self.canDiameter)
self.corner(-90, 0.25 * self.canDiameter)
self.edge(0.5 * self.canDiameter - (1 + x) * self.thickness + self.top_chute_height + self.bottom_chute_height - self.barrier_height)
self.corner(-90, 0.25 * self.canDiameter)
self.edge(0.5 * self.canDiameter)
self.corner(90, self.thickness)
self.edge(self.barrier_height)
self.edge(self.thickness)
class TopChuteEdge(edges.BaseEdge):
char = "b"
def __call__(self, length, **kw):
self.edge(0.2 * length - self.thickness)
self.corner(90, self.thickness)
self.edge(1.5*self.canDiameter - 2 * self.thickness)
self.corner(-90, self.thickness)
self.edge(0.6 * length - 2 * self.thickness)
self.corner(-90, self.thickness)
self.edge(1.5*self.canDiameter - 2 * self.thickness)
self.corner(90, self.thickness)
self.edge(0.2 * length - self.thickness)
class BarrierEdge(edges.BaseEdge):
char = "A"
def __call__(self, length, **kw):
self.edge(0.2*length)
self.corner(90,self.thickness/2)
self.corner(-90,self.thickness/2)
self.edge(0.6*length-2*self.thickness)
self.corner(-90,self.thickness/2)
self.corner(90,self.thickness/2)
self.edge(0.2*length)
def startwidth(self):
return self.boxes.thickness
class CanStorage(Boxes):
"""Storage box for round containers"""
description = """
for AA batteries:
![CanStorage for AA batteries](static/samples/CanStorageAA.jpg)
for canned tomatos:
"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, finger=2.0, space=2.0, surroundingspaces=0.0)
self.addSettingsArgs(edges.StackableSettings)
self.addSettingsArgs(fillHolesSettings)
self.argparser.add_argument(
"--top_edge", action="store",
type=ArgparseEdgeType("efhŠ"), choices=list("efhŠ"),
default="Š", help="edge type for top edge")
self.argparser.add_argument(
"--bottom_edge", action="store",
type=ArgparseEdgeType("eEš"), choices=list("eEš"),
default="š", help="edge type for bottom edge")
# Add non default cli params if needed (see argparse std lib)
self.argparser.add_argument(
"--canDiameter", action="store", type=float, default=75,
help="outer diameter of the cans to be stored (in mm)")
self.argparser.add_argument(
"--canHight", action="store", type=float, default=110,
help="hight of the cans to be stored (in mm)")
self.argparser.add_argument(
"--canNum", action="store", type=int, default=12,
help="number of cans to be stored")
self.argparser.add_argument(
"--chuteAngle", action="store", type=float, default=5.0,
help="slope angle of the chutes")
def DrawPusher(self, dbg = False):
with self.saved_context():
if dbg == False:
self.moveTo(0,self.thickness)
self.edge(0.25*self.pusherA)
self.corner(-90)
self.edge(self.thickness)
self.corner(90)
self.edge(0.5*self.pusherA)
self.corner(90)
self.edge(self.thickness)
self.corner(-90)
self.edge(0.25*self.pusherA)
self.corner(90-self.chuteAngle)
self.edge(0.25*self.pusherB)
self.corner(-90)
self.edge(self.thickness)
self.corner(90)
self.edge(0.5*self.pusherB)
self.corner(90)
self.edge(self.thickness)
self.corner(-90)
self.edge(0.25*self.pusherB)
self.corner(90+self.pusherAngle+self.chuteAngle)
self.edge(self.pusherC)
def cb_top_chute(self, nr):
if nr == 0:
# fill with holes
border = [
(0, 0),
(self.top_chute_depth, 0),
(self.top_chute_depth, 0.2 * self.width - self.thickness),
(self.top_chute_depth - self.thickness, 0.2 * self.width),
(self.top_chute_depth - 1.5*self.canDiameter, 0.2 * self.width),
(self.top_chute_depth - 1.5*self.canDiameter, 0.8 * self.width),
(self.top_chute_depth - self.thickness, 0.8 * self.width),
(self.top_chute_depth, 0.8 * self.width + self.thickness),
(self.top_chute_depth, self.width),
(0, self.width),
]
if self.fillHoles_fill_pattern != "no fill":
self.fillHoles(
pattern="hbar",
border=border,
max_radius = min(2*self.thickness, self.fillHoles_hole_max_radius) if self.fillHoles_fill_pattern in ["hbar", "vbar"] else min(2*self.thickness, self.width/30),
hspace=min(2*self.thickness, self.fillHoles_space_between_holes) if self.fillHoles_fill_pattern in ["hbar", "vbar"] else min(2*self.thickness, self.width/20),
bspace=min(2*self.thickness, self.fillHoles_space_to_border) if self.fillHoles_fill_pattern in ["hbar", "vbar"] else min(2*self.thickness, self.width/20),
bar_length=self.fillHoles_bar_length,
max_random=self.fillHoles_max_random,
)
def cb_top(self, nr):
if nr == 0:
# fill with holes
border = [
(0, 0),
(self.depth, 0),
(self.depth, self.width),
(0, self.width),
]
if self.fillHoles_fill_pattern != "no fill":
self.fillHoles(
pattern="hbar",
border=border,
max_radius = min(2*self.thickness, self.fillHoles_hole_max_radius) if self.fillHoles_fill_pattern in ["hbar", "vbar"] else min(2*self.thickness, self.width/30),
hspace=min(2*self.thickness, self.fillHoles_space_between_holes) if self.fillHoles_fill_pattern in ["hbar", "vbar"] else min(2*self.thickness, self.width/20),
bspace=min(2*self.thickness, self.fillHoles_space_to_border) if self.fillHoles_fill_pattern in ["hbar", "vbar"] else min(2*self.thickness, self.width/20),
bar_length=self.fillHoles_bar_length,
max_random=self.fillHoles_max_random,
)
def cb_bottom_chute(self, nr):
if nr == 1:
# holes for pusher
self.rectangularHole(self.width*0.85-0.5*self.thickness, 0.25*self.pusherA, self.thickness, 0.5*self.pusherA, center_x=False, center_y=False)
self.rectangularHole(self.width*0.5 -0.5*self.thickness, 0.25*self.pusherA, self.thickness, 0.5*self.pusherA, center_x=False, center_y=False)
self.rectangularHole(self.width*0.15-0.5*self.thickness, 0.25*self.pusherA, self.thickness, 0.5*self.pusherA, center_x=False, center_y=False)
def cb_back(self, nr):
if nr == 1:
# holes for pusher
self.rectangularHole(self.width*0.85-0.5*self.thickness, self.thickness + self.depth * math.tan(math.radians(self.chuteAngle)) + 0.25*self.pusherB, self.thickness, 0.5*self.pusherB + self.thickness, center_x=False, center_y=False)
self.rectangularHole(self.width*0.5 -0.5*self.thickness, self.thickness + self.depth * math.tan(math.radians(self.chuteAngle)) + 0.25*self.pusherB, self.thickness, 0.5*self.pusherB + self.thickness, center_x=False, center_y=False)
self.rectangularHole(self.width*0.15-0.5*self.thickness, self.thickness + self.depth * math.tan(math.radians(self.chuteAngle)) + 0.25*self.pusherB, self.thickness, 0.5*self.pusherB + self.thickness, center_x=False, center_y=False)
def cb_sides(self, nr):
if nr == 0:
# for debugging only
if self.debug:
# draw orientation points
self.hole(0, 0, 1, color=Color.ANNOTATIONS)
self.hole(0, self.thickness, 1, color=Color.ANNOTATIONS)
self.hole(0, self.thickness + self.canDiameter, 1, color=Color.ANNOTATIONS)
self.hole(0, self.thickness + self.canDiameter + self.bottom_chute_height, 1, color=Color.ANNOTATIONS)
self.hole(0, self.thickness + self.canDiameter + self.bottom_chute_height + self.top_chute_height + self.thickness, 1, color=Color.ANNOTATIONS)
self.hole(0, self.thickness + self.canDiameter + self.bottom_chute_height + self.top_chute_height + self.thickness + self.canDiameter, 1, color=Color.ANNOTATIONS)
self.hole(0, self.thickness + self.canDiameter + self.bottom_chute_height + self.top_chute_height + self.thickness + self.canDiameter + 1.0 * self.thickness, 1, color=Color.ANNOTATIONS)
with self.saved_context():
# draw cans, bottom row
self.moveTo(0, self.thickness, self.chuteAngle)
self.rectangularHole(2*self.thickness, 0, math.ceil(self.canNum / 2) * self.canDiameter, self.canDiameter, center_x=False, center_y=False, color=Color.ANNOTATIONS)
for i in range(math.ceil(self.canNum / 2)-1):
self.hole(2*self.thickness+(0.5 + i) * self.canDiameter, self.canDiameter / 2, self.canDiameter / 2, color=Color.ANNOTATIONS)
i+=1
self.hole(2*self.thickness+(0.5 + i) * self.canDiameter, self.canDiameter*0.8 , self.canDiameter / 2, color=Color.ANNOTATIONS)
with self.saved_context():
# draw pusher
self.moveTo(self.depth-self.pusherA, self.thickness + (self.depth-self.pusherA) * math.tan(math.radians(self.chuteAngle)))
self.moveTo(0,0,self.chuteAngle)
self.DrawPusher(True)
with self.saved_context():
# draw cans, top row
self.moveTo(0, self.thickness + self.canDiameter + self.bottom_chute_height + self.top_chute_height + 0.5 * self.thickness, -self.chuteAngle)
self.rectangularHole(0, 0.5 * self.thickness, math.ceil(self.canNum / 2) * self.canDiameter, self.canDiameter, center_x=False, center_y=False, color=Color.ANNOTATIONS)
for i in range(math.ceil(self.canNum / 2)):
self.hole((0.5 + i) * self.canDiameter, self.canDiameter / 2 + 0.5 * self.thickness, self.canDiameter / 2, color=Color.ANNOTATIONS)
with self.saved_context():
# draw barrier
self.moveTo(1.5 * self.thickness, 1.1 * self.thickness + self.burn + math.sin(math.radians(self.chuteAngle)) * 2 * self.thickness, 90)
self.rectangularHole(0, 0, self.barrier_height, self.thickness, center_x=False, center_y=True, color=Color.ANNOTATIONS)
# bottom chute
with self.saved_context():
self.moveTo(0, 0.5 * self.thickness, self.chuteAngle)
self.fingerHolesAt(0, 0, self.depth / math.cos(math.radians(self.chuteAngle)), 0)
# top chute
with self.saved_context():
self.moveTo(0, self.thickness + self.canDiameter + self.bottom_chute_height + self.top_chute_height + 0.5 * self.thickness, -self.chuteAngle)
self.fingerHolesAt(0, 0, self.top_chute_depth, 0)
# front barrier
with self.saved_context():
self.moveTo(1.5 * self.thickness, 1.1 * self.thickness + self.burn + math.sin(math.radians(self.chuteAngle)) * 2 * self.thickness, 90)
self.fingerHolesAt(0, 0, self.barrier_height, 0)
# fill with holes
border = [
(2*self.thickness, 0.5*self.thickness + 2*self.thickness * math.tan(math.radians(self.chuteAngle)) + 0.5*self.thickness/math.cos(math.radians(self.chuteAngle))),
(self.depth, self.thickness + self.depth * math.tan(math.radians(self.chuteAngle))),
(self.depth, self.height),
(self.thickness + 0.75 * self.canDiameter, self.height),
(self.thickness + 0.75 * self.canDiameter, 0.5*self.thickness + self.canDiameter + self.bottom_chute_height + self.top_chute_height + self.thickness - (self.thickness + 0.75 * self.canDiameter) * math.tan(math.radians(self.chuteAngle)) + 0.5*self.thickness/math.cos(math.radians(self.chuteAngle))),
(self.top_chute_depth * math.cos(math.radians(self.chuteAngle)), self.thickness + self.canDiameter + self.bottom_chute_height + self.top_chute_height + self.thickness - (self.top_chute_depth) * math.sin(math.radians(self.chuteAngle))),
(self.top_chute_depth * math.cos(math.radians(self.chuteAngle)), self.thickness + self.canDiameter + self.bottom_chute_height + self.top_chute_height - (self.top_chute_depth) * math.sin(math.radians(self.chuteAngle))),
(self.thickness + 0.75 * self.canDiameter, 1.5*self.thickness + self.canDiameter + self.bottom_chute_height + self.top_chute_height - (self.thickness + 0.75 * self.canDiameter) * math.tan(math.radians(self.chuteAngle)) - 0.5*self.thickness/math.cos(math.radians(self.chuteAngle))),
(self.thickness + 0.75 * self.canDiameter, 2*self.thickness + self.barrier_height ),
(2*self.thickness, 2*self.thickness + self.barrier_height),
]
self.fillHoles(
pattern=self.fillHoles_fill_pattern,
border=border,
max_radius=self.fillHoles_hole_max_radius,
hspace=self.fillHoles_space_between_holes,
bspace=self.fillHoles_space_to_border,
min_radius=self.fillHoles_hole_min_radius,
style=self.fillHoles_hole_style,
bar_length=self.fillHoles_bar_length,
max_random=self.fillHoles_max_random,
)
def render(self):
self.chuteAngle = self.chuteAngle
self.pusherAngle = 30 # angle of pusher
self.pusherA = 0.75 * self.canDiameter # length of pusher
self.pusherB = self.pusherA / math.sin(math.radians(180 - (90+self.chuteAngle) - self.pusherAngle)) * math.sin(math.radians(self.pusherAngle))
self.pusherC = self.pusherA / math.sin(math.radians(180 - (90+self.chuteAngle) - self.pusherAngle)) * math.sin(math.radians(90+self.chuteAngle))
self.addPart(FrontEdge(self, self))
self.addPart(TopChuteEdge(self, self))
self.addPart(BarrierEdge(self, self))
if self.canDiameter < 8 * self.thickness:
self.edges["f"].settings.setValues(self.thickness, True, finger=1.0)
self.edges["f"].settings.setValues(self.thickness, True, space=1.0)
self.edges["f"].settings.setValues(self.thickness, True, surroundingspaces=0.0)
if self.canDiameter < 4 * self.thickness:
raise ValueError("Can diameter has to be at least 4 times the material thickness!")
if self.canNum < 4:
raise ValueError("4 cans is the minimum!")
self.depth = self.canDiameter * (math.ceil(self.canNum / 2) + 0.1) + self.thickness
self.top_chute_height = max(self.depth * math.sin(math.radians(self.chuteAngle)), 0.1 * self.canDiameter)
self.top_chute_depth = (self.depth - 1.1 * self.canDiameter) / math.cos(math.radians(self.chuteAngle))
self.bottom_chute_height = max((self.depth - 1.1 * self.canDiameter) * math.sin(math.radians(self.chuteAngle)), 0.1 * self.canDiameter)
self.bottom_chute_depth = self.depth / math.cos(math.radians(self.chuteAngle))
self.barrier_height = 0.25 * self.canDiameter
if (self.top_chute_depth + self.bottom_chute_height - self.thickness) < (self.barrier_height + self.canDiameter * 0.1):
self.bottom_chute_height = self.barrier_height + self.canDiameter * 0.1 + self.thickness - self.top_chute_depth
self.height = self.thickness + self.canDiameter + self.bottom_chute_height + self.top_chute_height + 0.5 * self.thickness + self.canDiameter + 1.5 * self.thickness # measurements from bottom to top
self.width = 0.01 * self.canHight + self.canHight + 0.01 * self.canHight
edgs = self.bottom_edge + "h" + self.top_edge + "a"
# render your parts here
self.rectangularWall(self.depth, self.height, edges=edgs, callback=self.cb_sides, move="up", label="right")
self.rectangularWall(self.depth, self.height, edges=edgs, callback=self.cb_sides, move="up mirror", label="left")
self.rectangularWall(self.bottom_chute_depth, self.width, "fefe", callback=self.cb_bottom_chute, move="up", label="bottom chute")
self.rectangularWall(self.top_chute_depth, self.width, "fbfe", callback=self.cb_top_chute, move="up", label="top chute")
self.rectangularWall(self.barrier_height, self.width, "fAfe", move="right", label="barrier")
self.rectangularWall(self.height, self.width, "fefe", callback=self.cb_back, move="up", label="back")
self.rectangularWall(self.barrier_height, self.width, "fefe", move="left only", label="invisible")
if self.top_edge != "e":
self.rectangularWall(self.depth, self.width, "fefe", callback=self.cb_top, move="up", label="top")
pusherH = self.pusherB * math.cos(math.radians(self.chuteAngle)) + self.thickness
pusherV = self.pusherC * math.cos(math.radians(self.chuteAngle)) + self.thickness
self.move(pusherV, pusherH, where ="right", before=True, label="Pusher")
self.DrawPusher()
self.move(pusherV, pusherH, where ="right", before=False, label="Pusher")
self.move(pusherV, pusherH, where ="right", before=True, label="Pusher")
self.DrawPusher()
self.move(pusherV, pusherH, where ="right", before=False, label="Pusher")
self.move(pusherV, pusherH, where ="up", before=True, label="Pusher")
self.DrawPusher()
self.text("Glue the Pusher pieces into slots on bottom\nand back plates to prevent stuck cans.", pusherV+3,0, fontsize=4, color=Color.ANNOTATIONS)
self.move(pusherV, pusherH, where ="up", before=False, label="Pusher")
self.move(pusherV, pusherH, where ="left only", before=True, label="Pusher")
self.move(pusherV, pusherH, where ="left only", before=True, label="Pusher")
if self.bottom_edge == "š":
self.rectangularWall(self.edges["š"].settings.width+3*self.thickness, self.edges["š"].settings.height-4*self.burn, "eeee", move="right", label="Stabilizer 1")
self.rectangularWall(self.edges["š"].settings.width+3*self.thickness, self.edges["š"].settings.height-4*self.burn, "eeee", move="right", label="Stabilizer 2")
self.rectangularWall(self.edges["š"].settings.width+5*self.thickness, self.edges["š"].settings.height-4*self.burn, "eeee", move="right", label="Stabilizer 3")
self.rectangularWall(self.edges["š"].settings.width+5*self.thickness, self.edges["š"].settings.height-4*self.burn, "eeee", move="right", label="Stabilizer 4")
self.text("Glue a stabilizer on the inside of each bottom\nside stacking foot for lateral stabilization.",3 ,0 , fontsize=4, color=Color.ANNOTATIONS)

View File

@ -0,0 +1,165 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
# Copyright (C) 2018 jens persson <jens@persson.cx>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import edges, Boxes
class InsetEdgeSettings(edges.Settings):
"""Settings for InsetEdge"""
absolute_params = {
"thickness": 0,
}
class InsetEdge(edges.BaseEdge):
"""An edge with space to slide in a lid"""
def __call__(self, length, **kw):
t = self.settings.thickness
self.corner(90)
self.edge(t, tabs=2)
self.corner(-90)
self.edge(length, tabs=2)
self.corner(-90)
self.edge(t, tabs=2)
self.corner(90)
class FingerHoleEdgeSettings(edges.Settings):
"""Settings for FingerHoleEdge"""
absolute_params = {
"wallheight": 0,
}
class FingerHoleEdge(edges.BaseEdge):
"""An edge with room to get your fingers around cards"""
def __call__(self, length, **kw):
depth = self.settings.wallheight-self.thickness-10
self.edge(length/2-10, tabs=2)
self.corner(90)
self.edge(depth, tabs=2)
self.corner(-180, 10)
self.edge(depth, tabs=2)
self.corner(90)
self.edge(length/2-10, tabs=2)
class CardBox(Boxes):
"""Box for storage of playing cards"""
ui_group = "Box"
description = """
#### Building instructions
Place inner walls on floor first (if any). Then add the outer walls. Glue the two walls without finger joins to the inside of the side walls. Make sure there is no squeeze out on top, as this is going to form the rail for the lid.
Add the top of the rails to the sides and the grip rail to the lid.
Details of the lid and rails
![Details](static/samples/CardBox-detail.jpg)
Whole box (early version still missing grip rail on the lid):
"""
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser(h=30)
self.argparser.add_argument(
"--cardwidth", action="store", type=float, default=65,
help="Width of the cards")
self.argparser.add_argument(
"--cardheight", action="store", type=float, default=90,
help="Height of the cards")
self.argparser.add_argument(
"--num", action="store", type=int, default=2,
help="number of compartments")
@property
def boxwidth(self):
return self.num * (self.cardwidth + self.thickness) + self.thickness
def divider_bottom(self):
t = self.thickness
c = self.cardwidth
y = self.cardheight
for i in range(1, self.num):
self.fingerHolesAt(0.5*t + (c+t)*i, 0, y, 90)
def divider_back_and_front(self):
t = self.thickness
c = self.cardwidth
y = self.h
for i in range(1, self.num):
self.fingerHolesAt(0.5*t + (c+t)*i, 0, y, 90)
def render(self):
h = self.h
t = self.thickness
x = self.boxwidth
y = self.cardheight
s = InsetEdgeSettings(thickness=t)
p = InsetEdge(self, s)
p.char = "a"
self.addPart(p)
s = FingerHoleEdgeSettings(thickness=t, wallheight=h)
p = FingerHoleEdge(self, s)
p.char = "A"
self.addPart(p)
with self.saved_context():
self.rectangularWall(x-t*.2, y, "eeFe", move="right", label="Lid")
self.rectangularWall(x, y, "ffff", callback=[self.divider_bottom],
move="right", label="Bottom")
self.rectangularWall(x, y, "eEEE", move="up only")
self.rectangularWall(x-t*.2, t, "fEeE", move="up", label="Lid Lip")
with self.saved_context():
self.rectangularWall(x, h+t, "FFEF",
callback=[self.divider_back_and_front],
move="right",
label="Back")
self.rectangularWall(x, h+t, "FFaF",
callback=[self.divider_back_and_front],
move="right",
label="Front")
self.rectangularWall(x, h+t, "EEEE", move="up only")
with self.saved_context():
self.rectangularWall(y, h+t, "FfFf", move="right", label="Outer Side Left")
self.rectangularWall(y, h+t, "FfFf", move="right", label="Outer Side Right")
self.rectangularWall(y, h+t, "fFfF", move="up only")
with self.saved_context():
self.rectangularWall(y, h, "Aeee", move="right", label="Inner Side Left")
self.rectangularWall(y, h, "Aeee", move="right", label="Inner Side Right")
self.rectangularWall(y, h, "eAee", move="up only")
with self.saved_context():
self.rectangularWall(y, t, "eefe", move="right", label="Lip Left")
self.rectangularWall(y, t, "feee", move="right", label="Lip Right")
self.rectangularWall(y, t*2, "efee", move="up only")
for i in range(self.num - 1):
self.rectangularWall(h, y, "fAff", move="right", label="Divider")

View File

@ -0,0 +1,112 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2021 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class CardHolder(Boxes):
"""Shelf for holding (multiple) piles of playing cards / notes"""
ui_group = "Shelf"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.GroovedSettings)
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=1.0)
self.buildArgParser(sx="68*3", y=100, h=40, outside=False)
self.argparser.add_argument(
"--angle", action="store", type=float, default=7.5,
help="backward angle of floor")
self.argparser.add_argument(
"--stackable", action="store", type=boolarg, default=True,
help="make holders stackable")
def side(self):
t = self.thickness
a = math.radians(self.angle)
pos_y = self.y - abs(0.5 * t * math.sin(a))
pos_h = t - math.cos(a) * 0.5 * t
self.fingerHolesAt(pos_y, pos_h, self.y, 180-self.angle)
def fingerHoleCB(self, length, posy=0.0):
def CB():
t = self.thickness
px = -0.5 * t
for x in self.sx[:-1]:
px += x + t
self.fingerHolesAt(px, posy, length, 90)
return CB
def middleWall(self, move=None):
y, h = self.y , self.h
a = self.angle
t = self.thickness
tw = y + t
th = h
if self.move(tw, th, move, True):
return
self.moveTo(t, t, a)
self.edges["f"](y)
self.polyline(0, 90-a, h-t-y*math.sin(math.radians(a)), 90,
y*math.cos(math.radians(a)), 90)
self.edges["f"](h-t)
self.move(tw, th, move)
def render(self):
sx, y = self.sx, self.y
t = self.thickness
bottom = "š" if self.stackable else "e"
top = "S" if self.stackable else "e"
if self.outside:
self.sx = sx = self.adjustSize(sx)
h = self.h = self.adjustSize(self.h, bottom, top)
else:
h = self.h = self.h + t + y * math.sin(math.radians(self.angle))
self.x = x = sum(sx) + t * (len(sx) - 1)
self.rectangularWall(y, h, [bottom, "F", top, "e"],
ignore_widths=[1, 6],
callback=[self.side], move="up")
self.rectangularWall(y, h, [bottom, "F", top, "e"],
ignore_widths=[1, 6],
callback=[self.side], move="up mirror")
nx = len(sx)
f_lengths = []
for val in self.sx:
f_lengths.append(val)
f_lengths.append(t)
f_lengths = f_lengths[:-1]
frontedge = edges.CompoundEdge(
self, "e".join("z" * nx), f_lengths)
self.rectangularWall(x, y, [frontedge, "f", "e", "f"],
callback=[self.fingerHoleCB(y)], move="up")
self.rectangularWall(x, h, bottom + "f" + top + "f",
ignore_widths=[1, 6],
callback=[self.fingerHoleCB(h-t, t)], move="up")
for i in range(nx-1):
self.middleWall(move="right")

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class Castle(Boxes):
"Castle tower with two walls"
description = """This was done as a table decoration. May be at some point in the future someone will create a proper castle
with towers and gates and walls that can be attached in multiple configurations."""
ui_group = "Unstable"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
def render(self, t_x=70, t_h=250, w1_x=300, w1_h=120, w2_x=100, w2_h=120):
s = edges.FingerJointSettings(10.0, relative=True,
space=1, finger=1,
width=self.thickness)
s.edgeObjects(self, "pPQ")
self.moveTo(0, 0)
self.rectangularWall(t_x, t_h, edges="efPf", move="right", callback=[lambda: self.fingerHolesAt(t_x * 0.5, 0, w1_h, 90), ])
self.rectangularWall(t_x, t_h, edges="efPf", move="right")
self.rectangularWall(t_x, t_h, edges="eFPF", move="right", callback=[lambda: self.fingerHolesAt(t_x * 0.5, 0, w2_h, 90), ])
self.rectangularWall(t_x, t_h, edges="eFPF", move="right")
self.rectangularWall(w1_x, w1_h, "efpe", move="right")
self.rectangularWall(w2_x, w2_h, "efpe", move="right")

View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class ClosedBox(Boxes):
"""Fully closed box"""
ui_group = "Box"
description = """This box is more of a building block than a finished item.
Use a vector graphics program (like Inkscape) to add holes or adjust the base
plate.
See BasedBox for variant with a base."""
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser("x", "y", "h", "outside")
def render(self):
x, y, h = self.x, self.y, self.h
if self.outside:
x = self.adjustSize(x)
y = self.adjustSize(y)
h = self.adjustSize(h)
t = self.thickness
d2 = edges.Bolts(2)
d3 = edges.Bolts(3)
d2 = d3 = None
self.rectangularWall(x, h, "FFFF", bedBolts=[d2] * 4, move="right", label="Wall 1")
self.rectangularWall(y, h, "FfFf", bedBolts=[d3, d2, d3, d2], move="up", label="Wall 2")
self.rectangularWall(y, h, "FfFf", bedBolts=[d3, d2, d3, d2], label="Wall 4")
self.rectangularWall(x, h, "FFFF", bedBolts=[d2] *4, move="left up", label="Wall 3")
self.rectangularWall(x, y, "ffff", bedBolts=[d2, d3, d2, d3], move="right", label="Top")
self.rectangularWall(x, y, "ffff", bedBolts=[d2, d3, d2, d3], label="Bottom")

View File

@ -0,0 +1,133 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2021 Guillaume Collic
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import math
from boxes import Boxes, boolarg
class CoffeeCapsuleHolder(Boxes):
"""
Coffee capsule holder
"""
ui_group = "Misc"
description = """
You can store your coffee capsule near your expresso machine with this. It works both vertically, or upside down under a shelf.
"""
def __init__(self):
Boxes.__init__(self)
self.argparser.add_argument(
"--columns",
type=int,
default=4,
help="Number of columns of capsules.",
)
self.argparser.add_argument(
"--rows",
type=int,
default=5,
help="Number of capsules by columns.",
)
self.argparser.add_argument(
"--backplate",
type=boolarg,
default=True,
help="True if a backplate should be generated.",
)
def render(self):
self.lid_size = 37
self.lid_size_with_margin = 39
self.body_size = 30
self.column_spacing = 5
self.corner_radius = 3
self.screw_margin = 6
self.outer_margin = 7
# Add space for the opening. A full row is not necessary for it.
self.rows = self.rows + 0.6
self.render_plate(screw_hole=7, hole_renderer=self.render_front_hole)
self.render_plate(hole_renderer=self.render_middle_hole)
if self.backplate:
self.render_plate()
def render_plate(self, screw_hole=3.5, hole_renderer=None, move="right"):
width = (
self.columns * (self.lid_size_with_margin + self.column_spacing)
- self.column_spacing
+ 2 * self.outer_margin
)
height = self.rows * self.lid_size + 2 * self.outer_margin
if self.move(width, height, move, True):
return
with self.saved_context():
self.moveTo(self.corner_radius)
self.polyline(
width - 2 * self.corner_radius,
(90, self.corner_radius),
height - 2 * self.corner_radius,
(90, self.corner_radius),
width - 2 * self.corner_radius,
(90, self.corner_radius),
height - 2 * self.corner_radius,
(90, self.corner_radius),
)
if hole_renderer:
for col in range(self.columns):
with self.saved_context():
self.moveTo(
self.outer_margin + col * (self.lid_size_with_margin + self.column_spacing) - self.burn,
self.outer_margin + (self.rows - 0.5) * self.lid_size + self.burn,
-90,
)
hole_renderer()
if screw_hole:
for x in [self.screw_margin, width - self.screw_margin]:
for y in [self.screw_margin, height - self.screw_margin]:
self.hole(x, y + self.burn, d=screw_hole)
self.move(width, height, move)
def render_front_hole(self):
radians = math.acos(self.body_size / self.lid_size_with_margin)
height_difference = (self.lid_size / 2) * math.sin(radians)
degrees = math.degrees(radians)
half = [
0,
(degrees, self.lid_size_with_margin / 2),
0,
-degrees,
(self.rows - 1) * self.lid_size - height_difference,
]
path = (
half
+ [(180, self.body_size / 2)]
+ list(reversed(half))
+ [(180, self.lid_size_with_margin / 2)]
)
self.polyline(*path)
def render_middle_hole(self):
half = [(self.rows - 1) * self.lid_size, (180, self.lid_size_with_margin / 2)]
path = half * 2
self.polyline(*path)

View File

@ -0,0 +1,115 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class CoinHolderSideEdge(edges.BaseEdge):
char = "B"
def __call__(self, length, **kw):
a_l = self.coin_plate
a_l2 = self.settings.coin_plate * math.sin(self.settings.angle)
a = math.degrees(self.settings.angle)
print(a, a_l, a_l2)
self.corner(-a)
# Draw the angled edge, but set the thickness to two temporarily
# as two pieces will go on top of another
self.edges["F"].settings.thickness = self.thickness * 2
self.edges["F"](a_l)
self.edges["F"].settings.thickness = self.thickness
self.polyline(0, 90+a, a_l2, -90)
def margin(self):
return self.settings.coin_plate_x
class CoinDisplay(Boxes):
"""A showcase for a single coin"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser("x", "y", "h", "outside")
self.argparser.add_argument(
"--coin_d", action="store", type=float, default=20.0,
help="The diameter of the coin in mm")
self.argparser.add_argument(
"--coin_plate", action="store", type=float, default=50.0,
help="The size of the coin plate")
self.argparser.add_argument(
"--coin_showcase_h", action="store", type=float, default=50.0,
help="The height of the coin showcase piece")
self.argparser.add_argument(
"--angle", action="store", type=float, default=30,
help="The angle that the coin will tilt as")
def bottomHoles(self):
"""
Function that puts two finger holes at the bottom cube plate for the coin holder
"""
self.fingerHolesAt(self.x/2 - self.thickness - self.thickness/2 - (self.coin_plate/2), self.y/2+self.coin_plate_x/2-self.thickness, self.coin_plate_x, -90)
self.fingerHolesAt(self.x/2 - self.thickness + self.thickness/2 + (self.coin_plate/2), self.y/2+self.coin_plate_x/2-self.thickness, self.coin_plate_x, -90)
self.fingerHolesAt(self.x/2-self.coin_plate/2-self.thickness, self.y/2-self.coin_plate_x/2-self.thickness*1.5, self.coin_plate, 0)
def coinCutout(self):
"""
Function that puts a circular hole in the coin holder piece
"""
self.hole(self.coin_plate/2, self.coin_plate/2, self.coin_d/2)
def render(self):
x, y, h = self.x, self.y, self.h
if self.outside:
x = self.adjustSize(x)
y = self.adjustSize(y)
h = self.adjustSize(h)
t = self.thickness
d2 = edges.Bolts(2)
d3 = edges.Bolts(3)
d2 = d3 = None
self.addPart(CoinHolderSideEdge(self, self))
self.angle = math.radians(self.angle)
self.coin_plate_x = self.coin_plate * math.cos(self.angle)
self.rectangularWall(x, h, "FFFF", bedBolts=[d2] * 4, move="right", label="Wall 1")
self.rectangularWall(y, h, "FfFf", bedBolts=[d3, d2, d3, d2], move="up", label="Wall 2")
self.rectangularWall(y, h, "FfFf", bedBolts=[d3, d2, d3, d2], label="Wall 4")
self.rectangularWall(x, h, "FFFF", bedBolts=[d2] *4, move="left up", label="Wall 3")
self.rectangularWall(x, y, "ffff", bedBolts=[d2, d3, d2, d3], move="right", label="Top")
self.rectangularWall(x, y, "ffff", bedBolts=[d2, d3, d2, d3], move="right", label="Bottom", callback=[self.bottomHoles])
# Draw the coin holder side holsers
e = ["f", "f", "B", "e"]
self.rectangularWall(self.coin_plate_x, self.coin_showcase_h, e, move="right", label="CoinSide1")
self.rectangularWall(self.coin_plate_x, self.coin_showcase_h, e, move="right", label="CoinSide2")
self.rectangularWall(self.coin_plate, self.coin_plate, "efef", move="left down", label="Coin Plate Base")
self.rectangularWall(self.coin_plate, self.coin_plate, "efef", move="down", label="Coin Plate", callback=[self.coinCutout])
self.rectangularWall(self.coin_plate, self.coin_showcase_h, "fFeF", move="down", label="CoinSide3")

View File

@ -0,0 +1,65 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2017 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class ConcaveKnob(Boxes):
"""Round knob serrated outside for better gripping"""
ui_group = "Part"
def __init__(self):
Boxes.__init__(self)
# Add non default cli params if needed (see argparse std lib)
self.argparser.add_argument(
"--diameter", action="store", type=float, default=50.,
help="Diameter of the knob (mm)")
self.argparser.add_argument(
"--serrations", action="store", type=int, default=3,
help="Number of serrations")
self.argparser.add_argument(
"--rounded", action="store", type=float, default=.2,
help="Amount of circumference used for non convex parts")
self.argparser.add_argument(
"--angle", action="store", type=float, default=70.,
help="Angle between convex and concave parts")
self.argparser.add_argument(
"--bolthole", action="store", type=float, default=6.,
help="Diameter of the bolt hole (mm)")
self.argparser.add_argument(
"--dhole", action="store", type=float, default=1.,
help="D-Flat in fraction of the diameter")
self.argparser.add_argument(
"--hexhead", action="store", type=float, default=10.,
help="Width of the hex bolt head (mm)")
def render(self):
t = self.thickness
self.parts.concaveKnob(self.diameter, self.serrations,
self.rounded, self.angle,
callback=lambda:self.dHole(0, 0,
d=self.bolthole,
rel_w=self.dhole),
move="right")
self.parts.concaveKnob(self.diameter, self.serrations,
self.rounded, self.angle,
callback=lambda: self.nutHole(self.hexhead),
move="right")
self.parts.concaveKnob(self.diameter, self.serrations,
self.rounded, self.angle)

View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class Console(Boxes):
"""Console with slanted panel"""
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=.5)
self.addSettingsArgs(edges.StackableSettings)
self.buildArgParser(x=100, y=100, h=100, outside=False)
self.argparser.add_argument(
"--front_height", action="store", type=float, default=30,
help="height of the front below the panel (in mm)")
self.argparser.add_argument(
"--angle", action="store", type=float, default=50,
help="angle of the front panel (90°=upright)")
def render(self):
x, y, h, hf = self.x, self.y, self.h, self.front_height
t = self.thickness
if self.outside:
self.x = x = self.adjustSize(x)
self.y = y = self.adjustSize(y)
self.h = h = self.adjustSize(h)
panel = min((h-hf)/math.cos(math.radians(90-self.angle)),
y/math.cos(math.radians(self.angle)))
top = y - panel * math.cos(math.radians(self.angle))
h = hf + panel * math.sin(math.radians(self.angle))
if top>0.1*t:
borders = [y, 90, hf, 90-self.angle, panel, self.angle, top,
90, h, 90]
else:
borders = [y, 90, hf, 90-self.angle, panel, self.angle+90, h, 90]
if hf < 0.01*t:
borders[1:4] = [180-self.angle]
self.polygonWall(borders, move="right")
self.polygonWall(borders, move="right")
self.polygonWalls(borders, x)

View File

@ -0,0 +1,302 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2020 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class Console2(Boxes):
"""Console with slanted panel and service hatches"""
ui_group = "Box"
description = """
This box is designed as a housing for electronic projects. It has hatches that can be re-opened with simple tools. It intentionally cannot be opened with bare hands - if build with thin enough material.
#### Caution
There is a chance that the latches of the back wall or the back wall itself interfere with the front panel or it's mounting frame/lips. The generator does not check for this. So depending on the variant choosen you might need to make the box deeper (increase y parameter) or the panel angle steeper (increase angle parameter) until there is enough room.
It's also possible that the frame of the panel interferes with the floor if the hi parameter is too small.
#### Assembly instructions
The main body is easy to assemble by starting with the floor and then adding the four walls and (if present) the top piece.
If the back wall is removable you need to add the lips and latches. The U-shaped clamps holding the latches in place need to be clued in place without also gluing the latches themselves. Make sure the springs on the latches point inwards and the angled ends point to the side walls as shown here:
![Back wall details](static/samples/Console2-backwall-detail.jpg)
If the panel is removable you need to add the springs with the tabs to the side lips. This photo shows the variant which has the panel glued to the frame:
![Back wall details](static/samples/Console2-panel-detail.jpg)
If space is tight you may consider not glueing the cross pieces in place and remove them after the glue-up. This may prevent the latches of the back wall and the panel from interfereing with each other.
The variant using finger joints only has the two side lips without the cross bars.
#### Re-Opening
The latches at the back wall lock in place when closed. To open them they need to be pressed in and can then be moved aside.
To remove the panel you have to press in the four tabs at the side. It is easiest to push them in and then pull the panel up a little bit so the tabs stay in.
"""
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=.5)
self.addSettingsArgs(edges.StackableSettings)
self.buildArgParser(x=100, y=100, h=100, bottom_edge="s",
outside=False)
self.argparser.add_argument(
"--front_height", action="store", type=float, default=30,
help="height of the front below the panel (in mm)")
self.argparser.add_argument(
"--angle", action="store", type=float, default=50,
help="angle of the front panel (90°=upright)")
self.argparser.add_argument(
"--removable_backwall", action="store", type=boolarg, default=True,
help="have latches at the backwall")
self.argparser.add_argument(
"--removable_panel", action="store", type=boolarg, default=True,
help="The panel is held by tabs and can be removed")
self.argparser.add_argument(
"--glued_panel", action="store", type=boolarg, default=True,
help="the panel is glued and not held by finger joints")
def borders(self):
x, y, h, fh = self.x, self.y, self.h, self.front_height
t = self.thickness
panel = min((h-fh)/math.cos(math.radians(90-self.angle)),
y/math.cos(math.radians(self.angle)))
top = y - panel * math.cos(math.radians(self.angle))
h = fh + panel * math.sin(math.radians(self.angle))
if top>0.1*t:
borders = [y, 90, fh, 90-self.angle, panel, self.angle, top,
90, h, 90]
else:
borders = [y, 90, fh, 90-self.angle, panel, self.angle+90, h, 90]
return borders
def latch(self, move=None):
t = self.thickness
s = 0.1 * t
tw, th = 8*t, 3*t
if self.move(tw, th, move, True):
return
self.moveTo(0, 1.2*t)
self.polyline(t, -90, .2*t, 90, 2*t, -90, t, 90, t, 90, t, -90, 3*t,
90, t, -90, t, 90, t, 90, 2*t, 90, 0.5*t,
-94, 4.9*t, 94, .5*t, 86, 4.9*t, -176, 5*t,
-90, 1.0*t, 90, t, 90, 1.8*t, 90)
self.move(tw, th, move)
def latch_clamp(self, move=None):
t = self.thickness
s = 0.1 * t
tw, th = 4*t, 4*t
if self.move(tw, th, move, True):
return
self.moveTo(0.5*t)
self.polyline(t-0.5*s, 90, 2.5*t+.5*s, -90, t+s, -90, 2.5*t+.5*s, 90, t-0.5*s, 90,
t, -90, 0.5*t, 90, 2*t, 45, 2**.5*t, 45, 2*t, 45, 2**.5*t, 45, 2*t, 90, 0.5*t, -90, t, 90)
self.move(tw, th, move)
@restore
@holeCol
def latch_hole(self, posx):
t = self.thickness
s = 0.1 * t
self.moveTo(posx, 2*t, 180)
path = [1.5*t, -90, t, -90, t-0.5*s, 90]
path = path + [2*t] + list(reversed(path))
path = path[:-1] + [3*t] + list(reversed(path[:-1]))
self.polyline(*path)
def panel_side(self, l, move=None):
t = self.thickness
s = 0.1 * t
tw, th = l, 3*t
if not self.glued_panel:
th += t
if self.move(tw, th, move, True):
return
self.rectangularHole(3*t, 1.5*t, 3*t, 1.05*t)
self.rectangularHole(l-3*t, 1.5*t, 3*t, 1.05*t)
self.rectangularHole(l/2, 1.5*t, 2*t, t)
if self.glued_panel:
self.polyline(*([l, 90, t, 90, t, -90, t, -90, t, 90, t, 90]*2))
else:
self.polyline(l, 90, 3*t, 90)
self.edges["f"](l)
self.polyline(0, 90, 3*t, 90)
self.move(tw, th, move)
def panel_lock(self, l, move=None):
t = self.thickness
l -= 4*t
tw, th = l, 2.5*t
if self.move(tw, th, move, True):
return
end = [l/2-3*t, -90, 1.5*t, (90, .5*t), t, (90, .5*t),
t, 90, .5*t, -90, 0.5*t, -90, 0, (90, .5*t), 0, 90,]
self.moveTo(l/2-t, 2*t, -90)
self.polyline(*([t, 90, 2*t, 90, t, -90] + end + [l] +
list(reversed(end))))
self.move(tw, th, move)
def panel_cross_beam(self, l, move=None):
t = self.thickness
tw, th = l+2*t, 3*t
if self.move(tw, th, move, True):
return
self.moveTo(t, 0)
self.polyline(*([l, 90, t, -90, t, 90, t, 90, t, -90, t, 90]*2))
self.move(tw, th, move)
def side(self, borders, bottom="s", move=None, label=""):
t = self.thickness
bottom = self.edges.get(bottom, bottom)
tw = borders[0] + 2* self.edges["f"].spacing()
th = borders[-2] + bottom.spacing() + self.edges["f"].spacing()
if self.move(tw, th, move, True):
return
d1 = t * math.cos(math.radians(self.angle))
d2 = t * math.sin(math.radians(self.angle))
self.moveTo(t, 0)
bottom(borders[0])
self.corner(90)
self.edges["f"](borders[2]+bottom.endwidth()-d1)
self.edge(d1)
self.corner(borders[3])
if self.removable_panel:
self.rectangularHole(3*t, 1.5*t, 2.5*t, 1.05*t)
if not self.removable_panel and not self.glued_panel:
self.edges["f"](borders[4])
else:
self.edge(borders[4])
if self.removable_panel:
self.rectangularHole(-3*t, 1.5*t, 2.5*t, 1.05*t)
if len(borders) == 10:
self.corner(borders[5])
self.edge(d2)
self.edges["f"](borders[6]-d2)
self.corner(borders[-3])
if self.removable_backwall:
self.rectangularHole(self.latchpos, 1.55*t, 1.1*t, 1.1*t)
self.edge(borders[-2]-t)
self.edges["f"](t+bottom.startwidth())
else:
self.edges["f"](borders[-2]+bottom.startwidth())
self.corner(borders[-1])
self.move(tw, th, move, label=label)
def render(self):
x, y, h = self.x, self.y, self.h
t = self.thickness
bottom = self.edges.get(self.bottom_edge)
if self.outside:
self.x = x = self.adjustSize(x)
self.y = y = self.adjustSize(y)
self.h = h = self.adjustSize(h, bottom)
d1 = t * math.cos(math.radians(self.angle))
d2 = t * math.sin(math.radians(self.angle))
self.latchpos = latchpos = 6*t
borders = self.borders()
self.side(borders, bottom, move="right", label="Left Side")
self.side(borders, bottom, move="right", label="Right Side")
self.rectangularWall(borders[0], x, "ffff", move="right", label="Floor")
self.rectangularWall(
borders[2]-d1, x, ("F", "e", "F", bottom), ignore_widths=[7, 4],
move="right", label="Front")
if self.glued_panel:
self.rectangularWall(borders[4], x, "EEEE", move="right", label="Panel")
elif self.removable_panel:
self.rectangularWall(borders[4], x-2*t, "hEhE", move="right", label="Panel")
else:
self.rectangularWall(borders[4], x, "FEFE", move="right", label="Panel")
if len(borders) == 10:
self.rectangularWall(borders[6]-d2, x, "FEFe", move="right", label="Top")
if self.removable_backwall:
self.rectangularWall(
borders[-2]-1.05*t, x, "EeEe",
callback=[
lambda:self.latch_hole(latchpos),
lambda: self.fingerHolesAt(.5*t, 0, borders[-2]-4.05*t-latchpos),
lambda:self.latch_hole(borders[-2]-1.2*t-latchpos),
lambda: self.fingerHolesAt(.5*t, 3.05*t+latchpos, borders[-2]-4.05*t-latchpos)],
move="right",
label="Back Wall")
self.rectangularWall(2*t, borders[-2]-4.05*t-latchpos, "EeEf", move="right", label="Guide")
self.rectangularWall(2*t, borders[-2]-4.05*t-latchpos, "EeEf", move="right", label="Guide")
self.rectangularWall(t, x, ("F", bottom, "F", "e"),
ignore_widths=[0, 3], move="right", label="Bottom Back")
else:
self.rectangularWall(borders[-2], x, ("F", bottom, "F", "e"),
ignore_widths=[0, 3], move="right", label="Back Wall")
# hardware for panel
if self.removable_panel:
if self.glued_panel:
self.panel_cross_beam(x-2.05*t, "rotated right")
self.panel_cross_beam(x-2.05*t, "rotated right")
self.panel_lock(borders[4], "up")
self.panel_lock(borders[4], "up")
self.panel_side(borders[4], "up")
self.panel_side(borders[4], "up")
# hardware for back wall
if self.removable_backwall:
self.latch(move="up")
self.latch(move="up")
self.partsMatrix(4, 2, "up", self.latch_clamp)

View File

@ -0,0 +1,124 @@
#!/usr/bin/env python3
# Copyright (C) 2022 Erik Snider (SniderThanYou@gmail.com)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class DiceBox(Boxes):
"""Box with lid and integraded hinge for storing dice"""
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(
edges.FingerJointSettings,
surroundingspaces=2.0)
self.addSettingsArgs(
edges.ChestHingeSettings,
finger_joints_on_box=True,
finger_joints_on_lid=True)
self.buildArgParser(
x=100,
y=100,
h=18,
outside=True)
self.argparser.add_argument(
"--lidheight", action="store", type=float, default=18,
help="height of lid in mm")
self.argparser.add_argument(
"--hex_hole_corner_radius", action="store", type=float, default=5,
help="The corner radius of the hexagonal dice holes, in mm")
self.argparser.add_argument(
"--magnet_diameter", action="store", type=float, default=6,
help="The diameter of magnets for holding the box closed, in mm")
def diceCB(self):
t = self.thickness
xi = self.x - 2 * t
yi = self.y - 2 * t
xc = xi / 2
yc = yi / 2
cr = self.hex_hole_corner_radius
# -4*t because there are four gaps across:
# 2 between the outer holes and the finger joints
# 2 between the outer holes and the center hole
# /6 because there are 6 apothems across, 2 for each hexagon
apothem = (min(xi, yi) - 4 * t) / 6
r = apothem * 2 / math.sqrt(3)
# dice
centers = [[xc, yc]] # start with one in the center
polar_r = 2 * apothem + t # the full width of a hexagon, plus a gap of t width
for i in range(6):
theta = i * math.pi / 3 # 60 degrees each step
centers.append(
[
xc + polar_r * math.cos(theta),
yc + polar_r * math.sin(theta),
]
)
for center in centers:
self.regularPolygonHole(x=center[0], y=center[1], n=6, r=r, corner_radius=cr, a=30)
# magnets
d = self.magnet_diameter
mo = t + d/2
self.hole(mo, mo, d=d)
self.hole(xi-mo, mo, d=d)
def render(self):
x, y, h, hl = self.x, self.y, self.h, self.lidheight
if self.outside:
x = self.adjustSize(x)
y = self.adjustSize(y)
h = self.adjustSize(h)
hl = self.adjustSize(hl)
t = self.thickness
hy = self.edges["O"].startwidth()
hy2 = self.edges["P"].startwidth()
e1 = edges.CompoundEdge(self, "eF", (hy-t, h-hy+t))
e2 = edges.CompoundEdge(self, "Fe", (h-hy+t, hy-t))
e_back = ("F", e1, "F", e2)
p = self.edges["o"].settings.pin_height
e_inner_1 = edges.CompoundEdge(self, "fe", (y-p, p))
e_inner_2 = edges.CompoundEdge(self, "ef", (p, y-p))
e_inner_topbot = ("f", e_inner_1, "f", e_inner_2)
self.ctx.save()
self.rectangularWall(x, y, e_inner_topbot, move="up", callback=[self.diceCB])
self.rectangularWall(x, y, e_inner_topbot, move="up", callback=[self.diceCB])
self.rectangularWall(x, h, "FFFF", ignore_widths=[1,2,5,6], move="up")
self.rectangularWall(x, h, e_back, move="up")
self.rectangularWall(x, hl, "FFFF", ignore_widths=[1,2,5,6], move="up")
self.rectangularWall(x, hl-hy2+t, "FFqF", move="up")
self.ctx.restore()
self.rectangularWall(x, y, "ffff", move="right only")
self.rectangularWall(y, x, "ffff", move="up")
self.rectangularWall(y, x, "ffff", move="up")
self.rectangularWall(y, hl-hy2+t, "Ffpf", ignore_widths=[5,6], move="up")
self.rectangularWall(y, h-hy+t, "OfFf", ignore_widths=[5,6], move="up")
self.rectangularWall(y, h-hy+t, "Ffof", ignore_widths=[5,6], move="up")
self.rectangularWall(y, hl-hy2+t, "PfFf", ignore_widths=[5,6], move="up")

View File

@ -0,0 +1,149 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2020 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class DinRailEdge(edges.FingerHoleEdge):
def __init__(self, boxes, settings, width=35.0, offset=0.0):
super().__init__(boxes, settings)
self.width = width
self.offset = offset
def startwidth(self):
return 8 + self.settings.thickness
def __call__(self, length, bedBolts=None, bedBoltSettings=None, **kw):
with self.saved_context():
self.fingerHoles(
0, self.burn + 8 + self.settings.thickness / 2, length, 0,
bedBolts=bedBolts, bedBoltSettings=bedBoltSettings)
w = self.width
o = self.offset
l = length
self.polyline((l-w)/2-o, 45, 2.75*2**.5, 90, 2.75*2**.5, -45, .5, -90,
w+0.25,
-90, 1, 30, 5*2*3**-.5, 60, (l-w)/2+o-3.25)
class DinRailBox(Boxes):
"""Box for DIN rail used in electrical junction boxes"""
ui_group = "WallMounted"
def latch(self, l, move=None):
t = self.thickness
tw = l+3+6+t
th = 8
if self.move(tw, th, move, True):
return
self.moveTo(tw, th, 180)
self.polyline(2, 90, 0, (-180, 1.5), 0, 90, l+1.2*t, 90,
3, -90, 1, 30, 2*2*3**-.5, 90, 4.5*2*3**-.5, 60,
4+1.25, 90, 4.5, -90, t+4, -90, 2, 90, l-.8*t-9, 90, 2, -90, 5+t, 90, 4, 90)
self.move(tw, th, move)
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=.8)
self.buildArgParser(x=70, y=90, h=60)
# Add non default cli params if needed (see argparse std lib)
self.argparser.add_argument(
"--rail_width", action="store", type=float, default=35.,
help="width of the rail (typically 35 or 15mm)")
self.argparser.add_argument(
"--rail_offset", action="store", type=float, default=0.,
help="offset of the rail from the middle of the box (in mm)")
def spring(self):
t = self.thickness
l = min(self.x/2-1.5*t, 50)
self.moveTo(self.x/2-l, -6-t, 0)
self.polyline(l+0.525*t, 90 , 6, 90 , 1.1*t, 90, 3, -90, l-0.525*t,
180, l-0.525*t, -90, 1+0.1*t, 90, t-0.5, -90, 2)
def lid_lip(self, l, move=None):
t = self.thickness
tw, th = l+2, t+8
if self.move(tw, th, move, True):
return
self.moveTo(1, t)
self.edges["f"](l)
poly = [0, 90, 6, -60, 0, (120, 2*3**-.5), 0, 30, 2, 90, 5,
(-180, .5), 5, 90]
self.polyline(*(poly+[l-2*3]+list(reversed(poly))))
self.move(tw, th, move)
def lid_holes(self):
t = self.thickness
self.rectangularHole(0.55*t, 7, 1.1*t, 1.6)
self.rectangularHole(self.x-0.55*t, 7, 1.1*t, 1.6)
def render(self):
# adjust to the variables you want in the local scope
x, y, h = self.x, self.y, self.h
w = self.rail_width
o = self.rail_offset
t = self.thickness
self.rectangularWall(x, y, "EEEE", callback=[
lambda:self.fingerHolesAt(.55*t, .05*t, y-.1*t, 90), None,
lambda:self.fingerHolesAt(.55*t, .05*t, y-.1*t, 90), None],
move="right", label="Lid")
self.lid_lip(y-.1*t, move="rotated right")
self.lid_lip(y-.1*t, move="rotated right")
self.rectangularWall(x, y, "ffff",
callback=[
lambda:self.fingerHolesAt(0, (y-w)/2-0.5*t+o-9, x, 0)],
move="right", label="Back")
# Change h edge to 8mm!
self.edges["f"].settings.setValues(t, False, edge_width=8)
dr = DinRailEdge(self, self.edges["f"].settings, w, o)
self.rectangularWall(y, h, [dr, "F", "e", "F"],
ignore_widths=[1, 6], move="rotated right",
label="Left Side upsidedown")
self.rectangularWall(y, h, [dr, "F", "e", "F"],
ignore_widths=[1, 6], move="rotated mirror right",
label="Right Side")
self.rectangularWall(x, h, ["h", "f", "e", "f"],
ignore_widths=[1, 6], callback=[
self.spring, None, self.lid_holes],
move="up",
label="Bottom")
self.rectangularWall(x, h, ["h", "f", "e", "f"],
callback=[None, None, self.lid_holes],
ignore_widths=[1, 6], move="up",
label="Top")
self.rectangularWall(x, 8, "feee", callback=[
lambda:self.rectangularHole(x/2, 2.05-0.5*t, t, t+4.1)], move="up")
self.latch((y-w)/2+o, move="up")

View File

@ -0,0 +1,277 @@
#!/usr/bin/env python3
# coding: utf-8
# Copyright (C) 2019 chrysn <chrysn@fsfe.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, unicode_literals
from boxes import *
from math import sqrt, pi, sin, cos
def offset_radius_in_square(squareside, angle, outset):
"""From the centre of a square, rotate by an angle relative to the
vertical, move away from the center (down if angle = 0), and then in a
right angle until the border of the square. Return the length of that last
segment.
Note that for consistency with other boxes.py methods, angle is given in
degree.
>>> # Without rotation, it's always half the square length
>>> offset_radius_in_square(20, 0, 0)
10.0
>>> offset_radius_in_square(20, 0, 5)
10.0
>>> # Without offset, it's half squre length divided by cos(angle) -- at
>>> # least before it hits the next wall
>>> offset_radius_in_square(20, 15, 0) # doctest:+ELLIPSIS
10.35276...
>>> offset_radius_in_square(20, 45, 0) # doctest:+ELLIPSIS
14.1421...
>>> # Positive angles make the segment initially shorter...
>>> offset_radius_in_square(20, 5, 10) < 10
True
>>> # ... while negative angles make it longer.
>>> offset_radius_in_square(20, -5, 10) > 10
True
"""
if angle <= -90:
return offset_radius_in_square(squareside, angle + 180, outset)
if angle > 90:
return offset_radius_in_square(squareside, angle - 180, outset)
angle = angle / 180 * pi
step_right = outset * sin(angle)
step_down = outset * cos(angle)
try:
len_right = (squareside / 2 - step_right) / cos(angle)
except ZeroDivisionError:
return squareside / 2
if angle == 0:
return len_right
if angle > 0:
len_up = (squareside / 2 + step_down) / sin(angle)
return min(len_up, len_right)
else: # angle < 0
len_down = - (squareside / 2 - step_down) / sin(angle)
return min(len_down, len_right)
class DiscRack(Boxes):
"""A rack for storing disk-shaped objects vertically next to each other"""
ui_group = "Shelf"
def __init__(self):
Boxes.__init__(self)
self.buildArgParser(sx="20*10")
self.argparser.add_argument(
"--disc_diameter", action="store", type=float, default=150.0,
help="Disc diameter in mm")
self.argparser.add_argument(
"--disc_thickness", action="store", type=float, default=5.0,
help="Thickness of the discs in mm")
self.argparser.add_argument(
"--lower_factor", action="store", type=float, default=0.75,
help="Position of the lower rack grids along the radius")
self.argparser.add_argument(
"--rear_factor", action="store", type=float, default=0.75,
help="Position of the rear rack grids along the radius")
self.argparser.add_argument(
"--disc_outset", action="store", type=float, default=3.0,
help="Additional space kept between the disks and the outbox of the rack")
# These can be parameterized, but the default value of pulling them up
# to the box front is good enough for so many cases it'd only clutter
# the user interface.
#
# The parameters can be resurfaced when there is something like rare or
# advanced settings.
'''
self.argparser.add_argument(
"--lower_outset", action="store", type=float, default=0.0,
help="Space in front of the disk slits (0: automatic)")
self.argparser.add_argument(
"--rear_outset", action="store", type=float, default=0.0,
help="Space above the disk slits (0: automatic)")
'''
self.argparser.add_argument(
"--angle", action="store", type=float, default=18,
help="Backwards slant of the rack")
self.addSettingsArgs(edges.FingerJointSettings)
def parseArgs(self, *args, **kwargs):
Boxes.parseArgs(self, *args, **kwargs)
self.lower_outset = self.rear_outset = 0
self.calculate()
def calculate(self):
self.outer = self.disc_diameter + 2 * self.disc_outset
r = self.disc_diameter / 2
# distance between radius line and front (or rear) end of the slit
self.lower_halfslit = r * sqrt(1 - self.lower_factor**2)
self.rear_halfslit = r * sqrt(1 - self.rear_factor**2)
if True: # self.lower_outset == 0: # when lower_outset parameter is re-enabled
toplim = offset_radius_in_square(self.outer, self.angle, r * self.lower_factor)
# With typical positive angles, the lower surface of board will be limiting
bottomlim = offset_radius_in_square(self.outer, self.angle, r * self.lower_factor + self.thickness)
self.lower_outset = min(toplim, bottomlim) - self.lower_halfslit
if True: # self.rear_outset == 0: # when rear_outset parameter is re-enabled
# With typical positive angles, the upper surface of board will be limiting
toplim = offset_radius_in_square(self.outer, -self.angle, r * self.rear_factor)
bottomlim = offset_radius_in_square(self.outer, -self.angle, r * self.rear_factor + self.thickness)
self.rear_outset = min(toplim, bottomlim) - self.rear_halfslit
# front outset, space to radius, space to rear part, plus nothing as fingers extend out
self.lower_size = self.lower_outset + \
self.lower_halfslit + \
r * self.rear_factor
self.rear_size = r * self.lower_factor + \
self.rear_halfslit + \
self.rear_outset
self.warn_on_demand()
def warn_on_demand(self):
warnings = []
# Are the discs supported on the outer ends?
def word_thickness(length):
if length > 0:
return "very thin (%.2g mm at a thickness of %.2g mm)" % (
length, self.thickness)
if length < 0:
return "absent"
if self.rear_outset < self.thickness:
warnings.append("Rear upper constraint is %s. Consider increasing"
" the disc outset parameter, or move the angle away from 45°."
% word_thickness(self.rear_outset)
)
if self.lower_outset < self.thickness:
warnings.append("Lower front constraint is %s. Consider increasing"
" the disc outset parameter, or move the angle away from 45°."
% word_thickness(self.lower_outset))
# Are the discs supported where the grids meet?
r = self.disc_diameter / 2
inner_lowerdistance = r * self.rear_factor - self.lower_halfslit
inner_reardistance = r * self.lower_factor - self.rear_halfslit
if inner_lowerdistance < 0 or inner_reardistance < 0:
warnings.append("Corner is inside the disc radios, discs would not"
" be supported. Consider increasing the factor parameters.")
# Won't the type-H edge on the rear side make the whole contraption
# wiggle?
max_slitlengthplush = offset_radius_in_square(
self.outer, self.angle, r * self.rear_factor + self.thickness)
slitlengthplush = self.rear_halfslit + self.thickness * ( 1 + \
self.edgesettings['FingerJoint']['edge_width'])
if slitlengthplush > max_slitlengthplush:
warnings.append("Joint would protrude from lower box edge. Consider"
" increasing the the disc outset parameter, or move the"
" angle away from 45°.")
# Can the discs be removed at all?
# Does not need explicit checking, for Thales' theorem tells us that at
# the point wher there is barely support in the corner, three contact
# points on the circle form just a demicircle and the discs can be
# inserted/removed. When we keep the other contact points and move the
# slits away from the corner, the disc gets smaller and thus will fit
# through the opening that is as wide as the diameter of the largest
# possible circle.
# Act on warnings
if warnings:
self.argparser.error("\n".join(warnings))
def sidewall_holes(self):
r = self.disc_diameter / 2
self.moveTo(self.outer/2, self.outer/2, -self.angle)
# can now move down to paint horizontal lower part, or right to paint
# vertical rear part
with self.saved_context():
self.moveTo(
r * self.rear_factor,
-r * self.lower_factor - self.thickness/2,
90)
self.fingerHolesAt(0, 0, self.lower_size)
with self.saved_context():
self.moveTo(
r * self.rear_factor + self.thickness/2,
-r * self.lower_factor,
0)
self.fingerHolesAt(0, 0, self.rear_size)
if self.debug:
self.circle(0, 0, self.disc_diameter / 2)
def _draw_slits(self, inset, halfslit):
total_x = 0
for x in self.sx:
center_x = total_x + x / 2
total_x += x
self.rectangularHole(inset, center_x, 2 * halfslit, self.disc_thickness)
if self.debug:
self.ctx.rectangle(inset - halfslit, center_x - x/2, 2 * halfslit, x)
def lower_holes(self):
r = self.disc_diameter / 2
inset = self.lower_outset + self.lower_halfslit
self._draw_slits(inset, self.lower_halfslit)
def rear_holes(self):
r = self.disc_diameter / 2
inset = r * self.lower_factor
self._draw_slits(inset, self.rear_halfslit)
def render(self):
o = self.outer
self.lower_factor = min(self.lower_factor, 0.99)
self.rear_factor = min(self.rear_factor, 0.99)
self.rectangularWall(o, o, "eeee", move="right", callback=[self.sidewall_holes])
self.rectangularWall(o, o, "eeee", move="right mirror", callback=[self.sidewall_holes])
self.rectangularWall(self.lower_size, sum(self.sx), "fffe", move="right", callback=[self.lower_holes])
self.rectangularWall(self.rear_size, sum(self.sx), "fefh", move="right", callback=[self.rear_holes])

View File

@ -0,0 +1,116 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class FrontEdge(edges.BaseEdge):
"""An edge with room to get your fingers around cards"""
def __call__(self, length, **kw):
depth = self.settings.y * 2 / 3
t = self.settings.thickness
r = min(depth-t, length/4)
self.edge(length/4-t, tabs=2)
self.corner(90, t)
self.edge(depth-t-r, tabs=2)
self.corner(-90, r)
self.edge(length/2 - 2*r)
self.corner(-90, r)
self.edge(depth-t-r, tabs=2)
self.corner(90, t)
self.edge(length/4-t, tabs=2)
class Dispenser(Boxes):
"""Dispenser for stackable (flat) items of same size"""
description = """Set *bottomheight* to 0 for a wall mounting variant.
Please add mounting holes yourself."""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.addSettingsArgs(edges.StackableSettings)
self.buildArgParser(x=100, y=100, h=100)
self.argparser.add_argument(
"--slotheight", action="store", type=float, default=10.0,
help="height of the dispenser slot / items (in mm)")
self.argparser.add_argument(
"--bottomheight", action="store", type=float, default=0.0,
help="height underneath the dispenser (in mm)")
self.argparser.add_argument(
"--sideedges", action="store", type=ArgparseEdgeType("Fh"),
choices=list("Fh"), default="F",
help="edges used for holding the front panels and back")
def render(self):
x, y, h, hs = self.x, self.y, self.h, self.slotheight
hb = self.bottomheight
t = self.thickness
se = self.sideedges
fe = FrontEdge(self, self)
hb = max(0, hb-self.edges["š"].spacing())
th = h + (hb+t if hb else 0.0)
hh = hb + 0.5*t
with self.saved_context():
self.rectangularWall(x, y, [fe, "f", "f", "f"],
label="Floor", move="right")
self.rectangularWall(x, y, "eeee", label="Lid bottom", move="right")
self.rectangularWall(x, y, "EEEE", label="Lid top", move="right")
self.rectangularWall(x, y, "ffff", move="up only")
if hb:
frontedge = edges.CompoundEdge(self, "Ef", (hb+t+hs, h-hs))
self.rectangularWall(
y, th, ("š", frontedge, "e", "f"), ignore_widths=[6],
callback=[lambda:self.fingerHolesAt(0, hh, y, 0)],
label="Left wall", move="right mirror")
self.rectangularWall(
x, th, ["š", se, "e", se], ignore_widths=[1, 6],
callback=[lambda:self.fingerHolesAt(0, hh, x, 0)],
label="Back wall", move="right")
self.rectangularWall(
y, th, ("š", frontedge, "e", "f"), ignore_widths=[6],
callback=[lambda:self.fingerHolesAt(0, hh, y, 0)],
label="Right wall", move="right")
else:
frontedge = edges.CompoundEdge(self, "Ef", (hs, h-hs))
self.rectangularWall(
y, th, ("h", frontedge, "e", "f"),
label="Left wall", ignore_widths=[6], move="right mirror")
self.rectangularWall(
x, th, ["h", se, "e", se], ignore_widths=[1, 6],
label="Back wall", move="right")
self.rectangularWall(
y, th, ("h", frontedge, "e", "f"),
label="Right wall", ignore_widths=[6], move="right")
self.rectangularWall(x/3, h-hs, "eee" + se,
label="Left front", move="right")
self.rectangularWall(x/3, h-hs, "eee" + se,
label="Right front", move="mirror right")

View File

@ -0,0 +1,54 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import math
class Display(Boxes):
"""Diplay for flyers or leaflets"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.buildArgParser(x=150., h=200.0)
# Add non default cli params if needed (see argparse std lib)
self.argparser.add_argument(
"--radius", action="store", type=float, default=5.,
help="radius of the corners in mm")
self.argparser.add_argument(
"--angle", action="store", type=float, default=0.,
help="greater zero for top wider as bottom")
def render(self):
# adjust to the variables you want in the local scope
x, h, r = self.x, self.h, self.radius
a = self.angle
t = self.thickness
self.roundedPlate(0.7*x, x, r, "e", extend_corners=False, move="up")
oh = 1.2*h-2*r
if a > 0:
self.moveTo(math.sin(math.radians(a))*oh)
self.rectangularHole(x/2, h*0.2, 0.7*x+0.1*t, 1.3*t)
self.moveTo(r)
self.polyline(x-2*r, (90-a, r), oh, (90+a, r),
x-2*r+2*math.sin(math.radians(a))*oh,
(90+a, r), oh, (90-a, r))

View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
# Copyright (C) 2018 Alexander Bulimov
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class DisplayCase(Boxes):
"""Fully closed box intended to be cut from transparent acrylics and to serve as a display case."""
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser("x", "y", "h", "outside")
self.argparser.add_argument(
"--overhang",
action="store",
type=float,
default=2,
help="overhang for joints in mm",
)
def render(self):
x, y, h = self.x, self.y, self.h
if self.outside:
x = self.adjustSize(x)
y = self.adjustSize(y)
h = self.adjustSize(h)
t = self.thickness
d2 = edges.Bolts(2)
d3 = edges.Bolts(3)
d2 = d3 = None
self.rectangularWall(x, h, "ffff", bedBolts=[d2] * 4, move="right", label="Wall 1")
self.rectangularWall(y, h, "fFfF", bedBolts=[d3, d2, d3, d2], move="up", label="Wall 2")
self.rectangularWall(y, h, "fFfF", bedBolts=[d3, d2, d3, d2], label="Wall 4")
self.rectangularWall(x, h, "ffff", bedBolts=[d2] * 4, move="left up", label="Wall 3")
self.flangedWall(x, y, "FFFF", flanges=[self.overhang] * 4, move="right", label="Top")
self.flangedWall(x, y, "FFFF", flanges=[self.overhang] * 4, label="Bottom")

View File

@ -0,0 +1,81 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class DisplayShelf(Boxes): # change class name here and below
"""Shelf with slanted floors"""
ui_group = "Shelf"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser(x=400, y=100, h=300, outside=True)
self.argparser.add_argument(
"--num", action="store", type=int, default=3,
help="number of shelves")
self.argparser.add_argument(
"--front", action="store", type=float, default=20.0,
help="height of front walls")
self.argparser.add_argument(
"--angle", action="store", type=float, default=30.0,
help="angle of floors (negative values for slanting backwards)")
def side(self):
t = self.thickness
a = math.radians(self.angle)
hs = (self.sl+t) * math.sin(a) + math.cos(a) * t
for i in range(self.num):
pos_x = abs(0.5*t*math.sin(a))
pos_y = hs - math.cos(a)*0.5*t + i * (self.h-hs) / (self.num - 0.5)
self.fingerHolesAt(pos_x, pos_y, self.sl, -self.angle)
pos_x += math.cos(-a) * (self.sl+0.5*t) + math.sin(a)*0.5*t
pos_y += math.sin(-a) * (self.sl+0.5*t) + math.cos(a)*0.5*t
self.fingerHolesAt(pos_x, pos_y, self.front, 90-self.angle)
def render(self):
# adjust to the variables you want in the local scope
x, y, h = self.x, self.y, self.h
f = self.front
t = self.thickness
if self.outside:
x = self.adjustSize(x)
a = math.radians(self.angle)
self.sl = sl = (y - (t * (math.cos(a) + abs(math.sin(a)))) - max(0, math.sin(a) * f)) / math.cos(a)
# render your parts here
self.rectangularWall(y, h, callback=[self.side], move="up")
self.rectangularWall(y, h, callback=[self.side], move="up")
if f:
for i in range(self.num):
self.rectangularWall(x, sl, "ffef", move="up")
self.rectangularWall(x, f, "Ffef", move="up")
else:
for i in range(self.num):
self.rectangularWall(x, sl, "Efef", move="up")

View File

@ -0,0 +1,662 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from functools import partial
from boxes import Boxes, edges, boolarg
import math
class NotchSettings(edges.Settings):
"""Settings for Notches on the Dividers"""
absolute_params = {
"upper_radius": 1,
"lower_radius": 8,
"depth": 15,
}
class SlotSettings(edges.Settings):
"""Settings for Divider Slots
Values:
* absolute
* depth : 20 : depth of the slot in mm
* angle : 0 : angle at which slots are generated, in degrees. 0° is vertical.
* radius : 2 : radius of the slot entrance in mm
* extra_slack : 0.2 : extra slack (in addition to thickness and kerf) to help insert dividers in mm"""
absolute_params = {
"depth": 20,
"angle": 0,
"radius": 2,
"extra_slack": 0.2,
}
class DividerSettings(edges.Settings):
"""Settings for Dividers
Values:
* absolute_params
* bottom_margin : 0 : margin between box's bottom and divider's in mm
* relative (in multiples of thickness)
* play : 0.05 : play to avoid them clamping onto the walls (in multiples of thickness)
"""
absolute_params = {
"bottom_margin": 0,
}
relative_params = {
"play": 0.05,
}
class DividerTray(Boxes):
"""Divider tray - rows and dividers"""
description = """
Adding '0:' at the start of the sy parameter adds a slot at the very back. Adding ':0' at the end of sy adds a slot meeting the bottom at the very front. This is especially useful if slot angle is set above zero.
There are 4 different sets of dividers rendered:
* With asymetric tabs so the tabs fit on top of each other
* With tabs of half wall thickness that can go side by side
* With tabs of a full wall thickness
* One single divider spanning across all columns
You will likely need to cut each of the dividers you want multiple times.
"""
ui_group = "Tray"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.addSettingsArgs(edges.HandleEdgeSettings)
self.buildArgParser("sx", "sy", "h", "outside")
self.addSettingsArgs(SlotSettings)
self.addSettingsArgs(NotchSettings)
self.addSettingsArgs(DividerSettings)
self.argparser.add_argument(
"--notches_in_wall",
type=boolarg,
default=True,
help="generate the same notches on the walls that are on the dividers",
)
self.argparser.add_argument(
"--left_wall",
type=boolarg,
default=True,
help="generate wall on the left side",
)
self.argparser.add_argument(
"--right_wall",
type=boolarg,
default=True,
help="generate wall on the right side",
)
self.argparser.add_argument(
"--bottom", type=boolarg, default=False, help="generate wall on the bottom",
)
self.argparser.add_argument(
"--handle", type=boolarg, default=False, help="add handle to the bottom",
)
def render(self):
side_walls_number = len(self.sx) - 1 + sum([self.left_wall, self.right_wall])
if side_walls_number == 0:
raise ValueError("You need at least one side wall to generate this tray")
# We need to adjust height before slot generation
if self.outside:
if self.bottom:
self.h -= self.thickness
else:
# If the parameter 'h' is the inner height of the content itself,
# then the actual tray height needs to be adjusted with the angle
self.h = self.h * math.cos(math.radians(self.Slot_angle))
slot_descriptions = SlotDescriptionsGenerator().generate_all_same_angles(
self.sy,
self.thickness,
self.Slot_extra_slack,
self.Slot_depth,
self.h,
self.Slot_angle,
self.Slot_radius,
)
# If measures are outside, we need to readjust slots afterwards
if self.outside:
self.sx = self.adjustSize(self.sx, self.left_wall, self.right_wall)
side_wall_target_length = sum(self.sy) - 2 * self.thickness
slot_descriptions.adjust_to_target_length(side_wall_target_length)
self.ctx.save()
# Facing walls (outer) with finger holes to support side walls
facing_wall_length = sum(self.sx) + self.thickness * (len(self.sx) - 1)
side_edge = lambda with_wall: "F" if with_wall else "e"
bottom_edge = lambda with_wall, with_handle: ("f" if with_handle else "F") if with_wall else "e"
upper_edge = (
DividerNotchesEdge(
self,
list(reversed(self.sx)),
)
if self.notches_in_wall
else "e"
)
for _ in range(2):
self.rectangularWall(
facing_wall_length,
self.h,
[
bottom_edge(self.bottom, _ and self.handle),
side_edge(self.right_wall),
upper_edge,
side_edge(self.left_wall),
],
callback=[partial(self.generate_finger_holes, self.h)],
move="up", label = "Front" if _ else "Back",
)
# Side walls (outer & inner) with slots to support dividers
side_wall_length = slot_descriptions.total_length()
for _ in range(side_walls_number):
if _ < side_walls_number - (len(self.sx) - 1):
be = "F" if self.bottom else "e"
else:
be = "f" if self.bottom else "e"
se = DividerSlotsEdge(self, slot_descriptions.descriptions)
self.rectangularWall(
side_wall_length, self.h, [be, "f", se, "f"], move="up", label="Sidepiece " + str(_ + 1)
)
# Switch to right side of the file
self.ctx.restore()
self.rectangularWall(
max(facing_wall_length, side_wall_length), self.h, "ffff", move="right only", label="invisible"
)
# Bottom piece.
if self.bottom:
self.rectangularWall(
facing_wall_length,
side_wall_length,
[
"f",
"f" if self.right_wall else "e",
"Y" if self.handle else "f",
"f" if self.left_wall else "e",
],
callback=[partial(self.generate_finger_holes, side_wall_length)],
move="up", label="Bottom",
)
# Dividers
divider_height = (
# h, with angle adjustement
self.h / math.cos(math.radians(self.Slot_angle))
# removing what exceeds in the width of the divider
- self.thickness * math.tan(math.radians(self.Slot_angle))
# with margin
- self.Divider_bottom_margin
)
self.generate_divider(
self.sx, divider_height, "up",
first_tab_width=self.thickness if self.left_wall else 0,
second_tab_width=self.thickness if self.right_wall else 0
)
for tabs, asymetric_tabs in [(self.thickness, None),
(self.thickness / 2, None),
(self.thickness, 0.5),]:
with self.saved_context():
for i, length in enumerate(self.sx):
self.generate_divider(
[length],
divider_height,
"right",
first_tab_width=tabs if self.left_wall or i>0 else 0,
second_tab_width=tabs if self.right_wall or i<(len(self.sx) - 1) else 0,
asymetric_tabs=asymetric_tabs,
)
if asymetric_tabs:
self.moveTo(-tabs, self.spacing)
self.generate_divider(self.sx, divider_height, "up only")
if self.debug:
debug_info = ["Debug"]
debug_info.append(
"Slot_edge_outer_length:{0:.2f}".format(
slot_descriptions.total_length() + 2 * self.thickness
)
)
debug_info.append(
"Slot_edge_inner_lengths:{0}".format(
str.join(
"|",
[
"{0:.2f}".format(e.usefull_length())
for e in slot_descriptions.get_straigth_edges()
],
)
)
)
debug_info.append(
"Face_edge_outer_length:{0:.2f}".format(
facing_wall_length
+ self.thickness * sum([self.left_wall, self.right_wall])
)
)
debug_info.append(
"Face_edge_inner_lengths:{0}".format(
str.join("|", ["{0:.2f}".format(e) for e in self.sx])
)
)
debug_info.append("Tray_height:{0:.2f}".format(self.h))
debug_info.append(
"Content_height:{0:.2f}".format(
self.h / math.cos(math.radians(self.Slot_angle))
)
)
self.text(str.join("\n", debug_info), x=5, y=5, align="bottom left")
def generate_finger_holes(self, length):
posx = -0.5 * self.thickness
for x in self.sx[:-1]:
posx += x + self.thickness
self.fingerHolesAt(posx, 0, length)
def generate_divider(
self, widths, height, move,
first_tab_width=0, second_tab_width=0,
asymetric_tabs=None):
total_width = sum(widths) + (len(widths)-1) * self.thickness + first_tab_width + second_tab_width
if self.move(total_width, height, move, True):
return
play = self.Divider_play
left_tab_height = right_tab_height = self.Slot_depth
if asymetric_tabs:
left_tab_height = left_tab_height * asymetric_tabs - play
right_tab_height = right_tab_height * (1-asymetric_tabs)
# Upper: first tab width
if asymetric_tabs:
self.moveTo(first_tab_width - play)
else:
self.edge(first_tab_width - play)
# Upper edge with a finger notch
for nr, width in enumerate(widths):
if nr > 0:
self.edge(self.thickness)
DividerNotchesEdge(
self,
[width],
)(width)
self.polyline(
# Upper: second tab width if needed
second_tab_width - play,
# First side, with tab depth only if there is 2 walls
90,
left_tab_height,
90,
second_tab_width,
-90,
height - left_tab_height,
90,
)
# Lower edge
for width in reversed(widths[1:]):
self.polyline(
width - 2 * play,
90,
height - self.Slot_depth,
-90,
self.thickness + 2 * play,
-90,
height - self.Slot_depth,
90,
)
self.polyline(
# Second side tab
widths[0] - 2 * play,
90,
height - self.Slot_depth,
-90,
first_tab_width,
90,
right_tab_height,
90
)
if asymetric_tabs:
self.polyline(
first_tab_width - play,
-90,
self.Slot_depth-right_tab_height,
90
)
# Move for next piece
self.move(total_width, height, move, label="Divider")
class SlottedEdgeDescriptions:
def __init__(self):
self.descriptions = []
def add(self, description):
self.descriptions.append(description)
def get_straigth_edges(self):
return [x for x in self.descriptions if isinstance(x, StraightEdgeDescription)]
def get_last_edge(self):
return self.descriptions[-1]
def adjust_to_target_length(self, target_length):
actual_length = sum([d.tracing_length() for d in self.descriptions])
compensation = actual_length - target_length
compensation_ratio = compensation / sum(
[d.asked_length for d in self.get_straigth_edges()]
)
for edge in self.get_straigth_edges():
edge.outside_ratio = 1 - compensation_ratio
def total_length(self):
return sum([x.tracing_length() for x in self.descriptions])
class StraightEdgeDescription:
def __init__(
self,
asked_length,
round_edge_compensation=0,
outside_ratio=1,
angle_compensation=0,
):
self.asked_length = asked_length
self.round_edge_compensation = round_edge_compensation
self.outside_ratio = outside_ratio
self.angle_compensation = angle_compensation
def __repr__(self):
return (
"StraightEdgeDescription({0}, round_edge_compensation={1}, angle_compensation={2}, outside_ratio={3})"
).format(
self.asked_length,
self.round_edge_compensation,
self.angle_compensation,
self.outside_ratio,
)
def tracing_length(self):
"""
How much length should take tracing this straight edge
"""
return (
(self.asked_length * self.outside_ratio)
- self.round_edge_compensation
+ self.angle_compensation
)
def usefull_length(self):
"""
Part of the length which might be used by the content of the tray
"""
return self.asked_length * self.outside_ratio
class Memoizer(dict):
def __init__(self, computation):
self.computation = computation
def __missing__(self, key):
res = self[key] = self.computation(key)
return res
class SlotDescription:
_div_by_cos_cache = Memoizer(lambda a: 1 / math.cos(math.radians(a)))
_tan_cache = Memoizer(lambda a: math.tan(math.radians(a)))
def __init__(
self, width, depth=20, angle=0, radius=0, start_radius=None, end_radius=None
):
self.depth = depth
self.width = width
self.start_radius = radius if start_radius == None else start_radius
self.end_radius = radius if end_radius == None else end_radius
self.angle = angle
def __repr__(self):
return "SlotDescription({0}, depth={1}, angle={2}, start_radius={3}, end_radius={4})".format(
self.width, self.depth, self.angle, self.start_radius, self.end_radius
)
def _div_by_cos(self):
return SlotDescription._div_by_cos_cache[self.angle]
def _tan(self):
return SlotDescription._tan_cache[self.angle]
def angle_corrected_width(self):
"""
returns how much width is the slot when measured horizontally, since the angle makes it bigger.
It's the same as the slot entrance width when radius is 0°.
"""
return self.width * self._div_by_cos()
def round_edge_start_correction(self):
"""
returns by how much we need to stop tracing our straight lines at the start of the slot
in order to do a curve line instead
"""
return self.start_radius * (self._div_by_cos() - self._tan())
def round_edge_end_correction(self):
"""
returns by how much we need to stop tracing our straight lines at the end of the slot
in order to do a curve line instead
"""
return self.end_radius * (self._div_by_cos() + self._tan())
def _depth_angle_correction(self):
"""
The angle makes one side of the slot deeper than the other.
"""
extra_depth = self.width * self._tan()
return extra_depth
def corrected_start_depth(self):
"""
Returns the depth of the straigth part of the slot starting side
"""
extra_depth = self._depth_angle_correction()
return self.depth + max(0, extra_depth) - self.round_edge_start_correction()
def corrected_end_depth(self):
"""
Returns the depth of the straigth part of the slot ending side
"""
extra_depth = self._depth_angle_correction()
return self.depth + max(0, -extra_depth) - self.round_edge_end_correction()
def tracing_length(self):
"""
How much length this slot takes on an edge
"""
return (
self.round_edge_start_correction()
+ self.angle_corrected_width()
+ self.round_edge_end_correction()
)
class SlotDescriptionsGenerator:
def generate_all_same_angles(
self, sections, thickness, extra_slack, depth, height, angle, radius=2,
):
width = thickness + extra_slack
descriptions = SlottedEdgeDescriptions()
# Special case: if first slot start at 0, then radius is 0
first_correction = 0
current_section = 0
if sections[0] == 0:
slot = SlotDescription(
width, depth=depth, angle=angle, start_radius=0, end_radius=radius,
)
descriptions.add(slot)
first_correction = slot.round_edge_end_correction()
current_section += 1
first_length = sections[current_section]
current_section += 1
descriptions.add(
StraightEdgeDescription(
first_length, round_edge_compensation=first_correction
)
)
for l in sections[current_section:]:
slot = SlotDescription(width, depth=depth, angle=angle, radius=radius,)
# Fix previous edge length
previous_edge = descriptions.get_last_edge()
previous_edge.round_edge_compensation += slot.round_edge_start_correction()
# Add this slot
descriptions.add(slot)
# Add the straigth edge after this slot
descriptions.add(
StraightEdgeDescription(l, slot.round_edge_end_correction())
)
# We need to add extra space for the divider (or the actual content)
# to slide all the way down to the bottom of the tray in spite of walls
end_length = height * math.tan(math.radians(angle))
descriptions.get_last_edge().angle_compensation += end_length
return descriptions
class DividerNotchesEdge(edges.BaseEdge):
"""Edge with multiple notches for easier access to dividers"""
description = "Edge with multiple notches for easier access to dividers"
def __init__(self, boxes, sx):
super().__init__(boxes, None)
self.sx = sx
def __call__(self, _, **kw):
first = True
for width in self.sx:
if first:
first = False
else:
self.edge(self.thickness)
self.edge_with_notch(width)
def edge_with_notch(self, width):
# width (with notch if possible)
upper_third = (
width - 2 * self.Notch_upper_radius - 2 * self.Notch_lower_radius
) / 3
if upper_third > 0:
straightHeight = (
self.Notch_depth - self.Notch_upper_radius - self.Notch_lower_radius
)
self.polyline(
upper_third,
(90, self.Notch_upper_radius),
straightHeight,
(-90, self.Notch_lower_radius),
upper_third,
(-90, self.Notch_lower_radius),
straightHeight,
(90, self.Notch_upper_radius),
upper_third,
)
else:
# if there isn't enough room for the radius, we don't use it
self.edge(width)
class DividerSlotsEdge(edges.BaseEdge):
"""Edge with multiple angled rounded slots for dividers"""
description = "Edge with multiple angled rounded slots for dividers"
def __init__(self, boxes, descriptions):
super().__init__(boxes, None)
self.descriptions = descriptions
def __call__(self, length, **kw):
self.ctx.save()
for description in self.descriptions:
if isinstance(description, SlotDescription):
self.do_slot(description)
elif isinstance(description, StraightEdgeDescription):
self.do_straight_edge(description)
# rounding errors might accumulates :
# restore context and redo the move straight
self.ctx.restore()
self.moveTo(length)
def do_straight_edge(self, straight_edge):
self.edge(straight_edge.tracing_length())
def do_slot(self, slot):
self.ctx.save()
self.polyline(
0,
(90 - slot.angle, slot.start_radius),
slot.corrected_start_depth(),
-90,
slot.width,
-90,
slot.corrected_end_depth(),
(90 + slot.angle, slot.end_radius),
)
# rounding errors might accumulates :
# restore context and redo the move straight
self.ctx.restore()
self.moveTo(slot.tracing_length())

View File

@ -0,0 +1,128 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import boxes
import math
class DoubleFlexDoorBox(boxes.Boxes):
"""Box with two part lid with living hinges and round corners"""
ui_group = "FlexBox"
def __init__(self):
boxes.Boxes.__init__(self)
self.addSettingsArgs(boxes.edges.FingerJointSettings)
self.addSettingsArgs(boxes.edges.FlexSettings)
self.buildArgParser("x", "y", "h", "outside")
self.argparser.add_argument(
"--radius", action="store", type=float, default=15,
help="Radius of the latch in mm")
self.argparser.add_argument(
"--latchsize", action="store", type=float, default=8,
help="size of latch in multiples of thickness")
def flexBoxSide(self, x, y, r, callback=None, move=None):
t = self.thickness
ll = (x - 2*r) / 2 - self.latchsize
if self.move(x+2*t, y+2*t, move, True):
return
self.moveTo(t+r, t)
for i, l in zip(range(2), (x, y)):
self.cc(callback, i)
self.edges["f"](l - 2 * r)
self.corner(90, r)
self.cc(callback, 2)
self.edge(ll)
self.latch(self.latchsize)
self.latch(self.latchsize, reverse=True)
self.edge(ll)
self.corner(90, r)
self.cc(callback, 3)
self.edges["f"](y - 2 * r)
self.corner(90, r)
self.move(x+2*t, y+2*t, move)
def surroundingWall(self, x, y, h, r, move=None):
t = self.thickness
c4 = math.pi * r * 0.5
tw = 2*x + 2*y - 8*r + 4*c4
th = h + 2.5*t
if self.move(tw, th, move, True):
return
self.moveTo(0, 0.25*t, -90)
self.latch(self.latchsize, False, True)
self.edge((x-2*r)/2 - self.latchsize, False)
if y - 2 * r < t:
self.edges["X"](2 * c4 + y - 2 * r, h + 2 * t)
else:
self.edges["X"](c4, h + 2 * t)
self.edges["F"](y - 2 * r, False)
self.edges["X"](c4, h + 2 * t)
self.edges["F"](x - 2 * r, False)
if y - 2 * r < t:
self.edges["X"](2 * c4 + y - 2 * r, h + 2 * t)
else:
self.edges["X"](c4, h + 2 * t)
self.edges["F"](y - 2 * r)
self.edges["X"](c4, h + 2 * t)
self.edge((x-2*r)/2 - self.latchsize, False)
self.latch(self.latchsize, False)
self.edge(h + 2 * t)
self.latch(self.latchsize, False, True)
self.edge((x-2*r)/2 - self.latchsize, False)
self.edge(c4)
self.edges["F"](y - 2 * r)
self.edge(c4)
self.edges["F"](x - 2 * r, False)
self.edge(c4)
self.edges["F"](y - 2 * r, False)
self.edge(c4)
self.edge((x-2*r)/2 - self.latchsize)
self.latch(self.latchsize, False, False)
self.edge(h + 2 * t)
self.move(tw, th, move)
def render(self):
if self.outside:
self.x = self.adjustSize(self.x)
self.y = self.adjustSize(self.y)
self.h = self.adjustSize(self.h)
t = self.thickness
self.latchsize *= t
x, y, h = self.x, self.y, self.h
r = self.radius or min(x - 2*self.latchsize, y) / 2.0
r = min(r, y / 2.0)
self.radius = r = min(r, max(0, (x - 2*self.latchsize) / 2.0))
# swap y and h for more consistent axis names
self.surroundingWall(x, h, y, r, move="up")
self.flexBoxSide(x, h, r, move="right")
self.flexBoxSide(x, h, r, move="mirror")

View File

@ -0,0 +1,125 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import Boxes, edges, Color, ArgparseEdgeType
from boxes.lids import _TopEdge
class DrillBox(_TopEdge):
"""A parametrized box for drills"""
ui_group = "Tray"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings,
space=3, finger=3, surroundingspaces=1)
self.addSettingsArgs(edges.RoundedTriangleEdgeSettings, outset=1)
self.addSettingsArgs(edges.StackableSettings)
self.addSettingsArgs(edges.MountingSettings)
self.argparser.add_argument(
"--top_edge", action="store",
type=ArgparseEdgeType("eStG"), choices=list("eStG"),
default="e", help="edge type for top edge")
self.buildArgParser(sx="25*3", sy="60*4", sh="5:25:10",
bottom_edge="h")
self.argparser.add_argument(
"--holes",
action="store",
type=int,
default=3,
help="Number of holes for each size",
)
self.argparser.add_argument(
"--firsthole",
action="store",
type=float,
default=1.0,
help="Smallest hole",
)
self.argparser.add_argument(
"--holeincrement",
action="store",
type=float,
default=.5,
help="increment between holes",
)
def sideholes(self, l):
t = self.thickness
h = -0.5 * t
for d in self.sh[:-1]:
h += d + t
self.fingerHolesAt(0, h, l, angle=0)
def drillholes(self, description=False):
y = 0
d = self.firsthole
for dy in self.sy:
x = 0
for dx in self.sx:
iy = dy / self.holes
for k in range(self.holes):
self.hole(x + dx / 2, y + (k + 0.5) * iy, d=d + 0.05)
if description:
self.rectangularHole(x + dx / 2, y + dy / 2, dx - 2, dy - 2, color=Color.ETCHING)
self.text(
"%.1f" % d,
x + 2,
y + 2,
270,
align="right",
fontsize=6,
color=Color.ETCHING,
)
# TODO: make the fontsize dynamic to make the text fit in all cases
d += self.holeincrement
x += dx
y += dy
def render(self):
x = sum(self.sx)
y = sum(self.sy)
h = sum(self.sh) + self.thickness * (len(self.sh)-1)
b = self.bottom_edge
t1, t2, t3, t4 = self.topEdges(self.top_edge)
self.rectangularWall(
x, h, [b, "f", t1, "F"],
ignore_widths=[1, 6],
callback=[lambda: self.sideholes(x)], move="right")
self.rectangularWall(
y, h, [b, "f", t2, "F"], callback=[lambda: self.sideholes(y)],
ignore_widths=[1, 6],
move="up")
self.rectangularWall(
y, h, [b, "f", t3, "F"], callback=[lambda: self.sideholes(y)],
ignore_widths=[1, 6])
self.rectangularWall(
x, h, [b, "f", t4, "F"],
ignore_widths=[1, 6],
callback=[lambda: self.sideholes(x)], move="left up")
if b != "e":
self.rectangularWall(x, y, "ffff", move="right")
for d in self.sh[:-2]:
self.rectangularWall(
x, y, "ffff", callback=[self.drillholes], move="right")
self.rectangularWall(
x, y, "ffff",
callback=[lambda: self.drillholes(description=True)],
move="right")

View File

@ -0,0 +1,234 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2019 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import boxes
class DrillStand(Boxes):
"""Box for drills with each compartment of a different height"""
description = """Note: `sh` gives the hight of the rows front to back. It though should have the same number of entries as `sy`. These heights are the one on the left side and increase throughout the row. To have each compartement a bit higher than the previous one the steps in `sh` should be a bit bigger than `extra_height`.
Assembly:
![Parts](static/samples/DrillStand-drawing.png)
Start with putting the slots of the inner walls together. Be especially careful with adding the bottom. It is always assymetrical and flush with the right/lower side while being a little short on the left/higher side to not protrude into the side wall.
| | |
| ---- | ---- |
| ![Assembly inner walls](static/samples/DrillStand-assembly-1.jpg) | ![Assembly bottom](static/samples/DrillStand-assembly-2.jpg) |
| Then add the front and the back wall. | Add the very left and right walls last. |
| ![Assembly front and back](static/samples/DrillStand-assembly-3.jpg) | ![Assembly side walls](static/samples/DrillStand-assembly-4.jpg) |
"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.StackableSettings, height=1.0, width=3)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser(sx="25*6", sy="10:20:30", sh="25:40:60")
self.argparser.add_argument(
"--extra_height", action="store", type=float, default=15.0,
help="height difference left to right")
def yWall(self, nr, move=None):
t = self.thickness
x, sx, y, sy, sh = self.x, self.sx, self.y, self.sy, self.sh
eh = self.extra_height * (sum(sx[:nr])+ nr*t - t)/x
tw, th = sum(sy) + t * len(sy) + t, max(sh) + eh
if self.move(tw, th, move, True):
return
self.moveTo(t)
self.polyline(y, 90)
self.edges["f"](sh[-1]+eh)
self.corner(90)
for i in range(len(sy)-1, 0, -1):
s1 = max(sh[i]-sh[i-1], 0) + 4*t
s2 = max(sh[i-1]-sh[i], 0) + 4*t
self.polyline(sy[i], 90, s1, -90, t, -90, s2, 90)
self.polyline(sy[0], 90)
self.edges["f"](sh[0] + eh)
self.corner(90)
self.move(tw, th, move)
def sideWall(self, extra_height=0.0, foot_height=0.0, edges="šFf", move=None):
t = self.thickness
x, sx, y, sy, sh = self.x, self.sx, self.y, self.sy, self.sh
eh = extra_height
fh = foot_height
edges = [self.edges.get(e, e) for e in edges]
tw = sum(sy) + t * len(sy) + t
th = max(sh) + eh + fh + edges[0].spacing()
if self.move(tw, th, move, True):
return
self.moveTo(edges[0].margin())
edges[0](y+2*t)
self.edgeCorner(edges[0], "e")
self.edge(fh)
self.step(edges[1].startwidth() - t)
edges[1](sh[-1]+eh)
self.edgeCorner(edges[1], "e")
for i in range(len(sy)-1, 0, -1):
self.edge(sy[i])
if sh[i] > sh[i-1]:
self.fingerHolesAt(0.5*t, self.burn, sh[i]+eh, 90)
self.polyline(t, 90, sh[i] - sh[i-1], -90)
else:
self.polyline(0, -90, sh[i-1] - sh[i], 90, t)
self.fingerHolesAt(-0.5*t, self.burn, sh[i-1]+eh)
self.polyline(sy[0])
self.edgeCorner("e", edges[2])
edges[2](sh[0]+eh)
self.step(t - edges[2].endwidth())
self.polyline(fh)
self.edgeCorner("e", edges[0])
self.move(tw, th, move)
def xWall(self, nr, move=None):
t = self.thickness
x, sx, y, sy, sh = self.x, self.sx, self.y, self.sy, self.sh
eh = self.extra_height
tw, th = x + 2*t, sh[nr] + eh + t
a = math.degrees(math.atan(eh / x))
fa = 1 / math.cos(math.radians(a))
if self.move(tw, th, move, True):
return
self.moveTo(t, eh+t, -a)
for i in range(len(sx)-1):
self.edges["f"](fa*sx[i])
h = min(sh[nr - 1], sh[nr])
s1 = h - 3.95*t + self.extra_height * (sum(sx[:i+1]) + i*t)/x
s2 = h - 3.95*t + self.extra_height * (sum(sx[:i+1]) + i*t + t)/x
self.polyline(0, 90+a, s1, -90, t, -90, s2, 90-a)
self.edges["f"](fa*sx[-1])
self.polyline(0, 90+a)
self.edges["f"](sh[nr]+eh)
self.polyline(0, 90, x, 90)
self.edges["f"](sh[nr])
self.polyline(0, 90+a)
self.move(tw, th, move)
def xOutsideWall(self, h, edges="fFeF", move=None):
t = self.thickness
x, sx, y, sy, sh = self.x, self.sx, self.y, self.sy, self.sh
edges = [self.edges.get(e, e) for e in edges]
eh = self.extra_height
tw = x + edges[1].spacing() + edges[3].spacing()
th = h + eh + edges[0].spacing() + edges[2].spacing()
a = math.degrees(math.atan(eh / x))
fa = 1 / math.cos(math.radians(a))
if self.move(tw, th, move, True):
return
self.moveTo(edges[3].spacing(), eh+edges[0].margin(), -a)
self.edge(t*math.tan(math.radians(a)))
if isinstance(edges[0], boxes.edges.FingerHoleEdge):
with self.saved_context():
self.moveTo(0, 0, a)
self.fingerHolesAt(
0, 1.5*t, x*fa - t*math.tan(math.radians(a)), -a)
self.edge(x*fa - t*math.tan(math.radians(a)))
elif isinstance(edges[0], boxes.edges.FingerJointEdge):
edges[0](x*fa - t*math.tan(math.radians(a)))
else:
raise ValueError("Only edges h and f supported: ")
self.corner(a)
self.edgeCorner(edges[0], "e", 90)
self.corner(-90)
self.edgeCorner("e", edges[1], 90)
edges[1](eh+h)
self.edgeCorner(edges[1], edges[2], 90)
edges[2](x)
self.edgeCorner(edges[2], edges[3], 90)
edges[3](h)
self.edgeCorner(edges[3], "e", 90)
self.corner(-90)
self.edgeCorner("e", edges[0], 90)
self.moveTo(0, self.burn+edges[0].startwidth(), 0)
for i in range(1, len(sx)):
posx = sum(sx[:i]) + i*t - 0.5 * t
length = h + self.extra_height * (sum(sx[:i]) + i*t - t)/x
self.fingerHolesAt(posx, h, length, -90)
self.move(tw, th, move)
def bottomCB(self):
t = self.thickness
x, sx, y, sy, sh = self.x, self.sx, self.y, self.sy, self.sh
eh = self.extra_height
a = math.degrees(math.atan(eh / x))
fa = 1 / math.cos(math.radians(a))
posy = -0.5 * t
for i in range(len(sy)-1):
posy += sy[i] + t
posx = -t * math.tan(math.radians(a)) # left side is clipped
for j in range(len(sx)):
self.fingerHolesAt(posx, posy, fa*sx[j], 0)
posx += fa*sx[j] + fa*t
def render(self):
t = self.thickness
sx, sy, sh = self.sx, self.sy, self.sh
self.x = x = sum(sx) + len(sx)*t - t
self.y = y = sum(sy) + len(sy)*t - t
bottom_angle = math.atan(self.extra_height / x) # radians
self.xOutsideWall(sh[0], "hFeF", move="up")
for i in range(1, len(sy)):
self.xWall(i, move="up")
self.xOutsideWall(sh[-1], "hfef", move="up")
self.rectangularWall(x/math.cos(bottom_angle)-t*math.tan(bottom_angle), y, "fefe", callback=[self.bottomCB], move="up")
self.sideWall(foot_height=self.extra_height+2*t, move="right")
for i in range(1, len(sx)):
self.yWall(i, move="right")
self.sideWall(self.extra_height, 2*t, move="right")

View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class Edges(Boxes):
"""Print all registerd Edge types"""
webinterface = False
def __init__(self):
Boxes.__init__(self)
def render(self):
self.ctx = None
self._buildObjects()
chars = self.edges.keys()
for c in sorted(chars, key=lambda x:(x.lower(), x.isupper())):
print("%s %s - %s" %(c, self.edges[c].__class__.__name__,
self.edges[c].__doc__))

View File

@ -0,0 +1,99 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2017 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class ElectronicsBox(Boxes):
"""Closed box with screw on top and mounting holes"""
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser("x", "y", "h", "outside")
self.argparser.add_argument(
"--triangle", action="store", type=float, default=25.,
help="Sides of the triangles holding the lid in mm")
self.argparser.add_argument(
"--d1", action="store", type=float, default=2.,
help="Diameter of the inner lid screw holes in mm")
self.argparser.add_argument(
"--d2", action="store", type=float, default=3.,
help="Diameter of the lid screw holes in mm")
self.argparser.add_argument(
"--d3", action="store", type=float, default=3.,
help="Diameter of the mounting screw holes in mm")
self.argparser.add_argument(
"--outsidemounts", action="store", type=boolarg, default=True,
help="Add external mounting points")
self.argparser.add_argument(
"--holedist", action="store", type=float, default=7.,
help="Distance of the screw holes from the wall in mm")
def wallxCB(self):
t = self.thickness
self.fingerHolesAt(0, self.h-1.5*t, self.triangle, 0)
self.fingerHolesAt(self.x, self.h-1.5*t, self.triangle, 180)
def wallyCB(self):
t = self.thickness
self.fingerHolesAt(0, self.h-1.5*t, self.triangle, 0)
self.fingerHolesAt(self.y, self.h-1.5*t, self.triangle, 180)
def render(self):
t = self.thickness
self.h = h = self.h + 2*t # compensate for lid
x, y, h = self.x, self.y, self.h
d1, d2, d3 =self.d1, self.d2, self.d3
hd = self.holedist
tr = self.triangle
trh = tr / 3.
if self.outside:
self.x = x = self.adjustSize(x)
self.y = y = self.adjustSize(y)
self.h = h = h - 3*t
self.rectangularWall(x, h, "fFeF", callback=[self.wallxCB],
move="right", label="Wall 1")
self.rectangularWall(y, h, "ffef", callback=[self.wallyCB],
move="up", label="Wall 2")
self.rectangularWall(y, h, "ffef", callback=[self.wallyCB],
label="Wall 4")
self.rectangularWall(x, h, "fFeF", callback=[self.wallxCB],
move="left up", label="Wall 3")
if not self.outsidemounts:
self.rectangularWall(x, y, "FFFF", callback=[
lambda:self.hole(hd, hd, d=d3)] *4, move="right",
label="Bottom")
else:
self.flangedWall(x, y, edges="FFFF",
flanges=[0.0, 2*hd, 0., 2*hd], r=hd,
callback=[
lambda:self.hole(hd, hd, d=d3)] * 4, move='up',
label="Bottom")
self.rectangularWall(x, y, callback=[
lambda:self.hole(trh, trh, d=d2)] * 4, move='up', label="Top")
self.rectangularTriangle(tr, tr, "ffe", num=4,
callback=[None, lambda: self.hole(trh, trh, d=d1)])

View File

@ -0,0 +1,65 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2017 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class EuroRackSkiff(Boxes):
"""3U Height case with adjustable width and height and included rails"""
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser("h")
self.argparser.add_argument(
"--hp", action="store", type=int, default=84,
help="Width of the case in HP")
def wallxCB(self, x):
t = self.thickness
def wallyCB(self, y):
t = self.thickness
self.fingerHolesAt(6, self.h-1.5*t, y, 0)
def railHoles(self):
for i in range(0, self.hp):
self.hole(i*5.08 + 2.54, 3, d=3.0)
def render(self):
t = self.thickness
h = self.h
y = self.hp * 5.08
x = 128.5
self.rectangularWall(y, 6, "feee", callback=[self.railHoles] , move="up")
self.rectangularWall(y, 6, "feee", callback=[self.railHoles] , move="up")
self.rectangularWall(x, h, "fFeF", callback=[self.wallxCB(x)],
move="right")
self.rectangularWall(y, h, "ffef", callback=[self.wallyCB(y)], move="up")
self.rectangularWall(y, h, "ffef", callback=[self.wallyCB(y)])
self.rectangularWall(x, h, "fFeF", callback=[self.wallxCB(x)],
move="left up")
self.rectangularWall(x, y, "FFFF", callback=[], move="right")

View File

@ -0,0 +1,107 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class FanHole(Boxes):
"""Hole pattern for mounting a fan"""
ui_group = "Holes"
def __init__(self):
Boxes.__init__(self)
self.argparser.add_argument(
"--diameter", action="store", type=float, default=80,
help="diameter of the fan hole")
self.argparser.add_argument(
"--mounting_holes", action="store", type=float, default=3,
help="diameter of the fan mounting holes")
self.argparser.add_argument(
"--mounting_holes_inset", action="store", type=float, default=5,
help="distance of the fan mounting holes from the outside")
self.argparser.add_argument(
"--arms", action="store", type=int, default=10,
help="number of arms")
self.argparser.add_argument(
"--inner_disc", action="store", type=float, default=.2,
help="relative size of the inner disc")
self.argparser.add_argument(
"--style", action="store", type=str, default="CW Swirl",
choices=["CW Swirl", "CCW Swirl", "Hole"],
help="Style of the fan hole")
def arc(self, d, a):
r = abs(1/math.cos(math.radians(90-a/2))*d/2)
self.corner(-a/2)
self.corner(a, r)
self.corner(-a/2)
def swirl(self, r, ri_rel=.1, n=20):
d = 2*r
#r = d/2
ri = ri_rel * r
ai = 90
ao = 360/n * 0.8
# angle going in
a1 = math.degrees(math.atan(
ri*math.sin(math.radians(ai)) /
(r - ri*math.cos(math.radians(ai)))))
d1= (ri*math.sin(math.radians(ai))**2 +
(r - ri*math.cos(math.radians(ai)))**2)**.5
d2= (ri*math.sin(math.radians(ai-ao))**2 +
(r - ri*math.cos(math.radians(ai-ao)))**2)**.5
# angle coming out
a_i2 = math.degrees(math.atan(
(r*math.sin(math.radians(ao)) - ri*math.sin(math.radians(ai))) /
(r*math.cos(math.radians(ao)) - ri*math.cos(math.radians(ai)))))
a3 = a1 + a_i2
a2 = 90 + a_i2 - ao
self.moveTo(0, -r, 180)
for i in range(n):
with self.saved_context():
self.corner(-ao, r)
self.corner(-a2)
self.arc(d2, -90)
self.corner(-180+a3)
self.arc(d1, 85)
self.corner(-90-a1)
self.moveArc(-360./n, r)
def render(self):
r_h = self.mounting_holes / 2
d = self.diameter
inset = self.mounting_holes_inset
for px in (inset, d-inset):
for py in (inset, d-inset):
self.hole(px, py, r_h)
self.moveTo(d/2, d/2)
print(self.style)
if self.style == "CW Swirl":
self.ctx.scale(-1, 1)
self.swirl(d/2, self.inner_disc, self.arms)
elif self.style == "CCW Swirl":
self.swirl(d/2, self.inner_disc, self.arms)
else: #Hole
self.hole(0, 0, d=d)

View File

@ -0,0 +1,78 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
from shapely.geometry import *
import random
import time
class FillTest(Boxes): # Change class name!
"""Piece for testing different settings for hole filling"""
ui_group = "Part"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(fillHolesSettings, fill_pattern="hex")
self.buildArgParser(x=320, y=220)
def xHoles(self):
# border = [(5, 10), (245, 10), (225, 150), (235, 150), (255, 10), (290, 10), (270, 190), (45, 190), (45, 50), (35, 50), (35, 190), (5, 190)]
x, y = self.x, self.y
border = [
( 5/320*x, 10/220*y),
(245/320*x, 10/220*y),
(225/320*x, 150/220*y),
(235/320*x, 150/220*y),
(255/320*x, 10/220*y),
(290/320*x, 10/220*y),
(270/320*x, 190/220*y),
( 45/320*x, 190/220*y),
( 45/320*x, 50/220*y),
( 35/320*x, 50/220*y),
( 35/320*x, 190/220*y),
( 5/320*x, 190/220*y),
]
self.showBorderPoly(border)
self.text("Area to be filled", x/2, 190/220*y, align="bottom center", color=Color.ANNOTATIONS)
start_time = time.time()
self.fillHoles(
pattern=self.fillHoles_fill_pattern,
border=border,
max_radius=self.fillHoles_hole_max_radius,
hspace=self.fillHoles_space_between_holes,
bspace=self.fillHoles_space_to_border,
min_radius=self.fillHoles_hole_min_radius,
style=self.fillHoles_hole_style,
bar_length=self.fillHoles_bar_length,
max_random=self.fillHoles_max_random
)
end_time = time.time()
# print('fillHoles - Execution time:', (end_time-start_time)*1000, 'ms ', self.fillHoles_fill_pattern)
def render(self):
self.rectangularWall(self.x, self.y, "eeee", callback=[self.xHoles, None, None, None],)

View File

@ -0,0 +1,125 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import boxes
import math
class FlexBox(boxes.Boxes):
"""Box with living hinge and round corners"""
ui_group = "FlexBox"
def __init__(self):
boxes.Boxes.__init__(self)
self.addSettingsArgs(boxes.edges.FingerJointSettings)
self.addSettingsArgs(boxes.edges.FlexSettings)
self.buildArgParser("x", "y", "h", "outside")
self.argparser.add_argument(
"--radius", action="store", type=float, default=15,
help="Radius of the latch in mm")
self.argparser.add_argument(
"--latchsize", action="store", type=float, default=8,
help="size of latch in multiples of thickness")
def flexBoxSide(self, x, y, r, callback=None, move=None):
t = self.thickness
if self.move(x+2*t, y+t, move, True):
return
self.moveTo(t+r, t)
for i, l in zip(range(2), (x, y)):
self.cc(callback, i)
self.edges["f"](l - 2 * r)
self.corner(90, r)
self.cc(callback, 2)
self.edge(x - 2 * r)
self.corner(90, r)
self.cc(callback, 3)
self.latch(self.latchsize)
self.cc(callback, 4)
self.edges["f"](y - 2 * r - self.latchsize)
self.corner(90, r)
self.move(x+2*t, y+t, move)
def surroundingWall(self, move=None):
x, y, h, r = self.x, self.y, self.h, self.radius
t = self.thickness
c4 = math.pi * r * 0.5
tw = 2*x + 2*y - 8*r + 4*c4
th = h + 2.5*t
if self.move(tw, th, move, True):
return
self.moveTo(0, 0.25*t)
self.edges["F"](y - 2 * r - self.latchsize, False)
if x - 2 * r < t:
self.edges["X"](2 * c4 + x - 2 * r, h + 2 * t)
else:
self.edges["X"](c4, h + 2 * t)
self.edges["F"](x - 2 * r, False)
self.edges["X"](c4, h + 2 * t)
self.edges["F"](y - 2 * r, False)
if x - 2 * r < t:
self.edges["X"](2 * c4 + x - 2 * r, h + 2 * t)
else:
self.edges["X"](c4, h + 2 * t)
self.edge(x - 2 * r)
self.edges["X"](c4, h + 2 * t)
self.latch(self.latchsize, False)
self.edge(h + 2 * t)
self.latch(self.latchsize, False, True)
self.edge(c4)
self.edge(x - 2 * r)
self.edge(c4)
self.edges["F"](y - 2 * r, False)
self.edge(c4)
self.edges["F"](x - 2 * r, False)
self.edge(c4)
self.edges["F"](y - 2 * r - self.latchsize, False)
self.corner(90)
self.edge(h + 2 * t)
self.corner(90)
self.move(tw, th, move)
def render(self):
if self.outside:
self.x = self.adjustSize(self.x)
self.y = self.adjustSize(self.y)
self.h = self.adjustSize(self.h)
x, y, h = self.x, self.y, self.h
self.latchsize *= self.thickness
r = self.radius or min(x, y - self.latchsize) / 2.0
r = min(r, x / 2.0)
self.radius = r = min(r, max(0, (y - self.latchsize) / 2.0))
self.surroundingWall(move="up")
self.flexBoxSide(self.x, self.y, self.radius, move="right")
self.flexBoxSide(self.x, self.y, self.radius, move="mirror")

View File

@ -0,0 +1,121 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import math
class FlexBox2(Boxes):
"""Box with living hinge and top corners rounded"""
ui_group = "FlexBox"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.addSettingsArgs(edges.FlexSettings)
self.buildArgParser("x", "y", "h", "outside")
self.argparser.add_argument(
"--radius", action="store", type=float, default=15,
help="Radius of the corners in mm")
self.argparser.add_argument(
"--latchsize", action="store", type=float, default=8,
help="size of latch in multiples of thickness")
def flexBoxSide(self, y, h, r, callback=None, move=None):
t = self.thickness
if self.move(y+2*t, h+t, move, True):
return
self.moveTo(t, t)
self.cc(callback, 0)
self.edges["f"](y)
self.corner(90, 0)
self.cc(callback, 1)
self.edges["f"](h - r)
self.corner(90, r)
self.cc(callback, 2)
self.edge(y - 2 * r)
self.corner(90, r)
self.cc(callback, 3)
self.latch(self.latchsize)
self.cc(callback, 4)
self.edges["f"](h - r - self.latchsize)
self.corner(90)
self.move(y+2*t, h+t, move)
def surroundingWall(self, move=None):
y, h, x, r = self.y, self.h, self.x, self.radius
t = self.thickness
tw = y + h - 3*r + 2*self.c4 + self.latchsize + t
th = x + 2.5*t
if self.move(tw, th, move, True):
return
self.moveTo(t, .25*t)
self.edges["F"](h - r, False)
if (y - 2 * r < t):
self.edges["X"](2 * self.c4 + y - 2 * r, x + 2 * t)
else:
self.edges["X"](self.c4, x + 2 * t)
self.edge(y - 2 * r)
self.edges["X"](self.c4, x + 2 * t)
self.latch(self.latchsize, False)
self.edge(x + 2 * t)
self.latch(self.latchsize, False, True)
self.edge(self.c4)
self.edge(y - 2 * r)
self.edge(self.c4)
self.edges["F"](h - r)
self.corner(90)
self.edge(t)
self.edges["f"](x)
self.edge(t)
self.corner(90)
self.move(tw, th, move)
def render(self):
if self.outside:
self.y = self.adjustSize(self.y)
self.h = self.adjustSize(self.h)
self.x = self.adjustSize(self.x)
self.latchsize *= self.thickness
self.radius = self.radius or min(self.y / 2.0, self.h - self.latchsize)
self.radius = min(self.radius, self.y / 2.0)
self.radius = min(self.radius, max(0, self.h - self.latchsize))
self.c4 = c4 = math.pi * self.radius * 0.5
self.moveTo(2 * self.thickness, self.thickness)
with self.saved_context():
self.surroundingWall(move="right")
self.rectangularWall(self.y, self.x, edges="FFFF")
self.surroundingWall(move="up only")
self.flexBoxSide(self.y, self.h, self.radius, move="right")
self.flexBoxSide(self.y, self.h, self.radius, move= "mirror right")
self.rectangularWall(self.x, self.h - self.radius - self.latchsize, edges="fFeF")

View File

@ -0,0 +1,168 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import math
class FlexBox3(Boxes):
"""Box with living hinge"""
ui_group = "FlexBox"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=1)
self.addSettingsArgs(edges.FlexSettings)
self.buildArgParser("x", "y", "outside")
self.argparser.add_argument(
"--z", action="store", type=float, default=100.0,
help="height of the box")
self.argparser.add_argument(
"--h", action="store", type=float, default=10.0,
help="height of the lid")
self.argparser.add_argument(
"--radius", action="store", type=float, default=10.0,
help="radius of the lids living hinge")
self.argparser.add_argument(
"--c", action="store", type=float, default=1.0,
dest="d", help="clearance of the lid")
def flexBoxSide(self, x, y, r, callback=None, move=None):
t = self.thickness
if self.move(x+2*t, y+t, move, True):
return
self.moveTo(t, t)
self.cc(callback, 0)
self.edges["f"](x)
self.corner(90, 0)
self.cc(callback, 1)
self.edges["f"](y - r)
self.corner(90, r)
self.cc(callback, 2)
self.edge(x - r)
self.corner(90, 0)
self.cc(callback, 3)
self.edges["f"](y)
self.corner(90)
self.move(x+2*t, y+t, move)
def surroundingWall(self, move=None):
x, y, z, r, d = self.x, self.y, self.z, self.radius, self.d
t = self.thickness
tw = x + y - 2*r + self.c4 + 2*t + t
th = z + 4*t + 2*d
if self.move(tw, th, move, True):
return
self.moveTo(t, d + t)
self.edges["F"](y - r, False)
self.edges["X"](self.c4, z + 2 * t)
self.corner(-90)
self.edge(d)
self.corner(90)
self.edges["f"](x - r + t)
self.corner(90)
self.edges["f"](z + 2 * t + 2 * d)
self.corner(90)
self.edges["f"](x - r + t)
self.corner(90)
self.edge(d)
self.corner(-90)
self.edge(self.c4)
self.edges["F"](y - r)
self.corner(90)
self.edge(t)
self.edges["f"](z)
self.edge(t)
self.corner(90)
self.move(tw, th, move)
def lidSide(self, move=None):
x, y, z, r, d, h = self.x, self.y, self.z, self.radius, self.d, self.h
t = self.thickness
r2 = r + t if r + t <= h + t else h + t
if r < h:
r2 = r + t
base_l = x + 2 * t
if self.move(h+t, base_l+t, move, True):
return
self.edge(h + self.thickness - r2)
self.corner(90, r2)
self.edge(r - r2 + 1 * t)
else:
a = math.acos((r-h)/(r+t))
ang = math.degrees(a)
base_l = x + (r+t) * math.sin(a) - r + t
if self.move(h+t, base_l+t, move, True):
return
self.corner(90-ang)
self.corner(ang, r+t)
self.edges["F"](x - r + t)
self.edgeCorner("F", "f")
self.edges["g"](h)
self.edgeCorner("f", "e")
self.edge(base_l)
self.corner(90)
self.move(h+t, base_l+t, move)
def render(self):
if self.outside:
self.x = self.adjustSize(self.x)
self.y = self.adjustSize(self.y)
self.z = self.adjustSize(self.z)
x, y, z, d, h = self.x, self.y, self.z, self.d, self.h
r = self.radius = self.radius or min(x, y) / 2.0
thickness = self.thickness
self.c4 = c4 = math.pi * r * 0.5 * 0.95
self.latchsize = 8 * thickness
width = 2 * x + y - 2 * r + c4 + 14 * thickness + 3 * h # lock
height = y + z + 8 * thickness
s = edges.FingerJointSettings(self.thickness, finger=1.,
space=1., surroundingspaces=1)
s.edgeObjects(self, "gGH")
with self.saved_context():
self.surroundingWall(move="right")
self.rectangularWall(x, z, edges="FFFF", move="right")
self.rectangularWall(h, z + 2 * (d + self.thickness), edges="GeGF", move="right")
self.lidSide(move="right")
self.lidSide(move="mirror right")
self.surroundingWall(move="up only")
self.flexBoxSide(x, y, r, move="right")
self.flexBoxSide(x, y, r, move="mirror right")
self.rectangularWall(z, y, edges="fFeF")

View File

@ -0,0 +1,120 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2018 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import math
class FlexBox4(Boxes):
"""Box with living hinge and left corners rounded"""
ui_group = "FlexBox"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.addSettingsArgs(edges.FlexSettings)
self.buildArgParser("x", "y", "h", "outside")
self.argparser.add_argument(
"--radius", action="store", type=float, default=15,
help="Radius of the corners in mm")
self.argparser.add_argument(
"--latchsize", action="store", type=float, default=8,
help="size of latch in multiples of thickness")
def flexBoxSide(self, x, y, r, callback=None, move=None):
t = self.thickness
if self.move(x+2*t, y+t, move, True):
return
self.moveTo(t, t)
self.cc(callback, 0)
self.edges["f"](x)
self.corner(90, 0)
self.cc(callback, 1)
self.edges["f"](y - r)
self.corner(90, r)
self.cc(callback, 2)
self.edge(x - 2 * r)
self.corner(90, r)
self.cc(callback, 3)
self.edges["e"](y - r - self.latchsize)
self.cc(callback, 4)
self.latch(self.latchsize)
self.corner(90)
self.move(x+2*t, y+t, move)
def surroundingWall(self, move=None):
x, y, h, r = self.x, self.y, self.h, self.radius
c4 = self.c4
t = self.thickness
tw, th = 2*c4 + 2*y + x - 4*r + 2*t, h + 2.5*t
if self.move(tw, th, move, True):
return
self.moveTo(t, 0.25*t)
self.edges["F"](y - r, False)
if (x - 2 * r < self.thickness):
self.edges["X"](2 * c4 + x - 2 * r, h + 2 * self.thickness)
else:
self.edges["X"](c4, h + 2 * self.thickness)
self.edge(x - 2 * r)
self.edges["X"](c4, h + 2 * self.thickness)
self.edge(y - r - self.latchsize)
self.latch(self.latchsize+t, False)
self.edge(h + 2 * self.thickness)
self.latch(self.latchsize+t, False, True)
self.edge(y - r - self.latchsize)
self.edge(c4)
self.edge(x - 2 * r)
self.edge(c4)
self.edges["F"](y - r)
self.corner(90)
self.edge(self.thickness)
self.edges["f"](h)
self.edge(self.thickness)
self.corner(90)
self.move(tw, th, move)
def render(self):
if self.outside:
self.x = self.adjustSize(self.x)
self.y = self.adjustSize(self.y)
self.h = self.adjustSize(self.h)
self.latchsize *= self.thickness
self.radius = self.radius or min(self.x / 2.0, self.y - self.latchsize)
self.radius = min(self.radius, self.x / 2.0)
self.radius = min(self.radius, max(0, self.y - self.latchsize))
self.c4 = c4 = math.pi * self.radius * 0.5
self.surroundingWall(move="up")
self.flexBoxSide(self.x, self.y, self.radius, move="right")
self.flexBoxSide(self.x, self.y, self.radius, move="mirror right")
self.rectangularWall(self.x, self.h, edges="FeFF")

View File

@ -0,0 +1,122 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import boxes
import math
class FlexBox5(boxes.Boxes):
"""Box with living hinge and round corners"""
ui_group = "FlexBox"
def __init__(self):
boxes.Boxes.__init__(self)
self.addSettingsArgs(boxes.edges.FingerJointSettings)
self.addSettingsArgs(boxes.edges.FlexSettings)
self.buildArgParser("x", "h", "outside")
self.argparser.add_argument(
"--top_diameter", action="store", type=float, default=60,
help="diameter at the top")
self.argparser.add_argument(
"--bottom_diameter", action="store", type=float, default=60,
help="diameter at the bottom")
self.argparser.add_argument(
"--latchsize", action="store", type=float, default=8,
help="size of latch in multiples of thickness")
def flexBoxSide(self, callback=None, move=None):
t = self.thickness
r1, r2 = self.top_diameter/2., self.bottom_diameter/2
a = self.a
l = self.l
tw , th = l+r1+r2, 2*max(r1, r2)+2*t
if self.move(tw, th, move, True):
return
self.moveTo(r2, t)
self.cc(callback, 0)
self.edges["f"](l)
self.corner(180+2*a, r1)
self.cc(callback, 1)
self.latch(self.latchsize)
self.cc(callback, 2)
self.edges["f"](l - self.latchsize)
self.corner(180-2*a, r2)
self.move(tw, th, move)
def surroundingWall(self, move=None):
t = self.thickness
r1, r2 = self.top_diameter/2., self.bottom_diameter/2
h = self.h
a = self.a
l = self.l
c1 = math.radians(180+2*a) * r1
c2 = math.radians(180-2*a) * r2
tw = 2*l + c1 + c2
th = h + 2.5*t
if self.move(tw, th, move, True):
return
self.moveTo(0, 0.25*t)
self.edges["F"](l - self.latchsize, False)
self.edges["X"](c2, h + 2 * t)
self.edges["F"](l, False)
self.edges["X"](c1, h + 2 * t)
self.latch(self.latchsize, False)
self.edge(h + 2 * t)
self.latch(self.latchsize, False, True)
self.edge(c1)
self.edges["F"](l, False)
self.edge(c2)
self.edges["F"](l - self.latchsize, False)
self.corner(90)
self.edge(h + 2 * t)
self.corner(90)
self.move(tw, th, move)
def render(self):
if self.outside:
self.x = self.adjustSize(self.x)
self.h = self.adjustSize(self.h)
self.top_diameter = self.adjustSize(self.top_diameter)
self.bottom_diameter = self.adjustSize(self.bottom_diameter)
t = self.thickness
self.latchsize *= self.thickness
d_t, d_b = self.top_diameter, self.bottom_diameter
self.x = max(self.x, self.latchsize + 2*t + (d_t + d_b)/2)
d_c = self.x - d_t/2. - d_b/2.
self.a = math.degrees(math.asin((d_t-d_b)/2 / d_c))
self.l = d_c * math.cos(math.radians(self.a))
self.surroundingWall(move="up")
self.flexBoxSide(move="right")
self.flexBoxSide(move="mirror")

View File

@ -0,0 +1,46 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class FlexTest(Boxes):
"Piece for testing different flex settings"
ui_group = "Part"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FlexSettings)
self.buildArgParser("x", "y")
def render(self):
x, y = self.x, self.y
self.moveTo(5, 5)
self.edge(10)
self.edges["X"](x, y)
self.edge(10)
self.corner(90)
self.edge(y)
self.corner(90)
self.edge(x + 20)
self.corner(90)
self.edge(y)
self.corner(90)

View File

@ -0,0 +1,37 @@
#!/usr/bin/python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class FlexTest2(Boxes):
"Piece for testing 2D flex settings"
ui_group = "Part"
def __init__(self):
Boxes.__init__(self)
self.buildArgParser("x", "y")
self.argparser.add_argument(
"--fw", action="store", type=float, default=1,
help="distance of flex cuts in multiples of thickness")
def render(self):
x, y = self.x, self.y
self.rectangularWall(x, y, callback=[lambda: self.flex2D(x, y, self.fw)])

View File

@ -0,0 +1,49 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import math
class Folder(Boxes):
"""Book cover with flex for the spine"""
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FlexSettings)
self.buildArgParser("x", "y", "h")
self.argparser.add_argument(
"--r", action="store", type=float, default=10.0,
help="radius of the corners")
self.argparser.set_defaults(h=20)
def render(self):
x, y, r, h = self.x, self.y, self.r, self.h
c2 = math.pi * h
self.moveTo(r + self.thickness, self.thickness)
self.edge(x - r)
self.edges["X"](c2, y)
self.edge(x - r)
self.corner(90, r)
self.edge(y - 2 * r)
self.corner(90, r)
self.edge(2 * x - 2 * r + c2)
self.corner(90, r)
self.edge(y - 2 * r)
self.corner(90, r)

View File

@ -0,0 +1,96 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class Gears(Boxes):
"""Gears"""
ui_group = "Part"
def __init__(self):
Boxes.__init__(self)
self.argparser.add_argument(
"--teeth1", action="store", type=int, default=12,
help="number of teeth")
self.argparser.add_argument(
"--shaft1", action="store", type=float, default=6.,
help="diameter of the shaft 1")
self.argparser.add_argument(
"--dpercentage1", action="store", type=float, default=75,
help="percent of the D section of shaft 1 (100 for round shaft)")
self.argparser.add_argument(
"--teeth2", action="store", type=int, default=32,
help="number of teeth in the other size of gears")
self.argparser.add_argument(
"--shaft2", action="store", type=float, default=0.0,
help="diameter of the shaft2 (zero for same as shaft 1)")
self.argparser.add_argument(
"--dpercentage2", action="store", type=float, default=0,
help="percent of the D section of shaft 1 (0 for same as shaft 1)")
self.argparser.add_argument(
"--modulus", action="store", type=float, default=2,
help="size of teeth (diameter / #teeth) in mm")
self.argparser.add_argument(
"--pressure_angle", action="store", type=float, default=20,
help="angle of the teeth touching (in degrees)")
self.argparser.add_argument(
"--profile_shift", action="store", type=float, default=20,
help="in precent of the modulus")
def render(self):
# adjust to the variables you want in the local scope
t = self.thickness
self.teeth1 = max(2, self.teeth1)
self.teeth2 = max(2, self.teeth2)
if not self.shaft2:
self.shaft2 = self.shaft1
if not self.dpercentage2:
self.dpercentage2 = self.dpercentage1
self.gears(teeth=self.teeth2, dimension=self.modulus,
angle=self.pressure_angle, profile_shift=self.profile_shift,
callback=lambda:self.dHole(0, 0, d=self.shaft2,
rel_w=self.dpercentage2/100.),
move="up")
r2, d2, d2 = self.gears.sizes(
teeth=self.teeth2, dimension=self.modulus,
angle=self.pressure_angle, profile_shift=self.profile_shift)
self.gears(teeth=self.teeth1, dimension=self.modulus,
angle=self.pressure_angle, profile_shift=self.profile_shift,
callback=lambda:self.dHole(0, 0, d=self.shaft1,
rel_w=self.dpercentage1/100.),
move="up")
r1, d1, d1 = self.gears.sizes(
teeth=self.teeth1, dimension=self.modulus,
angle=self.pressure_angle, profile_shift=self.profile_shift)
r = max(self.shaft1, self.shaft2)/2
self.hole(t+r, t+r, self.shaft1/2)
self.hole(t+r+r1+r2, t+r, self.shaft2/2)
self.moveTo(0, 2*r+t)
self.text("""Pitch radius 1: %.1fmm
Outer diameter 1: %.1fmm
Pitch radius 2: %.1fmm
Outer diameter 2: %.1fmm
Axis distance: %.1fmm
""" % (r1, d1, r2, d2, r1+r2), align="bottom left")

View File

@ -0,0 +1,98 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class GearBox(Boxes):
"""Gearbox with multiple identical stages"""
ui_group = "Part"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.argparser.add_argument(
"--teeth1", action="store", type=int, default=8,
help="number of teeth on ingoing shaft")
self.argparser.add_argument(
"--teeth2", action="store", type=int, default=20,
help="number of teeth on outgoing shaft")
self.argparser.add_argument(
"--modulus", action="store", type=float, default=3,
help="modulus of the theeth in mm")
self.argparser.add_argument(
"--shaft", action="store", type=float, default=6.,
help="diameter of the shaft")
self.argparser.add_argument(
"--stages", action="store", type=int, default=4,
help="number of stages in the gear reduction")
def render(self):
if self.teeth2 < self.teeth1:
self.teeth2, self.teeth1 = self.teeth1, self.teeth2
pitch1, size1, xxx = self.gears.sizes(teeth=self.teeth1, dimension=self.modulus)
pitch2, size2, xxx = self.gears.sizes(teeth=self.teeth2, dimension=self.modulus)
t = self.thickness
x = 1.1 * t * self.stages
if self.stages == 1:
y = size1 + size2
y1 = y / 2 - (pitch1 + pitch2) + pitch1
y2 = y / 2 + (pitch1 + pitch2) - pitch2
else:
y = 2 * size2
y1 = y / 2 - (pitch1 + pitch2) / 2
y2 = y / 2 + (pitch1 + pitch2) / 2
h = max(size1, size2) + t
b = "F"
t = "e" # prepare for close box
mh = self.shaft
def sideCB():
self.hole(y1, h / 2, mh / 2)
self.hole(y2, h / 2, mh / 2)
self.moveTo(self.thickness, self.thickness)
self.rectangularWall(y, h, [b, "f", t, "f"], callback=[sideCB], move="right")
self.rectangularWall(x, h, [b, "F", t, "F"], move="up")
self.rectangularWall(x, h, [b, "F", t, "F"])
self.rectangularWall(y, h, [b, "f", t, "f"], callback=[sideCB], move="left")
self.rectangularWall(x, h, [b, "F", t, "F"], move="up only")
self.rectangularWall(x, y, "ffff", move="up")
profile_shift = 20
pressure_angle = 20
for i in range(self.stages - 1):
self.gears(teeth=self.teeth2, dimension=self.modulus, angle=pressure_angle,
mount_hole=mh, profile_shift=profile_shift, move="up")
self.gears(teeth=self.teeth2, dimension=self.modulus, angle=pressure_angle,
mount_hole=mh, profile_shift=profile_shift, move="right")
for i in range(self.stages):
self.gears(teeth=self.teeth1, dimension=self.modulus, angle=pressure_angle,
mount_hole=mh, profile_shift=profile_shift, move="down")

View File

@ -0,0 +1,155 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class HalfBox(Boxes):
"""Configurable half of a box which can be: a bookend, a hanging shelf, an angle clamping jig, ..."""
description = """This can be used to create:
* a hanging shelf:
![HalfBox as hanging shelf](static/samples/HalfBox_Shelf_usage.jpg)
* an angle clamping jig:
![HalfBox as an angle clamping jig](static/samples/HalfBox_AngleJig_usage.jpg)
* a bookend:
![HalfBox as a bookend](static/samples/HalfBox_Bookend_usage.jpg)
and many more...
"""
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, finger=2.0,space=2.0)
self.addSettingsArgs(edges.MountingSettings)
self.buildArgParser(x=100, sy="50:50", h=100)
self.argparser.add_argument("--Clamping", action="store", type=boolarg, default=False, help="add clamping holes")
self.argparser.add_argument("--ClampingSize", action="store", type=float, default=25.0, help="diameter of clamping holes")
self.argparser.add_argument("--Mounting", action="store", type=boolarg, default=False, help="add mounting holes")
self.argparser.add_argument("--Sturdy", action="store", type=boolarg, default=False, help="create sturdy construction (e.g. shelf, clamping jig, ...)")
def polygonWallExt(self, borders, edge="f", turtle=False, callback=None, move=None):
# extended polygon wall.
# same as polygonWall, but with extended border parameters
# each border dataset consists of
# length
# turn angle
# radius of turn (without radius correction)
# edge type
for i in range(0, len(borders), 4):
self.cc(callback, i)
length = borders[i]
next_angle = borders[i+1]
next_radius = borders[i+2]
next_edge = borders[i+3]
e = self.edges.get(next_edge, next_edge)
if i == 0:
self.moveTo(0,e.margin(),0)
e(length)
if self.debug:
self.hole(0, 0, 1, color=Color.ANNOTATIONS)
self.corner(next_angle, tabs=0, radius=next_radius)
def xHoles(self):
posy = -0.5 * self.thickness
for y in self.sy[:-1]:
posy += y + self.thickness
self.fingerHolesAt(posy, 0, self.x)
def hHoles(self):
posy = -0.5 * self.thickness
for y in reversed(self.sy[1:]):
posy += y + self.thickness
self.fingerHolesAt(posy, 0, self.h)
def render(self):
# adjust to the variables you want in the local scope
x, h = self.x, self.h
d = self.ClampingSize
t = self.thickness
# triangle with sides: x (horizontal), h (upwards) and l
# angles: 90° between x & h
# b between h & l
# c between l & x
l = math.sqrt(x * x + h * h)
b = math.degrees(math.asin(x / l))
c = math.degrees(math.asin(h / l))
if x > h:
if 90 + b + c < 179:
b = 180 - b
else:
if 90 + b + c < 179:
c = 180 - c
# small triangle top: 2*t, h1, l1
h1 = (2*t)/x*h
l1 = (2*t)/x*l
# small triangle left: x2, 2*t, l2
x2 = (2*t)/h*x
l2 = (2*t)/h*l
# render your parts here
if self.Sturdy:
width = sum(self.sy) + (len(self.sy) - 1) * t
self.rectangularWall(x, width, "fffe", callback=[None, self.xHoles, None, None], move="right", label="bottom")
self.rectangularWall(h, width, "fGfF" if self.Mounting else "fefF", callback=[None, None, None, self.hHoles], move="up", label="back")
self.rectangularWall(x, width, "fffe", callback=[None, self.xHoles, None, None], move="left only", label="invisible")
for i in range(2):
self.move(x+x2+2*t + self.edges["f"].margin(), h+h1+2*t + self.edges["f"].margin(), "right", True, label="side " + str(i))
self.polygonWallExt(borders=[x2, 0, 0, "e", x, 0, 0, "h",2*t, 90, 0, "e", 2*t, 0, 0, "e", h, 0, 0, "h",h1, 180-b, 0, "e", l+l1+l2, 180-c, 0, "e"])
if self.Clamping:
self.hole(0, 0, 1, color=Color.ANNOTATIONS)
self.rectangularHole(x/2+x2,2*t+d/2,dx=d,dy=d,r=d/8)
self.rectangularHole((x+x2+2*t)-2*t-d/2,h/2+2*t,dx=d,dy=d,r=d/8)
self.move(x+x2+2*t + self.edges["f"].margin(), h+h1+2*t + self.edges["f"].margin(), "right", False, label="side " + str(i))
if len(self.sy) > 1:
for i in range((len(self.sy) - 1)):
self.move(x + self.edges["f"].margin(), h + self.edges["f"].margin(), "right", True, label="support " + str(i))
self.polygonWallExt(borders=[x, 90, 0, "f", h, 180-b, 0, "f", l, 180-c, 0, "e"])
if self.Clamping:
self.rectangularHole(x/2,d/2-t/2,dx=d,dy=d+t,r=d/8)
self.rectangularHole(x-d/2+t/2,h/2,dx=d+t,dy=d,r=d/8)
self.move(x + self.edges["f"].margin(), h + self.edges["f"].margin(), "right", False, label="support " + str(i))
else:
self.sy.insert(0,0)
self.sy.append(0)
width = sum(self.sy) + (len(self.sy) - 1) * t
self.rectangularWall(x, width, "efee", callback=[None, self.xHoles, None, None], move="right", label="bottom")
self.rectangularWall(h, width, "eGeF" if self.Mounting else "eeeF", callback=[None, None, None, self.hHoles], move="up", label="side")
self.rectangularWall(x, width, "efee", callback=[None, self.xHoles, None, None], move="left only", label="invisible")
for i in range((len(self.sy) - 1)):
self.move(x + self.edges["f"].margin(), h + self.edges["f"].margin(), "right", True, label="support " + str(i))
self.polygonWallExt(borders=[x, 90, 0, "f", h, 180-b, 0, "f", l, 180-c, 0, "e"])
if self.Clamping:
self.rectangularHole(x/2,d/2,dx=d,dy=d,r=d/8)
self.rectangularHole(x-d/2,h/2,dx=d,dy=d,r=d/8)
self.move(x + self.edges["f"].margin(), h + self.edges["f"].margin(), "right", False, label="support " + str(i))

View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class HeartBox(Boxes):
"""Box in the form of an heart"""
ui_group = "FlexBox"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, finger=1.0,space=1.0)
self.addSettingsArgs(edges.FlexSettings)
self.buildArgParser(x=150, h=50)
self.argparser.add_argument(
"--top", action="store", type=str, default="closed",
choices=["closed", "hole", "lid",],
help="style of the top and lid")
def CB(self):
x = self.x
t = self.thickness
l = 2/3. * x - t
r = l/2. - t
d = 2 *t
if self.top == "closed":
return
for i in range(2):
self.moveTo(t, t)
self.polyline((l, 2), (180, r), (d, 1), -90,
(d, 1), (180, r), (l, 2), 90)
l -= t
r -= t
d += t
if self.top == "hole":
return
def render(self):
x, h = self.x, self.h
t = self.thickness
l = 2/3. * x
r = l/2. - 0.5*t
borders = [l, (180, r), t, -90, t, (180, r), l, 90]
self.polygonWalls(borders, h)
self.rectangularWall(0, h, "FFFF", move="up only")
self.polygonWall(borders, callback=[self.CB], move="right")
self.moveTo(-2*t)
self.polygonWall(borders, move="mirror right")
if self.top == "lid":
self.polygonWall([l+t, (180, r+t), 0, -90, 0, (180, r+t), l+t, 90], 'e')

View File

@ -0,0 +1,84 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class HingeBox(Boxes):
"""Box with lid attached by cabinet hinges"""
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.addSettingsArgs(edges.CabinetHingeSettings)
self.buildArgParser("x", "y", "h", "outside")
self.argparser.add_argument(
"--lidheight", action="store", type=float, default=20.0,
help="height of lid in mm")
self.argparser.add_argument(
"--splitlid", action="store", type=float, default=0.0,
help="split the lid in y direction (mm)")
def render(self):
x, y, h, hl = self.x, self.y, self.h, self.lidheight
s = self.splitlid
if self.outside:
x = self.adjustSize(x)
y = self.adjustSize(y)
h = self.adjustSize(h)
s = self.adjustSize(s, None) # reduce by half of the walls
if s > x or s < 0.0: s = 0.0
t = self.thickness
# bottom walls
if s:
self.rectangularWall(x, h, "FFuF", move="right")
else:
self.rectangularWall(x, h, "FFeF", move="right")
self.rectangularWall(y, h, "Ffef", move="up")
self.rectangularWall(y, h, "Ffef")
self.rectangularWall(x, h, "FFuF", move="left up")
# lid
self.rectangularWall(x, hl, "UFFF", move="right")
if s:
self.rectangularWall(s, hl, "eeFf", move="right")
self.rectangularWall(y-s, hl, "efFe", move="up")
self.rectangularWall(y-s, hl, "eeFf")
self.rectangularWall(s, hl, "efFe", move="left")
self.rectangularWall(x, hl, "UFFF", move="left up")
else:
self.rectangularWall(y, hl, "efFf", move="up")
self.rectangularWall(y, hl, "efFf")
self.rectangularWall(x, hl, "eFFF", move="left up")
self.rectangularWall(x, y, "ffff", move="right only")
self.rectangularWall(x, y, "ffff")
if s:
self.rectangularWall(x, s, "ffef", move="left up")
self.rectangularWall(x, y-s, "efff", move="up")
else:
self.rectangularWall(x, y, "ffff", move="left up")
self.edges['u'].parts(move="up")
if s:
self.edges['u'].parts(move="up")

View File

@ -0,0 +1,77 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2022 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
from shapely.geometry import *
import random
import time
class HolePattern(Boxes):
"""Generate hole patterns in different simple shapes"""
ui_group = "Holes"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(fillHolesSettings, fill_pattern="hex")
self.buildArgParser("x", "y")
self.argparser.add_argument(
"--shape", action="store", type=str, default="rectangle",
choices=["rectangle", "ellipse", "oval", "hexagon", "octagon"],
help="Shape of the hole pattern")
def render(self):
x, y = self.x, self.y
if self.shape == "ellipse":
border = [(x * (.5+math.sin(math.radians(a))/2),
y * (.5+math.cos(math.radians(a))/2))
for a in range(0, 360, 18)]
elif self.shape == "oval":
r = min(x, y) / 2
dx = max(x-y, 0)
dy = max(y-x, 0)
border = [(r * (.5+math.cos(math.radians(a))/2) +
(dx if q in [0, 3] else 0),
r * (.5+math.sin(math.radians(a))/2) +
(dy if q in [1, 2] else 0))
for q in range(4)
for a in range(90*q, 90*q+91, 18)]
elif self.shape == "hexagon":
dx = min(y / (3**.5) / 2, x / 2)
border = [(dx, 0), (x-dx, 0), (x, .5*y),
(x-dx, y), (dx, y), (0, .5*y)]
elif self.shape == "octagon":
d = (2**.5/(2+2*2**.5))
d2 = 1 -d
border = [(d*x, 0), (d2*x, 0), (x, d*y), (x, d2*y),
(d2*x, y), (d*x, y), (0, d2*y), (0, d*y)]
else: # "rectangle"
border = [(0, 0), (x, 0), (x, y), (0, y)]
self.fillHoles(
pattern=self.fillHoles_fill_pattern,
border=border,
max_radius=self.fillHoles_hole_max_radius,
hspace=self.fillHoles_space_between_holes,
bspace=self.fillHoles_space_to_border,
min_radius=self.fillHoles_hole_min_radius,
style=self.fillHoles_hole_style,
bar_length=self.fillHoles_bar_length,
max_random=self.fillHoles_max_random
)

View File

@ -0,0 +1,123 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2018 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import math
class Hook(Boxes):
"""A hook wit a rectangular mouth to mount at the wall"""
ui_group = "Misc" # see ./__init__.py for names
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=0.5)
self.argparser.add_argument("--width", action="store",
type=float, default=40.,
help="width of the hook (back plate is a bit wider)")
self.argparser.add_argument("--height", action="store",
type=float, default=40.,
help="inner height of the hook")
self.argparser.add_argument("--depth", action="store",
type=float, default=40.,
help="inner depth of the hook")
self.argparser.add_argument("--strength", action="store",
type=float, default=20.,
help="width of the hook from the side")
self.argparser.add_argument("--angle", action="store",
type=float, default=45.,
help="angle of the support underneeth")
def render(self):
self.angle = min(self.angle, 80)
t = self.thickness
w = self.width - 2*t # inner width
d, h, s = self.depth, self.height, self.strength
self.rectangularWall(w + 4*t, self.height_back, 'Eeee', callback=self.back_callback, move='right')
self.sidewall(d, h, s, self.angle, move='right')
self.sidewall(d, h, s, self.angle, move='right')
self.rectangularWall(d, w, 'FFFf', move='right')
self.rectangularWall(h - t, w, 'FFFf', move='right', callback=[
lambda: self.hole((h - t)/2, w/2, d=17)])
self.rectangularWall(s-t, w, 'FeFf', move='right')
def back_callback(self, n):
if n != 0:
return
t = self.thickness
h = self.h_a + self.strength
self.fingerHolesAt(1.5*t, 0, h)
self.fingerHolesAt(self.width + .5*t, 0, h)
self.fingerHolesAt(2*t, h + t/2, self.width - 2*t, 0)
x_h = self.width/2 + t
y1 = h + self.height/2
y2 = self.strength/2
y3 = (y1 + y2) / 2
self.hole(x_h, y1, d=3)
self.hole(x_h, y2, d=3)
self.hole(x_h, y3, d=3)
@property
def height_back(self):
return self.strength + self.height + self.h_a
@property
def h_a(self):
return self.depth * math.tan(math.radians(self.angle))
def sidewall(self, depth, height, strength, angle=60., move=None):
t = self.thickness
h_a = depth * math.tan(math.radians(angle))
l_a = depth / math.cos(math.radians(angle))
f_edge = self.edges['f']
x_total = depth + strength + f_edge.margin()
y_total = strength + height + h_a
if self.move(x_total, y_total, move, before=True):
return
self.moveTo(f_edge.margin())
self.polyline(strength, angle, l_a, 90-angle, height+strength, 90)
f_edge(strength - t)
self.corner(90)
f_edge(height - t)
self.polyline(t, -90, t)
f_edge(depth)
self.corner(90)
f_edge(h_a + strength)
self.corner(90)
self.move(x_total, y_total, move)

View File

@ -0,0 +1,66 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class IntegratedHingeBox(Boxes):
"""Box with lid and integraded hinge"""
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.addSettingsArgs(edges.ChestHingeSettings)
self.buildArgParser("x", "y", "h", "outside")
self.argparser.add_argument(
"--lidheight", action="store", type=float, default=20.0,
help="height of lid in mm")
def render(self):
x, y, h, hl = self.x, self.y, self.h, self.lidheight
if self.outside:
x = self.adjustSize(x)
y = self.adjustSize(y)
h = self.adjustSize(h)
t = self.thickness
hy = self.edges["O"].startwidth()
hy2 = self.edges["P"].startwidth()
e1 = edges.CompoundEdge(self, "Fe", (h-hy, hy))
e2 = edges.CompoundEdge(self, "eF", (hy, h-hy))
e_back = ("F", e1, "e", e2)
self.rectangularWall(y, h-hy, "FfOf", ignore_widths=[2], move="up")
self.rectangularWall(y, hl-hy2, "pfFf", ignore_widths=[1], move="up")
self.rectangularWall(y, h-hy, "Ffof", ignore_widths=[5], move="up")
self.rectangularWall(y, hl-hy2, "PfFf", ignore_widths=[6], move="up")
self.rectangularWall(x, h, "FFeF", move="up")
self.rectangularWall(x, h, e_back, move="up")
self.rectangularWall(x, hl, "FFeF", move="up")
self.rectangularWall(x, hl-hy2, "FFqF", move="up")
self.rectangularWall(y, x, "ffff", move="up")
self.rectangularWall(y, x, "ffff")

View File

@ -0,0 +1,87 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import random
class JigsawPuzzle(Boxes): # change class name here and below
"""Fractal jigsaw puzzle. Still aplha"""
webinterface = False # Change to make visible in web interface
def __init__(self):
Boxes.__init__(self)
self.count = 0
self.argparser.add_argument(
"--size", action="store", type=float, default=100,
help="size of the puzzle in mm")
self.argparser.add_argument(
"--depth", action="store", type=int, default=5,
help="depth of the recursion/level of detail")
def peano(self, level):
if level == 0:
self.edge(self.size / self.depth)
return
self.peano(self, level - 1)
self.corner()
def edge(self, l):
self.count += 1
Boxes.edge(self, l)
# if (self.count % 2**5) == 0: #level == 3 and parity>0:
# self.corner(-360, 0.25*self.size/2**self.depth)
def hilbert(self, level, parity=1):
if level == 0:
return
# rotate and draw first subcurve with opposite parity to big curve
self.corner(parity * 90)
self.hilbert(level - 1, -parity)
# interface to and draw second subcurve with same parity as big curve
self.edge(self.size / 2 ** self.depth)
self.corner(parity * -90)
self.hilbert(level - 1, parity)
# third subcurve
self.edge(self.size / 2 ** self.depth)
self.hilbert(level - 1, parity)
# if level == 3: self.corner(-360, 0.4*self.size/2**self.depth)
# fourth subcurve
self.corner(parity * -90)
self.edge(self.size / 2 ** self.depth)
self.hilbert(level - 1, -parity)
# a final turn is needed to make the turtle
# end up facing outward from the large square
self.corner(parity * 90)
# if level == 3 and parity>0: # and random.random() < 100*0.5**(self.depth-2):
# self.corner(-360, 0.4*self.size/2**self.depth)
# with self.savedcontext():
# self.corner(parity*-90)
# self.edge(self.size/2**self.depth)
def render(self):
size = self.size
t = self.thickness
self.burn = 0.0
self.moveTo(10, 10)
self.hilbert(self.depth)

View File

@ -0,0 +1,65 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class JointPanel(Boxes):
"""Create pieces larger than your laser cutter by joining them with Dove Tails"""
description = """This can be used to just create a big panel in a smaller laser cutter. But the actual use is to split large parts into multiple smaller pieces. Copy the outline onto the sheet and then use the pieces to cut it into multiple parts that each can fit your laser cutter. Note that each piece must be cut with the sheet surrounding it to ensure the burn correction (aka kerf) is correct. Depending on your vector graphics software you may need to duplicate your part multiple times and then generate the intersection between one copy and each rectangular part.
The Boxes.py drawings assume that the laser is cutting in the center of the line and the width of the line represents the material that is cut away. Make sure your changes work the same way and you do not cutting away the kerf.
Small dove tails make it easier to fit parts in without problems. Lookout for pieces cut loose where the dove tails meet the edge of the parts. Move your part if necessary to avoid dove tails or details of your part colliding in a weird way.
For plywood this method works well with a very stiff press fit. Aim for needing a hammer to join the pieces together. This way they will feel like they have been welder together.
"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(
edges.DoveTailSettings, size=1, depth=.5, radius=.1)
self.buildArgParser(sx="400/2", sy="400/3")
self.argparser.add_argument(
"--separate", action="store", type=boolarg, default=False,
help="draw pieces apart so they can be cut to form a large sheet")
def render(self):
sx, sy = self.sx, self.sy
t = self.thickness
for ny, y in enumerate(sy):
t0 = "e" if ny == 0 else "d"
t2 = "e" if ny == len(sy) - 1 else "D"
with self.saved_context():
for nx, x in enumerate(sx):
t1 = "e" if nx == len(sx) - 1 else "d"
t3 = "e" if nx == 0 else "D"
self.rectangularWall(x, y, [t0, t1, t2, t3])
if self.separate:
self.rectangularWall(x, y, [t0, t1, t2, t3],
move="right only")
else:
self.moveTo(x)
if self.separate:
self.rectangularWall(x, y, [t0, t1, t2, t3],
move="up only")
else:
self.moveTo(0, y - self.edges["d"].spacing() if ny == 0 else y)

View File

@ -0,0 +1,285 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2021 Guillaume Collic
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import argparse
from boxes import Boxes, boolarg
class Keyboard:
"""
Code to manage Cherry MX compatible switches and Kailh hotswap socket.
Reference :
* https://www.cherrymx.de/en/dev.html
* https://cdn.sparkfun.com/datasheets/Components/Switches/MX%20Series.pdf
* https://www.kailhswitch.com/uploads/201815927/PG151101S11.pdf
"""
STANDARD_KEY_SPACING = 19.05
SWITCH_CASE_SIZE = 15.6
FRAME_CUTOUT = 14
def __init__(self):
pass
def add_common_keyboard_parameters(
self,
add_hotswap_parameter=True,
add_pcb_mount_parameter=True,
add_led_parameter=True,
add_diode_parameter=True,
add_cutout_type_parameter=True,
default_columns_definition=None,
):
if add_hotswap_parameter:
self.argparser.add_argument(
"--hotswap_enable",
action="store",
type=boolarg,
default=True,
help=("enlarge switches holes for hotswap pcb sockets"),
)
if add_pcb_mount_parameter:
self.argparser.add_argument(
"--pcb_mount_enable",
action="store",
type=boolarg,
default=True,
help=("adds holes for pcb mount switches"),
)
if add_led_parameter:
self.argparser.add_argument(
"--led_enable",
action="store",
type=boolarg,
default=False,
help=("adds pin holes under switches for leds"),
)
if add_diode_parameter:
self.argparser.add_argument(
"--diode_enable",
action="store",
type=boolarg,
default=False,
help=("adds pin holes under switches for diodes"),
)
if add_cutout_type_parameter:
self.argparser.add_argument(
"--cutout_type",
action="store",
type=str,
default="castle",
help=(
"Shape of the plate cutout: 'castle' allows for modding, and 'simple' is a tighter and simpler square"
),
)
if default_columns_definition:
self.argparser.add_argument(
"--columns_definition",
type=self.argparseColumnsDefinition,
default=default_columns_definition,
help=(
"Each column is separated by '/', and is in the form 'nb_rows @ offset x repeat_count'. "
"Nb_rows is the number of rows for this column. "
"The offset is in mm and optional. "
"Repeat_count is optional and repeats this column multiple times. "
"Spaces are not important."
"For example '3x2 / 4@11' means we want 3 columns, the two first with "
"3 rows without offset, and the last with 4 rows starting at 11mm high."
),
)
def argparseColumnsDefinition(self, s):
"""
Parse columns definition parameter
:param s: string to parse
Each column is separated by '/', and is in the form 'nb_rows @ offset x repeat_count'.
Nb_rows is the number of rows for this column.
The offset is in mm and optional.
Repeat_count is optional and repeats this column multiple times.
Spaces are not important.
For example '3x2 / 4@11' means we want 3 columns, the two first with
3 rows without offset, and the last with 4 rows starting at 11mm high
"""
result = []
try:
for column_string in s.split("/"):
m = re.match(r"^\s*(\d+)\s*@?\s*(\d*\.?\d*)(?:\s*x\s*(\d+))?\s*$", column_string)
keys_count = int(m.group(1))
offset = float(m.group(2)) if m.group(2) else 0
n = int(m.group(3)) if m.group(3) else 1
result.extend([(offset, keys_count)]*n)
except:
raise argparse.ArgumentTypeError("Don't understand columns definition string")
return result
def pcb_holes(
self, with_hotswap=True, with_pcb_mount=True, with_led=False, with_diode=False
):
grid_unit = 1.27
main_hole_size = 4
pcb_mount_size = 1.7
led_hole_size = 1
if with_hotswap:
pin_hole_size = 2.9
else:
pin_hole_size = 1.5
def grid_hole(x, y, d):
self.hole(grid_unit * x, grid_unit * y, d=d)
# main hole
grid_hole(0, 0, main_hole_size)
# switch pins
grid_hole(-3, 2, pin_hole_size)
grid_hole(2, 4, pin_hole_size)
if with_pcb_mount:
grid_hole(-4, 0, pcb_mount_size)
grid_hole(4, 0, pcb_mount_size)
if with_led:
grid_hole(-1, -4, led_hole_size)
grid_hole(1, -4, led_hole_size)
if with_diode:
grid_hole(-3, -4, led_hole_size)
grid_hole(3, -4, led_hole_size)
def apply_callback_on_columns(self, cb, columns_definition, spacing=None, reverse=False):
if spacing == None:
spacing = self.STANDARD_KEY_SPACING
if reverse:
columns_definition = list(reversed(columns_definition))
for offset, nb_keys in columns_definition:
self.moveTo(0, offset)
for _ in range(nb_keys):
cb()
self.moveTo(0, spacing)
self.moveTo(spacing, -nb_keys * spacing)
self.moveTo(0, -offset)
total_width = len(columns_definition) * spacing
self.moveTo(-1 * total_width)
def outer_hole(self, radius=2, centered=True):
"""
Draws a rounded square big enough to go around a whole switch (15.6mm)
"""
half_size = Keyboard.SWITCH_CASE_SIZE / 2
if centered:
self.moveTo(-half_size, -half_size)
# draw clock wise to work with burn correction
straight_edge = Keyboard.SWITCH_CASE_SIZE - 2 * radius
polyline = [straight_edge, (-90, radius)] * 4
self.moveTo(self.burn, radius, 90)
self.polyline(*polyline)
self.moveTo(0, 0, 270)
self.moveTo(0, -radius)
self.moveTo(-self.burn)
if centered:
self.moveTo(half_size, half_size)
def castle_shaped_plate_cutout(self, centered=True):
"""
This cutout shaped like a castle enables switch modding and rotation.
More information (type 4) on https://geekhack.org/index.php?topic=59837.0
"""
half_size = Keyboard.SWITCH_CASE_SIZE / 2
if centered:
self.moveTo(-half_size, -half_size)
# draw clock wise to work with burn correction
btn_half_side = [0.98, 90, 0.81, -90, 3.5, -90, 0.81, 90, 2.505]
btn_full_side = [*btn_half_side, 0, *btn_half_side[::-1]]
btn = [*btn_full_side, -90] * 4
self.moveTo(self.burn+0.81, 0.81, 90)
self.polyline(*btn)
self.moveTo(0, 0, 270)
self.moveTo(-self.burn-0.81, -0.81)
if centered:
self.moveTo(half_size, half_size)
def configured_plate_cutout(self, support=False):
"""
Choose which cutout to use based on configured type.
support: if true, not the main cutout, but one to glue against the first
1.5mm cutout to strengthen it, without the clipping part.
"""
if self.cutout_type.lower() == "castle":
if support:
self.outer_hole()
else:
self.castle_shaped_plate_cutout()
else:
self.simple_plate_cutout(with_notch=support)
def simple_plate_cutout(self, radius=0.2, with_notch=False):
"""
A simple plate cutout, a 14mm rectangle, as specified in this reference sheet
https://cdn.sparkfun.com/datasheets/Components/Switches/MX%20Series.pdf
With_notch shoul be used for a secondary lower plate, strengthening the first one.
A notch is added to let the hooks grasp the main upper plate.
Current position should be switch center.
Radius should be lower or equal to 0.3 mm
"""
size = Keyboard.FRAME_CUTOUT
half_size = size / 2
if with_notch:
notch_length = 5
notch_depth = 1
straight_part = 0.5 * (size - 2 * radius - 2 * notch_depth - notch_length)
self.moveTo(-half_size + self.burn, 0, 90)
polyline_quarter = [
half_size - radius,
(-90, radius),
straight_part,
(90, notch_depth / 2),
0,
(-90, notch_depth / 2),
notch_length / 2,
]
polyline = (
polyline_quarter
+ [0]
+ list(reversed(polyline_quarter))
+ [0]
+ polyline_quarter
+ [0]
+ list(reversed(polyline_quarter))
)
self.polyline(*polyline)
self.moveTo(0, 0, -90)
self.moveTo(half_size - self.burn)
else:
self.rectangularHole(0, 0, size, size, r=radius)

View File

@ -0,0 +1,139 @@
"""Generator for keypads with mechanical switches."""
from copy import deepcopy
from boxes import Boxes, boolarg
from boxes.edges import FingerJointSettings
from .keyboard import Keyboard
class Keypad(Boxes, Keyboard):
"""Generator for keypads with mechanical switches."""
description = "Note that top layers use a different material thickness according to the top1_thickness and top2_thickness (if enabled)."
ui_group = 'Box'
btn_size = 15.6
space_between_btn = 4
box_padding = 10
triangle = 25.0
def __init__(self):
super().__init__()
self.argparser.add_argument(
'--h', action='store', type=int, default=30,
help='height of the box'
)
self.argparser.add_argument(
'--top1_thickness', action='store', type=float, default=1.5,
help=('thickness of the button hold layer, cherry like switches '
'need 1.5mm or smaller to snap in')
)
self.argparser.add_argument(
'--top2_enable', action='store', type=boolarg, default=False,
help=('enables another top layer that can hold CPG151101S11 '
'hotswap sockets')
)
self.argparser.add_argument(
'--top2_thickness', action='store', type=float, default=1.5,
help=('thickness of the hotplug layer, CPG151101S11 hotswap '
'sockets need 1.2mm to 1.5mm')
)
# Add parameter common with other keyboard projects
self.add_common_keyboard_parameters(
# Hotswap already depends on top2_enable setting, a second parameter
# for it would be useless
add_hotswap_parameter=False,
# By default, 3 columns of 4 rows
default_columns_definition="4x3"
)
self.addSettingsArgs(FingerJointSettings, surroundingspaces=1)
def _get_x_y(self):
"""Gets the keypad's size based on the number of buttons."""
spacing = self.btn_size + self.space_between_btn
border = 2*self.box_padding - self.space_between_btn
x = len(self.columns_definition) * spacing + border
y = max(offset + keys * spacing for (offset, keys) in self.columns_definition) + border
return x, y
def render(self):
"""Renders the keypad."""
# deeper edge for top to add multiple layers
deep_edge = deepcopy(self.edges['f'].settings)
deep_edge.thickness = self.thickness + self.top1_thickness
if self.top2_enable:
deep_edge.thickness += self.top2_thickness
deep_edge.edgeObjects(self, 'gGH', True)
d1, d2 = 2., 3.
x, y = self._get_x_y()
h = self.h
# box sides
self.rectangularWall(x, h, "GFEF", callback=[self.wallx_cb], move="right")
self.rectangularWall(y, h, "GfEf", callback=[self.wally_cb], move="up")
self.rectangularWall(y, h, "GfEf", callback=[self.wally_cb])
self.rectangularWall(x, h, "GFEF", callback=[self.wallx_cb], move="left up")
# keypad lids
self.rectangularWall(x, y, "ffff", callback=self.to_grid_callback(self.support_hole), move="right")
self.rectangularWall(x, y, "ffff", callback=self.to_grid_callback(self.key_hole), move="up")
if self.top2_enable:
self.rectangularWall(x, y, "ffff", callback=self.to_grid_callback(self.hotplug))
# screwable
tr = self.triangle
trh = tr / 3
self.rectangularWall(
x, y,
callback=[lambda: self.hole(trh, trh, d=d2)] * 4,
move='left up'
)
self.rectangularTriangle(
tr, tr, "ffe", num=4,
callback=[None, lambda: self.hole(trh, trh, d=d1)]
)
def to_grid_callback(self, inner_callback):
def callback():
# move to first key center
key_margin = self.box_padding + self.btn_size / 2
self.moveTo(key_margin, key_margin)
self.apply_callback_on_columns(
inner_callback, self.columns_definition, self.btn_size + self.space_between_btn
)
return [callback]
def hotplug(self):
"""Callback for the key stabelizers."""
self.pcb_holes(
with_pcb_mount=self.pcb_mount_enable,
with_diode=self.diode_enable,
with_led=self.led_enable,
)
def support_hole(self):
self.configured_plate_cutout(support=True)
def key_hole(self):
self.configured_plate_cutout()
# stolen form electronics-box
def wallx_cb(self):
"""Callback for triangle holes on x-side."""
x, _ = self._get_x_y()
t = self.thickness
self.fingerHolesAt(0, self.h - 1.5 * t, self.triangle, 0)
self.fingerHolesAt(x, self.h - 1.5 * t, self.triangle, 180)
# stolen form electronics-box
def wally_cb(self):
"""Callback for triangle holes on y-side."""
_, y = self._get_x_y()
t = self.thickness
self.fingerHolesAt(0, self.h - 1.5 * t, self.triangle, 0)
self.fingerHolesAt(y, self.h - 1.5 * t, self.triangle, 180)

View File

@ -0,0 +1,134 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import math
"""
22x7.5x7cm
D=23cm, d=21cm
d = 8" D = 9"
"""
class RoundedTriangleSettings(edges.Settings):
absolute_params = {
"angle": 60,
"radius": 30,
"r_hole": 0.0,
}
class RoundedTriangle(edges.Edge):
char = "t"
def __call__(self, length, **kw):
angle = self.settings.angle
r = self.settings.radius
if self.settings.r_hole:
x = 0.5 * (length - 2 * r) * math.tan(math.radians(angle))
y = 0.5 * (length)
self.hole(x, y, self.settings.r_hole)
l = 0.5 * (length - 2 * r) / math.cos(math.radians(angle))
self.corner(90 - angle, r)
self.edge(l)
self.corner(2 * angle, r)
self.edge(l)
self.corner(90 - angle, r)
def startAngle(self):
return 90
def endAngle(self):
return 90
class Lamp(Boxes):
webinterface = False
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser(x=220, y=75, h=70)
self.argparser.add_argument(
"--radius", action="store", type=float, default="105",
help="radius of the lamp")
self.argparser.add_argument(
"--width", action="store", type=float, default="10",
help="width of the ring")
def side(self, y, h):
return
self.edges["f"](y)
self.corner(90)
self.edges["f"](h)
self.roundedTriangle(y, 75, 25)
self.edges["f"](h)
self.corner(90)
def render(self):
"""
r : radius of lamp
w : width of surrounding ring
x : length box
y : width box
h : height box
"""
# self.edges["f"].settings = (5, 5) # XXX
x, y, h = self.x, self.y, self.h
r, w = self.radius, self.width
s = RoundedTriangleSettings(self.thickness, angle=72, r_hole=2)
self.addPart(RoundedTriangle(self, s))
self.flexSettings = (3, 5.0, 20.0)
self.edges["f"].settings.setValues(self.thickness, finger=5, space=5, relative=False)
d = 2 * (r + w)
self.roundedPlate(d, d, r, move="right", callback=[
lambda: self.hole(w, r + w, r), ])
# dist = ((2**0.5)*r-r) / (2**0.5) + 4
# pos = (w-dist, dist)
self.roundedPlate(d, d, r, holesMargin=w / 2.0) # , callback=[
# lambda: self.hole(pos[0], pos[1], 7),])
self.roundedPlate(d, d, r, move="only left up")
hole = lambda: self.hole(w, 70, 2)
self.surroundingWall(d, d, r, 120, top='h', bottom='h', callback=[
None, hole, None, hole], move="up")
with self.saved_context():
self.rectangularWall(x, y, edges="fFfF", holesMargin=5, move="right")
self.rectangularWall(x, y, edges="fFfF", holesMargin=5, move="right")
# sides
self.rectangularWall(y, h, "fftf", move="right")
self.rectangularWall(y, h, "fftf")
self.rectangularWall(x, y, edges="fFfF", holesMargin=5,
move="up only")
self.rectangularWall(x, h, edges='hFFF', holesMargin=5, move="right")
self.rectangularWall(x, h, edges='hFFF', holesMargin=5)

View File

@ -0,0 +1,200 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
from math import *
class LaptopStand(Boxes): # Change class name!
"""A simple X shaped frame to support a laptop on a given angle"""
ui_group = "Misc" # see ./__init__.py for names
def __init__(self):
Boxes.__init__(self)
self.argparser.add_argument(
"--l_depth",
action="store",
type=float,
default=250,
help="laptop depth - front to back (mm)",
)
self.argparser.add_argument(
"--l_thickness",
action="store",
type=float,
default=10,
help="laptop thickness (mm)",
)
self.argparser.add_argument(
"--angle",
action="store",
type=float,
default=15,
help="desired tilt of keyboard (deg)",
)
self.argparser.add_argument(
"--ground_offset",
action="store",
type=float,
default=10,
help="desired height between bottom of laptop and ground at lowest point (front of laptop stand)",
)
self.argparser.add_argument(
"--nub_size",
action="store",
type=float,
default=10,
help="desired thickness of the supporting edge",
)
def render(self):
calcs = self.perform_calculations()
self.laptopstand_triangles(calcs, move="up")
def perform_calculations(self):
# a
angle_rads_a = math.radians(self.angle)
# h
height = self.l_depth * math.sin(angle_rads_a)
# y
base = sqrt(2) * self.l_depth * math.cos(angle_rads_a)
# z
hyp = self.l_depth * sqrt(math.pow(math.cos(angle_rads_a), 2) + 1)
# b
angle_rads_b = math.atan(math.tan(angle_rads_a) / math.sqrt(2))
# g
base_extra = (
1
/ math.cos(angle_rads_b)
* (self.nub_size - self.ground_offset * math.sin(angle_rads_b))
)
# x
lip_outer = (
self.ground_offset / math.cos(angle_rads_b)
+ self.l_thickness
- self.nub_size * math.tan(angle_rads_b)
)
bottom_slot_depth = (height / 4) + (self.ground_offset / 2)
top_slot_depth_big = (
height / 4 + self.ground_offset / 2 + (self.thickness * height) / (2 * base)
)
top_slot_depth_small = (
height / 4 + self.ground_offset / 2 - (self.thickness * height) / (2 * base)
)
half_hyp = (hyp * (base - self.thickness)) / (2 * base)
return dict(
height=height,
base=base,
hyp=hyp,
angle=math.degrees(angle_rads_b),
base_extra=base_extra,
lip_outer=lip_outer,
bottom_slot_depth=bottom_slot_depth,
top_slot_depth_small=top_slot_depth_small,
top_slot_depth_big=top_slot_depth_big,
half_hyp=half_hyp,
)
def laptopstand_triangles(self, calcs, move=None):
tw = calcs["base"] + self.spacing + 2 * (calcs["base_extra"] + math.sin(math.radians(calcs["angle"]))*(calcs["lip_outer"]+1))
th = calcs["height"] + 2 * self.ground_offset + self.spacing
if self.move(tw, th, move, True):
return
self.moveTo(calcs["base_extra"]+self.spacing + math.sin(math.radians(calcs["angle"]))*(calcs["lip_outer"]+1))
self.draw_triangle(calcs, top=False)
self.moveTo(calcs["base"] - self.spacing,
th, 180)
self.draw_triangle(calcs, top=True)
self.move(tw, th, move)
@restore
def draw_triangle(self, calcs, top):
# Rear end
self.moveTo(0, calcs["height"] + self.ground_offset, -90)
self.edge(calcs["height"] + self.ground_offset)
self.corner(90)
foot_length = 10 + self.nub_size
base_length_without_feet = (
calcs["base"] - foot_length * 2 - 7 # -7 to account for extra width gained by 45deg angles
)
if top:
# Bottom without slot
self.polyline(
foot_length, 45,
5, -45,
base_length_without_feet, -45,
5, 45,
foot_length + calcs["base_extra"], 0,
)
else:
# Bottom with slot
self.polyline(
foot_length, 45,
5, -45,
(base_length_without_feet - self.thickness) / 2, 90,
calcs["bottom_slot_depth"] - 3.5, -90,
self.thickness, -90,
calcs["bottom_slot_depth"] - 3.5, 90,
(base_length_without_feet - self.thickness) / 2, -45,
5, 45,
foot_length + calcs["base_extra"], 0,
)
# End nub
self.corner(90 - calcs["angle"])
self.edge(calcs["lip_outer"])
self.corner(90, 1)
self.edge(self.nub_size - 2)
self.corner(90, 1)
self.edge(self.l_thickness)
self.corner(-90)
if top:
# Top with slot
self.edge(calcs["half_hyp"])
self.corner(90 + calcs["angle"])
self.edge(calcs["top_slot_depth_small"])
self.corner(-90)
self.edge(self.thickness)
self.corner(-90)
self.edge(calcs["top_slot_depth_big"])
self.corner(90 - calcs["angle"])
self.edge(calcs["half_hyp"])
else:
# Top without slot
self.edge(calcs["hyp"])
self.corner(90 + calcs["angle"])

View File

@ -0,0 +1,97 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class LaserClamp(Boxes):
"""A clamp to hold down material to a knife table"""
description = """You need a tension spring of the proper length to make the clamp work.
Increace extraheight to get more space for the spring and to make the
sliding mechanism less likely to bind. You may need to add some wax on the
parts sliding on each other to reduce friction.
"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=0)
self.argparser.add_argument(
"--minheight", action="store", type=float, default=25.,
help="minimal clamping height in mm")
self.argparser.add_argument(
"--maxheight", action="store", type=float, default=50.,
help="maximal clamping height in mm")
self.argparser.add_argument(
"--extraheight", action="store", type=float, default=0.,
help="extra height to make operation smoother in mm")
def topPart(self, l, move=None):
t = self. thickness
tw, th = 12*t, l+4*t
if self.move(tw, th, move, True):
return
self.moveTo(8*t, 0)
self.rectangularHole(t, 2*t+l/2, 1.05*t, l)
self.polyline(2*t, (90, t), l+1.5*t, (-90, 0.5*t),
2*t, -90, 0, (180, 0.5*t), 0,
(90, 1.5*t), 9*t,
(180, 4*t), 2*t, (-90, t))
self.hole(-5*t, -3*t, 2.5*t)
self.polyline(l-5.5*t, (90, t))
self.move(tw, th, move)
def bottomPart(self, h_min, h_extra, move=None):
t = self. thickness
tw, th = 14*t, h_min+4*t
if self.move(tw, th, move, True):
return
ls = t/2*(2**.5)
self.moveTo(2*t, 0)
self.fingerHolesAt(3*t, 2*t, h_min+h_extra, 90)
if h_extra:
self.polyline(4*t, (90,t), h_extra-2*t, (-90, t))
else:
self.polyline(6*t)
self.polyline(4*t, (90, 2*t), 3*t, 135, 2*ls, 45, 1*t, -90, 6*t, -90)
self.polyline(h_min, (90, t), 2*t, (90, t),
h_min+h_extra-0*t, (-90, t), t, (180, t),
0, 90, 0, (-180, 0.5*t), 0 , 90)
self.move(tw, th, move)
def render(self):
t = self. thickness
h_max, h_min, h_extra = self.maxheight, self.minheight,self.extraheight
if h_extra and h_extra < 2*t:
h_extra = 2*t
self.topPart(h_max+h_extra, move="right")
self.bottomPart(h_min, h_extra, move="right")
self.roundedPlate(4*t, h_min+h_extra+4*t, edge="e", r=t,
extend_corners=False, move="right",
callback=[lambda: self.fingerHolesAt(1*t, 2*t, h_min+h_extra)])
self.rectangularWall(1.1*t, h_min+h_extra, "efef")

View File

@ -0,0 +1,43 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class LaserHoldfast(Boxes):
"""A holdfast for honey comb tables of laser cutters"""
ui_group = "Part"
def __init__(self):
Boxes.__init__(self)
self.buildArgParser(x=25, h=40)
self.argparser.add_argument(
"--hookheight", action="store", type=float, default=5.0,
help="height of the top hook")
self.argparser.add_argument(
"--shaftwidth", action="store", type=float, default=5.0,
help="width of the shaft")
def render(self):
# adjust to the variables you want in the local scope
x, hh, h, sw = self.x, self.hookheight, self.h, self.shaftwidth
t = self.thickness
a = 30
r = x/math.radians(a)
self.polyline(hh+h, (180, sw/2), h, -90+a/2, 0, (-a, r), 0, (180, hh/2), 0, (a, r+hh), 0 , -a/2, sw-math.sin(math.radians(a/2))*hh , 90)

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class LBeam(Boxes):
"""Simple L-Beam: two pieces joined with a right angle"""
ui_group = "Part"
def __init__(self):
Boxes.__init__(self)
self.buildArgParser("x", "y", "h", "outside")
self.addSettingsArgs(edges.FingerJointSettings)
def render(self):
x, y, h = self.x, self.y, self.h
t = self.thickness
if self.outside:
x = self.adjustSize(x, False)
y = self.adjustSize(y, False)
self.rectangularWall(x, h, "eFee", move="right")
self.rectangularWall(y, h, "eeef")

View File

@ -0,0 +1,92 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
from boxes.lids import _TopEdge
class MagazinFile(Boxes):
"""Open magazine file"""
def __init__(self):
Boxes.__init__(self)
self.buildArgParser(x=100, y=200, h=300, hi=0, outside=False)
self.addSettingsArgs(edges.FingerJointSettings)
self.addSettingsArgs(edges.MountingSettings, margin=0, num=1)
self.argparser.add_argument(
"--top_edge", action="store",
type=ArgparseEdgeType("eG"), choices=list("eG"),
default="e", help="edge type for top edge")
def side(self, w, h, hi, top_edge):
r = min(h - hi, w) / 2.0
if (h - hi) > w:
r = w / 2.0
lx = 0
ly = (h - hi) - w
else:
r = (h - hi) / 2.0
lx = (w - 2 * r) / 2.0
ly = 0
top_edge = self.edges.get(top_edge, top_edge)
e_w = self.edges["F"].startwidth()
self.moveTo(3, 3)
self.edge(e_w)
self.edges["F"](w)
self.edge(e_w)
self.corner(90)
self.edge(e_w)
self.edges["F"](hi)
self.corner(90)
self.edge(e_w)
top_edge(lx)
self.corner(-90, r)
self.edge(ly)
self.corner(90, r)
top_edge(lx)
self.edge(e_w)
self.corner(90)
self.edges["F"](h)
self.edge(e_w)
self.corner(90)
def render(self):
if self.outside:
self.x = self.adjustSize(self.x)
self.y = self.adjustSize(self.y)
self.h = self.adjustSize(self.h, e2=False)
x, y, h, = self.x, self.y, self.h
self.hi = hi = self.hi or (h / 2.0)
t = self.thickness
t1, t2, t3, t4 = _TopEdge.topEdges(self, self.top_edge)
with self.saved_context():
self.rectangularWall(x, h, ["F", "f", t2, "f"], move="up")
self.rectangularWall(x, hi, "Ffef", move="up")
self.rectangularWall(x, y, "ffff")
self.rectangularWall(x, h, "Ffef", move="right only")
self.side(y, h, hi, t1)
self.moveTo(y + 15, h + hi + 15, 180)
self.side(y, h, hi, t3)

View File

@ -0,0 +1,133 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class MakitaPowerSupply(Boxes):
"""Bench power supply powered with Maktia 18V battery or laptop power supply"""
description = """
Vitamins: DSP5005 (or similar) power supply, two banana sockets, two 4.8mm flat terminals with flat soldering tag
To allow powering by laptop power supply: flip switch, Lenovo round socket (or adjust right hole for different socket)
"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.argparser.add_argument("--banana_socket_diameter", action="store",
type=float, default=8.0,
help="diameter of the banana socket mounting holes")
self.argparser.add_argument("--flipswitch_diameter", action="store",
type=float, default=6.3,
help="diameter of the flipswitch mounting hole")
def side(self, l, h=14, move=None):
t = self.thickness
tw, th = h+t, l
if self.move(tw, th, move, True):
return
self.moveTo(t, 0)
self.polyline(h, 90, l-h/3**0.5, 60, h*2/3**0.5, 120)
self.edges["f"](l)
self.move(tw, th, move)
def side2(self, l, h=14, move=None):
t = self.thickness
tw, th = h, l-10
if self.move(tw, th, move, True):
return
if h > 14:
self.polyline(h, 90, l-12, 90, h-14, 90, 50-12, -90, 8, -90)
else:
self.polyline(h, 90, l-50, 90, h-6, -90)
self.polyline(11, 90, 1, -90, 27, (90, 1),
3, (90, 1), l-12, 90)
self.move(tw, th, move)
def bottom(self):
t = self.thickness
m = self.x / 2
self.fingerHolesAt(m-30.5-0.5*t, 10, self.l)
self.fingerHolesAt(m+30.5+0.5*t, 10, self.l)
self.rectangularHole(m-19, 10+34, 0.8, 6.25)
self.rectangularHole(m+19, 10+34, 0.8, 6.25)
self.rectangularHole(m, 7.5, 35, 5)
def front(self):
d_b = self.banana_socket_diameter
d_f = self.flipswitch_diameter
self.hole(10, self.h/2, d=d_b)
self.hole(30, self.h/2, d=d_b)
self.hole(50, self.h/2, d=d_f)
self.rectangularHole(76, 6.4, 12.4, 12.4)
def back(self):
n = int((self.h-2*self.thickness) // 8)
offs = (self.h - n*8.0) / 2 + 4
for i in range(n):
self.rectangularHole(self.x/2, i*8+offs, self.x-20, 5, r=2.5)
def regulatorCB(self):
self.rectangularHole(21, 9.5, 35, 5)
self.rectangularHole(5, 33+12, 10, 10)
self.rectangularHole(42-5, 33+12, 10, 10)
for x in [3.5, 38.5]:
for y in [3.5, 65]:
self.hole(x, y, 1.0)
def render(self):
# adjust to the variables you want in the local scope
t = self.thickness
l = self.l = 64
hm = 15.5
self.x, self.y, self.h = x, y, h = 85, 75, 35
self.rectangularWall(x, h, "FFFF", callback=[self.front], move="right")
self.rectangularWall(y, h, "FfFf", move="up")
self.rectangularWall(y, h, "FfFf")
self.rectangularWall(x, h, "FFFF", callback=[self.back], move="left up")
self.rectangularWall(x, y, "ffff", callback=[self.bottom], move="right")
self.rectangularWall(x, y, "ffff", callback=[
lambda: self.rectangularHole(x/2, y-20-5, 76, 40)], move="")
self.rectangularWall(x, y, "ffff", move="left up only")
self.side(l, hm, move="right")
self.side(l, hm, move="right mirror")
self.side2(l, hm, move="right")
self.side2(l, hm, move="right mirror")

View File

@ -0,0 +1,202 @@
#!/usr/bin/env python3
# Copyright (C) 2019 Gabriel Morell
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import decimal
from boxes import Boxes, edges, boolarg
class SBCMicroRack(Boxes):
"""Stackable rackable racks for SBC Pi-Style Computers"""
webinterface = True
ui_group = "Shelf" # see ./__init__.py for names
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.addSettingsArgs(edges.StackableSettings)
self.buildArgParser(x=56, y=85)
# count
self.argparser.add_argument(
"--sbcs", action="store", type=int, default=5,
help="how many slots for sbcs",
)
# spaces
self.argparser.add_argument(
"--clearance_x", action="store", type=int, default=3,
help="clearance for the board in the box (x) in mm"
)
self.argparser.add_argument(
"--clearance_y", action="store", type=int, default=3,
help="clearance for the board in the box (y) in mm"
)
self.argparser.add_argument(
"--clearance_z", action="store", type=int, default=28,
help="SBC Clearance in mm",
)
# mounting holes
self.argparser.add_argument(
"--hole_dist_edge", action="store", type=float, default=3.5,
help="hole distance from edge in mm"
)
self.argparser.add_argument(
"--hole_grid_dimension_x", action="store", type=int, default=58,
help="width of x hole area"
)
self.argparser.add_argument(
"--hole_grid_dimension_y", action="store", type=int, default=49,
help="width of y hole area"
)
self.argparser.add_argument(
"--hole_diameter", action="store", type=float, default=2.75,
help="hole diameters"
)
# i/o holes
self.argparser.add_argument(
"--netusb_z", action="store", type=int, default=18,
help="height of the net/usb hole mm"
)
self.argparser.add_argument(
"--netusb_x", action="store", type=int, default=53,
help="width of the net/usb hole in mm"
)
# features
self.argparser.add_argument(
"--stable", action='store', type=boolarg, default=False,
help="draw some holes to put a 1/4\" dowel through at the base and top"
)
self.argparser.add_argument(
"--switch", action='store', type=boolarg, default=False,
help="adds an additional vertical segment to hold the switch in place, works best w/ --stable"
)
# TODO flesh this idea out better
#self.argparser.add_argument(
# "--fan", action='store', type=int, default=0, required=False,
# help="ensure that the x width is at least this much and as well, draw a snug holder for a fan someplace"
# )
def paint_mounting_holes(self):
cy = self.clearance_y
cx = self.clearance_x
h2r = self.hole_diameter
hde = self.hole_dist_edge
hgdx = self.hole_grid_dimension_x
hgdy = self.hole_grid_dimension_y
self.hole(
h2r + cx + hde / 2,
h2r + cy + hde / 2,
h2r / 2
)
self.hole(
h2r + cx + hgdx + hde / 2,
h2r + cy + hde / 2,
h2r / 2
)
self.hole(
h2r + cx + hde / 2,
h2r + cy + hgdy + hde / 2,
h2r / 2
)
self.hole(
h2r + cx + hgdx + hde / 2,
h2r + cy + hgdy + hde / 2,
h2r / 2
)
def paint_stable_features(self):
if self.stable:
self.hole(
10, 10, d=6.5
)
def paint_netusb_holes(self):
t = self.thickness
x = self.x
w = x + self.hole_dist_edge * 2
height_per = self.clearance_z + t
usb_height = self.netusb_z
usb_width = self.netusb_x
for i in range(self.sbcs):
self.rectangularHole(w/2, (height_per)*i+15 , usb_width, usb_height, r=1)
def paint_finger_holes(self):
t = self.thickness
height_per = self.clearance_z + t
for i in range(self.sbcs):
self.fingerHolesAt((height_per) * i + +height_per/2 + 1.5, self.hole_dist_edge, self.x, 90)
def render(self):
# adjust to the variables you want in the local scope
x, y = self.x, self.y
t = self.thickness
height_per = self.clearance_z + t
height_total = self.sbcs * height_per
# render your parts here
with self.saved_context():
self.rectangularWall(height_total + height_per/2,
x + self.hole_dist_edge * 2,
"eseS",
callback=[self.paint_finger_holes,
self.paint_netusb_holes],
move="up")
self.rectangularWall(height_total + height_per/2,
x + self.hole_dist_edge * 2,
"eseS",
callback=[self.paint_finger_holes,
self.paint_stable_features],
move="up")
if self.switch:
self.rectangularWall(height_total + height_per / 2,
x + self.hole_dist_edge * 2,
"eseS",
callback=[self.paint_stable_features],
move="up")
self.rectangularWall(height_total + height_per/2,
x + self.hole_dist_edge * 2,
"eseS",
move="right only")
self.rectangularWall(y + self.hole_dist_edge * 2,
x + self.hole_dist_edge * 2,
"efef",
move="up")
for i in range(self.sbcs):
self.rectangularWall(y + self.hole_dist_edge * 2,
x + self.hole_dist_edge * 2,
"efef",
callback=[self.paint_mounting_holes],
move="up")

View File

@ -0,0 +1,55 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2017 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class NemaMount(Boxes):
"""Mounting braket for a Nema motor"""
ui_group = "Part"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.argparser.add_argument(
"--size", action="store", type=int, default=8,
choices=list(sorted(self.nema_sizes.keys())),
help="Nema size of the motor")
def render(self):
motor, flange, holes, screws = self.nema_sizes.get(
self.size, self.nema_sizes[8])
t = self.thickness
x = y = h = motor + 2*t
self.rectangularWall(x, y, "ffef", callback=[
lambda: self.NEMA(self.size, x/2, y/2)], move="right")
self.rectangularTriangle(x, h, "fFe", num=2, move="right")
self.rectangularWall(x, h, "FFeF", callback=[
lambda:self.rectangularHole((x-holes)/2, y/2, screws, holes,
screws/2),
None,
lambda:self.rectangularHole((x-holes)/2, y/2, screws, holes,
screws/2)],
move="right")
self.moveTo(t, 0)
self.fingerHolesAt(0.5*t, t, x, 90)
self.fingerHolesAt(1.5*t+x, t, x, 90)
self.fingerHolesAt(t, 0.5*t, x, 0)

View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2017 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class NemaPattern(Boxes):
"""Mounting holes for a Nema motor"""
ui_group = "Holes"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.argparser.add_argument(
"--size", action="store", type=int, default=8,
choices=list(sorted(self.nema_sizes.keys())),
help="Nema size of the motor")
self.argparser.add_argument(
"--screwholes", action="store", type=float, default=0.0,
help="Size of the screw holes in mm - 0 for default size")
def render(self):
motor, flange, holes, screws = self.nema_sizes.get(
self.size, self.nema_sizes[8])
self.NEMA(self.size, motor/2, motor/2, screwholes=self.screwholes)

View File

@ -0,0 +1,108 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2019 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
from boxes.edges import Edge
class USlotEdge(Edge):
def __call__(self, length, bedBolts=None, bedBoltSettings=None, **kw):
l = length
d = self.settings
r = min(3*self.thickness, (l-2*d)/2)
self.edges["f"](d)
self.polyline(0, 90, 0, (-90, r), l-2*d-2*r, (-90, r), 0, 90)
self.edges["f"](d)
def margin(self):
return self.edges["f"].margin()
class HalfStackableEdge(edges.StackableEdge):
char = 'H'
def __call__(self, length, **kw):
s = self.settings
r = s.height / 2.0 / (1 - math.cos(math.radians(s.angle)))
l = r * math.sin(math.radians(s.angle))
p = 1 if self.bottom else -1
if self.bottom:
self.boxes.fingerHolesAt(0, s.height + self.settings.holedistance + 0.5 * self.boxes.thickness,
length, 0)
self.boxes.edge(s.width, tabs=1)
self.boxes.corner(p * s.angle, r)
self.boxes.corner(-p * s.angle, r)
self.boxes.edge(length - 1 * s.width - 2 * l)
def endwidth(self):
return self.settings.holedistance + self.settings.thickness
class NotesHolder(Boxes):
"""Box for holding a stack of paper, coasters etc"""
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=1)
self.addSettingsArgs(edges.StackableSettings)
self.buildArgParser(x=78, y=78, h=35, bottom_edge="s")
self.argparser.add_argument(
"--opening", action="store", type=float, default=40,
help="percent of front that's open")
def render(self):
x, y, h = self.x, self.y, self.h
t = self.thickness
o = max(0, min(self.opening, 100))
sides = x * (1-o/100) / 2
b = self.edges.get(self.bottom_edge, self.edges["F"])
if self.bottom_edge == "s":
b2 = HalfStackableEdge(self, self.edges["s"].settings,
self.edges["f"].settings)
else:
b2 = b
with self.saved_context():
self.rectangularWall(y, h, [b, "F", "e", "F"],
ignore_widths=[1, 6], move="right")
if self.opening == 0.0:
self.rectangularWall(x, h, [b, "f", "e", "f"],
ignore_widths=[1, 6], move="right")
else:
self.rectangularWall(sides, h, [b2, "e", "e", "f"],
ignore_widths=[1, 6], move="right")
self.rectangularWall(sides, h, [b2, "e", "e", "f"],
ignore_widths=[1, 6], move="right mirror")
self.rectangularWall(x, h, [b, "F", "e", "F"],
ignore_widths=[1, 6], move="up only")
with self.saved_context():
self.rectangularWall(y, h, [b, "F", "e", "F"], ignore_widths=[1, 6], move="right")
self.rectangularWall(x, h, [b, "f", "e", "f"], ignore_widths=[1, 6], move="right")
self.rectangularWall(y, h, [b, "F", "e", "F"], move="up only")
if self.bottom_edge != "e":
if self.opening == 0.0:
self.rectangularWall(x, y, ["f", "f", "f", "f"], move="up")
else:
self.rectangularWall(x, y, [USlotEdge(self, sides), "f", "f", "f"], move="up")

View File

@ -0,0 +1,47 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class OpenBox(Boxes):
"""Box with top and front open"""
ui_group = "Box"
def __init__(self):
Boxes.__init__(self)
self.buildArgParser("x", "y", "h", "outside")
self.argparser.add_argument(
"--edgetype", action="store",
type=ArgparseEdgeType("Fh"), choices=list("Fh"),
default="F",
help="edge type")
self.addSettingsArgs(edges.FingerJointSettings)
def render(self):
x, y, h = self.x, self.y, self.h
t = self.thickness
if self.outside:
x = self.adjustSize(x)
y = self.adjustSize(y, False)
h = self.adjustSize(h, False)
e = self.edgetype
self.rectangularWall(x, h, [e, e, "e", e], move="right")
self.rectangularWall(y, h, [e, "e", "e", "f"], move="up")
self.rectangularWall(y, h, [e, "e", "e", "f"])
self.rectangularWall(x, y, "efff", move="left")

View File

@ -0,0 +1,149 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2013-2018 Florian Festi
#
# Based on pipecalc by Christian F. Coors
# https://github.com/ccoors/pipecalc
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
from math import *
pitches = ['c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#' ,'b']
pressure_units = { 'Pa' : 1.0,
'mBar' : 100.,
'mmHg' : 133.322,
'mmH2O' : 9.80665,
}
class OrganPipe(Boxes): # Change class name!
"""Rectangular organ pipe based on pipecalc"""
ui_group = "Unstable" # see ./__init__.py for names
def getFrequency(self, pitch, octave, base_freq=440):
steps = pitches.index(pitch) + (octave-4)*12 - 9
return base_freq * 2**(steps/12.)
def getRadius(self, pitch, octave, intonation):
steps = pitches.index(pitch) + (octave-2)*12 + intonation
return 0.5 * 0.15555 * 0.957458**steps
def getAirSpeed(self, wind_pressure, air_density=1.2):
return (2.0 * (wind_pressure / air_density))**.5
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, finger=3.0, space=3.0,
surroundingspaces=1.0)
"""
air_temperature: f64,
"""
# Add non default cli params if needed (see argparse std lib)
self.argparser.add_argument(
"--pitch", action="store", type=str, default="c",
choices=pitches,
help="pitch")
self.argparser.add_argument(
"--octave", action="store", type=int, default=2,
help="Octave in International Pitch Notation (2 == C)")
self.argparser.add_argument(
"--intonation", action="store", type=float, default=2.0,
help="Intonation Number. 2 for max. efficiency, 3 max.")
self.argparser.add_argument(
"--mouthratio", action="store", type=float, default=0.25,
help="mouth to circumference ratio (0.1 to 0.45). Determines the width to depth ratio")
self.argparser.add_argument(
"--cutup", action="store", type=float, default=0.3,
help="Cutup to mouth ratio")
self.argparser.add_argument(
"--mensur", action="store", type=int, default=0,
help=u"Distance in halftones in the Normalmensur by Töpfer")
self.argparser.add_argument(
"--windpressure", action="store", type=float, default=588.4,
help="uses unit selected below")
self.argparser.add_argument(
"--windpressure_units", action="store", type=str, default='Pa',
choices=pressure_units.keys(),
help="in Pa")
self.argparser.add_argument(
"--stopped", action="store", type=boolarg, default=False,
help="pipe is closed at the top")
def render(self):
t = self.thickness
f = self.getFrequency(self.pitch, self.octave, 440)
self.windpressure *= pressure_units.get(self.windpressure_units, 1.0)
speed_of_sound = 343.6 # XXX util::speed_of_sound(self.air_temperature); // in m/s
air_density = 1.2
air_speed = self.getAirSpeed(self.windpressure, air_density)
i = self.intonation;
radius = self.getRadius(self.pitch, self.octave, i) * 1000
cross_section = pi * radius**2
circumference = pi * radius * 2.0
mouth_width = circumference * self.mouthratio
mouth_height = mouth_width * self.cutup
mouth_area = mouth_height * mouth_width
pipe_depth = cross_section / mouth_width
base_length = max(mouth_width, pipe_depth)
jet_thickness = (f**2 * i**2 * (.01 * mouth_height)**3) / air_speed**2
sound_power = (0.001 * pi * (air_density / speed_of_sound) * f**2
* (1.7 * (jet_thickness * speed_of_sound * f * mouth_area * mouth_area**.5)**.5)**2)
air_consumption_rate = air_speed * mouth_width * jet_thickness * 1E6;
wavelength = speed_of_sound / f * 1000;
if self.stopped:
theoretical_resonator_length = wavelength / 4.0
resonator_length = (-0.73 * (f * cross_section *1E-6 - 0.342466 * speed_of_sound * mouth_area**.5 * 1E-3)
/ (f * mouth_area**.5 * 1E-3))
else:
theoretical_resonator_length = wavelength / 2.0
resonator_length = (-0.73 * (f * cross_section * 1E-6 + 0.465753 * f * mouth_area**.5 * cross_section**.5 * 1E-6 - 0.684932 * speed_of_sound * mouth_area**.5 * 1E-3)
/ (f * mouth_area**.5 * 1E-3)) * 1E3
air_hole_diameter = 2.0 * ((mouth_width * jet_thickness * 10.0)**.5 / pi)
total_length = resonator_length + base_length
e = ["f", "e",
edges.CompoundEdge(self, "fef", (resonator_length - mouth_height - 10*t, mouth_height + 10*t, base_length)), "f"]
self.rectangularWall(total_length, pipe_depth, e, callback=[
lambda: self.fingerHolesAt(base_length-0.5*t, 0, pipe_depth-jet_thickness)],
move="up")
self.rectangularWall(total_length, pipe_depth, e, callback=[
lambda: self.fingerHolesAt(base_length-0.5*t, 0, pipe_depth-jet_thickness)],
move="up")
self.rectangularWall(total_length, mouth_width, "FeFF", callback=[
lambda: self.fingerHolesAt(base_length-0.5*t, 0, mouth_width)],
move="up")
e = [edges.CompoundEdge(self, "EF", (t*10, resonator_length - mouth_height - t*10)), 'e',
edges.CompoundEdge(self, "FE", (resonator_length - mouth_height - t*10, t*10)), 'e']
self.rectangularWall(resonator_length - mouth_height, mouth_width, e, move="up")
self.rectangularWall(base_length, mouth_width, "FeFF", move="right")
self.rectangularWall(mouth_width, pipe_depth, "fFfF", callback=[
lambda:self.hole(mouth_width/2, pipe_depth/2, d=air_hole_diameter)], move="right")
self.rectangularWall(mouth_width, pipe_depth - jet_thickness, "ffef", move="right")

View File

@ -0,0 +1,167 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
try:
from gettext import gettext as _
except ImportError:
def _(message):
return message
from boxes import *
class OttoBody(Boxes):
"""Otto LC - a laser cut chassis for Otto DIY - body"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.addSettingsArgs(edges.ChestHingeSettings)
def bottomCB(self):
self.hole(6, self.y/2, 6)
self.hole(6, self.y/2-6, 3)
self.hole(self.x-6, self.y/2, 6)
self.hole(self.x-6, self.y/2-6, 3)
#self.rectangularHole(20, self.y/2, 4, 30, 1.5)
#self.rectangularHole(self.x-20, self.y/2, 4, 30, 1.5)
self.rectangularHole(self.x/2, self.y/2, 10, 5, 1.5)
# switch
self.rectangularHole(self.x-7, self.y-2.8, 7, 4)
self.moveTo(0, self.y-12)
self.hexHolesCircle(12, HexHolesSettings(
self, diameter=2, distance=2, style='circle'))
def leftBottomCB(self):
self.hole(7, self.y-7, 6)
self.hole(6, self.y/2+9, 0.9)
self.rectangularHole(6, self.y/2-5.5, 12, 23)
self.hole(6, self.y/2-20, 0.9)
def rightBottomCB(self):
self.hole(7, self.y-5, 2)
self.hole(8, self.y/2+9, 0.9)
self.rectangularHole(8, self.y/2-5.5, 12, 23)
self.hole(8, self.y/2-20, 0.9)
def eyeCB(self):
self.hole(self.x/2+13,self.hl/2, 8)
self.hole(self.x/2-13,self.hl/2, 8)
def frontCB(self):
t = self.thickness
self.rectangularHole(0.5*t, 2+t, t, 2.5)
self.rectangularHole(self.x-0.5*t, 2+t, t, 2.5)
def IOCB(self):
self.rectangularHole(26, 18, 12, 10)
# self.rectangularHole(42.2, 10.2, 9.5, 11.5)
def buttonCB(self):
px, py = 7.5, 7.5
self.rectangularHole(px, py-2.25, 5.2, 2.5)
self.rectangularHole(px, py+2.25, 5.2, 2.5)
def PCB_Clip(self, x , y, move=None):
if self.move(x+4, y, move, True):
return
self.moveTo(1.5)
self.polyline(x-1.5, 90, (y, 2), 90, x, 85, (y-2-4, 2), -30, 2, 120, 1, -90, 2, (180, 1.), y-7, -175, y-5)
self.move(x+4, y, move)
def PCB_Clamp(self, w, s, h, move=None):
t = self. thickness
f = 2**0.5
if self.move(w+4, h+8+t, move, True):
return
self.polyline(w, 90, s, -90, 1, (90, 1), (h-s-1, 2), 90, w-2, 90,
h-8, (-180, 1), h-8+3*t, 135, f*(4), 90, f*2, -45,
(h+t, 2))
self.move(w+4, h+8+t, move)
def render(self):
self.x = x = 60.
self.y = y = 60.
self.h = h = 35.
self.hl = hl = 30.
t = self.thickness
hx = self.edges["O"].startwidth()
hx2 = self.edges["P"].startwidth()
e1 = edges.CompoundEdge(self, "Fe", (h-hx, hx))
e2 = edges.CompoundEdge(self, "eF", (hx, h-hx))
e_back = ("F", e1, "e", e2)
# sides
self.moveTo(hx)
self.rectangularWall(x, h-hx, "FfOf", ignore_widths=[2], move="up", label=_("Left bottom side"))
self.rectangularWall(x, hl-hx2, "pfFf", ignore_widths=[1], move="up", label=_("Left top side"))
self.moveTo(-hx)
self.rectangularWall(x, h-hx, "Ffof", ignore_widths=[5], callback=[
lambda: self.rectangularHole(y-7.5, h-4-7.5, 6.2, 7.)],
move="up", label=_("Right bottom side"))
self.rectangularWall(x, hl-hx2, "PfFf", ignore_widths=[6],
callback=[None, None, self.IOCB], move="up",
label=_("Right top side"))
# lower walls
self.rectangularWall(y, h, "FFeF", callback=[
None, None, self.frontCB], move="up",
label=_("Lower front"))
self.rectangularWall(y, h, e_back, move="up",
label=_("Lower back"))
# upper walls
self.rectangularWall(y, hl, "FFeF", callback=[self.eyeCB], move="up", label=_("Upper front"))
self.rectangularWall(y, hl-hx2, "FFqF", move="up", label=_("Upper back"))
# top
self.rectangularWall(x, y, "ffff", move="up", label=_("Top"))
# bottom
self.rectangularWall(x, y, "ffff", callback=[self.bottomCB], move="up", label=_("Bottom"))
# PCB mounts
with self.saved_context():
self.PCB_Clamp(y-53.5, 4.5, hl, move="right")
self.PCB_Clamp(y-50, 4.5, hl, move="right")
self.PCB_Clip(3.5, hl, move="right")
self.rectangularWall(15, 15, callback=[self.buttonCB])
self.PCB_Clamp(y-53.5, 4.5, hl, move="up only")
# servo mounts
self.moveTo(0, 50)
self.rectangularWall(y, 14, callback=[None, None, None,
self.leftBottomCB], move="up",
label=_("Servo mount"))
self.rectangularWall(y-5.6, 14, callback=[
None, None, None, self.rightBottomCB], move="up",
label=_("Servo mount"))

View File

@ -0,0 +1,151 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
from boxes import edges
class LegEdge(edges.BaseEdge):
def __call__(self, l, **kw):
d0 = (l - 12.0) /2
self.hole(l/2, 6, 3.0)
self.polyline(d0, 90, 0, (-180, 6), 0, 90, d0)
class OttoLegs(Boxes):
"""Otto LC - a laser cut chassis for Otto DIY - legs"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, finger=1.0, space=1.0,
surroundingspaces=1.0)
self.argparser.add_argument(
"--anklebolt1", action="store", type=float, default=3.0,
help="diameter for hole for ankle bolts - foot side")
self.argparser.add_argument(
"--anklebolt2", action="store", type=float, default=2.6,
help="diameter for hole for ankle bolts - leg side")
self.argparser.add_argument(
"--length", action="store", type=float, default=34.0,
help="length of legs (34mm min)")
def foot(self, x, y, ly, l, r=5., move=None):
if self.move(x, y, move, True):
return
t = self.thickness
w = ly + 5.5 + 2 * t
self.fingerHolesAt(x/2 - w/2, 0, l, 90)
self.fingerHolesAt(x/2 + w/2, 0, l, 90)
self.moveTo(r, 0)
for l in (x, y, x, y):
self.polyline((l - 2*r, 2), 45, r*2**0.5, 45)
self.move(x, y, move)
def ankles(self, x, h, edge="f", callback=None, move=None):
f = 0.5
tw = x
th = 2 * h + self.thickness
if self.move(tw, th, move, True):
return
self.moveTo(0, self.thickness)
for i in range(2):
self.cc(callback, 0)
self.edges[edge](x)
self.polyline(0, 90)
self.cc(callback, 1)
self.polyline((h, 2), 90, (f*x, 1), 45, ((2**0.5)*(1-f)*x, 1), 45,
(h-(1-f)*x, 1), 90)
self.moveTo(tw, th, 180)
self.ctx.stroke()
self.move(tw, th, move)
def ankle1(self):
# from vertical edge
self.hole(15, 10, 3.45) # 3.45 for servo arm, 2.3 for knob
def servoring(self, move=""):
if self.move(20, 20, move, True):
return
self.moveTo(10, 10, 90)
self.moveTo(3.45, 0, -90)
self.polyline(0, (-264, 3.45), 0, 36, 6.55, 108, 0, (330, 9.0, 4), 0, 108, 6.55)
self.move(20, 20, move)
def ankle2(self):
# from vertical edge
self.hole(15, 10, self.anklebolt1/2)
def servoHole(self):
self.hole(6, 6, 11.6/2)
self.hole(6, 12, 5.5/2)
def render(self):
# adjust to the variables you want in the local scope
t = self.thickness
ws = 25
lx, ly, lh = 12.4, 23.5, max(self.length, ws+6+t)
self.ctx.save()
# Legs
c1 = edges.CompoundEdge(self, "FE", (ly-7.0, 7.0))
c2 = edges.CompoundEdge(self, "EF", (7.0, lh-7.0))
e = [c1, c2, "F", "F"]
for i in range(2):
# front
self.rectangularWall(lx, lh-7., [LegEdge(self, None), "f", "F", "f"], callback=[None, lambda:self.fingerHolesAt(ws-7., 0, lx)], move="right")
# back
self.rectangularWall(lx, lh, "FfFf", callback=[
lambda:self.hole(lx/2, 7, self.anklebolt2/2)], move="right")
# sides
self.rectangularWall(ly, lh, e, callback=[None,
lambda:self.fingerHolesAt(ws, 7.0, ly-7.0-3.0)], move="right")
self.rectangularWall(ly, lh, e, callback=[
lambda:self.rectangularHole(ly/2, ws+3+0.5*t, 12, 6, 3),
lambda:self.fingerHolesAt(ws, 7.0, ly-7.0-3.0)], move="right")
# top
self.partsMatrix(2, 1, "right", self.rectangularWall, ly, lx, "ffff",
callback=[None, lambda: self.hole(lx/2, ly/2, 2.3)])
self.partsMatrix(2, 1, "right", self.rectangularWall, lx, ly, "eeee", callback=[lambda: self.hole(lx/2, ly/2, 1.5)])
# hold servo at the front
self.partsMatrix(2, 1, "right", self.rectangularWall, 4.6, lx, "efee")
# bottom
self.partsMatrix(2, 1, "right", self.rectangularWall, lx, ly-7.0, "efff")
# hold servo inside
self.partsMatrix(2, 1, "right", self.rectangularWall, lx, ly-7.0-3.0, "efef")
self.ctx.restore()
self.rectangularWall(lx, lh, "ffff", move="up only")
# feet
self.foot(60, 40, ly, 30, move="right")
self.foot(60, 40, ly, 30, move="right")
self.ankles(30, 25, callback=[None, self.ankle1], move="right")
self.ankles(30, 25, callback=[None, self.ankle2], move="right")
self.partsMatrix(2, 2, "right", self.servoring)

View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2018 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class OttoSoles(Boxes):
"""Foam soles for the OttO bot"""
ui_group = "Misc"
def __init__(self):
Boxes.__init__(self)
self.buildArgParser(x=58., y=38.)
self.argparser.add_argument(
"--width", action="store", type=float, default=4.,
help="width of sole stripe")
self.argparser.add_argument(
"--chamfer", action="store", type=float, default=5.,
help="chamfer at the corners")
self.argparser.add_argument(
"--num", action="store", type=int, default=2,
help="number of soles")
def render(self):
x, y = self.x, self.y
c = self.chamfer
c2 = c * 2**0.5
w = min(self.width, c2 / 2. / math.tan(math.radians(22.5)))
w = self.width
w2 = w * 2**0.5 - c2 / 2
d = w * math.tan(math.radians(22.5))
self.edges["d"].settings.setValues(w, size=0.4, depth=0.3,
radius=0.05)
self.moveTo(0, y, -90)
for i in range(self.num*2):
if c2 >= 2 * d:
self.polyline((c2, 1), 45, (y-2*c, 1), 45, c2/2., 90)
self.edges["d"](w)
self.polyline(0, 90, c2/2-d, -45, (y-2*c-2*d, 1), -45,
(c2-2*d, 1), -45,
(x-2*c-2*d, 1), -45, c2/2-d, 90)
self.edges["D"](w)
self.polyline(0, 90, c2/2., 45, (x-2*c, 1), 45)
self.moveTo(0, w + c2/2. + 2*2**0.5*self.burn)
else:
self.polyline((c2, 1), 45, (y-2*c, 1), 45, c2/2., 90)
self.edges["d"](w2)
self.polyline(0, 45, (y-2*w, 1), -90, (x-2*w, 1), 45)
self.edges["D"](w2)
self.polyline(0, 90, c2/2., 45, (x-2*c, 3), 45)
self.moveTo(0, w * 2**0.5 + 2*2**0.5*self.burn)

View File

@ -0,0 +1,135 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import Boxes, edges, boolarg
class PaintStorage(Boxes):
"""Stackable storage for hobby paint or other things"""
webinterface = True
ui_group = "Shelf" # see ./__init__.py for names
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.addSettingsArgs(edges.StackableSettings)
self.buildArgParser(x=100, y=300)
self.argparser.add_argument(
"--canheight", action="store", type=int, default=50,
help="Height of the paintcans")
self.argparser.add_argument(
"--candiameter", action="store", type=int, default=30,
help="Diameter of the paintcans")
self.argparser.add_argument(
"--minspace", action="store", type=int, default=10,
help="Minimum space between the paintcans")
self.argparser.add_argument(
"--hexpattern", action="store", type=boolarg, default=False,
help="Use hexagonal arrangement for the holes instead of orthogonal")
self.argparser.add_argument(
"--drawer", action="store", type=boolarg, default=False,
help="Create a stackable drawer instead")
def paintholes(self):
"Place holes for the paintcans evenly"
if self.hexpattern:
self.moveTo(self.minspace/2, self.minspace/2)
settings = self.hexHolesSettings
settings.diameter = self.candiameter
settings.distance = self.minspace
settings.style = 'circle'
self.hexHolesRectangle(self.y - 1*self.minspace,
self.x - 1*self.minspace,
settings)
return
n_x = int(self.x / (self.candiameter+self.minspace))
n_y = int(self.y / (self.candiameter+self.minspace))
if n_x <= 0 or n_y <= 0:
return
spacing_x = (self.x - n_x*self.candiameter)/n_x
spacing_y = (self.y - n_y*self.candiameter)/n_y
for i in range(n_y):
for j in range(n_x):
self.hole(i * (self.candiameter+spacing_y) + (self.candiameter+spacing_y)/2,
j * (self.candiameter+spacing_x) + (self.candiameter+spacing_x)/2,
self.candiameter/2)
def render(self):
# adjust to the variables you want in the local scope
x, y = self.x, self.y
t = self.thickness
stack = self.edges['s'].settings
h = self.canheight - stack.height - stack.holedistance + t
hx = 1/2.*x
hh = h/4.
hr = min(hx, hh) / 2
if not self.drawer:
wall_keys = "EsES"
wall_callbacks = [
lambda: self.rectangularHole(h / 3, (x / 2.0) - t, hh, hx, r=hr),
lambda: self.fingerHolesAt(0, self.canheight / 3, x, 0),
]
bottom_keys = "EfEf"
else:
wall_keys = "FsFS"
wall_callbacks = [
lambda: self.rectangularHole(h / 3, (x / 2.0) - t, hh, hx, r=hr)
]
bottom_keys = "FfFf"
# Walls
self.rectangularWall(
h, x - 2 * t, wall_keys,
ignore_widths=[1, 2, 5, 6],
callback=wall_callbacks, move="up",
)
self.rectangularWall(
h, x - 2 * t, wall_keys,
ignore_widths=[1, 2, 5, 6],
callback=wall_callbacks, move="right"
)
# Plates
self.rectangularWall(
0.8 * stack.height + stack.holedistance, x, "eeee", move=""
)
self.rectangularWall(
0.8 * stack.height + stack.holedistance, x, "eeee", move="down right"
)
# Bottom
self.rectangularWall(
y, x-2*t, bottom_keys, ignore_widths=[1, 2, 5, 6], move="up"
)
if not self.drawer:
# Top
self.rectangularWall(y, x, "efef", callback=[self.paintholes], move="up")
else:
# Sides
self.rectangularWall(y, h, "efff", move="up")
self.rectangularWall(y, h, "efff", move="up")

View File

@ -0,0 +1,277 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2020 Guillaume Collic
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import math
from boxes import Boxes
class PaperBox(Boxes):
"""
Box made of paper, with lid.
"""
ui_group = "Misc"
description = """
This box is made of paper.
There is marks in the "outside leftover paper" to help see where to fold
(cutting with tabs helps use them). The cut is very precise, and could be too tight if misaligned when glued. A plywood box (such as a simple TypeTray) of the same size is a great guide during folding and glueing. Just fold the box against it. Accurate quick and easy.
A paper creaser (or bone folder) is also useful.
"""
def __init__(self):
Boxes.__init__(self)
self.buildArgParser("x", "y", "h")
self.argparser.add_argument(
"--design",
action="store",
type=str,
default="automatic",
choices=("automatic", "widebox", "tuckbox"),
help="different design for paper consumption optimization. The tuckbox also has locking cut for its lid.",
)
self.argparser.add_argument(
"--lid_heigth",
type=float,
default=15,
help="Height of the lid (part which goes inside the box)",
)
self.argparser.add_argument(
"--lid_radius",
type=float,
default=7,
help="Angle, in radius, of the round corner of the lid",
)
self.argparser.add_argument(
"--lid_sides",
type=float,
default=20,
help="Width of the two sides upon which goes the lid",
)
self.argparser.add_argument(
"--margin",
type=float,
default=0,
help="Margin for the glued sides",
)
self.argparser.add_argument(
"--mark_length",
type=float,
default=1.5,
help="Length of the folding outside mark",
)
self.argparser.add_argument(
"--tab_angle_rad",
type=float,
default=math.atan(2 / 25),
help="Angle (in radian) of the sides which are to be glued inside the box",
)
self.argparser.add_argument(
"--finger_hole_diameter",
type=float,
default=15,
help="Diameter of the hole to help catch the lid",
)
def render(self):
if self.design == "automatic":
self.design = "tuckbox" if self.h > self.y else "widebox"
path = (
self.tuckbox(self.x, self.y, self.h)
if self.design == "tuckbox"
else self.widebox(self.x, self.y, self.h)
)
self.polyline(*path)
def tuckbox(self, width, length, height):
lid_cut_length = min(10, length / 2, width / 5)
half_side = (
self.mark(self.mark_length)
+ [
0,
90,
]
+ self.ear_description(length, lid_cut_length)
+ [
0,
-90,
length,
0,
]
+ self.lid_cut(lid_cut_length)
+ self.lid(width - 2 * self.thickness)
+ [0]
+ self.lid_cut(lid_cut_length, reverse=True)
+ [
length,
-90,
]
+ self.ear_description(length, lid_cut_length, reverse=True)
+ self.mark(self.mark_length)
)
return (
[height, 0]
+ half_side
+ self.side_with_finger_hole(width, self.finger_hole_diameter)
+ self.mark(self.mark_length)
+ [
0,
90,
]
+ self.tab_description(length - self.margin - self.thickness, height)
+ [
0,
90,
]
+ self.mark(self.mark_length)
+ [width]
+ list(reversed(half_side))
)
def widebox(self, width, length, height):
half_side = (
self.mark(self.mark_length)
+ [
0,
90,
]
+ self.tab_description(length / 2 - self.margin, height)
+ [
0,
-90,
height,
0,
]
+ self.mark(self.mark_length)
+ [
0,
90,
]
+ self.tab_description(self.lid_sides, length)
+ [
0,
90,
]
+ self.mark(self.mark_length)
+ [
height,
-90,
]
+ self.tab_description(length / 2 - self.margin, height)
+ [
length,
0,
]
+ self.mark(self.mark_length)
)
return (
self.side_with_finger_hole(width, self.finger_hole_diameter)
+ half_side
+ self.lid(width)
+ list(reversed(half_side))
)
def lid(self, width):
return [
self.lid_heigth - self.lid_radius,
(90, self.lid_radius),
width - 2 * self.lid_radius,
(90, self.lid_radius),
self.lid_heigth - self.lid_radius,
]
def mark(self, length):
if length == 0:
return []
return [
0,
-90,
length,
180,
length,
-90,
]
def lid_cut(self, length, reverse=False):
path = [
90,
length + self.thickness,
-180,
length,
90,
]
return [0] + (list(reversed(path)) if reverse else path)
def side_with_finger_hole(self, width, finger_hole_diameter):
half_width = (width - finger_hole_diameter) / 2
return [
half_width,
90,
0,
(-180, finger_hole_diameter / 2),
0,
90,
half_width,
0,
]
def tab_description(self, height, width):
deg = math.degrees(self.tab_angle_rad)
side = height / math.cos(self.tab_angle_rad)
end_width = width - 2 * height * math.tan(self.tab_angle_rad)
return [
0,
deg - 90,
side,
90 - deg,
end_width,
90 - deg,
side,
deg - 90,
]
def ear_description(self, length, lid_cut_length, reverse=False):
ear_depth = max(lid_cut_length, self.lid_heigth)
radius = min(self.lid_radius, ear_depth - lid_cut_length)
start_margin = self.thickness
end_margin = 2 * self.burn
path = [
start_margin,
-90,
lid_cut_length,
90,
0,
(-90, radius),
0,
90,
length - radius - start_margin - end_margin,
90,
ear_depth,
-90,
end_margin,
]
return (list(reversed(path)) if reverse else path) + [0]

View File

@ -0,0 +1,306 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2021 Guillaume Collic
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import math
from functools import partial
from boxes import Boxes, edges
class PhoneHolder(Boxes):
"""
Smartphone desk holder
"""
ui_group = "Misc"
description = """
This phone stand holds your phone between two tabs, with access to its
bottom, in order to connect a charger, headphones, and also not to obstruct
the mic.
Default values are currently based on Galaxy S7.
"""
def __init__(self):
Boxes.__init__(self)
self.argparser.add_argument(
"--phone_height",
type=float,
default=142,
help="Height of the phone.",
)
self.argparser.add_argument(
"--phone_width",
type=float,
default=73,
help="Width of the phone.",
)
self.argparser.add_argument(
"--phone_depth",
type=float,
default=11,
help=(
"Depth of the phone. Used by the bottom support holding the "
"phone, and the side tabs depth as well. Should be at least "
"your material thickness for assembly reasons."
),
)
self.argparser.add_argument(
"--angle",
type=float,
default=25,
help="angle at which the phone stands, in degrees. 0° is vertical.",
)
self.argparser.add_argument(
"--bottom_margin",
type=float,
default=30,
help="Height of the support below the phone",
)
self.argparser.add_argument(
"--tab_size",
type=float,
default=76,
help="Length of the tabs holding the phone",
)
self.argparser.add_argument(
"--bottom_support_spacing",
type=float,
default=16,
help=(
"Spacing between the two bottom support. Choose a value big "
"enough for the charging cable, without getting in the way of "
"other ports."
),
)
self.addSettingsArgs(edges.FingerJointSettings)
def render(self):
self.h = self.phone_height + self.bottom_margin
tab_start = self.bottom_margin
tab_length = self.tab_size
tab_depth = self.phone_depth
support_depth = self.phone_depth
support_spacing = self.bottom_support_spacing
rad = math.radians(self.angle)
self.stand_depth = self.h * math.sin(rad)
self.stand_height = self.h * math.cos(rad)
self.render_front_plate(tab_start, tab_length, support_spacing, move="right")
self.render_back_plate(move="right")
self.render_side_plate(tab_start, tab_length, tab_depth, move="right")
for move in ["right mirror", "right"]:
self.render_bottom_support(tab_start, support_depth, tab_length, move=move)
def render_front_plate(
self,
tab_start,
tab_length,
support_spacing,
support_fingers_length=None,
move="right",
):
if not support_fingers_length:
support_fingers_length = tab_length
be = BottomEdge(self, tab_start, support_spacing)
se1 = SideEdge(self, tab_start, tab_length)
se2 = SideEdge(self, tab_start, tab_length, reverse=True)
self.rectangularWall(
self.phone_width,
self.h,
[be, se1, "e", se2],
move=move,
callback=[
partial(
lambda: self.front_plate_holes(
tab_start, support_fingers_length, support_spacing
)
)
],
)
def render_back_plate(
self,
move="right",
):
be = BottomEdge(self, 0, 0)
self.rectangularWall(
self.phone_width,
self.stand_height,
[be, "F", "e", "F"],
move=move,
)
def front_plate_holes(
self, support_start_height, support_fingers_length, support_spacing
):
margin = (self.phone_width - support_spacing - self.thickness) / 2
self.fingerHolesAt(
margin,
support_start_height,
support_fingers_length,
)
self.fingerHolesAt(
self.phone_width - margin,
support_start_height,
support_fingers_length,
)
def render_side_plate(self, tab_start, tab_length, tab_depth, move):
te = TabbedEdge(self, tab_start, tab_length, tab_depth, reverse=True)
self.rectangularTriangle(
self.stand_depth,
self.stand_height,
["e", "f", te],
move=move,
num=2,
)
def render_bottom_support(
self, support_start_height, support_depth, support_fingers_length, move="right"
):
full_height = support_start_height + support_fingers_length
rad = math.radians(self.angle)
floor_length = full_height * math.sin(rad)
angled_height = full_height * math.cos(rad)
bottom_radius = min(support_start_height, 3 * self.thickness + support_depth)
smaller_radius = 0.5
support_hook_height = 5
full_width = floor_length + (support_depth + 3 * self.thickness) * math.cos(rad)
if self.move(full_width, angled_height, move, True):
return
self.polyline(
floor_length,
self.angle,
3 * self.thickness + support_depth - bottom_radius,
(90, bottom_radius),
support_hook_height + support_start_height - bottom_radius,
(180, self.thickness),
support_hook_height - smaller_radius,
(-90, smaller_radius),
self.thickness + support_depth - smaller_radius,
-90,
)
self.edges["f"](support_fingers_length)
self.polyline(
0,
180 - self.angle,
angled_height,
90,
)
# Move for next piece
self.move(full_width, angled_height, move)
class BottomEdge(edges.BaseEdge):
def __init__(self, boxes, support_start_height, support_spacing):
super().__init__(boxes, None)
self.support_start_height = support_start_height
self.support_spacing = support_spacing
def __call__(self, length, **kw):
cable_hole_radius = 2.5
self.support_spacing = max(self.support_spacing, 2 * cable_hole_radius)
side = (length - self.support_spacing - 2 * self.thickness) / 2
half = [
side,
90,
self.support_start_height,
-90,
self.thickness,
-90,
self.support_start_height,
90,
self.support_spacing / 2 - cable_hole_radius,
90,
2 * cable_hole_radius,
]
path = half + [(-180, cable_hole_radius)] + list(reversed(half))
self.polyline(*path)
class SideEdge(edges.BaseEdge):
def __init__(self, boxes, tab_start, tab_length, reverse=False):
super().__init__(boxes, None)
self.tab_start = tab_start
self.tab_length = tab_length
self.reverse = reverse
def __call__(self, length, **kw):
tab_start = self.tab_start
tab_end = length - self.tab_start - self.tab_length
if self.reverse:
tab_start, tab_end = tab_end, tab_start
self.edges["F"](tab_start)
self.polyline(
0,
90,
self.thickness,
-90,
)
self.edges["f"](self.tab_length)
self.polyline(0, -90, self.thickness, 90)
self.edges["F"](tab_end)
def startwidth(self):
return self.boxes.thickness
class TabbedEdge(edges.BaseEdge):
def __init__(self, boxes, tab_start, tab_length, tab_depth, reverse=False):
super().__init__(boxes, None)
self.tab_start = tab_start
self.tab_length = tab_length
self.tab_depth = tab_depth
self.reverse = reverse
def __call__(self, length, **kw):
tab_start = self.tab_start
tab_end = length - self.tab_start - self.tab_length
if self.reverse:
tab_start, tab_end = tab_end, tab_start
self.edges["f"](tab_start)
self.ctx.save()
self.fingerHolesAt(0, -self.thickness / 2, self.tab_length, 0)
self.ctx.restore()
self.polyline(
0,
-90,
self.thickness,
(90, self.tab_depth),
self.tab_length - 2 * self.tab_depth,
(90, self.tab_depth),
self.thickness,
-90,
)
self.edges["f"](tab_end)
def margin(self):
return self.tab_depth + self.thickness

View File

@ -0,0 +1,107 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import math
class Planetary(Boxes):
"""Planetary Gear with possibly multiple identical stages"""
ui_group = "Part"
def __init__(self):
Boxes.__init__(self)
self.argparser.add_argument(
"--sunteeth", action="store", type=int, default=8,
help="number of teeth on sun gear")
self.argparser.add_argument(
"--planetteeth", action="store", type=int, default=20,
help="number of teeth on planets")
self.argparser.add_argument(
"--maxplanets", action="store", type=int, default=0,
help="limit the number of planets (0 for as much as fit)")
self.argparser.add_argument(
"--deltateeth", action="store", type=int, default=0,
help="enable secondary ring with given delta to the ring gear")
self.argparser.add_argument(
"--modulus", action="store", type=float, default=3,
help="modulus of the theeth in mm")
self.argparser.add_argument(
"--shaft", action="store", type=float, default=6.,
help="diameter of the shaft")
# self.argparser.add_argument(
# "--stages", action="store", type=int, default=4,
# help="number of stages in the gear reduction")
def render(self):
ringteeth = self.sunteeth + 2 * self.planetteeth
spoke_width = 3 * self.shaft
pitch1, size1, xxx = self.gears.sizes(teeth=self.sunteeth,
dimension=self.modulus)
pitch2, size2, xxx = self.gears.sizes(teeth=self.planetteeth,
dimension=self.modulus)
pitch3, size3, xxx = self.gears.sizes(
teeth=ringteeth, internal_ring=True, spoke_width=spoke_width,
dimension=self.modulus)
t = self.thickness
planets = int(math.pi / (math.asin(float(self.planetteeth + 2) / (self.planetteeth + self.sunteeth))))
if self.maxplanets:
planets = min(self.maxplanets, planets)
# Make sure the teeth mash
ta = self.sunteeth + ringteeth
# There are sunteeth+ringteeth mashing positions for the planets
if ta % planets:
planetpositions = [round(i * ta / planets) * 360 / ta for i in range(planets)]
else:
planetpositions = planets
# XXX make configurable?
profile_shift = 20
pressure_angle = 20
self.parts.disc(size3, callback=lambda: self.hole(0, 0, self.shaft / 2), move="up")
self.gears(teeth=ringteeth, dimension=self.modulus,
angle=pressure_angle, internal_ring=True,
spoke_width=spoke_width, mount_hole=self.shaft,
profile_shift=profile_shift, move="up")
self.gears.gearCarrier(pitch1 + pitch2, spoke_width, planetpositions,
2 * spoke_width, self.shaft / 2, move="up")
self.gears(teeth=self.sunteeth, dimension=self.modulus,
angle=pressure_angle,
mount_hole=self.shaft, profile_shift=profile_shift, move="up")
numplanets = planets
if self.deltateeth:
numplanets += planets
deltamodulus = self.modulus * ringteeth / (ringteeth - self.deltateeth)
self.gears(teeth=ringteeth - self.deltateeth, dimension=deltamodulus,
angle=pressure_angle, internal_ring=True,
spoke_width=spoke_width, mount_hole=self.shaft,
profile_shift=profile_shift, move="up")
for i in range(numplanets):
self.gears(teeth=self.planetteeth, dimension=self.modulus,
angle=pressure_angle,
mount_hole=self.shaft, profile_shift=profile_shift, move="up")

View File

@ -0,0 +1,210 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
import math
class Planetary2(Boxes):
"""Balanced force Difference Planetary Gear (not yet working properly)"""
ui_group = "Unstable"
description = """Still has issues. The middle planetary gears set must not have a mashing sun gear as it can't be a proper gear set."""
def __init__(self):
Boxes.__init__(self)
self.buildArgParser("nema_mount")
self.argparser.add_argument(
"--profile", action="store", type=str, default="GT2_2mm",
choices=pulley.Pulley.getProfiles(),
help="profile of the teeth/belt")
self.argparser.add_argument(
"--sunteeth", action="store", type=int, default=20,
help="number of teeth on sun gear")
self.argparser.add_argument(
"--planetteeth", action="store", type=int, default=20,
help="number of teeth on planets")
self.argparser.add_argument(
"--maxplanets", action="store", type=int, default=0,
help="limit the number of planets (0 for as much as fit)")
self.argparser.add_argument(
"--deltateeth", action="store", type=int, default=1,
help="enable secondary ring with given delta to the ring gear")
self.argparser.add_argument(
"--modulus", action="store", type=float, default=1.0,
help="modulus of the theeth in mm")
self.argparser.add_argument(
"--shaft", action="store", type=float, default=6.,
help="diameter of the shaft")
self.argparser.add_argument(
"--screw1", action="store", type=float, default=2.4,
help="diameter of lower part of the screw hole")
self.argparser.add_argument(
"--screw2", action="store", type=float, default=4.,
help="diameter of upper part of the screw hole")
self.argparser.add_argument(
"--pinsize", action="store", type=float, default=3.1,
help="diameter of alignment pins")
# self.argparser.add_argument(
# "--stages", action="store", type=int, default=4,
# help="number of stages in the gear reduction")
def pins(self, r, rh, nr=0, angle=0.0):
self.moveTo(0, 0, angle)
if nr < 8:
ang = 20 + 10 * nr
else:
ang = 15 + 10 * (nr-8)
ang = 180 - ang
for a in (0, ang, -ang):
self.moveTo(0, 0, a)
self.hole(r, 0, rh)
self.moveTo(0, 0, -a)
def render(self):
ringteeth = self.sunteeth + 2 * self.planetteeth
t = self.thickness
spoke_width = 4 * t
pinsize = self.pinsize / 2.
pitch1, size1, xxx = self.gears.sizes(teeth=self.sunteeth,
dimension=self.modulus)
pitch2, size2, xxx = self.gears.sizes(teeth=self.planetteeth,
dimension=self.modulus)
pitch3, size3, xxx = self.gears.sizes(
teeth=ringteeth, internal_ring=True, spoke_width=spoke_width,
dimension=self.modulus)
planets = int(math.pi / (math.asin(float(self.planetteeth + 2) / (self.planetteeth + self.sunteeth))))
if self.maxplanets:
planets = min(self.maxplanets, planets)
# Make sure the teeth mash
ta = self.sunteeth + ringteeth
# There are sunteeth+ringteeth mashing positions for the planets
planetpositions = [round(i * ta / planets) * 360 / ta for i in range(planets)]
secondary_offsets = [((pos % (360. / (ringteeth - self.deltateeth))) -
(pos % (360. / ringteeth)) * ringteeth / self.planetteeth)
for pos in planetpositions]
ratio = (1 + (ringteeth / self.sunteeth)) * (-ringteeth/self.deltateeth)
# XXX make configurable?
profile_shift = 20
pressure_angle = 20
screw = self.screw1 / 2
# output
# XXX simple guess
belt = self.profile
pulleyteeth = int((size3-2*t) * math.pi / pulley.Pulley.spacing[belt][1])
numplanets = planets
deltamodulus = self.modulus * ringteeth / (ringteeth - self.deltateeth)
def holes(r):
def h():
self.hole(2*t, 2*t, r)
self.hole(size3-2*t, 2*t, r)
self.hole(2*t, size3-2*t, r)
self.hole(size3-2*t, size3-2*t, r)
return h
def planets():
self.moveTo(size3/2, size3/2)
for angle in planetpositions:
angle += 180 # compensate for 3 postion in callback
self.moveTo(0, 0, angle)
self.hole((pitch1+pitch2), 0, size2/2)
self.moveTo(0, 0, -angle)
# Base
self.rectangularWall(size3, size3, callback=[
lambda: self.NEMA(self.nema_mount, size3 / 2, size3 / 2),
holes(screw), planets],
move="up")
def gear():
self.moveTo(size3 / 2, size3 / 2)
self.gears(teeth=ringteeth, dimension=self.modulus,
angle=pressure_angle, internal_ring=True,
spoke_width=spoke_width, teeth_only=True,
profile_shift=profile_shift, move="up")
# Lower primary ring gear
self.rectangularWall(size3, size3, callback=[gear, holes(screw)], move="up")
tl = 0.5*size3*(2**0.5-1)*2**0.5
screw = self.screw2 / 2
self.rectangularTriangle(tl, tl, num=8, callback=[
None, lambda:self.hole(2*t, 2*t, screw)], move='up')
# Secondary ring gears
def ring():
self.gears(teeth=ringteeth - self.deltateeth,
dimension=deltamodulus,
angle=pressure_angle, internal_ring=True,
spoke_width=spoke_width, teeth_only=True,
profile_shift=profile_shift)
for i in range(3):
self.hole((size3-6*t)/2+0.5*pinsize, 0, pinsize)
self.moveTo(0, 0, 120)
self.pulley(pulleyteeth, belt, callback=ring, move="up")
self.pulley(pulleyteeth, belt, callback=ring, move="up")
# Upper primary ring gear
self.rectangularWall(size3, size3, callback=[gear, holes(screw)], move="up")
# top cover plate
self.rectangularWall(size3, size3, callback=[holes(screw)], move="up")
# Sun gear
def sunpins():
self.hole(0.5*self.shaft+1.5*pinsize ,0, pinsize)
self.hole(-0.5*self.shaft-1.5*pinsize ,0, pinsize)
self.partsMatrix(4, 4, 'up', self.gears, teeth=self.sunteeth,
dimension=self.modulus, callback=sunpins,
angle=pressure_angle, mount_hole=self.shaft,
profile_shift=profile_shift)
# Planets
for i in range(numplanets):
with self.saved_context():
self.gears(teeth=self.planetteeth, dimension=self.modulus,
angle=pressure_angle,
callback=lambda:self.pins(0.25*size2, pinsize, i),
profile_shift=profile_shift, move="right")
for j in range(2):
self.gears(teeth=self.planetteeth, dimension=self.modulus,
angle=pressure_angle,
callback=lambda:self.pins(0.25*size2, pinsize, i,
secondary_offsets[i]),
profile_shift=profile_shift, move="right")
self.gears(teeth=self.planetteeth, dimension=self.modulus,
angle=pressure_angle,
callback=lambda:self.pins(0.25*size2, pinsize, i),
profile_shift=profile_shift, move="right")
self.gears(teeth=self.planetteeth, dimension=self.modulus,
angle=pressure_angle,
profile_shift=profile_shift, move="up only")
self.text("1:%.1f" % abs(ratio))

View File

@ -0,0 +1,128 @@
#!/usr/bin/env python3
# Copyright (C) 2020 Norbert Szulc
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
from boxes.edges import FingerJointBase, FingerJointEdge
from math import sin, pi
class UnevenFingerJointEdge(FingerJointEdge):
"""Uneven finger joint edge """
char = 'u'
description = "Uneven Finger Joint"
positive = True
def __call__(self, length, bedBolts=None, bedBoltSettings=None, **kw):
# copied from original
positive = self.positive
s, f, thickness = self.settings.space, self.settings.finger, self.settings.thickness
p = 1 if positive else -1
fingers, leftover = self.calcFingers(length, bedBolts)
if not positive:
play = self.settings.play
f += play
s -= play
leftover -= play
shift = (f + s) / 2 # we shift all fingers to make them un even
if (leftover < shift):
leftover = shift
self.edge((leftover + shift)/2, tabs=1) # Whole point of this class
l1,l2 = self.fingerLength(self.settings.angle)
h = l1-l2
d = (bedBoltSettings or self.bedBoltSettings)[0]
for i in range(fingers):
if i != 0:
if not positive and bedBolts and bedBolts.drawBolt(i):
self.hole(0.5 * s,
0.5 * self.settings.thickness, 0.5 * d)
if positive and bedBolts and bedBolts.drawBolt(i):
self.bedBoltHole(s, bedBoltSettings)
else:
self.edge(s)
if positive and self.settings.style == "springs":
self.polyline(
0, -90 * p, 0.8*h, (90 * p, 0.2*h),
0.1 * h, 90, 0.9*h, -180, 0.9*h, 90,
f - 0.6*h,
90, 0.9*h, -180, 0.9*h, 90, 0.1*h,
(90 * p, 0.2 *h), 0.8*h, -90 * p)
else:
self.polyline(0, -90 * p, h, 90 * p, f, 90 * p, h, -90 * p)
self.edge((leftover - shift)/2, tabs=1) # Whole point of this class
# Unstable
class UnevenFingerJointEdgeCounterPart(UnevenFingerJointEdge):
"""Uneven finger joint edge - other side"""
char = 'U'
description = "Uneven Finger Joint (opposing side)"
positive = False
class Platonic(Boxes):
"""Platonic solids generator"""
ui_group = "Unstable" # see ./__init__.py for names
description = """![Icosahedron](static/samples/Platonic-Icosahedron.jpg)
"""
SOLIDS = {
"tetrahedron": (4, 3),
"cube": (6, 4),
"octahedron": (8, 3),
"dodecahedron": (12, 5),
"icosahedro": (20, 3),
}
def __init__(self):
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=0)
self.buildArgParser(x=60, outside=True) # x should be treated as edge length, TODO: change that
self.argparser.add_argument(
"--type", action="store", type=str, default=list(self.SOLIDS)[0],
choices=list(self.SOLIDS),
help="type of platonic solid")
def render(self):
# adjust to the variables you want in the local scope
e = self.x
t = self.thickness
faces, corners = self.SOLIDS[self.type]
u = UnevenFingerJointEdge(self, self.edges["f"].settings)
self.addPart(u)
uc = UnevenFingerJointEdgeCounterPart(self, self.edges["f"].settings)
self.addPart(uc)
for _ in range(faces):
self.regularPolygonWall(corners, side=e, edges="u", move="right")

View File

@ -0,0 +1,129 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
class PoleHook(Boxes): # change class name here and below
"""Hook for pole like things to be clamped to another pole"""
def __init__(self):
Boxes.__init__(self)
# Uncomment the settings for the edge types you use
self.addSettingsArgs(edges.FingerJointSettings)
# Add non default cli params if needed (see argparse std lib)
self.argparser.add_argument(
"--diameter", action="store", type=float, default=50.,
help="diameter of the thing to hook")
self.argparser.add_argument(
"--screw", action="store", type=float, default=7.8,
help="diameter of the screw in mm")
self.argparser.add_argument(
"--screwhead", action="store", type=float, default=13.,
help="with of the screw head in mm")
self.argparser.add_argument(
"--screwheadheight", action="store", type=float, default=5.5,
help="height of the screw head in mm")
self.argparser.add_argument(
"--pin", action="store", type=float, default=4.,
help="diameter of the pin in mm")
def fork(self, d, w, edge="e", full=True, move=None):
tw = d + 2 * w
th = 2 * d
if self.move(tw, th, move, True):
return
e = self.edges.get(edge, edge)
self.moveTo(0, e.margin())
if e is self.edges["e"]:
self.bedBoltHole(tw)
else:
e(tw, bedBolts=edges.Bolts(1))
if full:
self.hole(-0.5*w, 2*d, self.pin/2)
self.polyline(0, 90, 2*d, (180, w/2), d, (-180, d/2),
0.5*d, (180, w/2), 1.5 * d, 90)
else:
self.polyline(0, 90, d, 90, w, 90, 0, (-180, d/2),
0.5*d, (180, w/2), 1.5 * d, 90)
self.move(tw, th, move)
def lock(self, l1, l2, w, move=None):
l1 += w/2
l2 += w/2
if self.move(l1, l2, move, True):
return
self.hole(w/2, w/2, self.pin/2)
self.moveTo(w/2, 0)
self.polyline(l2-w, (180, w/2), l2-2*w, (-90, w/2), l1-2*w, (180, w/2),
l1-w, (90, w/2))
self.move(l1, l2, move)
def backplate(self):
tw = self.diameter + 2*self.ww
t = self.thickness
b = edges.Bolts(1)
bs = (0.0, )
self.fingerHolesAt(-tw/2, -2*t, tw, 0, bedBolts=b, bedBoltSettings=bs)
self.fingerHolesAt(-tw/2, 0, tw, 0, bedBolts=b, bedBoltSettings=bs)
self.fingerHolesAt(-tw/2, +2*t, tw, 0, bedBolts=b, bedBoltSettings=bs)
def clamp(self):
d = self.diameter + 2 * self.ww
self.moveTo(10, -0.5*d, 90)
self.edge(d)
self.moveTo(0, -8, -180)
self.edge(d)
def render(self):
# adjust to the variables you want in the local scope
d = self.diameter
t = self.thickness
shh = self.screwheadheight
self.bedBoltSettings = (self.screw, self.screwhead, shh, d/4+shh, d/4) # d, d_nut, h_nut, l, l
self.ww = ww = 4*t
self.fork(d, ww, "f", move="right")
self.fork(d, ww, "f", move="right")
self.fork(d, ww, "f", full=False, move="right")
self.fork(d, ww, full=False, move="right")
self.fork(d, ww, full=False, move="right")
self.parts.disc(d+2*ww, callback=self.backplate, hole=self.screw, move="right")
self.parts.disc(d+2*ww, hole=self.screw, move="right")
self.parts.disc(d+2*ww, callback=self.clamp, hole=self.screw+0.5*t, move="right")
self.parts.disc(d+2*ww, hole=self.screw+0.5*t, move="right")
self.parts.waivyKnob(50, callback=lambda:self.nutHole(self.screwhead),
move="right")
self.parts.waivyKnob(50, callback=lambda:self.nutHole(self.screwhead),
move="right")
self.parts.waivyKnob(50, hole=self.screw+0.5*t, move="right")
ll = ((d**2 + (0.5*(d+ww))**2)**0.5) - 0.5 * d
for i in range(3):
self.lock(ll, ll, ww, move="right")
for i in range(2):
self.parts.disc(ww, move="up")

View File

@ -0,0 +1,82 @@
#!/usr/bin/python3
# Copyright (C) 2013-2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
from boxes import pulley
import math
class Pulley(Boxes):
"""Timing belt pulleys for different profiles"""
ui_group = "Part"
def __init__(self):
Boxes.__init__(self)
# remove cli params you do not need
self.buildArgParser(h=6.)
self.argparser.add_argument(
"--profile", action="store", type=str, default="GT2_2mm",
choices=pulley.Pulley.getProfiles(),
help="profile of the teeth/belt")
self.argparser.add_argument(
"--teeth", action="store", type=int, default=20,
help="number of teeth")
self.argparser.add_argument(
"--axle", action="store", type=float, default=5,
help="diameter of the axle")
self.argparser.add_argument(
"--insideout", action="store", type=BoolArg(), default=False,
help="create a ring gear with the belt being pushed against from within")
self.argparser.add_argument(
"--top", action="store", type=float, default=0,
help="overlap of top rim (zero for none)")
# Add non default cli params if needed (see argparse std lib)
# self.argparser.add_argument(
# "--XX", action="store", type=float, default=0.5,
# help="DESCRIPTION")
def disk(self, diameter, hole, callback=None, move=""):
w = diameter + 2 * self.spacing
if self.move(w, w, move, before=True):
return
self.moveTo(w / 2, w / 2)
self.cc(callback, None, 0.0, 0.0)
if hole:
self.hole(0, 0, hole / 2.0)
self.moveTo(diameter / 2 + self.burn, 0, 90)
self.corner(360, diameter / 2)
self.move(w, w, move)
def render(self):
# adjust to the variables you want in the local scope
t = self.thickness
if self.top:
self.disk(
self.pulley.diameter(self.teeth, self.profile) + 2 * self.top,
self.axle, move="right")
for i in range(int(math.ceil(self.h / self.thickness))):
self.pulley(self.teeth, self.profile, insideout=self.insideout, r_axle=self.axle / 2.0, move="right")

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# Copyright (C) 2018 Sebastian Reichel
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from boxes import *
from boxes.generators.rack19box import Rack19Box
class Rack10Box(Rack19Box):
"""Closed box with screw on top for mounting in a 10" rack."""
def render(self):
self._render(type=10)

Some files were not shown because too many files have changed in this diff Show More