remove git repo status from boxes
This commit is contained in:
parent
803f35fd41
commit
7e149e5d9c
@ -0,0 +1 @@
|
||||
This page is intentionally left blank.
|
165
extensions/fablabchemnitz/boxes.py/boxes/CONTRIBUTING.rst
Normal file
165
extensions/fablabchemnitz/boxes.py/boxes/CONTRIBUTING.rst
Normal 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.
|
674
extensions/fablabchemnitz/boxes.py/boxes/LICENSE.txt
Normal file
674
extensions/fablabchemnitz/boxes.py/boxes/LICENSE.txt
Normal 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>.
|
62
extensions/fablabchemnitz/boxes.py/boxes/README.rst
Normal file
62
extensions/fablabchemnitz/boxes.py/boxes/README.rst
Normal 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>.
|
16
extensions/fablabchemnitz/boxes.py/boxes/boxes/Color.py
Normal file
16
extensions/fablabchemnitz/boxes.py/boxes/boxes/Color.py
Normal 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
|
2895
extensions/fablabchemnitz/boxes.py/boxes/boxes/__init__.py
Executable file
2895
extensions/fablabchemnitz/boxes.py/boxes/boxes/__init__.py
Executable file
File diff suppressed because it is too large
Load Diff
1013
extensions/fablabchemnitz/boxes.py/boxes/boxes/drawing.py
Normal file
1013
extensions/fablabchemnitz/boxes.py/boxes/boxes/drawing.py
Normal file
File diff suppressed because it is too large
Load Diff
2629
extensions/fablabchemnitz/boxes.py/boxes/boxes/edges.py
Normal file
2629
extensions/fablabchemnitz/boxes.py/boxes/boxes/edges.py
Normal file
File diff suppressed because it is too large
Load Diff
44
extensions/fablabchemnitz/boxes.py/boxes/boxes/extents.py
Normal file
44
extensions/fablabchemnitz/boxes.py/boxes/boxes/extents.py
Normal 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})'
|
91
extensions/fablabchemnitz/boxes.py/boxes/boxes/formats.py
Normal file
91
extensions/fablabchemnitz/boxes.py/boxes/boxes/formats.py
Normal 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)
|
740
extensions/fablabchemnitz/boxes.py/boxes/boxes/gears.py
Normal file
740
extensions/fablabchemnitz/boxes.py/boxes/boxes/gears.py
Normal 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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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)
|
@ -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)
|
||||
|
@ -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))
|
||||
|
||||
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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
|
@ -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")
|
||||
|
||||
|
||||
|
@ -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")
|
@ -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))
|
@ -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")
|
@ -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")
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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")
|
@ -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)
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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")
|
@ -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")
|
||||
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
||||
|
@ -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)
|
@ -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")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
@ -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)
|
@ -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")
|
@ -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")
|
@ -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])
|
@ -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")
|
||||
|
@ -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))
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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())
|
@ -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")
|
@ -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")
|
@ -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")
|
||||
|
@ -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__))
|
||||
|
@ -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)])
|
||||
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
@ -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],)
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
||||
|
@ -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")
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)])
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
||||
|
||||
|
@ -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))
|
@ -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')
|
@ -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")
|
||||
|
||||
|
||||
|
@ -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
|
||||
)
|
@ -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)
|
@ -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")
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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"])
|
@ -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")
|
@ -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)
|
@ -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")
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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")
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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)
|
@ -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")
|
||||
|
@ -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")
|
@ -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")
|
||||
|
@ -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"))
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
@ -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")
|
@ -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]
|
@ -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
|
@ -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")
|
||||
|
||||
|
||||
|
@ -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))
|
@ -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")
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user