Showing preview only (215K chars total). Download the full file or copy to clipboard to get everything.
Repository: michaelgale/cq-gridfinity
Branch: main
Commit: d317286ec15d
Files: 37
Total size: 203.8 KB
Directory structure:
gitextract_j4oewo7c/
├── .devcontainer/
│ ├── Dockerfile
│ ├── devcontainer.json
│ └── entrypoint.sh
├── .github/
│ └── workflows/
│ └── checks.yaml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── cqgridfinity/
│ ├── __init__.py
│ ├── constants.py
│ ├── gf_baseplate.py
│ ├── gf_box.py
│ ├── gf_drawer.py
│ ├── gf_helpers.py
│ ├── gf_obj.py
│ ├── gf_ruggedbox.py
│ ├── scripts/
│ │ ├── __init__.py
│ │ ├── gridfinitybase.py
│ │ ├── gridfinitybox.py
│ │ └── ruggedbox.py
│ └── shims/
│ ├── README.md
│ ├── cqgi_gf_baseplate.py
│ ├── cqgi_gf_box.py
│ ├── cqgi_gf_drawerspacer.py
│ └── cqgi_gf_ruggedbox.py
├── examples/
│ └── demo1.assy
├── partcad.yaml
├── requirements.in
├── requirements.txt
├── setup.py
└── tests/
├── common_test.py
├── test_baseplate.py
├── test_box.py
├── test_rbox.py
├── test_spacer.py
└── testfiles/
└── .gitkeep
================================================
FILE CONTENTS
================================================
================================================
FILE: .devcontainer/Dockerfile
================================================
FROM condaforge/mambaforge:23.3.1-1
# Create non-root user
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID
RUN groupadd --gid $USER_GID $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
&& apt-get update \
&& apt-get install -y sudo \
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
&& chmod 0440 /etc/sudoers.d/$USERNAME
# Install system dependencies
RUN export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends \
git=1:2.* \
libgl1-mesa-dev \
xvfb \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
# Create conda environment and install packages
RUN mamba create -n cqdev python=3.12 -y \
&& mamba init bash \
&& mamba install -n cqdev -c conda-forge -y \
cadquery=2.4 \
pytest=8.* \
black=24.* \
flake8=7.* \
isort=5.* \
&& mamba clean --all -f -y
# Install pip dependencies
SHELL ["/bin/bash", "-c"]
RUN source /opt/conda/etc/profile.d/conda.sh \
&& conda activate cqdev \
&& pip install --no-cache-dir \
ezdxf==1.* \
cqkit \
importlib_metadata
# Set up X11 and OpenGL
RUN mkdir -p /tmp/runtime-root \
&& chown ${USERNAME}:${USERNAME} /tmp/runtime-root
ENV XDG_RUNTIME_DIR=/tmp/runtime-root
ENV DISPLAY=:99
ENV LIBGL_ALWAYS_INDIRECT=1
# Configure conda environment
ENV PATH=/opt/conda/envs/cqdev/bin:$PATH
RUN echo "conda activate cqdev" >> /home/${USERNAME}/.bashrc
# Set up virtual framebuffer
COPY .devcontainer/entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
# Switch to non-root user
USER ${USERNAME}
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["sleep", "infinity"]
================================================
FILE: .devcontainer/devcontainer.json
================================================
{
"name": "CQ Gridfinity Development",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"workspaceFolder": "/workspaces/cq-gridfinity",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/cq-gridfinity,type=bind",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.black-formatter",
"ms-python.flake8",
"ms-python.isort"
],
"settings": {
"python.defaultInterpreterPath": "/opt/conda/envs/cqdev/bin/python",
"python.testing.pytestEnabled": true,
"python.condaPath": "/opt/conda/bin/conda"
}
}
},
"forwardPorts": [
8080
],
"postCreateCommand": "pip install -e .",
"remoteUser": "vscode"
}
================================================
FILE: .devcontainer/entrypoint.sh
================================================
#!/bin/bash
source /opt/conda/etc/profile.d/conda.sh
conda activate cqdev
Xvfb :99 -screen 0 1024x768x16 &
sleep 1
exec "$@"
================================================
FILE: .github/workflows/checks.yaml
================================================
name: Run Tests
on:
pull_request:
branches:
- main # Runs on pull requests targeting the main branch
jobs:
tests:
name: Test (${{ matrix.python-version }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash -l {0}
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest"]
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- name: Check out code
uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v2
with:
python-version: ${{ matrix.python-version }}
mamba-version: "*"
channels: conda-forge,defaults
channel-priority: true
activate-environment: cq
- name: Install cadquery
run: mamba install cadquery
- name: Install pip dependencies
run: pip install cqkit pytest
- name: Install current version of cq-gridfinity
run: pip install .
- name: Run tests
run: pytest tests
================================================
FILE: .gitignore
================================================
# python intermediate files
*.py[cod]
# intermediate and cached 3D solid files
/cache/*
/tests/testfiles/*.step
/tests/testfiles/*.iges
/tests/testfiles/*.stl
*.graffle
# editors
/.vscode
# C extensions
*.so
# OS litter
.DS_Store
Desktop.ini
._*
Thumbs.db
.Trashes
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
__pycache__
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
.venv/
================================================
FILE: CHANGELOG.md
================================================
## Changelog
- v.0.1.0 - Initial release
- v.0.1.1 - fixed release
- v.0.2.0 - Added new "lite" style box
- v.0.2.1 - Added new unsupported magnet hole types
- v.0.2.2 - Added SVG export and integrated STL exporter
- v.0.2.3 - Updated to python build tools to make distribution
- v.0.3.0 - Added console generator scripts: gridfinitybox and gridfinitybase
- v.0.4.0 - Added `GridfinityRuggedBox` class and `ruggedbox` console script. Various other improvements.
- v.0.4.1 - Fixed docstring in `__init__.py`
- v.0.4.2 - Improved script automatic renaming
- v.0.4.3 - Fixed regression bug with using multilevel extrusion functions from cq-kit
- v.0.4.4 - IMPORTANT FIX: generated geometry breaks using CadQuery v.2.4+ due to changes in CadQuery's `extrude` method. This version should work with any CQ version since it detects which CQ extrusion implementation is used at runtime.
- v.0.4.5 - IMPORTANT FIX: fixes error in v.0.4.4 for extrusion angle
- v.0.5.0 - Improved rugged box to make viable boxes down to 3U x 3U x 4U
- v.0.5.1 - Increased the resolution of the gridfinity extruded base profile
- v.0.5.2 - Adjusted geometry of box/bin floor/lip heights to exactly 7.00 mm intervals
- v.0.5.3 - Removed a potential namespace collision for computing the height of boxes
- v.0.5.4 - Optimized the geometry of the baseplate top height
- v.0.5.5 - Added underside bin clearance and variable wall thickness interior radiusing
- v.0.5.6 - Added adjustable magnet hole diameter to box. Prevent drawer spacers being rendered which fall below minimum size
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2023 Michael Gale
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
.PHONY: clean clean-test clean-pyc clean-build test
.DEFAULT_GOAL := help
define PRINT_HELP_PYSCRIPT
import re, sys
for line in sys.stdin:
match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
if match:
target, help = match.groups()
print("%-20s %s" % (target, help))
endef
export PRINT_HELP_PYSCRIPT
help:
@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
clean-build: ## remove build artifacts
@rm -fr build/
@rm -fr dist/
@rm -fr .eggs/
@find . -name '*.egg-info' -exec rm -fr {} +
@find . -name '*.egg' -exec rm -f {} +
clean-pyc: ## remove Python file artifacts
@find . -name '*.pyc' -exec rm -f {} +
@find . -name '*.pyo' -exec rm -f {} +
@find . -name '*~' -exec rm -f {} +
@find . -name '__pycache__' -exec rm -fr {} +
clean-test: ## remove test and coverage artifacts
@find . -name '*.step' -exec rm -f {} +
@find . -name '*.stl' -exec rm -f {} +
@find . -name '*.svg' -exec rm -f {} +
@rm -f .coverage
@rm -fr htmlcov/
lint: ## check style with black
@black cqgridfinity/*.py
@black cqgridfinity/scripts/*.py
@black tests/*.py
lint-check: ## check if lint status is consistent between commits
@black --diff --check cqgridfinity/*.py
@black --diff --check cqgridfinity/scripts/*.py
@black --diff --check tests/*.py
test: ## run tests quickly with the default Python
py.test -s -v --cov -W ignore::DeprecationWarning:nptyping.typing_
# @export SKIP_TEST_BOX="all" && \
# export SKIP_TEST_RBOX="all" && \
# export SKIP_TEST_SPACER="all" && \
# export SKIP_TEST_BASEPLATE="all" && \
# export EXPORT_STEP_FILES="all" && \
test-some: ## run selective tests quickly with the default Python
@export SKIP_TEST_BOX="all" && \
export SKIP_TEST_RBOX="all" && \
export SKIP_TEST_BASEPLATE="all" && \
export EXPORT_STEP_FILES="all" && \
py.test -s -v --cov -W ignore::DeprecationWarning:nptyping.typing_
test-files: ## run tests and export test files artifacts
@export EXPORT_STEP_FILES="all" && \
py.test -s -v -W ignore::DeprecationWarning:nptyping.typing_
coverage: ## check code coverage quickly with the default Python
coverage run --source cqgridfinity -m pytest
coverage report -m
coverage html
open htmlcov/index.html
release: clean dist ## package and upload a release
twine check dist/*
twine upload dist/*
dist: clean ## builds source and wheel package
@python -m build
@twine check dist/*
@ls -l dist
install: clean ## install the package to the active Python's site-packages
@pip install .
================================================
FILE: README.md
================================================
<!-- <img src=./images/logo.png width=320> -->

# cq-gridfinity
[](https://pypi.org/project/cqgridfinity/)

[](https://github.com/CadQuery/cadquery)
[](https://github.com/michaelgale/cq-kit)

[](http://github.com/psf/black)
[](https://github.com/michaelgale/cq-gridfinity/actions/workflows/checks.yaml)
This repository contains a python library to build [Gridfinity](https://gridfinity.xyz) boxes, baseplates, and other objects based on the [CadQuery](https://github.com/CadQuery/cadquery) python library. The Gridfinity system was created by [Zach Freedman](https://www.youtube.com/c/ZackFreedman) as a versatile system of modular organization and storage modules. A vibrant community of user contributed modules and utilities has grown around the Gridfinity system. This repository contains python classes to create gridfinity compatible parameterized components such as baseplates and boxes.
Examples of how I am starting to use Gridfinity to organize my tools are shown below using components built with this python library:
<img src=./images/examples.png width=800>
# Quick Links
- [Installation / Usage](#installation)
- [Shell Command Scripts](#shell-command-scripts)
- [gridfinitybox](#gridfinitybox)
- [gridfinitybase](#gridfinitybase)
- [ruggedbox](#ruggedbox)
- [Classes](#classes)
- [GridfinityBaseplate](#gridfinitybaseplate)
- [GridfinityBox](#gridfinitybox-1)
- [GridfinityDrawerSpacer](#gridfinitydrawerspacer)
- [GridfinityRuggedBox](#gridfinityruggedbox)
- [GridfinityObject](#gridfinityobject)
- [References](#references)
## Installation
**cq-gridfinity** has the following installation dependencies:
- [CadQuery](https://github.com/CadQuery/cadquery)
- [cq-kit](https://github.com/michaelgale/cq-kit)
Assuming these dependencie are installed, you can install **cq-gridfinity** using a [PyPI package](https://pypi.org/project/cqgridfinity/) as follows:
```bash
$ pip install cqgridfinity
```
Alternatively, the **cq-gridfinity** package can be installed directly from the source code:
```bash
$ git clone https://github.com/michaelgale/cq-gridfinity.git
$ cd cq-gridfinity
$ pip install .
```
## Development with VS Code Dev Container
This project includes a development container configuration that provides a consistent development environment with all required dependencies pre-installed.
### Prerequisites
1. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/)
2. Install [Visual Studio Code](https://code.visualstudio.com/)
3. Install the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) in VS Code
### Getting Started
1. Clone this repository:
```bash
git clone https://github.com/michaelgale/cq-gridfinity.git
cd cq-gridfinity
```
2. Open the project in VS Code:
```bash
code .
```
3. When VS Code detects the dev container configuration, it will prompt you to "Reopen in Container". Click this prompt, or:
- Press `F1` or `Ctrl+Shift+P` (Cmd+Shift+P on Mac)
- Type "Dev Containers: Reopen in Container" and select it
4. Wait for the container to build (this may take a few minutes the first time)
### What's Included
The development container comes with:
- Python 3.12
- CadQuery 2.4
- All required dependencies (pytest, black, flake8, etc.)
- A pre-configured environment for CAD development
### Troubleshooting
If you encounter issues:
1. Ensure Docker is running
2. Try rebuilding the container:
- Press `F1` or `Ctrl+Shift+P`
- Select "Dev Containers: Rebuild Container"
## Basic Usage
After installation, the package can imported:
```shell
$ python
>>> import cqgridfinity
>>> cqgridfinity.__version__
```
An example of the package can be seen below:
```python
from cqgridfinity import *
# make a simple box
box = GridfinityBox(3, 2, 5, holes=True, no_lip=False, scoops=True, labels=True)
box.save_stl_file()
# Output a STL file of box:
# gf_box_3x2x5_holes_scoops_labels.stl
```
# Shell Command Scripts
- [gridfinitybox](#gridfinitybox)
- [gridfinitybase](#gridfinitybase)
- [ruggedbox](#ruggedbox)
This package can be used to make your own python scripts to generate Gridfinity objects. This gives the flexibility to customize the object and combine with other code to add custom cutouts, add text labels, etc.
However, for simple generation of standard objects such as baseplates and boxes, console scripts can be used for quick command line usage. These console scripts are installed automatically into the path of your python environment and should be accessible from your terminal shell.
## `gridfinitybox`
<img src=./images/box_script.png width=600>
Make a customized/parameterized Gridfinity compatible box with many optional features.
```
usage: gridfinitybox [-h] [-m] [-u] [-n] [-s] [-l] [-e] [-d] [-r RATIO] [-ld LENGTHDIV] [-wd WIDTHDIV] [-wt WALL]
[-f FORMAT] [-o OUTPUT]
length width height
Make a customized/parameterized Gridfinity compatible box with many optional features.
positional arguments:
length Box length in U (1U = 42 mm)
width Box width in U (1U = 42 mm)
height Box height in U (1U = 7 mm)
options:
-h, --help show this help message and exit
-m, --magnetholes Add bottom magnet/mounting holes
-u, --unsupported Add bottom magnet holes with 3D printer friendly strips without support
-n, --nolip Do not add mating lip to the top perimeter
-s, --scoops Add finger scoops against each length-wise back wall
-l, --labels Add label strips against each length-wise front wall
-e, --ecolite Make economy / lite style box with no elevated floor
-d, --solid Make solid (filled) box for customized storage
-r RATIO, --ratio RATIO
Solid box fill ratio 0.0 = minimum, 1.0 = full height
-ld LENGTHDIV, --lengthdiv LENGTHDIV
Split box length-wise with specified number of divider walls
-wd WIDTHDIV, --widthdiv WIDTHDIV
Split box width-wise with specified number of divider walls
-wt WALL, --wall WALL
Wall thickness (default=1 mm)
-f FORMAT, --format FORMAT
Output file format (STEP, STL, SVG) default=STEP
-o OUTPUT, --output OUTPUT
Output filename (inferred output file format with extension)
```
Examples:
```shell
# 2x3x5 box with magnet holes saved to STL file with default filename:
$ gridfinitybox 2 3 5 -m -f stl
# gf_box_2x3x5_holes.stl
# 1x3x4 box with scoops, label strip, 3 internal partitions and specified name:
$ gridfinitybox 1 3 4 -s -l -ld 3 -o MyBox.step
# MyBox.step
# Solid 3x3x3 box with 50% fill, unsupported magnet holes and no top lip:
$ gridfinitybox 3 3 3 -d -r 0.5 -u -n
# gf_box_3x3x3_basic_holes_solid.step
# Lite style box 3x2x3 with label strip, partitions, output to default SVG file:
$ gridfinitybox 3 2 3 -e -l -ld 2 -f svg
# gf_box_lite_3x2x3_div2_labels.svg
```
## `gridfinitybase`
<img src=./images/base_script.png width=600>
Make a customized/parameterized Gridfinity compatible simple baseplate.
```
usage: gridfinitybase [-h] [-f FORMAT] [-s] [-d DEPTH] [-hd HOLEDIAM] [-hc CSKDIAM] [-ca CSKANGLE] [-o OUTPUT]
length width
Make a customized/parameterized Gridfinity compatible simple baseplate.
positional arguments:
length Box length in U (1U = 42 mm)
width Box width in U (1U = 42 mm)
options:
-h, --help show this help message and exit
-f FORMAT, --format FORMAT
Output file format (STEP, STL, SVG) default=STEP
-s, --screws Add screw mounting tabs to the corners (adds +5 mm to depth)
-d DEPTH, --depth DEPTH
Extrude extended depth under baseplate by this amount
-hd HOLEDIAM, --holediam HOLEDIAM
Corner mounting screw hole diameter (default=5)
-hc CSKDIAM, --cskdiam CSKDIAM
Corner mounting screw countersink diameter (default=10)
-ca CSKANGLE, --cskangle CSKANGLE
Corner mounting screw countersink angle (deg) (default=82)
-o OUTPUT, --output OUTPUT
Output filename (inferred output file format with extension)
```
Examples:
```shell
# 7 x 4 baseplate with screw corners to default STL file:
$ gridfinitybase 7 4 -s -f stl
# gf_baseplate_7x4x5.0_screwtabs.stl
```
## `ruggedbox`
<img src=./images/rugged_box.png width=600>
Make a parameterized rugged storage box compatible with gridfinity. This box is based on the [superb design by Pred on Printables](https://www.printables.com/model/543553-gridfinity-storage-box-by-pred-now-parametric). This implementation makes a few improvements and additions to Pred's design in addition to making almost all of the box's features optional and tunable. Using either the `ruggedbox` console script or the `GridfinityRuggedBox` class, you can make vast variety of different boxes of various sizes and features. By default, almost all of the boxes features are enabled, but by using the desired command line options you can customize your desired feature set.
```
usage: ruggedbox [-h] [+l] [-l] [+p] [-p] [+w] [-w] [-wt WINDOWTHICKNESS] [+a] [-a] [+c] [-c] [+s] [-s] [+v] [-v]
[+e] [-e] [+b] [-b] [-r] [+r] [-f FORMAT] [-o OUTPUT] [-gb] [-gl] [-ga] [-gh] [-ge] [-gn] [-gt]
[-gw]
length width height
Make a customized/parameterized Gridfinity compatible rugged box enclosure.
The minimum box size is 3U x 3U x 4U.
positional arguments:
length Box length in U (1U = 42 mm)
width Box width in U (1U = 42 mm)
height Box height in U (1U = 7 mm)
options:
-h, --help show this help message and exit
+l, --label Add label window across the front wall
-l, --nolabel Remove label window across the front wall
+p, --lidbaseplate Add baseplate to top of the lid
-p, --nolidbaseplate Smooth/plain lid
+w, --lidwindow Add window slot to the lid
-w, --nolidwindow Do not add window slot to the lid
-wt WINDOWTHICKNESS, --windowthickness WINDOWTHICKNESS
Thickness of lid windows (mm)
+a, --handle Add front handle
-a, --nohandle No front handle
+c, --clasps Add clasps to the left and right side walls
-c, --noclasps No clasps on the left and right side walls
+s, --stackable Add stackable mating features to top and bottom
-s, --notstackable Non-stackable box
+v, --veegroove Add v-cut grooves to side walls
-v, --noveegroove No v-cut grooves (plain) side walls
+e, --sidehandle Add handles to side walls
-e, --nosidehandle No handles on side walls
+b, --backfeet Add standing feet to back wall
-b, --nobackfeet No standing feet added to back wall
-r, --normalstyle Make normal style box
+r, --ribstyle Make rib style box with exposed vertical ribs
-f FORMAT, --format FORMAT
Output file format (STEP, STL, SVG) default=STEP
-o OUTPUT, --output OUTPUT
Output filename (inferred output file format with extension)
-gb, --box Generate box
-gl, --lid Generate lid
-ga, --acc Generate accessory components
-gh, --hinge Generate hinge element
-ge, --genlabel Generate label panel insert
-gn, --genhandle Generate front handle
-gt, --genlatch Generate latch component
-gw, --genwindow Generate lid window component
example usage:
5 x 4 x 6 rugged box shell and lid saved to STL files:
$ ruggedbox 5 4 6 --box --lid -f stl
```
Examples:
5 x 4 x 6 rugged box component saved to STL file:
```shell
$ ruggedbox 5 4 6 -gb -f stl
____ _ ____
| _ \ _ _ __ _ __ _ ___ __| | __ ) _____ __
| |_) | | | |/ _` |/ _` |/ _ \/ _` | _ \ / _ \ \/ /
| _ <| |_| | (_| | (_| | __/ (_| | |_) | (_) > <
|_| \_\\__,_|\__, |\__, |\___|\__,_|____/ \___/_/\_\
|___/ |___/
Version: 0.5.7
Gridfinity rugged box: 5U x 4U x 6U
Exterior dim: 230.0 mm x 188.0 mm x 55.0 mm
Interior dim: 210.0 mm x 168.0 mm x 45.8 mm
Internal volume: 1.616 L
Wall Vgrooves : Y
Front Handle : Y
Stackable : Y
Side Clasps : Y
Lid Baseplate : Y
Inside Baseplate : Y
Side Handles : Y
Front Label : Y
Back Feet : Y
Rib Style : N
Lid Window : N
Rendering box...
Component generated and saved as gf_ruggedbox_5x4x6_body_fr-hl_sd-hc_stack_lidbp.stl in STL format
$
```
```shell
# same 5 x 4 x 6 rugged box with the lid saved to STL file:
$ ruggedbox 5 4 6 --lid -f stl
# gf_ruggedbox_5x4x6_lid_fr-hl_sd-hc_stack_lidbp.stl
# 5 x 5 x 9 rugged box, smooth lid, non-stackable, and no handle; full assembly saved to STEP file
$ ruggedbox 5 5 9 --nohandle --nolidbaseplate --notstackable
# gf_ruggedbox_5x5x9_fr-l_sd-hc.step
# Render the box, lid, and hinge for a 5x4x6 rugged box all at once:
$ ruggedbox 5 4 6 --box --lid --hinge
# gf_ruggedbox_5x4x6_fr-hl_sd-hc_stack_lidbp.step
# gf_ruggedbox_5x4x6_lid_fr-hl_sd-hc_stack_lidbp.step
# gf_ruggedbox_5x4x6_hinge_fr-hl_sd-hc_stack_lidbp.step
# Then render the latches and handle components for the same box:
$ ruggedbox 5 4 6 --acc
# gf_ruggedbox_5x4x6_acc_fr-hl_sd-hc_stack_lidbp.step
# Or render individual components as STL files with your preferred name:
$ ruggedbox 5 4 6 --genhandle --genlatch -o orange.stl
# orange_handle.stl
# orange_latch.stl
```
# Classes
- [GridfinityBaseplate](#gridfinitybaseplate)
- [GridfinityBox](#gridfinitybox-1)
- [GridfinityDrawerSpacer](#gridfinitydrawerspacer)
- [GridfinityRuggedBox](#gridfinityruggedbox)
- [GridfinityObject](#gridfinityobject)
## `GridfinityBaseplate`
Gridfinity baseplates can be made with the `GridfinityBaseplate` class. The baseplate style is the basic style initially proposed by Zach Freedman. Therefore, it does not have magnet or mounting holes. An example usage is as follows:
```python
# Create 4 x 3 baseplate
baseplate = GridfinityBaseplate(4, 3)
baseplate.save_step_file()
# gf_baseplate_4x3.step
```
<img src=./images/baseplate4x3.png width=512>
Baseplates can be rendered with extra depth to make a taller overall baseplate using the `ext_depth` attribute. Furthermore, mounting screws corner tabs can be added to the baseplate with the `corner_screws` attribute. A baseplate with this feature is shown below.
<img src=./images/baseplate6x3.png width=512>
### Optional Keyword Arguments
```python
ext_depth = 0 # extended depth to extrude below baseplate
straight_bottom = False # add/remove 0.8 mm chamfer on bottom of baseplate
corner_screws = False # add corner mounting screw tabs
corner_tab_size = 21 # size of screw mounting tab (mm)
csk_hole = 5.0 # hole diameter of countersink mounting screw (mm)
csk_diam = 10.0 # countersink diameter (mm)
csk_angle = 82 # countersink angle (deg)
```
## `GridfinityBox`
Gridfinity boxes with many optional features can be created with the `GridfinityBox` class. As a minimum, this class is initialized with basic 3D unit dimensions for length, width, and height. The length and width are multiples of 42 mm Gridfinity intervals and height represents multiples of 7 mm.
### Simple Box
```python
# Create a simple 3 x 2 box, 5U high
box = GridfinityBox(3, 2, 5)
box.save_step_file()
# Output a STEP file of box named:
# gf_box_3x2x5.step
```
<img src=./images/basic_box.png width=512>
### Lite Style Box
"Lite" style boxes are simplified for faster 3D printing with less material. They remove the continuous floor at 7.2 mm and the box becomes a homogenous 1 mm thick walled shell. "lite" style boxes can include labels and dividers; however, the number of dividers must correspond to the same bottom partition ridges, i.e. `length_div` must be `length_u - 1` and `width_div` must be `width_u - 1`. "lite" style cannot be combined with solid boxes, finger scoops, or magnet holes.
```python
# Create a "lite" style 3 x 2 box, 5U high
box = GridfinityBox(3, 2, 5, lite_style=True)
box.save_step_file()
# Output a STEP file of box named:
# gf_box_lite_3x2x5.step
```
<img src=./images/box_lite.png width=512>
### Magnet Holes
```python
# add magnet holes to the box
box = GridfinityBox(3, 2, 5, holes=True)
box.save_step_file()
# gf_box_3x2x5_holes.step
```
<img src=./images/box_holes.png width=512>
The `unsupported_holes` attribute can specify either regular holes or modified/unsupported holes which are more suitable for 3D-printing. These modified holes include thin filler strips which allow the slicer to avoid using supports to render the underside holes.
```python
# add magnet holes to the box
box = GridfinityBox(1, 1, 5, holes=True, unsupported_holes=True)
box.save_step_file()
# gf_box_1x1x5_holes.step
```
<img src=./images/box_holetypes.png width=512>
### Simple Box with No Top Lip
```python
# remove top mounting lip
box = GridfinityBox(3, 2, 5, no_lip=True)
box.save_step_file()
# gf_box_3x2x5_basic.step
```
<img src=./images/box_nolip.png width=512>
### Scoops and Labels
```python
# add finger scoops and label top flange
box = GridfinityBox(3, 2, 5, scoops=True, labels=True)
box.save_step_file()
# gf_box_3x2x5_scoops_labels.step
```
<img src=./images/box_options.png width=512>
### Dividing Walls
```python
# add dividing walls
box = GridfinityBox(3, 2, 5, length_div=2, width_div=1, scoops=True, labels=True)
box.save_step_file()
# gf_box_3x2x5_div2x1_scoops_labels.step
```
<img src=./images/box_div.png width=512>
### Solid Box
```python
# make a partially solid box
box = GridfinityBox(3, 2, 5, solid=True, solid_ratio=0.7)
box.save_step_file()
# gf_box_3x2x5_solid.step
```
<img src=./images/box_solid.png width=512>
### Optional Keyword Arguments
```python
length_div=0 # add dividing walls along length
width_div=0 # add dividing walls along width
holes=False # add magnet holes to bottom
unsupported_holes=False # 3D-printer friendly hole style requiring no supports
no_lip=False # remove top mating lip feature
scoops=False # add finger scoops
scoop_rad=11 # radius of optional interior scoops
labels=False # add a label flange to the top
label_width=12 # width of the label strip
label_height=10 # thickness height of label overhang
label_lip_height=0.8 # thickness of label vertical lip
lite_style=False # make a "lite" version of box without elevated floor
solid=False # make a solid box
solid_ratio=1.0 # ratio of solid height range 0.0 to 1.0 (max height)
wall_th=1.0 # wall thickness (0.5-2.5 mm)
fillet_interior=True # enable/disable internal fillet edges
```
## `GridfinityDrawerSpacer`
The `GridfinityDrawerSpacer` class can be used to make spacer components to fit a drawer with any arbitrary dimensions. Initialize with specified width and depth of the drawer (in mm) and the best fit of integer gridfinity baseplate units is computed. Rarely, integer multiples of 42 mm gridfinity baseplates fit perfectly inside a drawer; therefore, spacers are required to secure the baseplate snuggly inside the drawer. Spacers consist of 4x identical corner sections, 2x spacers for the left and right sides and 2x spacers for the front and back edges.
If the computed spacer width falls below a configurable threshold (default 4 mm), then no spacer component is made in that dimension. The spacer components are made by default with interlocking "jigsaw" type features to assist with assembly and to secure the spacers within the drawer. Also, alignment arrows (default but optional) are placed on the components to indicate the installation orientation in the direction of the drawer movement.
```python
# make drawer spacers for Craftsman tool chest drawer 23" wide x 19" deep
spacer = GridfinityDrawerSpacer(582, 481, verbose=True)
# Best fit for 582.00 x 481.00 mm is 13U x 11U
# with 18.00 mm margin each side and 9.50 mm margin front and back
# Corner spacers : 4U wide x 3U deep
# Front/back spacers : 5U wide x 9.25 mm +0.25 mm tolerance
# Left/right spacers : 5U deep x 17.75 mm +0.25 mm tolerance
```
<img src=./images/drawer_photo.png width=600>
A full set of components (optionally including a full baseplate) can be rendered with the `render_full_set()` method. This method is mostly used to verify the fit and placement of the spacers.
<img src=./images/full_set.png width=600>
Normally, the `render_half_set()` method used to render half of the components compactly arranged conveniently for 3D printing. This set can be printed twice to make a full set for a single drawer.
<img src=./images/half_set.png width=600>
### Optional Keyword Arguments
```python
thickness=GR_BASE_HEIGHT # thickness of spacers, default=5 mm
chamf_rad=1.0 # chamfer radius of spacer top/bottom edges
show_arrows=True # show orientation arrows indicating drawer in/out direction
align_features=True # add "jigsaw" interlocking feautures
align_tol=0.15 # tolerance of the interlocking joint
align_min=8 # minimum spacer width for adding interlocking feature
min_margin=4 # minimum size to make a spacer, nothing is made for smaller gaps
tolerance=GR_TOL # overall tolerance for spacer components, default=0.5 mm
```
### Example with IKEA ALEX narrow drawer
An example use case to make a set of spacer components for a typical IKEA narrow ALEX drawer is as follows:
```python
spacers = GridfinityDrawerSpacer(INCHES(11.5), INCHES(20.5), verbose=True)
spacers.render_full_set(include_baseplate=True)
spacers.save_step_file("ikea_alex_full_set.step")
# make a half set for 3D printing
spacers.render_half_set()
spacers.save_stl_file("ikea_alex_half_set.stl")
```
<img src=./images/alexdrawer.png width=600>
## `GridfinityRuggedBox`
<img src=./images/rugged_box.png width=600>
The `GridfinityRuggedBox` class can be used to make gridfinity compatible rugged storage boxes. This box is based on the [superb design by Pred on Printables](https://www.printables.com/model/543553-gridfinity-storage-box-by-pred-now-parametric).
The **cq-gridfinity** derivative version of Pred's box is completely parameterized and generated completely with code in the `GridfinityRuggedBox` class. This lets you render the most minimalist box configuration with no added features up to a full-featured box as shown below:
<img src=./images/min_rugged_box.png width=600>
The desired box size and features are specified with keyword arguments/attributes such as the ones illustrated below:
<img src=./images/rugged_box_features.png width=600>
A alternative "rib style" rugged box is also available. This adds vertical rib stiffeners around the perimeter of the box and it is recommended to disable the side handles to allow for ribs to be generated on all sides.
<img src=./images/ribstylebox.png width=600>
Lastly, the lid baseplate can be substituted with a lid window which makes the contents of the box visible. The window consists of a seperately prepared 1 mm thick transparent acrylic sheet cut to the required dimensions. These dimensions can be queried with the `lid_window_size()` method or will be printed to the console when using the `ruggedbox` shell script.
<img src=./images/lid_window.png width=600>
After the lid has been printed the process to install the lid window is as follows:
1. Cut the lid window to the required dimensions. It is recommended to chamfer or round off the leading edge corners with a file prior to insertion.
2. Slide the window into the lid starting from the back and along the tapered window groove slot around the inside perimeter of the lid.
3. The window should be inserted just past the retention slots for the hinges.
4. Secure the lid with 3x M2 screws along the back of the lid. Carefully drill 2.5 mm clearance holes into the window in situ prior to installation of the screws. Alternatively, the lid can be secured with a few drops of super glue along the rear edge.
5. Install the lid hinges. The hinges must be installed last since they act as a physical retainer along the back edge of the window.
The lid window should nominally be 1 mm thick; however if it necessary to use a different thickness material, the `window_th` attribute can be set. It recommended to keep the window thickness in the range of 0.8 to 1.6 mm.
The rugged box can be rendered either as a complete assembly or as individual components. This is useful for making individual asset files for 3D printing. The render methods include the `render_assembly()` method as shown above for the complete assembly, as well as individual render methods summarized below:
`render()` - renders just the main box body shell:
<img src=./images/rugged_box_shell.png width=600>
`render_lid()` - renders the lid:
<img src=./images/rugged_box_lid.png width=600>
`render_accessories()` - renders the accessory component elements as a group in the quantities required for the desired box:
<img src=./images/rugged_box_acc.png width=600>
Lastly, each individual component has an individual render method.
- `render_hinge()`
- `render_latch()`
- `render_label()`
- `render_handle()`
### Optional Keyword Arguments
```python
lid_height = 10 # lid height (should be multiple of 10 mm for stacking)
wall_vgrooves = True # enable horizontal v-grooves to body shell
front_handle = True # enable front handle
stackable = True # add mating stackable features
side_clasps = True # add extra side latching clasps
lid_baseplate = True # enable top/lid baseplate
inside_baseplate = True # enable interior baseplate
side_handles = True # enable side handles to box
front_label = True # enable front label panel
label_length = None # length of front label panel, None=auto size
label_height = None # height of front label panel, None=auto size
label_th = GR_LABEL_TH # thickness of label panel, default=0.5 mm
back_feet = True # add rear back feet matching hinges to allow the stand box vertically
hinge_width = GR_HINGE_SZ # Size of hinge, default=32 mm
hinge_bolted = False # printed or bolted hinge construction
box_color = cq.Color(0.25, 0.25, 0.25) # colors for the assembly STEP file
lid_color = cq.Color(0.25, 0.5, 0.75)
handle_color = cq.Color(0.75, 0.5, 0.25)
latch_color = cq.Color(0.75, 0.5, 0.25)
hinge_color = cq.Color(0.75, 0.5, 0.25)
label_color = cq.Color(0.7, 0.7, 0.7)
```
## `GridfinityObject`
The `GridfinityObject` is the base class for `GridfinityBox`, `GridfinityBaseplate`, etc. It has several useful methods and attributes including:
### File export and naming
`obj.filename(self, prefix=None, path=None)` returns a filename string with descriptive attributes such as the object size and enabled features.
```python
box = GridfinityBox(3, 2, 5, holes=True)
box.filename()
# gf_box_3x2x5_holes
box.filename(prefix="MyBox")
# MyBox_3x2x5_holes
box.filename(path="./outputfiles")
# ./outputfiles/gf_box_3x2x5_holes
box2 = GridfinityBox(4, 3, 3, holes=True, length_div=2, width_div=1)
box2.filename()
# gf_box_4x3x3_holes_div2x1
```
```python
# Export object to STEP, STL, or SVG file
obj.save_step_file(filename=None, path=None, prefix=None)
obj.save_stl_file(filename=None, path=None, prefix=None)
obj.save_svg_file(filename=None, path=None, prefix=None)
```
The automatic filename assignment is aware of the last object generated with a particular class's render method. Therefore, you can call any render method and then call any of the `save_step_file`, `save_stl_file`, `save_svg_file` methods and the filename will adapt to the last object rendered. For example:
```python
b1 = GridfinityRuggedBox(5, 4, 6)
b1.render_accessories()
b1.save_step_file()
# saved as "gf_ruggedbox_5x4x6_acc_fr-hl_sd-hc_stack_lidbp.step"
b1.render_handle()
b1.save_stl_file()
# saved as "gf_ruggedbox_5x4x6_handle_fr-hl_sd-hc_stack_lidbp.stl"
b1.render_hinge()
b1.save_svg_file(path="./mystuff")
# saved as "./mystuff/gf_ruggedbox_5x4x6_hinge_fr-hl_sd-hc_stack_lidbp.svg"
b1.render_assembly()
b1.save_step_file()
# saved as "gf_ruggedbox_5x4x6_assembly_fr-hl_sd-hc_stack_lidbp.step"
```
### Useful properties
```obj.cq_obj``` returns a rendered CadQuery Workplane object
```obj.length``` returns length in mm
```obj.width``` returns width in mm
```obj.height``` returns height in mm
```obj.top_ref_height``` returns the height of the top surface of a solid box or the floor height of an empty box. This can be useful for making custom boxes with cutouts since the reference height can be used to orient the cutting solid to the correct height.
# To-do
- add more example scripts
- improve documentation
# Releases
- v.0.1.0 - First release on PyPI
- v.0.1.1 - Fixed release
- v.0.2.0 - Added new "lite" style box
- v.0.2.1 - Added new unsupported magnet hole types
- v.0.2.2 - Added SVG export and integrated STL exporter
- v.0.2.3 - Updated to python build tools to make distribution
- v.0.3.0 - Added console generator scripts: `gridfinitybox` and `gridfinitybase`
- v.0.4.0 - Added `GridfinityRuggedBox` class and `ruggedbox` console script. Various other improvements.
- v.0.4.1 - Fixed docstring in `__init__.py`
- v.0.4.2 - Improved script automatic renaming
- v.0.4.3 - Fixed regression bug with using multilevel extrusion functions from cq-kit
- v.0.4.4 - IMPORTANT FIX: generated geometry breaks using CadQuery v.2.4+ due to changes in CadQuery's `extrude` method. This version should work with any CQ version since it detects which CQ extrusion implementation is used at runtime.
- v.0.4.5 - IMPORTANT FIX: fixes error in v.0.4.4 for extrusion angle
- v.0.5.0 - Improved rugged box to make viable boxes down to 3U x 3U x 4U
- v.0.5.1 - Increased the resolution of the gridfinity extruded base profile
- v.0.5.2 - Adjusted geometry of box/bin floor/lip heights to exactly 7.00 mm intervals
- v.0.5.3 - Removed a potential namespace collision for computing the height of boxes
- v.0.5.4 - Optimized the geometry of the baseplate top height
- v.0.5.5 - Added underside bin clearance and variable wall thickness interior radiusing
- v.0.5.6 - Added adjustable magnet hole diameter to box. Prevent drawer spacers being rendered which fall below minimum size
- v.0.5.7 - Added scoops to lite-style boxes. Added new "rib style" rugged box. Added a lid window feature to the rugged box.
# References
- [Zach Freedman's YouTube Channel](https://www.youtube.com/c/ZackFreedman)
- [The video that started it all!](https://youtu.be/ra_9zU-mnl8?si=EOT1LFV65VZfiepi)
- [Gridfinity Documentation repo](https://github.com/Stu142/Gridfinity-Documentation)
- [Gridfinity Unofficial wiki](https://gridfinity.xyz)
- Catalogs
- [gridfinity-catalog](https://github.com/jeffbarr/gridfinity-catalog)
- [Master Collection on Printables](https://www.printables.com/model/242711-gridfinity-master-collection)
- Software/Tools
- [Online Gridfinity Creator](https://gridfinity.bouwens.co)
- [Gridfinity rebuilt OpenSCAD library](https://github.com/kennetek/gridfinity-rebuilt-openscad)
- [Gridfinity Fusion360 generator plugin](https://github.com/Le0Michine/FusionGridfinityGenerator)
- [FreeCAD Gridfinity Parametric Files (on Printables)](https://www.printables.com/@Stu142_524934/collections/969910)
- [Gridfinity eco (low-cost Gridfinity resources)](https://github.com/jrymk/gridfinity-eco)
- [Another CadQuery based Gridfinity script](https://github.com/kmeisthax/gridfinity-cadquery)
- Videos
- [Zach Freedman's follow-up Jul 2022](https://youtu.be/Bd4NnHvTRAY?si=rvgb9geXnq83mhOv)
- [Zach Freedman's follow-up Dec 2022](https://youtu.be/7FCwMq-rVsY?si=tdqAe8MthGjfWEbR)
- [The Next Layer tips video](https://youtu.be/KtbKwAuwv9s?si=1hYPjOvqf8tb5NO9)
## Authors
**cq-gridfinity** was written by [Michael Gale](https://github.com/michaelgale)
================================================
FILE: cqgridfinity/__init__.py
================================================
"""cqgridfinity - A python library to make Gridfinity compatible objects with CadQuery."""
import os
# fmt: off
__project__ = 'cqgridfinity'
__version__ = '0.5.7'
# fmt: on
VERSION = __project__ + "-" + __version__
script_dir = os.path.dirname(__file__)
from .constants import *
from .gf_obj import GridfinityObject
from .gf_baseplate import GridfinityBaseplate
from .gf_box import GridfinityBox, GridfinitySolidBox
from .gf_drawer import GridfinityDrawerSpacer
from .gf_ruggedbox import GridfinityRuggedBox
================================================
FILE: cqgridfinity/constants.py
================================================
#! /usr/bin/env python3
#
# Copyright (C) 2023 Michael Gale
# This file is part of the cq-gridfinity python module.
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Globally useful constants representing Gridfinity geometry
from math import sqrt
SQRT2 = sqrt(2)
EPS = 1e-5
M2_DIAM = 1.8
M2_CLR_DIAM = 2.5
M3_DIAM = 3
M3_CLR_DIAM = 3.5
M3_CB_DIAM = 5.5
M3_CB_DEPTH = 3.5
GRU = 42
GRU2 = GRU / 2
GRHU = 7
GRU_CUT = 42.2 # base extrusion width
GR_WALL = 1.0 # nominal exterior wall thickness
GR_DIV_WALL = 1.2 # width of dividing walls
GR_TOL = 0.5 # nominal tolerance
GR_RAD = 4 # nominal exterior filleting radius
GR_BASE_CLR = 0.25 # clearance above the nominal base height
GR_BASE_HEIGHT = 4.75 # nominal base height
# baseplate extrusion profile
GR_BASE_CHAMF_H = 0.98994949 / SQRT2
GR_STR_H = 1.8
GR_BASE_TOP_CHAMF = GR_BASE_HEIGHT - GR_BASE_CHAMF_H - GR_STR_H
GR_BASE_PROFILE = (
(GR_BASE_TOP_CHAMF * SQRT2, 45),
GR_STR_H,
(GR_BASE_CHAMF_H * SQRT2, 45),
)
GR_STR_BASE_PROFILE = (
(GR_BASE_TOP_CHAMF * SQRT2, 45),
GR_STR_H + GR_BASE_CHAMF_H,
)
GR_BOT_H = 7 # bin nominal floor height
GR_FILLET = 1.1 # inside filleting radius
GR_FLOOR = GR_BOT_H - GR_BASE_HEIGHT # floor offset
# box/bin extrusion profile
GR_BOX_CHAMF_H = 1.1313708 / SQRT2
GR_BOX_TOP_CHAMF = GR_BASE_HEIGHT - GR_BOX_CHAMF_H - GR_STR_H + GR_BASE_CLR
GR_BOX_PROFILE = (
(GR_BOX_TOP_CHAMF * SQRT2, 45),
GR_STR_H,
(GR_BOX_CHAMF_H * SQRT2, 45),
)
# bin mating lip extrusion profile
GR_UNDER_H = 1.6
GR_TOPSIDE_H = 1.2
GR_LIP_PROFILE = (
(GR_UNDER_H * SQRT2, 45),
GR_TOPSIDE_H,
(0.7 * SQRT2, -45),
1.8,
(1.3 * SQRT2, -45),
)
GR_LIP_H = 0
for h in GR_LIP_PROFILE:
if isinstance(h, tuple):
GR_LIP_H += h[0] / SQRT2
else:
GR_LIP_H += h
GR_NO_PROFILE = (GR_LIP_H,)
# bottom hole nominal dimensions
GR_HOLE_D = 6.5
GR_HOLE_H = 2.4
GR_BOLT_D = 3.0
GR_BOLT_H = 3.6 + GR_HOLE_H
GR_HOLE_DIST = 26 / 2
GR_HOLE_SLICE = 0.25
# Rugged Box constant parameters
GR_RBOX_WALL = 2.5
GR_RBOX_FLOOR = 1.2
GR_RBOX_CWALL = 10.0
GR_RBOX_CORNER_W = 56
GR_RBOX_BACK_L = 66
GR_RBOX_FRONT_L = 56
GR_RBOX_RAD = 3.745
GR_RBOX_CRAD = 14
GR_RBOX_CHAN_W = 20
GR_RBOX_CHAN_D = GR_RBOX_CWALL - GR_RBOX_WALL
GR_RBOX_VCUT_D = 1
GR_CLASP_SLIDE_D = 39
GR_CLASP_SLIDE_W = 4
GR_RIB_W = 2
GR_RIB_L = 5
GR_RIB_GAP = 1
GR_RIB_H = 3.5
GR_RIB_SEP = 4
GR_RIB_CTR = 10
GR_REG_L = 5
GR_REG_W = 2.5
GR_REG_H = 2.5
GR_REG_R0 = 10.75
GR_REG_R1 = 8.25
GR_BREG_R0 = GR_REG_R0 + 0.25
GR_BREG_R1 = GR_REG_R1 - 0.25
GR_HANDLE_L1 = 12
GR_HANDLE_L2 = 28
GR_HANDLE_H = 7.5
GR_HANDLE_W = 5
GR_HANDLE_SEP = 12.5
GR_HANDLE_OFS = 61.5
GR_HANDLE_SZ = 30
GR_HANDLE_TH = 7
GR_HANDLE_RAD = 11
GR_LID_HANDLE_W = 70
GR_SIDE_HANDLE_W = 60
GR_HINGE_SZ = 32
GR_HINGE_D = 3
GR_HINGE_W1 = 5.5
GR_HINGE_H1 = 2.7
GR_HINGE_W2 = 2.1
GR_HINGE_H2 = 9
GR_HINGE_CTR = 30.625
GR_HINGE_W3 = 2
GR_HINGE_SEP = 1
GR_HINGE_OFFS = 2.65
GR_HINGE_SKEW = 0.15
GR_HINGE_RAD = 3.5
GR_HINGE_TOL = 0.4
GR_HEX_H = 3
GR_HEX_W = 4
GR_HEX_D = 1.3
GR_LID_WINDOW_H = 6.5
GR_LABEL_SLOT_TH = 2.5
GR_LABEL_TH = 0.8
GR_LABEL_H = 31
GR_LATCH_L = 32.5
GR_LATCH_W = 19.6
GR_LATCH_H = 7
GR_LATCH_IW = 14.75
GR_LATCH_IL = 5.2
================================================
FILE: cqgridfinity/gf_baseplate.py
================================================
#! /usr/bin/env python3
#
# Copyright (C) 2023 Michael Gale
# This file is part of the cq-gridfinity python module.
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Gridfinity Baseplates
import cadquery as cq
from cqgridfinity import *
from cqkit.cq_helpers import (
rounded_rect_sketch,
composite_from_pts,
rotate_x,
recentre,
)
from cqkit import VerticalEdgeSelector, HasZCoordinateSelector
class GridfinityBaseplate(GridfinityObject):
"""Gridfinity Baseplate
This class represents a basic Gridfinity baseplate object. This baseplate
more or less conforms to the original simple baseplate released by
Zach Freedman. As such, it does not include features such as mounting
holes, magnet holes, weight slots, etc.
length_u - length in U (42 mm / U)
width_u - width in U (42 mm / U)
ext_depth - extrude bottom face by an extra amount in mm
straight_bottom - remove bottom chamfer and replace with straight side
corner_screws - add countersink mounting screws to the inside corners
corner_tab_size - size of mounting screw corner tabs
csk_hole - mounting screw hole diameter
csk_diam - mounting screw countersink diameter
csk_angle - mounting screw countersink angle
"""
def __init__(self, length_u, width_u, **kwargs):
super().__init__()
self.length_u = length_u
self.width_u = width_u
self.ext_depth = 0
self.straight_bottom = False
self.corner_screws = False
self.corner_tab_size = 21
self.csk_hole = 5.0
self.csk_diam = 10.0
self.csk_angle = 82
for k, v in kwargs.items():
if k in self.__dict__ and v is not None:
self.__dict__[k] = v
if self.corner_screws:
self.ext_depth = max(self.ext_depth, 5.0)
def _corner_pts(self):
oxy = self.corner_tab_size / 2
return [
(i * (self.length / 2 - oxy), j * (self.width / 2 - oxy), 0)
for i in (-1, 1)
for j in (-1, 1)
]
def render(self):
profile = GR_BASE_PROFILE if not self.straight_bottom else GR_STR_BASE_PROFILE
if self.ext_depth > 0:
profile = [*profile, self.ext_depth]
rc = self.extrude_profile(
rounded_rect_sketch(GRU_CUT, GRU_CUT, GR_RAD), profile
)
rc = rotate_x(rc, 180).translate((GRU2, GRU2, GR_BASE_HEIGHT + self.ext_depth))
rc = recentre(composite_from_pts(rc, self.grid_centres), "XY")
r = (
cq.Workplane("XY")
.rect(self.length, self.width)
.extrude(GR_BASE_HEIGHT + self.ext_depth)
.edges("|Z")
.fillet(GR_RAD)
.faces(">Z")
.cut(rc)
)
if self.corner_screws:
rs = cq.Sketch().rect(self.corner_tab_size, self.corner_tab_size)
rs = cq.Workplane("XY").placeSketch(rs).extrude(self.ext_depth)
rs = rs.faces(">Z").cskHole(
self.csk_hole, cskDiameter=self.csk_diam, cskAngle=self.csk_angle
)
r = r.union(recentre(composite_from_pts(rs, self._corner_pts()), "XY"))
bs = VerticalEdgeSelector(self.ext_depth) & HasZCoordinateSelector(0)
r = r.edges(bs).fillet(GR_RAD)
return r
================================================
FILE: cqgridfinity/gf_box.py
================================================
#! /usr/bin/env python3
#
# Copyright (C) 2023 Michael Gale
# This file is part of the cq-gridfinity python module.
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Gridfinity Boxes
import math
import cadquery as cq
from cqkit import HasZCoordinateSelector, VerticalEdgeSelector, FlatEdgeSelector
from cqkit.cq_helpers import rounded_rect_sketch, composite_from_pts
from cqgridfinity import *
class GridfinityBox(GridfinityObject):
"""Gridfinity Box
This class represents a Gridfinity compatible box module. As a minimum,
this class is initialized with basic 3D unit dimensions for length,
width, and height. length and width are multiples of 42 mm Gridfinity
intervals and height represents multiples of 7 mm.
Many box features can be enabled with attributes provided either as
keywords or direct dotted access. These attributes include:
- solid : renders the box without an interior, i.e. a solid block. This
is useful for making custom Gridfinity modules by subtracting
out shapes from the solid interior. Normally, the box is
rendered solid up to its maximum size; however, the
solid_ratio attribute can specify a solid fill of between
0.0 to 1.0, i.e. 0 to 100% fill.
- holes : adds bottom mounting holes for magnets or screws
- scoops : adds a radiused bottom edge to the interior to help fetch
parts from the box
- labels : adds a flat flange along each compartment for adding a label
- no_lip : removes the contoured lip on the top module used for stacking
- length_div, width_div : subdivides the box into sub-compartments in
length and/or width.
- lite_style : render box as an economical shell without elevated floor
- unsupported_holes : render bottom holes as 3D printer friendly versions
which can be printed without supports
- label_width : width of top label ledge face overhang
- label_height : height of label ledge overhang
- scoop_rad : radius of the bottom scoop feature
- wall_th : wall thickness
- hole_diam : magnet/counterbore bolt hole diameter
"""
def __init__(self, length_u, width_u, height_u, **kwargs):
super().__init__()
self.length_u = length_u
self.width_u = width_u
self.height_u = height_u
self.length_div = 0
self.width_div = 0
self.scoops = False
self.labels = False
self.solid = False
self.holes = False
self.no_lip = False
self.solid_ratio = 1.0
self.lite_style = False
self.unsupported_holes = False
self.label_width = 12 # width of the label strip
self.label_height = 10 # thickness of label overhang
self.label_lip_height = 0.8 # thickness of label vertical lip
self.scoop_rad = 14 # radius of optional interior scoops
self.fillet_interior = True
self.wall_th = GR_WALL
self.hole_diam = GR_HOLE_D # magnet/bolt hole diameter
for k, v in kwargs.items():
if k in self.__dict__:
self.__dict__[k] = v
self._int_shell = None
self._ext_shell = None
def __str__(self):
s = []
s.append(
"Gridfinity Box %dU x %dU x %dU (%.2f x %.2f x %.2f mm)"
% (
self.length_u,
self.width_u,
self.height_u,
self.length - GR_TOL,
self.width - GR_TOL,
self.height,
)
)
sl = "no mating top lip" if self.no_lip else "with mating top lip"
ss = "Lite style box " if self.lite_style else ""
s.append(" %sWall thickness: %.2f mm %s" % (ss, self.wall_th, sl))
s.append(
" Floor height : %.2f mm Inside height: %.2f mm Top reference height: %.2f mm"
% (self.floor_h + GR_BASE_HEIGHT, self.int_height, self.top_ref_height)
)
if self.solid:
s.append(" Solid filled box with fill ratio %.2f" % (self.solid_ratio))
if self.holes:
s.append(" Bottom mounting holes with %.2f mm diameter" % (self.hole_diam))
if self.unsupported_holes:
s.append(" Holes are 3D printer friendly and can be unsupported")
if self.scoops:
s.append(" Lengthwise scoops with %.2f mm radius" % (self.scoop_rad))
if self.labels:
s.append(
" Lengthwise label shelf %.2f mm wide with %.2f mm overhang"
% (self.label_width, self.label_height)
)
if self.length_div:
xl = (self.inner_l - GR_DIV_WALL * (self.length_div)) / (
self.length_div + 1
)
s.append(
" %dx lengthwise divisions for %.2f mm compartment lengths"
% (self.length_div, xl)
)
if self.width_div:
yl = (self.inner_w - GR_DIV_WALL * (self.width_div)) / (self.width_div + 1)
s.append(
" %dx widthwise divisions for %.2f mm compartment widths"
% (self.width_div, yl)
)
s.append(" Auto filename: %s" % (self.filename()))
return "\n".join(s)
def render(self):
"""Returns a CadQuery Workplane object representing this Gridfinity box."""
self._int_shell = None
if self.lite_style:
# just force the dividers to the desired quantity in both dimensions
# rather than raise a exception
if self.length_div:
self.length_div = self.length_u - 1
if self.width_div:
self.width_div = self.width_u - 1
if self.solid:
raise ValueError(
"Cannot select both solid and lite box styles together"
)
if self.holes:
raise ValueError(
"Cannot select both holes and lite box styles together"
)
if self.wall_th > 1.5:
raise ValueError(
"Wall thickness cannot exceed 1.5 mm for lite box style"
)
if self.wall_th > 2.5:
raise ValueError("Wall thickness cannot exceed 2.5 mm")
if self.wall_th < 0.5:
raise ValueError("Wall thickness must be at least 0.5 mm")
r = self.render_shell()
rd = self.render_dividers()
rs = self.render_scoops()
rl = self.render_labels()
for e in (rd, rl, rs):
if e is not None:
r = r.union(e)
if not self.solid and self.fillet_interior:
heights = [GR_FLOOR]
if self.labels:
heights.append(self.safe_label_height(backwall=True, from_bottom=True))
heights.append(self.safe_label_height(backwall=False, from_bottom=True))
bs = (
HasZCoordinateSelector(heights, min_points=1, tolerance=0.5)
+ VerticalEdgeSelector(">5")
- HasZCoordinateSelector("<%.2f" % (self.floor_h))
)
if self.lite_style and self.scoops:
bs = bs - HasZCoordinateSelector("<=%.2f" % (self.floor_h))
bs = bs - VerticalEdgeSelector()
r = self.safe_fillet(r, bs, self.safe_fillet_rad)
if self.lite_style and not self.has_dividers:
bs = FlatEdgeSelector(self.floor_h)
if self.wall_th < 1.2:
r = self.safe_fillet(r, bs, 0.5)
elif self.wall_th < 1.25:
r = self.safe_fillet(r, bs, 0.25)
if not self.labels and self.has_dividers:
bs = VerticalEdgeSelector(
GR_TOPSIDE_H, tolerance=0.05
) & HasZCoordinateSelector(GRHU * self.height_u - GR_BASE_HEIGHT)
r = self.safe_fillet(r, bs, GR_TOPSIDE_H - EPS)
if self.holes:
r = self.render_holes(r)
r = r.translate((-self.half_l, -self.half_w, GR_BASE_HEIGHT))
if self.unsupported_holes:
r = self.render_hole_fillers(r)
return r
@property
def top_ref_height(self):
"""The height of the top surface of a solid box or the floor
height of an empty box."""
if self.solid:
return self.max_height * self.solid_ratio + GR_BOT_H
if self.lite_style:
return self.floor_h
return GR_BOT_H
@property
def bin_height(self):
return self.height - GR_BASE_HEIGHT
def safe_label_height(self, backwall=False, from_bottom=False):
lw = self.label_width
if backwall:
lw += self.lip_width
lh = self.label_height * (lw / self.label_width)
yl = self.max_height - self.label_height + self.wall_th
if backwall:
yl -= self.lip_width
if yl < 0:
lh = self.max_height - 1.5 * GR_FILLET - 0.1
elif yl < 1.5 * GR_FILLET:
lh -= 1.5 * GR_FILLET - yl + 0.1
if from_bottom:
ws = math.sin(math.atan2(self.label_height, self.label_width))
if backwall:
lh = self.max_height + GR_FLOOR - lh + ws * self.wall_th
else:
lh = self.max_height + GR_FLOOR - lh + ws * GR_DIV_WALL
return lh
@property
def has_dividers(self):
return self.length_div > 0 or self.width_div > 0
@property
def interior_solid(self):
if self._int_shell is not None:
return self._int_shell
self._int_shell = self.render_interior()
return self._int_shell
def render_interior(self, force_solid=False):
"""Renders the interior cutting solid of the box."""
wall_u = self.wall_th - GR_WALL
wall_h = self.int_height + wall_u
under_h = ((GR_UNDER_H - wall_u) * SQRT2, 45)
profile = GR_NO_PROFILE if self.no_lip else [under_h, *GR_LIP_PROFILE[1:]]
profile = [wall_h, *profile]
if self.int_height < 0:
profile = [self.height - GR_BOT_H]
rci = self.extrude_profile(
rounded_rect_sketch(*self.inner_dim, self.inner_rad), profile
)
rci = rci.translate((*self.half_dim, self.floor_h))
if self.solid or force_solid:
hs = self.max_height * self.solid_ratio
ri = rounded_rect_sketch(*self.inner_dim, self.inner_rad)
rf = cq.Workplane("XY").placeSketch(ri).extrude(hs)
rf = rf.translate((*self.half_dim, self.floor_h))
rci = rci.cut(rf)
if self.scoops and not self.no_lip and not self.lite_style:
rf = (
cq.Workplane("XY")
.rect(self.inner_l, 2 * self.under_h)
.extrude(self.max_height)
.translate((self.half_l, -self.half_in, self.floor_h))
)
rci = rci.cut(rf)
if self.lite_style:
r = composite_from_pts(self.base_interior(), self.grid_centres)
rci = rci.union(r)
return rci
def solid_shell(self):
"""Returns a completely solid box object useful for intersecting with other solids."""
if self._ext_shell is not None:
return self._ext_shell
r = self.render_shell(as_solid=True)
self._ext_shell = r.cut(self.render_interior(force_solid=True))
return self._ext_shell
def mask_with_obj(self, obj):
"""Intersects a solid object with this box."""
return obj.intersect(self.solid_shell())
def base_interior(self):
profile = [GR_BASE_HEIGHT, *GR_BOX_PROFILE]
zo = GR_BASE_HEIGHT + GR_BASE_CLR
if self.int_height < 0:
h = self.bin_height - GR_BASE_HEIGHT
profile = [h, *profile]
zo += h
r = self.extrude_profile(
rounded_rect_sketch(GRU - GR_TOL, GRU - GR_TOL, self.outer_rad),
profile,
)
rx = r.faces("<Z").shell(-self.wall_th)
r = r.cut(rx).mirror(mirrorPlane="XY").translate((0, 0, zo))
return r
def render_shell(self, as_solid=False):
"""Renders the box shell without any added features."""
r = self.extrude_profile(
rounded_rect_sketch(GRU, GRU, self.outer_rad + GR_BASE_CLR), GR_BOX_PROFILE
)
r = r.translate((0, 0, -GR_BASE_CLR))
r = r.mirror(mirrorPlane="XY")
r = composite_from_pts(r, self.grid_centres)
rs = rounded_rect_sketch(*self.outer_dim, self.outer_rad)
rw = (
cq.Workplane("XY")
.placeSketch(rs)
.extrude(self.bin_height - GR_BASE_CLR)
.translate((*self.half_dim, GR_BASE_CLR))
)
rc = (
cq.Workplane("XY")
.placeSketch(rs)
.extrude(-GR_BASE_HEIGHT - 1)
.translate((*self.half_dim, 0.5))
)
rc = rc.intersect(r).union(rw)
if not as_solid:
return rc.cut(self.interior_solid)
return rc
def render_dividers(self):
r = None
if self.length_div > 0 and not self.solid:
wall_w = (
cq.Workplane("XY")
.rect(GR_DIV_WALL, self.outer_w)
.extrude(self.max_height)
.translate((0, 0, self.floor_h))
)
xl = self.inner_l / (self.length_div + 1)
pts = [
((x + 1) * xl - self.half_in, self.half_w)
for x in range(self.length_div)
]
r = composite_from_pts(wall_w, pts)
if self.width_div > 0 and not self.solid:
wall_l = (
cq.Workplane("XY")
.rect(self.outer_l, GR_DIV_WALL)
.extrude(self.max_height)
.translate((0, 0, self.floor_h))
)
yl = self.inner_w / (self.width_div + 1)
pts = [
(self.half_l, (y + 1) * yl - self.half_in)
for y in range(self.width_div)
]
rw = composite_from_pts(wall_l, pts)
if r is not None:
r = r.union(rw)
else:
r = rw
return r
def render_scoops(self):
if not self.scoops or self.solid:
return None
# front wall scoop
# prevent the scoop radius exceeding the internal height
srad = min(self.scoop_rad, self.int_height - 0.1)
rs = cq.Sketch().rect(srad, srad).vertices(">X and >Y").circle(srad, mode="s")
rsc = cq.Workplane("YZ").placeSketch(rs).extrude(self.inner_l)
rsc = rsc.translate((0, 0, srad / 2 + GR_FLOOR))
yo = -self.half_in + srad / 2
# offset front wall scoop by top lip overhang if applicable
if not self.no_lip and not self.lite_style:
yo += self.under_h
zo = -GR_BOT_H + self.wall_th if self.lite_style else 0
rs = rsc.translate((-self.half_in, yo, zo))
# intersect to prevent solids sticking out of rounded corners
r = rs.intersect(self.interior_solid)
if self.width_div > 0:
# add scoops along each internal dividing wall in the width dimension
yl = self.inner_w / (self.width_div + 1)
pts = [
(-self.half_in, (y + 1) * yl - self.half_in)
for y in range(self.width_div)
]
rs = composite_from_pts(rsc, pts)
r = r.union(rs.translate((0, GR_DIV_WALL / 2 + srad / 2, zo)))
r = r.intersect(self.render_shell(as_solid=True))
return r
def render_labels(self):
if not self.labels or self.solid:
return None
# back wall label flange with compensated width and height
lw = self.label_width + self.lip_width
rs = (
cq.Sketch()
.segment((0, 0), (lw, 0))
.segment((lw, -self.safe_label_height(backwall=True)))
.segment((0, -self.label_lip_height))
.close()
.assemble()
.vertices("<X")
.vertices("<Y")
.fillet(self.label_lip_height / 2)
)
rsc = cq.Workplane("YZ").placeSketch(rs).extrude(self.inner_l)
yo = -lw + self.outer_w / 2 + self.half_w + self.wall_th / 4
rs = rsc.translate((-self.half_in, yo, self.floor_h + self.max_height))
# intersect to prevent solids sticking out of rounded corners
r = rs.intersect(self.interior_solid)
if self.width_div > 0:
# add label flanges along each dividing wall
rs = (
cq.Sketch()
.segment((0, 0), (self.label_width, 0))
.segment((self.label_width, -self.safe_label_height(backwall=False)))
.segment((0, -self.label_lip_height))
.close()
.assemble()
.vertices("<X")
.vertices("<Y")
.fillet(self.label_lip_height / 2)
)
rsc = cq.Workplane("YZ").placeSketch(rs).extrude(self.inner_l)
rsc = rsc.translate((0, -self.label_width, self.floor_h + self.max_height))
yl = self.inner_w / (self.width_div + 1)
pts = [
(-self.half_in, (y + 1) * yl - self.half_in + GR_DIV_WALL / 2)
for y in range(self.width_div)
]
r = r.union(composite_from_pts(rsc, pts))
return r
def render_holes(self, obj):
if not self.holes:
return obj
h = GR_HOLE_H
if self.unsupported_holes:
h += GR_HOLE_SLICE
return (
obj.faces("<Z")
.workplane()
.pushPoints(self.hole_centres)
.cboreHole(GR_BOLT_D, self.hole_diam, h, depth=GR_BOLT_H)
)
def render_hole_fillers(self, obj):
rc = (
cq.Workplane("XY")
.rect(self.hole_diam / 2, self.hole_diam)
.extrude(GR_HOLE_SLICE)
)
xo = self.hole_diam / 2
rs = composite_from_pts(rc, [(-xo, 0, GR_HOLE_H), (xo, 0, GR_HOLE_H)])
rs = composite_from_pts(rs, self.hole_centres)
return obj.union(rs.translate((-self.half_l, self.half_w, 0)))
class GridfinitySolidBox(GridfinityBox):
"""Convenience class to represent a solid Gridfinity box."""
def __init__(self, length_u, width_u, height_u, **kwargs):
super().__init__(length_u, width_u, height_u, **kwargs, solid=True)
================================================
FILE: cqgridfinity/gf_drawer.py
================================================
#! /usr/bin/env python3
#
# Copyright (C) 2023 Michael Gale
# This file is part of the cq-gridfinity python module.
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Gridfinity Drawer Spacers
import math
import cadquery as cq
from cqgridfinity import *
from cqkit.cq_helpers import rotate_x, rotate_y, rotate_z
class GridfinityDrawerSpacer(GridfinityObject):
"""Gridfinity Drawer Spacers
This class is used for making spacer elements which help fit Gridfinity baseplates
snugly into a drawer. The spacers consist of 4x corner elements plus a left/right
pair and front/back pair. If the spacers are wide enough, they will include
interlocking alignment pegs/holes.
Normally spacers are made for the front and back of the drawer of the same size.
However, if front_and_back is False, then only a back spacer (2x thicker) is made.
This will place the gridfinity grid flush behind the drawer face, rather than
equally spaced between the face and back wall.
"""
def __init__(self, dr_width=None, dr_depth=None, **kwargs):
super().__init__()
self.length_u = 1
self.width_u = 1
self.length_th = 10
self.width_th = 10
self.thickness = GR_BASE_HEIGHT
self.chamf_rad = 1.0
self.show_arrows = True
self.arrow_h = 0.8
self.length_fill = 0
self.width_fill = 0
self.align_features = True
self.align_l = 16
self.align_tol = 0.15
self.align_min = 8
self.min_margin = 4
self.tolerance = GR_TOL
self.front_and_back = True
for k, v in kwargs.items():
if k in self.__dict__:
self.__dict__[k] = v
if dr_width is not None and dr_depth is not None:
verbose = kwargs["verbose"] if "verbose" in kwargs else False
self.best_fit_to_dim(dr_width, dr_depth, verbose=verbose)
def best_fit_to_dim(self, length, width, verbose=False):
"""Computes the best fit of Gridfinity units to fill a drawer dimensions.
The geometry of all the spacer elements is then computed to securely
centre the Gridfinity baseplate(s) inside the drawer footprint."""
self.size = length, width
lu, wu = (math.floor(x / GRU) for x in (length, width))
lg, wg = (x * GRU for x in (lu, wu))
lm, wm = (length - lg) / 2, (width - wg) / 2
self.size_u = lu, wu
self.width_th, self.length_th = lm - self.tolerance, wm - self.tolerance
self.length_u, self.width_u = math.floor(lu / 3), math.floor(wu / 3)
self.length_fill, self.width_fill = lg - 2 * self.length, wg - 2 * self.width
if self.wide_enough:
self.align_l = 1.5 * self.width_th
if self.deep_enough:
self.align_l = min(self.align_l, 1.5 * self.length_th)
self.align_l = min(self.align_l, 16)
if verbose:
print("Best fit for %.2f x %.2f mm is %dU x %dU" % (length, width, lu, wu))
if self.front_and_back:
print(
"with %.2f mm margin each side and %.2f mm margin front and back"
% (lm, wm)
)
else:
print(
"with %.2f mm margin each side and %.2f mm back (or front) margin"
% (lm, 2 * wm)
)
if not self.front_and_back:
print("Corner spacers only generated for either front or back wall")
if self.wide_enough and self.deep_enough:
print(
"Corner spacers : %dU wide x %dU deep"
% (self.length_u, self.width_u)
)
elif self.wide_enough:
print(
"Corner spacers : %dU deep x %.2f mm"
% (self.width_u, self.width_th)
)
elif self.deep_enough:
print(
"Corner spacers : %dU wide x %.2f mm"
% (self.length_u, self.fb_length_th)
)
if self.deep_enough:
if self.front_and_back:
print(
"Front/back spacers : %dU wide x %.2f mm +%.2f mm tolerance"
% (self.length_fill / GRU, self.length_th, self.tolerance)
)
else:
print(
"Back spacer : %dU wide x %.2f mm +%.2f mm tolerance"
% (self.length_fill / GRU, self.fb_length_th, self.tolerance)
)
else:
print("Front/back spacers : not required")
if self.wide_enough:
print(
"Left/right spacers : %dU deep x %.2f mm +%.2f mm tolerance"
% (self.width_fill / GRU, self.width_th, self.tolerance)
)
if not self.front_and_back:
print(
"Extra left/right spacers generated %dU deep" % (self.width_u)
)
else:
print("Left/right spacers : not required")
@property
def fillet_rad(self):
rads = [GR_RAD]
if self.wide_enough:
rads.append(self.width_th / 6)
if self.deep_enough:
rads.append(self.length_th / 6)
return min(rads)
@property
def safe_chamfer_rad(self):
rads = [self.chamf_rad]
if self.wide_enough:
rads.append(self.width_th / 6)
if self.deep_enough:
rads.append(self.length_th / 6)
return min(rads)
@property
def wide_enough(self):
return self.width_th > self.min_margin
@property
def deep_enough(self):
return self.length_th > self.min_margin
@property
def fb_length_th(self):
if not self.front_and_back:
return 2 * self.length_th
return self.length_th
def check_dimensions(self):
"""Check required size does not fall below specified minimum margin."""
if not self.wide_enough and not self.deep_enough:
print("Drawer spacers NOT required since resulting margins are:")
print(
" %.2f mm +/-%.2f mm (tolerance) widthwise which is not above the %.2f margin threshold"
% (self.length_th, self.tolerance, self.min_margin)
)
print(
" %.2f mm +/-%.2f mm (tolerance) depthwise which is not above the %.2f margin threshold"
% (self.width_th, self.tolerance, self.min_margin)
)
return False
return True
def render(self, arrows_top=True, arrows_bottom=True, front_and_back=True):
"""Renders a corner spacer component. This component can be used for any of
the four corners due to symmetry. Optional arrows can be cut into the
component on the top or bottom to show the drawer sliding/depth-wise direction
"""
if not self.check_dimensions():
return None
sp_length = self.length + self.width_th + self.tolerance
sp_width = self.width + self.fb_length_th + self.tolerance
r, rd = None, None
if self.deep_enough and front_and_back:
r = (
cq.Workplane("XY")
.rect(sp_length, self.fb_length_th)
.extrude(self.thickness)
)
er = min(GR_RAD, max(self.length_th, self.width_th) / 4)
r = r.translate((sp_length / 2, self.fb_length_th / 2, 0))
r = r.edges("|Z").edges("<XY").fillet(er)
r = r.edges("|Z").fillet(self.fillet_rad)
if self.wide_enough:
if not front_and_back:
sp_width -= self.fb_length_th
rd = (
cq.Workplane("XY").rect(self.width_th, sp_width).extrude(self.thickness)
)
er = min(GR_RAD, max(self.length_th, self.width_th) / 4)
rd = rd.translate((self.width_th / 2, sp_width / 2, 0))
rd = rd.edges("|Z").edges("<Y").fillet(er)
rd = rd.edges("|Z").fillet(self.fillet_rad)
if r is not None and rd is not None:
r = r.union(rd)
elif r is None and rd is not None:
r = rd
r = r.faces(">Z or <Z").chamfer(self.safe_chamfer_rad)
r = self.orientation_arrows(
r, self.width_th / 2, sp_width / 2, top=arrows_top, bottom=arrows_bottom
)
if self.align_features and self.fb_length_th > self.align_min:
rc = self.alignment_feature(as_cutter=True)
r = r.cut(rc.translate((sp_length, self.fb_length_th / 2, 0)))
if self.align_features and self.width_th > self.align_min:
rc = self.alignment_feature(as_cutter=False, horz=False)
r = r.union(rc.translate((self.width_th / 2, sp_width, 0)))
self._cq_obj = r
self._obj_label = "corner_spacer"
return r
def alignment_feature(self, as_cutter=False, horz=True):
"""Renders optional mating alignment pegs/holes for connecting the spacer components."""
x, y = self.align_l, self.fb_length_th / 2
if not horz:
y = self.width_th / 2
fr = min(GR_RAD / 2, y / 3)
if as_cutter:
x += 2 * self.align_tol
y += 2 * self.align_tol
fr += self.align_tol
rs = (
cq.Sketch()
.segment((0, y / 3), (x / 2, y / 2))
.segment((x / 2, -y / 2))
.segment((0, -y / 3))
.segment((-x / 2, -y / 2))
.segment((-x / 2, y / 2))
.close()
.assemble()
.vertices()
.fillet(fr)
)
r = cq.Workplane("XY").placeSketch(rs).extrude(self.thickness)
if not horz:
r = rotate_z(r, 90)
if not as_cutter:
r = r.faces(">Z or <Z").chamfer(self.safe_chamfer_rad)
return r
def orientation_arrows(self, obj, x, y, up=True, down=True, top=True, bottom=True):
"""Renders optional orientation arrows which show the sliding (depth-wise)
direction of the drawer."""
if self.show_arrows and self.wide_enough:
la = self.width_th / 2
ra = (
cq.Sketch()
.segment((0, 0), (la / 2, la))
.segment((la, 0))
.close()
.assemble()
)
ru = (
cq.Workplane("XY")
.placeSketch(ra)
.extrude(self.arrow_h)
.translate((-la / 2, -la / 2, 0))
)
rd = ru.rotate((0, 0, 0), (0, 0, 1), 180)
th = self.thickness - self.arrow_h
yo = 10 * self.width_th / 15 if up and down else 0
if up and top:
obj = obj.cut(ru.translate((x, y + yo, th)))
if up and bottom:
obj = obj.cut(ru.translate((x, y + yo, 0)))
if down and top:
obj = obj.cut(rd.translate((x, y - yo, th)))
if down and bottom:
obj = obj.cut(rd.translate((x, y - yo, 0)))
return obj
def render_length_filler(self, alignment_type="peg"):
"""Renders the centre filler element used along the front/back walls
of the drawer."""
if not self.deep_enough:
return None
r = (
cq.Workplane("XY")
.rect(self.length_fill, self.fb_length_th)
.extrude(self.thickness)
)
r = r.edges("|Z").fillet(self.fillet_rad)
r = r.faces(">Z or <Z").chamfer(self.safe_chamfer_rad)
if self.align_features and self.fb_length_th > self.align_min:
if alignment_type == "hole":
ra = self.alignment_feature(as_cutter=True)
r = r.cut(ra.translate((self.length_fill / 2, 0, 0)))
r = r.cut(ra.translate((-self.length_fill / 2, 0, 0)))
else:
ra = self.alignment_feature(as_cutter=False)
r = r.union(ra.translate((self.length_fill / 2, 0, 0)))
r = r.union(ra.translate((-self.length_fill / 2, 0, 0)))
self._cq_obj = r
self._obj_label = "length_spacer"
return r
def render_width_filler(self, arrows_top=True, arrows_bottom=True):
"""Renders the centre filler element used along the left/right walls
of the drawer."""
if not self.wide_enough:
return None
r = (
cq.Workplane("XY")
.rect(self.width_th, self.width_fill)
.extrude(self.thickness)
)
r = r.edges("|Z").fillet(self.fillet_rad)
r = r.faces(">Z or <Z").chamfer(self.safe_chamfer_rad)
r = self.orientation_arrows(r, 0, 0, top=arrows_top, bottom=arrows_bottom)
if self.align_features and self.width_th > self.align_min:
ra = self.alignment_feature(horz=False, as_cutter=True)
r = r.cut(ra.translate((0, self.width_fill / 2, 0)))
r = r.cut(ra.translate((0, -self.width_fill / 2, 0)))
self._cq_obj = r
self._obj_label = "width_spacer"
return r
def render_full_set(self, include_baseplate=False):
"""Renders a complete set of spacer components including the four corners plus
left/right and front/back spacer pairs. The components are placed in their
respective installed position in the drawer so that the resulting object can
be used to preview final composition of components."""
# Four corners top/bottom left + top/bottom right
if not self.check_dimensions():
return None
if self.front_and_back:
bl = self.render()
tl = rotate_x(bl, 180).translate((0, self.size[1], self.thickness))
br = rotate_y(bl, 180).translate((self.size[0], 0, self.thickness))
tr = rotate_z(bl, 180).translate((*self.size, 0))
else:
bl = self.render(arrows_bottom=False)
br = self.render(arrows_top=False)
br = rotate_y(br, 180).translate((self.size[0], 0, self.thickness))
tl = self.render(arrows_bottom=False, front_and_back=False)
tl = rotate_z(tl, 180).translate((self.width_th, self.size[1], 0))
tr = self.render(arrows_top=False, front_and_back=False)
tr = rotate_y(tr, 180)
tr = rotate_z(tr, 180)
tr = tr.translate((*self.size, 0))
tr = tr.translate((-self.width_th, 0, self.thickness))
r = bl.union(tl).union(br).union(tr)
# 2x length-wise (drawer width) fillers
if self.deep_enough:
lf = self.render_length_filler()
r = r.union(lf.translate((self.size[0] / 2, self.fb_length_th / 2, 0)))
if self.front_and_back:
r = r.union(
lf.translate(
(self.size[0] / 2, self.size[1] - self.fb_length_th / 2, 0)
)
)
# 2x width-wise (drawer depth) fillers
if self.wide_enough:
wf = self.render_width_filler()
yo = self.size[1] / 2
if not self.front_and_back:
yo += self.fb_length_th / 2
r = r.union(wf.translate((self.width_th / 2, yo, 0)))
r = r.union(wf.translate((self.size[0] - self.width_th / 2, yo, 0)))
if include_baseplate:
bp = GridfinityBaseplate(*self.size_u)
rb = bp.render().translate((self.size[0] / 2, self.size[1] / 2, 0))
if not self.front_and_back:
rb = rb.translate((0, self.fb_length_th / 2, 0))
r = r.union(rb)
self._cq_obj = r
self._obj_label = "full_set"
return r
def render_half_set(self):
"""Renders half of the full set of spacer components arranged for convenience
for 3D printing. This resulting compound object can then be printed twice to
yield a complete set of spacer components for a drawer.
If front_and_back is False, then this function will render all of the
components to fill the drawer since only one set of corner spacers is
required and the remaining spacers are typically slim enough to fit together
on a build plate."""
# one of each corner
if not self.check_dimensions():
return None
bl = self.render(arrows_bottom=False)
br = self.render(arrows_top=False)
if self.deep_enough:
xo = self.length + 2.5 * self.width_th
yo = 1.5 * self.fb_length_th
else:
xo = 2.5 * self.width_th
yo = 0
br = rotate_y(br, 180).translate((xo, yo, self.thickness))
r = bl.union(br)
# length-wise (drawer width) filler
if self.deep_enough:
xl = self.length_fill / 2 - (
self.length_fill - (self.length + self.width_th)
)
if self.fb_length_th > self.align_min:
xl -= self.align_l / 2
if self.wide_enough:
yt = self.width + self.fb_length_th
if self.width_th > self.align_min:
yt += self.align_l / 2
yl = max(yt, self.width_fill)
yl += max(self.fb_length_th, self.align_l / 2)
else:
yl = 3.5 * self.fb_length_th
r = r.union(self.render_length_filler().translate((xl, yl, 0)))
# width-wise (drawer depth) filler
if self.wide_enough:
wf = self.render_width_filler(arrows_bottom=False)
r = r.union(wf.translate((-self.width_th, self.width_fill / 2, 0)))
if not self.front_and_back:
r = r.union(
wf.translate((-2.5 * self.width_th, self.width_fill / 2, 0))
)
fb = self.render(arrows_bottom=False, front_and_back=False)
r = r.union(fb.translate((-4.5 * self.width_th, 0, 0)))
r = r.union(fb.translate((-6 * self.width_th, 0, 0)))
self._cq_obj = r
self._obj_label = "half_set"
return r
================================================
FILE: cqgridfinity/gf_helpers.py
================================================
#! /usr/bin/env python3
#
# Copyright (C) 2023 Michael Gale
# This file is part of the cq-gridfinity python module.
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Gridfinity Helper Functions
import cadquery as cq
from cqkit import rotate_z
def quarter_circle(
outer_rad, inner_rad, height, quad="tr", chamf=0.5, chamf_face=">Z", ext=0
):
"""Renders a quarter circle shaped slot in any of 4 quadrants"""
r = cq.Workplane("XY").circle(outer_rad).extrude(height)
rc = cq.Workplane("XY").circle(inner_rad).extrude(height)
r = r.cut(rc)
rc = cq.Workplane("XY").rect(outer_rad, outer_rad).extrude(height)
pos = {
"tr": (outer_rad / 2, outer_rad / 2, 0),
"tl": (-outer_rad / 2, outer_rad / 2, 0),
"br": (outer_rad / 2, -outer_rad / 2, 0),
"bl": (-outer_rad / 2, -outer_rad / 2, 0),
}
pt = pos[quad]
r = r.intersect(rc.translate(pt))
r = r.translate((-pt[0], -pt[1], 0))
if ext > 0:
faces = {
"tl": "<Y >X",
"tr": "<X <Y",
"br": "<X >Y",
"bl": ">Y >X",
}
for face in faces[quad].split():
r = r.faces(face).wires().toPending().workplane().extrude(ext, combine=True)
if chamf > 0:
r = r.faces(chamf_face).chamfer(chamf)
return r
def chamf_cyl(rad, height, chamf=0.5):
"""Chamfered cylinder."""
r = cq.Workplane("XY").circle(rad).extrude(height)
if chamf > 0:
return r.faces("<Z or >Z").chamfer(chamf)
return r
def chamf_rect(length, width, height, angle=0, tol=0.5, z_offset=0):
"""Chamfer rectangular box"""
if not z_offset > 0:
length += tol
width += tol
height += tol
r = cq.Workplane("XY").rect(length, width).extrude(height)
r = r.faces(">Z").chamfer(0.5).translate((0, 0, z_offset))
return rotate_z(r, angle)
================================================
FILE: cqgridfinity/gf_obj.py
================================================
#! /usr/bin/env python3
#
# Copyright (C) 2023 Michael Gale
# This file is part of the cq-gridfinity python module.
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Gridfinity base object class
import math
import os
from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.StlAPI import StlAPI_Writer
import cadquery as cq
from cadquery import exporters
from cqgridfinity import *
from cqkit import export_step_file
# Special test to see which version of CadQuery is installed and
# therefore if any compensation is required for extruded zlen
# CQ versions < 2.4.0 typically require zlen correction, i.e.
# scaling the vertical extrusion extent by 1/cos(taper)
ZLEN_FIX = True
_r = cq.Workplane("XY").rect(2, 2).extrude(1, taper=45)
_bb = _r.vals()[0].BoundingBox()
if abs(_bb.zlen - 1.0) < 1e-3:
ZLEN_FIX = False
class GridfinityObject:
"""Base Gridfinity object class
This class bundles glabally relevant constants, properties, and methods
for derived Gridfinity object classes.
"""
def __init__(self, **kwargs):
self.length_u = 1
self.width_u = 1
self.height_u = 1
self._cq_obj = None
self._obj_label = None
for k, v in kwargs.items():
if k in self.__dict__:
self.__dict__[k] = v
@property
def cq_obj(self):
if self._cq_obj is None:
return self.render()
return self._cq_obj
@property
def length(self):
return self.length_u * GRU
@property
def width(self):
return self.width_u * GRU
@property
def height(self):
return 3.8 + GRHU * self.height_u
@property
def int_height(self):
h = self.height - GR_LIP_H - GR_BOT_H
if self.lite_style:
return h + self.wall_th
return h
@property
def max_height(self):
return self.int_height + GR_UNDER_H + GR_TOPSIDE_H
@property
def floor_h(self):
if self.lite_style:
return GR_FLOOR - self.wall_th
return GR_FLOOR
@property
def lip_width(self):
if self.no_lip:
return self.wall_th
return GR_UNDER_H + self.wall_th
@property
def outer_l(self):
return self.length_u * GRU - GR_TOL
@property
def outer_w(self):
return self.width_u * GRU - GR_TOL
@property
def outer_dim(self):
return self.outer_l, self.outer_w
@property
def inner_l(self):
return self.outer_l - 2 * self.wall_th
@property
def inner_w(self):
return self.outer_w - 2 * self.wall_th
@property
def inner_dim(self):
return self.inner_l, self.inner_w
@property
def half_l(self):
return (self.length_u - 1) * GRU2
@property
def half_w(self):
return (self.width_u - 1) * GRU2
@property
def half_dim(self):
return self.half_l, self.half_w
@property
def half_in(self):
return GRU2 - self.wall_th - GR_TOL / 2
@property
def outer_rad(self):
return GR_RAD - GR_TOL / 2
@property
def inner_rad(self):
return self.outer_rad - self.wall_th
@property
def under_h(self):
return GR_UNDER_H - (self.wall_th - GR_WALL)
@property
def safe_fillet_rad(self):
if not any([self.scoops, self.labels, self.length_div, self.width_div]):
return GR_FILLET
return min(GR_FILLET, (GR_UNDER_H + GR_WALL) - self.wall_th - 0.05)
@property
def grid_centres(self):
return [
(x * GRU, y * GRU)
for x in range(self.length_u)
for y in range(self.width_u)
]
@property
def hole_centres(self):
return [
(x * GRU - GR_HOLE_DIST * i, -(y * GRU - GR_HOLE_DIST * j))
for x in range(self.length_u)
for y in range(self.width_u)
for i in (-1, 1)
for j in (-1, 1)
]
def safe_fillet(self, obj, selector, rad):
if len(obj.edges(selector).vals()) > 0:
return obj.edges(selector).fillet(rad)
return obj
def filename(self, prefix=None, path=None):
"""Returns a descriptive readable filename which represents a Gridfinity object.
The filename can be optionally prefixed with arbitrary text and
an optional path prefix can also be specified."""
from cqgridfinity import (
GridfinityBaseplate,
GridfinityBox,
GridfinityDrawerSpacer,
GridfinityRuggedBox,
)
if prefix is not None:
prefix = prefix
elif isinstance(self, GridfinityBaseplate):
prefix = "gf_baseplate_"
elif isinstance(self, GridfinityBox):
prefix = "gf_box_"
if self.lite_style:
prefix = prefix + "lite_"
elif isinstance(self, GridfinityDrawerSpacer):
prefix = "gf_drawer_"
elif isinstance(self, GridfinityRuggedBox):
prefix = "gf_ribbox_" if self.rib_style else "gf_ruggedbox_"
else:
prefix = ""
fn = ""
if path is not None:
fn = fn.replace(os.sep, "")
fn = path + os.sep
fn = fn + prefix
fn = fn + "%dx%d" % (self.length_u, self.width_u)
if isinstance(self, GridfinityBox):
fn = fn + "x%d" % (self.height_u)
if self.length_div and not self.solid:
fn = fn + "_div%d" % (self.length_div)
if self.width_div and not self.solid:
if self.length_div:
fn = fn + "x%d" % (self.width_div)
else:
fn = fn + "_div_x%d" % (self.width_div)
if abs(self.wall_th - GR_WALL) > 1e-3:
fn = fn + "_%.2f" % (self.wall_th)
if self.no_lip:
fn = fn + "_basic"
if self.holes:
fn = fn + "_holes"
if self.solid:
fn = fn + "_solid"
else:
if self.scoops:
fn = fn + "_scoops"
if self.labels:
fn = fn + "_labels"
elif isinstance(self, GridfinityRuggedBox):
fn = fn + "x%d" % (self.height_u)
if self._obj_label is not None:
fn = fn + "_%s" % (self._obj_label)
if self.front_handle or self.front_label:
fn = fn + "_fr-"
if self.front_handle:
fn = fn + "h"
if self.front_label:
fn = fn + "l"
if self.side_handles or self.side_clasps:
fn = fn + "_sd-"
if self.side_handles:
fn = fn + "h"
if self.side_clasps:
fn = fn + "c"
if self.stackable:
fn = fn + "_stack"
if self.lid_baseplate:
fn = fn + "_lidbp"
if self.lid_window:
fn = fn + "_win"
elif isinstance(self, GridfinityDrawerSpacer):
if self._obj_label is not None:
fn = fn + "_%s" % (self._obj_label)
elif isinstance(self, GridfinityBaseplate):
if self.ext_depth > 0:
fn = fn + "x%.1f" % (self.ext_depth)
if self.corner_screws:
fn = fn + "_screwtabs"
return fn
def save_step_file(self, filename=None, path=None, prefix=None):
fn = (
filename
if filename is not None
else self.filename(path=path, prefix=prefix)
)
if not fn.lower().endswith(".step"):
fn = fn + ".step"
if isinstance(self.cq_obj, cq.Assembly):
self.cq_obj.save(fn)
else:
export_step_file(self.cq_obj, fn)
def save_stl_file(
self, filename=None, path=None, prefix=None, tol=1e-2, ang_tol=0.1
):
fn = (
filename
if filename is not None
else self.filename(path=path, prefix=prefix)
)
if not fn.lower().endswith(".stl"):
fn = fn + ".stl"
obj = self.cq_obj.val().wrapped
mesh = BRepMesh_IncrementalMesh(obj, tol, True, ang_tol, True)
mesh.Perform()
writer = StlAPI_Writer()
writer.Write(obj, fn)
def save_svg_file(self, filename=None, path=None, prefix=None):
fn = (
filename
if filename is not None
else self.filename(path=path, prefix=prefix)
)
if not fn.lower().endswith(".svg"):
fn = fn + ".svg"
r = self.cq_obj.rotate((0, 0, 0), (0, 0, 1), 75)
r = r.rotate((0, 0, 0), (1, 0, 0), -90)
exporters.export(
r,
fn,
opt={
"width": 600,
"height": 400,
"showAxes": False,
"marginTop": 20,
"marginLeft": 20,
"projectionDir": (1, 1, 1),
},
)
def extrude_profile(self, sketch, profile, workplane="XY", angle=None):
taper = profile[0][1] if isinstance(profile[0], (list, tuple)) else 0
zlen = profile[0][0] if isinstance(profile[0], (list, tuple)) else profile[0]
if abs(taper) > 0:
if angle is None:
zlen = zlen if ZLEN_FIX else zlen / SQRT2
else:
zlen = zlen / math.cos(math.radians(taper)) if ZLEN_FIX else zlen
r = cq.Workplane(workplane).placeSketch(sketch).extrude(zlen, taper=taper)
for level in profile[1:]:
if isinstance(level, (tuple, list)):
if angle is None:
zlen = level[0] if ZLEN_FIX else level[0] / SQRT2
else:
zlen = (
level[0] / math.cos(math.radians(level[1]))
if ZLEN_FIX
else level[0]
)
r = r.faces(">Z").wires().toPending().extrude(zlen, taper=level[1])
else:
r = r.faces(">Z").wires().toPending().extrude(level)
return r
@classmethod
def to_step_file(
cls,
length_u,
width_u,
height_u=None,
filename=None,
prefix=None,
path=None,
**kwargs
):
"""Convenience method to create, render and save a STEP file representation
of a Gridfinity object."""
obj = GridfinityObject.as_obj(cls, length_u, width_u, height_u, **kwargs)
obj.save_step_file(filename=filename, path=path, prefix=prefix)
@classmethod
def to_stl_file(
cls,
length_u,
width_u,
height_u=None,
filename=None,
prefix=None,
path=None,
**kwargs
):
"""Convenience method to create, render and save a STEP file representation
of a Gridfinity object."""
obj = GridfinityObject.as_obj(cls, length_u, width_u, height_u, **kwargs)
obj.save_stl_file(filename=filename, path=path, prefix=prefix)
@staticmethod
def as_obj(cls, length_u=None, width_u=None, height_u=None, **kwargs):
if "GridfinityBox" in cls.__name__:
obj = GridfinityBox(length_u, width_u, height_u, **kwargs)
if "GridfinitySolidBox" in cls.__name__:
obj.solid = True
elif "GridfinityBaseplate" in cls.__name__:
obj = GridfinityBaseplate(length_u, width_u, **kwargs)
elif "GridfinityDrawerSpacer" in cls.__name__:
obj = GridfinityDrawerSpacer(**kwargs)
return obj
================================================
FILE: cqgridfinity/gf_ruggedbox.py
================================================
#! /usr/bin/env python3
#
# Copyright (C) 2023 Michael Gale
# This file is part of the cq-gridfinity python module.
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Gridfinity Rugged Boxes
import math
import cadquery as cq
from cadquery.selectors import StringSyntaxSelector
from cqkit import (
HasXCoordinateSelector,
HasYCoordinateSelector,
HasZCoordinateSelector,
VerticalEdgeSelector,
EdgeLengthSelector,
RadiusSelector,
FlatEdgeSelector,
rounded_rect_sketch,
recentre,
composite_from_pts,
rotate_x,
rotate_y,
rotate_z,
size_2d,
size_3d,
bounds_3d,
inverse_fillet,
inverse_chamfer,
Ribbon,
)
# from cqkit import Ribbon
from cqgridfinity import *
from .gf_helpers import *
class GridfinityRuggedBox(GridfinityObject):
def __init__(self, length_u, width_u, height_u, **kwargs):
super().__init__()
self.length_u = length_u
self.width_u = width_u
self.height_u = height_u
self.lid_height = 10
self.wall_vgrooves = True
self.front_handle = True
self.stackable = True
self.side_clasps = True
self.lid_baseplate = True
self.inside_baseplate = True
self.side_handles = True
self.front_label = True
self.label_length = None
self.label_height = None
self.label_th = GR_LABEL_TH
self.back_feet = True
self.hinge_width = GR_HINGE_SZ
self.hinge_bolted = False
self.rib_style = False
self._lid_window = False
self.window_th = 1.0
self.box_color = cq.Color(0.25, 0.25, 0.25)
self.lid_color = cq.Color(0.25, 0.5, 0.75)
self.handle_color = cq.Color(0.75, 0.5, 0.25)
self.latch_color = cq.Color(0.75, 0.5, 0.25)
self.hinge_color = cq.Color(0.75, 0.5, 0.25)
self.label_color = cq.Color(0.7, 0.7, 0.7)
self.window_color = cq.Color(0.9, 0.9, 0.9, 0.25)
for k, v in kwargs.items():
if k in self.__dict__:
self.__dict__[k] = v
def check_dimensions(self):
"""Verifies that the specified box dimensions are within specification."""
assert self.length_u >= 3
assert self.width_u >= 3
assert self.height_u >= 4
@property
def box_length(self):
return self.length_u * GRU + 2 * GR_RBOX_WALL
@property
def int_length(self):
return self.length_u * GRU
@property
def box_width(self):
return self.width_u * GRU + 2 * GR_RBOX_WALL
@property
def int_width(self):
return self.width_u * GRU
@property
def clasp_pos(self):
return self.int_length / 2 - GRU2, self.int_width / 2 - GRU2
@property
def box_height(self):
return self.height_u * GRHU + 3
@property
def clasp_heights(self):
h0 = GR_RIB_CTR / 2 + GR_RIB_L / 2
h1 = h0 + GR_RIB_CTR
return [GR_RIB_L / 2, self.box_height - h0, self.box_height - h1]
@property
def side_clasp_centres(self):
xo = self.box_length / 2 + GR_RBOX_CHAN_D / 2
yo = self.clasp_pos[1]
return [(-xo, yo, 0), (xo, yo, 0), (-xo, -yo, 0), (xo, -yo, 0)]
@property
def front_clasp_centres(self):
xo = self.clasp_pos[0]
yo = self.box_width / 2 + GR_RBOX_CHAN_D / 2
return [(-xo, -yo, 0), (xo, -yo, 0)]
@property
def clasp_notch_points(self):
return [
(
x * GR_RBOX_CHAN_W / 2,
-GR_RBOX_CHAN_D / 2,
self.box_height - self.lid_height,
)
for x in (-1, 1)
]
@property
def hinge_centres(self):
xo = self.box_length / 2 - GR_HINGE_CTR
yo = self.box_width / 2 + GR_RBOX_CWALL - GR_RBOX_WALL
zo = self.box_height
return [(-xo, yo, zo), (xo, yo, zo)]
@property
def align_centres(self):
ro = GR_RBOX_CHAN_D / 2 - GR_REG_W / 2
xo, xc = self.box_length / 2 - GRU, self.box_length / 2 + ro
yo, yc = self.box_width / 2 - GRU, self.box_width / 2 + ro
pts = [
(-xo, -yc, 0),
(xo, -yc, 0),
(-xc, -yo, 0),
(xc, -yo, 0),
(-xc, yo, 0),
(xc, yo, 0),
]
rots = [0, 0, 90, 90, 90, 90]
return pts, rots
@property
def right_qtr_centre(self):
return (
self.box_length / 2 - GR_RBOX_WALL / 2 + 0.125,
-self.box_width / 2 + GR_RBOX_WALL / 2 - 0.125,
self.box_height,
)
@property
def left_qtr_centre(self):
return -self.right_qtr_centre[0], *self.right_qtr_centre[1:]
@property
def bottom_qtr_centres(self):
return self.qtr_centres(tol=0.25)
def qtr_centres(self, tol=0.25, at_height=0, front=True, back=True):
xo = self.box_length / 2 - GR_RBOX_WALL / 2 + tol
yo = self.box_width / 2 - GR_RBOX_WALL / 2 + tol
qd = {}
if front:
qd["br"] = (xo, -yo, at_height)
qd["bl"] = (-xo, -yo, at_height)
if back:
qd["tr"] = (xo, yo, at_height)
qd["tl"] = (-xo, yo, at_height)
return qd
@property
def long_enough_for_handle(self):
return self.right_handle_centre[0] > GRU / 2
@property
def right_handle_centre(self):
zo = (self.box_height + self.lid_height) / 2
if (zo + GR_HANDLE_SZ / 2) > self.box_height:
zo = self.box_height / 2
return self.box_length / 2 - GR_HANDLE_OFS, -self.box_width / 2, zo
@property
def left_handle_centre(self):
return -self.right_handle_centre[0], *self.right_handle_centre[1:]
@property
def back_corner_centres(self):
xo = self.box_length / 2 - GR_RBOX_BACK_L / 2 + GR_RBOX_CWALL - GR_RBOX_WALL
yo = self.box_width / 2 - GR_RBOX_CORNER_W / 2 + GR_RBOX_CWALL - GR_RBOX_WALL
return [(-xo, yo, 0), (xo, yo, 0)]
@property
def front_corner_centres(self):
xo = self.box_length / 2 - GR_RBOX_FRONT_L / 2 + GR_RBOX_CWALL - GR_RBOX_WALL
yo = -self.back_corner_centres[0][1]
return [(-xo, yo, 0), (xo, yo, 0)]
@property
def label_centre(self):
zo = self.left_handle_centre[2]
zt = zo + self.label_size()[1] / 2
# ensure the front label fits vertically
if zt > self.box_height:
zo = self.box_height / 2
return (0, -self.box_width / 2, zo)
@property
def lid_window(self):
return self._lid_window
@lid_window.setter
def lid_window(self, enable):
self._lid_window = enable
if self._lid_window:
self.lid_baseplate = False
def lid_window_size(self, width_ext=None, tol=None):
tol = tol if tol is not None else GR_TOL
width_ext = width_ext if width_ext is not None else 4
return self.length - 2 - tol, self.width + width_ext - tol
def lid_window_hole_pos(self, z=0):
pts = [
(-x * (self.box_length / 2 - GR_RBOX_CORNER_W), self.width / 2 + 2, z)
for x in (-1, 1)
]
if self.rib_style:
pts.append((0, self.width / 2 + 2, z))
return pts
def label_size(self, as_insert=False, as_aperture=False, tol=0):
# use provided label size if applicable otherwise auto size
if self.label_length is not None:
length = self.label_length
else:
length = self.box_length - 2 * GR_RBOX_CORNER_W + (GR_RBOX_CWALL) / 2
if self.label_height is not None:
height = self.label_height
else:
height = GR_LABEL_H
# ensure the label is not too tall
if height >= self.box_height:
height = self.box_height - 5
# trim label size if handles are enabled
if self.front_handle and self.long_enough_for_handle:
length = length - 2 * (GR_HANDLE_SEP + GR_HANDLE_W)
# return the desired size variant
if as_insert:
length -= 5
if as_aperture:
length -= 8
height -= 8
return length - 2 * tol, height - 2 * tol
def body_shell(self, as_lid=False):
"""General purpose render function for both the box and the lid."""
height = self.box_height if not as_lid else self.lid_height
# render overall box shape
rs = rounded_rect_sketch(self.box_length, self.box_width, GR_RAD)
r = cq.Workplane("XY").placeSketch(rs).extrude(height)
# back corners
if self.rib_style:
lb = self.box_length + 2 * (GR_RBOX_CWALL - GR_RBOX_WALL)
yo = self.back_corner_centres[0]
rc = cq.Workplane("XY").rect(lb, GR_RBOX_CORNER_W).extrude(height)
r = r.union(rc.translate((0, yo[1], 0)))
if not as_lid or (as_lid and not self.side_handles):
h = height / 2 if self.side_handles else height
wb = self.box_width - GR_RBOX_CORNER_W
rc = cq.Workplane("XY").rect(lb, wb).extrude(h)
r = r.union(rc)
else:
rc = (
cq.Workplane("XY")
.rect(GR_RBOX_BACK_L, GR_RBOX_CORNER_W)
.extrude(height)
)
r = r.union(composite_from_pts(rc, self.back_corner_centres))
# front corners
rc = cq.Workplane("XY").rect(GR_RBOX_FRONT_L, GR_RBOX_CORNER_W).extrude(height)
r = r.union(composite_from_pts(rc, self.front_corner_centres))
# fillet external edges
vs = VerticalEdgeSelector()
cs = StringSyntaxSelector("(<XY) or (>X and <Y) or (<X and >Y) or (>XY)")
r = r.edges(vs - cs).fillet(GR_RBOX_RAD).edges(cs).fillet(GR_RBOX_CRAD)
if self.stackable or as_lid:
# bottom stacking mates
for k, v in self.qtr_centres(back=not as_lid).items():
rq = quarter_circle(
GR_BREG_R0, GR_BREG_R1, GR_REG_H + 0.5, k, chamf=0, ext=0.25
)
r = r.cut(rq.translate(v))
pts, rots = self.align_centres
for pt, rot in zip(pts, rots):
rc = chamf_rect(GR_REG_L, GR_REG_W, GR_REG_H, angle=rot)
r = r.cut(rc.translate(pt))
# chamfer top edges
r = r.edges(">Z").chamfer(GR_RBOX_VCUT_D)
# front lid overhang
if as_lid:
w = min(GR_LID_HANDLE_W, self.box_length - 2 * GR_RBOX_FRONT_L)
r = r.union(self.lid_handle(width=w).translate((0, -self.box_width / 2, 0)))
hw = w / 2
vs = VerticalEdgeSelector([9]) & HasXCoordinateSelector([-hw, hw])
r = r.edges(vs).fillet(2.5 - EPS)
# chamfer cuts
if self.wall_vgrooves:
if self.rib_style:
r = r.cut(self.render_vcut())
else:
r = r.intersect(self.render_vcut())
# chamfer bottom edges
r = r.edges("<Z").chamfer(GR_RBOX_VCUT_D)
# apply rib style cutouts if applicable
if self.rib_style and not as_lid:
r = r.intersect(self.rib_style_cut())
# add clasp features
rc = self.clasp_cut(as_lid=as_lid)
if self.side_clasps:
for pt in self.side_clasp_centres:
r = r.cut(rc.translate(pt))
side = "left" if pt[0] < 0 else "right"
r = r.union(self.clasp_ribs(side=side, as_lid=as_lid).translate(pt))
rc = rotate_z(rc, 90)
for pt in self.front_clasp_centres:
r = r.cut(rc.translate(pt))
r = r.union(self.clasp_ribs(side="front", as_lid=as_lid).translate(pt))
return r
def render_vcut(self):
"""Renders a matching box shape with side v-cuts to intersect with main box."""
# rib style implements v-notches along the clasp channels
if self.rib_style:
rc = cq.Workplane("XY").rect(2, 2).extrude(SQRT2, taper=45)
rc = rc.rotate_x(-90)
rc = composite_from_pts(rc, self.clasp_notch_points)
r = composite_from_pts(rc, self.front_clasp_centres)
if self.side_clasps:
for pt in self.side_clasp_centres:
if pt[0] < 0:
r = r.union(rc.rotate_z(-90).translate(pt))
else:
r = r.union(rc.rotate_z(90).translate(pt))
return r
else:
xl = self.box_length + 2 * GR_RBOX_CWALL - 2 * GR_RBOX_WALL
yl = self.box_width + 2 * GR_RBOX_CWALL - 2 * GR_RBOX_WALL
lead_height = self.lid_height - GR_RBOX_VCUT_D
mid_height = self.box_height - 2 * (self.lid_height + GR_RBOX_VCUT_D)
cut_half = GR_RBOX_VCUT_D * SQRT2
profile = [
lead_height,
(cut_half, 45),
(cut_half, -45),
mid_height,
(cut_half, 45),
(cut_half, -45),
lead_height,
]
rs = rounded_rect_sketch(xl, yl, GR_RBOX_CRAD)
return self.extrude_profile(rs, profile)
def rib_style_cut(self):
"""Render cutouts for a rib style box"""
xl = self.box_length + 2 * GR_RBOX_CWALL - 2 * GR_RBOX_WALL
yl = self.box_width + 2 * GR_RBOX_CWALL - 2 * GR_RBOX_WALL
wd = GR_RBOX_CWALL - GR_RBOX_WALL
lead_height = self.lid_height - GR_RBOX_VCUT_D
cut_half = wd * SQRT2
mid_height = self.box_height - 2 * (lead_height + wd)
profile = [
lead_height,
(cut_half, 45),
mid_height,
(cut_half, -45),
lead_height,
]
rs = rounded_rect_sketch(xl, yl, GR_RBOX_CRAD)
r = self.extrude_profile(rs, profile)
w = GR_RBOX_CHAN_W + 3 * GR_RBOX_WALL
rc = cq.Workplane("XY").rect(GR_RBOX_CHAN_D, w).extrude(self.box_height)
if self.side_clasps:
for pt in self.side_clasp_centres:
r = r.union(rc.translate(pt))
else:
rd = (
cq.Workplane("XY")
.rect(GR_RBOX_CHAN_D, 1.5 * GR_RBOX_WALL)
.extrude(self.box_height)
)
xo = self.box_length / 2 + GR_RBOX_CHAN_D / 2
yo = self.clasp_pos[1] + GR_RBOX_CHAN_W / 2 + 1.5 * GR_RBOX_WALL / 2
for pt in [(x * xo, y * yo, 0) for x in (-1, 1) for y in (-1, 1)]:
r = r.union(rd.translate(pt))
rc = rotate_z(rc, 90)
for pt in self.front_clasp_centres:
r = r.union(rc.translate(pt))
w = 1.5 * GR_RBOX_WALL
rc = cq.Workplane("XY").rect(w, wd).extrude(self.box_height)
for pt in self.hinge_centres:
r = r.union(rc.translate((pt[0] - GR_HINGE_SZ / 2 - w, pt[1] - wd / 2, 0)))
r = r.union(rc.translate((pt[0] + GR_HINGE_SZ / 2 + w, pt[1] - wd / 2, 0)))
yo = self.box_width / 2 + GR_RBOX_CWALL - GR_RBOX_WALL - wd / 2
for x in range(self.length_u):
xo = -self.int_length / 2 + x * GRU
if abs(xo) < (self.box_length / 2 - GR_RBOX_BACK_L):
r = r.union(rc.translate((xo, yo, 0)))
if not self.side_handles:
xo = self.box_length / 2 + GR_RBOX_CWALL - GR_RBOX_WALL - wd / 2
rc = rotate_z(rc, 90)
ylim = self.int_width / 2
if self.side_clasps:
ylim -= GR_RBOX_CORNER_W
for y in range(self.width_u):
yo = -self.int_width / 2 + y * GRU
if abs(yo) < ylim:
r = r.union(rc.translate((xo, yo, 0)))
r = r.union(rc.translate((-xo, yo, 0)))
hm = self.box_height - 2 * lead_height
r = r.edges(VerticalEdgeSelector([mid_height, hm])).fillet(1)
return r
def lid_handle(self, width=None):
"""Renders the front overhanging handle lip for the lid."""
width = width if width is not None else GR_LID_HANDLE_W
l0, l1, h1, h2, hw = 3, 5, 4, self.lid_height - GR_RBOX_VCUT_D, width / 2
rs = (
cq.Sketch()
.segment((l0, 0), (-l1, 0))
.segment((-l1, h1))
.segment((l0, h2 + l0))
.close()
.assemble()
)
r = cq.Workplane("YZ").placeSketch(rs).extrude(width).translate((-hw, 0, 0))
vs = VerticalEdgeSelector([h1]) & HasXCoordinateSelector([-hw, hw])
r = r.edges(vs).fillet(2.45).faces("<Z").shell(-2.5)
vs = VerticalEdgeSelector(3) & HasYCoordinateSelector(-l1 + 2.5)
r = r.edges(vs).fillet(1)
rc = cq.Workplane("XY").rect(4 * hw, 4 * hw).extrude(self.lid_height)
r = r.intersect(rc)
return r
def side_handle(self, width=None):
"""Renders the handles for the left and right box sides."""
width = width if width is not None else GR_LID_HANDLE_W
l0, l1, h1, hw = GR_RBOX_WALL, 7, 4, width / 2
l2 = GR_RBOX_WALL / 2
h2 = self.lid_height - GR_RBOX_VCUT_D + 2
# handle shape
rs = (
cq.Sketch()
.segment((l0, 0), (-l1, 0))
.segment((-l1, h1))
.segment((l0, h2 + l0))
.close()
.assemble()
)
r = cq.Workplane("YZ").placeSketch(rs).extrude(width)
# vertical under support
rs = (
cq.Sketch()
.segment((0, 0), (0, -h2 + 2.5))
.segment((-l1, 0.5))
.segment((-l1, h1))
.segment((0, h2))
.close()
.assemble()
)
rw = cq.Workplane("YZ").placeSketch(rs).extrude(2.5)
rw = inverse_fillet(
rw, ">Y", 5, (StringSyntaxSelector("<Z") & EdgeLengthSelector(GR_RBOX_WALL))
)
rh = []
bs = VerticalEdgeSelector() & (HasYCoordinateSelector("<0"))
for coord in [[0, 2.5], [0], [2.5]]:
es = bs & HasXCoordinateSelector(coord, min_points=2)
rh.append(rw.edges(es - HasZCoordinateSelector(">4")).chamfer(0.5))
r = r.faces("<Z").shell(-2.5)
bs = (
HasZCoordinateSelector(0, min_points=2)
- EdgeLengthSelector("<%.1f" % (width - 2.5))
- HasYCoordinateSelector(">=0")
)
r = r.edges(bs).chamfer(0.5).translate((-hw, 0, -2))
r = r.union(rh[2].translate((-hw, 0, -2)))
if width > GR_LID_HANDLE_W / 2:
r = r.union(rh[0].translate((-l2, 0, -2)))
r = r.union(rh[1].translate((hw - GR_RBOX_WALL, 0, -2)))
vs = VerticalEdgeSelector([h1 - 0.5]) & HasXCoordinateSelector([-hw, hw])
r = r.edges(vs).fillet(2)
vs = VerticalEdgeSelector(2.9) & HasYCoordinateSelector(-l1 + GR_RBOX_WALL)
r = r.edges(vs).fillet(1)
rc = cq.Workplane("XY").rect(4 * hw, 4 * hw).extrude(self.lid_height + 2 * h2)
r = r.intersect(rc.translate((0, 0, -2 * h2)))
return r
def label_slot(self):
"""Renders the front label holder."""
rs = rounded_rect_sketch(*self.label_size(), GR_RAD)
r = self.extrude_profile(rs, [(GR_LABEL_SLOT_TH * SQRT2, 45)], workplane="XZ")
rc = (
cq.Workplane("XZ")
.rect(*self.label_size(as_aperture=True))
.extrude(GR_LABEL_SLOT_TH)
)
r = r.cut(rc.edges(EdgeLengthSelector(GR_LABEL_SLOT_TH)).chamfer(2.5))
xl, yl = self.label_size(as_insert=True)
xl -= 8
rc = cq.Workplane("XZ").rect(xl, yl).extrude(GR_LABEL_SLOT_TH)
r = r.cut(rc.translate((0, 0, 5)))
rc = (
cq.Workplane("XZ")
.rect(*self.label_size(as_insert=True))
.extrude(GR_LABEL_SLOT_TH / 2)
)
rc = rc.edges("|Y and <Z").fillet(GR_LABEL_SLOT_TH / 2)
r = r.cut(rc.translate((0, 0, GR_LABEL_SLOT_TH)))
# simple restraining ramps to prevent the label slipping out
rc = (
cq.Workplane("XZ")
.rect(10, 2.5)
.extrude(1.25)
.edges("<Y")
.chamfer(1.25 - EPS)
)
pts = [(-xl / 4, 0, yl / 2 - 2.0), (xl / 4, 0, yl / 2 - 2.0)]
if self.length_u < 5:
pts = [(0, 0, yl / 2 - 2.0)]
for pt in pts:
r = r.union(rc.translate(pt))
return r
def render_label(self):
"""Renders a label panel insert"""
rs = rounded_rect_sketch(*self.label_size(tol=3), GR_RAD)
r = cq.Workplane("XZ").placeSketch(rs).extrude(self.label_th)
self._obj_label = "label"
self._cq_obj = r
return self._cq_obj
def clasp_cut(self, as_lid=False):
"""Renders the vertical channel where the clasps / latch are installed."""
height = GR_CLASP_SLIDE_D + 6 if as_lid else self.box_height
w = GR_RBOX_CHAN_W + GR_CLASP_SLIDE_W
rs = cq.Sketch().slot(GR_CLASP_SLIDE_D, GR_CLASP_SLIDE_W, angle=90)
rs = cq.Workplane("XZ").placeSketch(rs).extrude(w).translate((0, w / 2, 0))
rc = cq.Workplane("XY").rect(GR_RBOX_CHAN_D, GR_RBOX_CHAN_W).extrude(height)
zo = -GR_CLASP_SLIDE_D / 2 + GR_CLASP_SLIDE_W / 2
# ensure clasp channel is deep enough for box heights <6U
height = max(height, GR_CLASP_SLIDE_D + 5.2)
pts = [(0, 0, height + zo), (0, 0, zo)]
return rc.union(composite_from_pts(rs, pts))
def clasp_rib(self, chamfered=False):
"""Renders a single clasp rib feature."""
r = cq.Workplane("XY").rect(GR_RIB_L, GR_RIB_W).extrude(GR_RIB_H)
r = r.faces(">Z").edges("<X or >X").chamfer(1.0)
if chamfered:
rc = (
cq.Workplane("XZ")
.moveTo(0, 0)
.lineTo(0, GR_RIB_H)
.lineTo(GR_RIB_L / 6, GR_RIB_H)
.close()
.extrude(GR_RIB_W)
)
r = r.cut(rc.translate((-GR_RIB_L / 1.85, GR_RIB_W / 2, 0)))
rc = cq.Workplane("XY").rect(GR_RIB_L / 2, GR_RIB_W).extrude(GR_RIB_H / 3)
rc = rc.faces(">Z").edges("<X or >X").chamfer(GR_RIB_H / 3 - EPS)
r = r.union(rc.translate((-GR_RIB_L / 2.33, 0, 0)))
return r
def clasp_ribs(self, side="left", as_lid=False):
"""Renders a group of clasp ribs for any side for both the box and lid."""
y1 = GR_RIB_SEP / 2 + GR_RIB_W / 2
y2 = y1 + GR_RIB_W + GR_RIB_GAP
zo = -GR_RBOX_CHAN_D / 2
pts = [(0, -y2, zo), (0, -y1, zo), (0, y1, zo), (0, y2, zo)]
rh = composite_from_pts(self.clasp_rib(), pts)
rc = composite_from_pts(self.clasp_rib(chamfered=True), pts)
if self.stackable or as_lid:
r = rh.translate((self.clasp_heights[0], 0, 0))
if not as_lid:
rc = composite_from_pts(rc, [(h, 0, 0) for h in self.clasp_heights[1:]])
if not self.stackable:
r = rc
else:
r = r.union(rc)
r = rotate_y(r, -90)
if side == "front":
r = rotate_z(r, 90)
elif side == "right":
r = rotate_z(r, 180)
return r
def handle_mount(self, side="left"):
"""Mounting features for front handle"""
def _bracket(small_hole=False, side="left"):
l1 = GR_HANDLE_L1 / 2
l2 = min(GR_HANDLE_L2 / 2, (self.box_height - 6) / 2)
d2 = M3_DIAM / 2 if small_hole else M3_CLR_DIAM / 2
rs = (
cq.Sketch()
.segment((0, 0), (-l2, 0))
.segment((-l1, GR_HANDLE_H))
.segment((l1, GR_HANDLE_H))
.segment((l2, 0))
.close()
.assemble()
.vertices(">Y")
.vertices("<X or >X")
.fillet(GR_RAD)
.reset()
.push([(0, GR_HANDLE_H / 2)])
.circle(d2, mode="s")
)
r = cq.Workplane("YZ").placeSketch(rs).extrude(GR_HANDLE_W)
if not small_hole:
face = ">X" if side == "left" else "<X"
r = (
r.faces(face)
.workplane()
.pushPoints([(0, GR_HANDLE_H / 2)])
.hole(M3_CB_DIAM, M3_CB_DEPTH)
)
r = inverse_fillet(r, "<Z", GR_RAD, EdgeLengthSelector(GR_HANDLE_W))
r = r.faces(">Z").chamfer(0.75)
return rotate_x(r, 90)
h1 = _bracket(small_hole=True, side=side)
h2 = _bracket(small_hole=False, side=side)
xo = GR_HANDLE_SEP if side == "left" else -GR_HANDLE_SEP
r = recentre(h1.union(h2.translate((xo, 0, 0))), "xz")
return r
def render_handle(self):
"""Renders the front handle"""
self.check_dimensions()
x2 = self.right_handle_centre[0]
if not self.long_enough_for_handle:
print("Rugged box length dimension too small to include a handle")
return None
wt, h, rh = GR_HANDLE_TH, GR_HANDLE_SZ, GR_HANDLE_RAD
lt, ht = (2 * x2) - 2 * rh, h - rh - wt / 2
path = {
"start": "(%f,%f) dir:-90 width:%f" % (x2, h, wt),
"path": "L:%f A:%f,90 L:%f A:%f,90 L:%f" % (ht, rh, lt, rh, ht),
}
cw = Ribbon("XZ", path)
cw.direction = -90
r = cw.render().extrude(wt).faces(">Z").edges("|X").fillet(wt / 2 - EPS)
r = recentre(r.edges().chamfer(1), "XY")
rc = cq.Workplane("YZ").circle(M3_CLR_DIAM / 2).extrude(8 * lt)
r = r.cut(rc.translate((-4 * lt, 0, h - M3_CLR_DIAM)))
self._obj_label = "handle"
self._cq_obj = r
return self._cq_obj
def render_back_foot(self):
"""Renders a corresponding rear foot the same depth as the hinge for standing
the box vertically."""
rs = cq.Sketch().slot(2 * GR_HINGE_OFFS, 2 * GR_HINGE_RAD, 0)
rc = cq.Workplane("YZ").placeSketch(rs).extrude(self.hinge_width - 0.4)
return recentre(rc).edges().chamfer(1).translate((0, 0, GR_HINGE_RAD))
def hinge_mount(self):
"""Mounting cutout for hinge"""
l1, l2, l3 = self.hinge_width + 2, self.hinge_width, (self.hinge_width - 2) / 2
r = cq.Workplane("XY").rect(l1, GR_HINGE_W1).extrude(GR_HINGE_H1)
r = r.translate((0, -GR_HINGE_W1 / 2, -GR_HINGE_H1))
r2 = cq.Workplane("XY").rect(l2, GR_HINGE_W2).extrude(GR_HINGE_H2)
r2 = r2.translate((0, -GR_HINGE_D - GR_HINGE_W2 / 2, -GR_HINGE_H2))
bs = HasZCoordinateSelector(-GR_HINGE_H1) & EdgeLengthSelector(
[l2, GR_HINGE_W2]
)
r = r.union(r2).edges(bs).edges(">Y or <X or >X").chamfer(0.75)
rs = rounded_rect_sketch(l3, GR_HINGE_W3, 0.5)
r3 = cq.Workplane("XY").placeSketch(rs).extrude(GR_HINGE_H2)
xo, yo = GR_HINGE_SEP / 2 + l3 / 2, -GR_HINGE_W1 - 1.2 - GR_HINGE_W3 / 2
rh = self.hex_cut().translate(
(0, 0, GR_HINGE_H2 - GR_HINGE_H1 - GR_HEX_H / 2 + GR_HINGE_SKEW)
)
for pt in [(-xo, yo, -GR_HINGE_H2), (xo, yo, -GR_HINGE_H2)]:
r = r.union(r3.translate(pt))
r = r.union(rh.translate(pt))
return r
def hex_cut(self, depth=None):
"""Hexagonal shaped latch for hinge attachment"""
l1 = 2 if depth is None else 1.7
l2 = 3.5 if depth is None else 3.0
d = depth if depth is not None else 4.0
h = GR_HEX_H if depth is None else GR_HEX_H - 0.4
rs = (
cq.Sketch()
.segment((0, 0), (-l1, 0))
.segment((-l2, h / 2))
.segment((-l1, h))
.segment((l1, h))
.segment((l2, h / 2))
.segment((l1, 0))
.close()
.assemble()
)
r = cq.Workplane("XZ").placeSketch(rs).extrude(d).translate((0, d, -h / 2))
if depth is not None:
r = r.edges("<Z and >Y").chamfer(depth - EPS)
return r
def render_latch(self):
"""Renders the latch element used to secure the box and the lid."""
l2, w2, h2 = GR_LATCH_L / 2, GR_LATCH_W / 2, GR_LATCH_H / 2
c2, th = GR_RIB_CTR / 2, 2.5
hf = GR_LATCH_H - th
yc = (-1.575, 1.575)
r = cq.Workplane("XY").rect(GR_LATCH_L, GR_LATCH_W).extrude(GR_LATCH_H)
r = r.edges("|Y").edges(">X").chamfer(1.0)
rs = cq.Sketch().slot(10, GR_LATCH_H, 0)
rc = cq.Workplane("XZ").placeSketch(rs).extrude(GR_LATCH_W)
r = r.union(rc.translate((-l2 + 4.5, w2, h2)))
rc = cq.Workplane("XY").rect(16, 15.6).extrude(10).edges("|Z").fillet(4.0)
r = r.cut(rc.translate((-l2 - 8, 0, 0)))
rc = cq.Workplane("XY").rect(5, GR_LATCH_W - 2.4).extrude(10)
rc = rc.faces("<Z").edges("|X").fillet(1.5).edges("|Z").fillet(1.0)
r = r.cut(rc.translate((l2, 0, 2.0))).edges().chamfer(0.25)
rc = cq.Workplane("XY").rect(GR_LATCH_IL, GR_LATCH_IW).extrude(hf)
for x in (-GR_RIB_CTR, 0, GR_RIB_CTR):
r = r.cut(rc.translate((x - 1.25, 0, th)))
r = r.faces(">Z").edges(EdgeLengthSelector(GR_LATCH_IW)).chamfer(1.5)
r = r.faces(">Z").edges(EdgeLengthSelector(GR_LATCH_IL)).chamfer(0.25)
rc = cq.Workplane("XY").rect(20, 2.4).extrude(hf)
r = r.cut(rc.translate((0, 0, th)))
r = r.faces(">Z").edges(EdgeLengthSelector(1.8)).edges("|X").chamfer(0.25)
rc = cq.Workplane("XY").rect(8.5, 0.75).extrude(4.5)
rc = rc.faces(">Z").edges("|Y").chamfer(1.5)
bs = EdgeLengthSelector(">0.8") - HasZCoordinateSelector(0, min_points=2)
rc = rc.edges(bs).chamfer(0.2)
(_, _, _), (xm, _, _) = bounds_3d(r)
for pt in [(x - 1.25, y, th) for x in (-c2, c2) for y in yc]:
r = r.union(rc.translate(pt))
rd = cq.Workplane("XY").rect(3.5, 1).extrude(7)
for x, xo in [(-xm, 2.25), (13.75, -2.25)]:
rx = rc.intersect(rd.translate((xo, 0, 0)))
for pt in [(x, y, th) for y in yc]:
r = r.union(rx.translate(pt))
rc = cq.Workplane("XZ").rect(2, 3.2).extrude(0.6).edges("<Y").chamfer(0.6 - EPS)
xo = xm - self.lid_height
for angle, y in [(0, -w2), (180, w2)]:
r = r.union(rotate_z(rc, angle).translate((xo, y, h2)))
rc = cq.Workplane("XZ").rect(6.0, 0.4).extrude(-1.6)
for pt in [(xo, y, h2 + z) for y in (-w2, w2 - 1.6) for z in (-2.1, 2.1)]:
r = r.cut(rc.translate(pt))
r = (
r.edges(HasYCoordinateSelector([-w2, w2], min_points=2))
.edges(EdgeLengthSelector([6.0, 0.4]))
.chamfer(0.3 - EPS)
)
rc = cq.Workplane("XZ").circle(3.8 / 2).extrude(2).faces("<Y").chamfer(0.5)
re = cq.Workplane("XY").rect(50, 50).extrude(20).translate((0, 0, -1.7))
rc = rc.intersect(rotate_x(re, -10))
for angle, y in [(0, -w2), (180, w2)]:
r = r.union(rotate_z(rc, angle).translate((-17.45, y, h2)))
self._cq_obj = rotate_z(recentre(r, "xy"), -90)
self._obj_label = "latch"
return self._cq_obj
def render_hinge(self, as_closed=False, section=None):
"""Renders the rear hinge."""
tol = 0.125
cl = 2 * (GR_HINGE_OFFS + GR_HINGE_D + GR_HINGE_W2 / 2)
wh, dh = GR_HINGE_W2 - GR_HINGE_TOL, GR_HINGE_H2 - 1
ls, ws = cl / 2, GR_HINGE_H1 - GR_HINGE_TOL
h = self.hinge_width - GR_HINGE_TOL
h3 = h / 3
ha, hb, hc, hd = h3 - tol, h3 + tol, 2 * h3 - tol, 2 * h3 + tol
cro, cri, crb, crs = GR_HINGE_RAD + GR_HINGE_TOL, GR_HINGE_RAD, 4.5 / 2, 4.0 / 2
ctr = (cl / 2 + wh / 2, -GR_HINGE_SKEW)
def _bracket(side="left"):
xo = wh / 2 if side == "left" else cl + wh / 2
r = cq.Workplane("XY").rect(wh, dh).extrude(h).translate((xo, dh / 2, 0))
xo = ls / 2 if side == "left" else cl + wh - ls / 2
rc = cq.Workplane("XY").rect(ls, ws).extrude(h).translate((xo, ws / 2, 0))
r = r.union(rc)
bs = VerticalEdgeSelector() & HasYCoordinateSelector(ws)
if side == "left":
r = r.edges(VerticalEdgeSelector()).edges("<XY").chamfer(1.0)
bs = bs & HasXCoordinateSelector(wh)
else:
r = r.edges(VerticalEdgeSelector()).edges(">X and <Y").chamfer(1.0)
bs = bs & HasXCoordinateSelector(cl)
r = r.edges(bs).chamfer(1.1)
r = r.faces(">Y").edges(EdgeLengthSelector(wh)).chamfer(1.5)
return r
rl = _bracket(side="left")
for pt in [0, hc]:
rl = rl.cut(chamf_cyl(cro, hb, 0).translate((*ctr, pt)))
rr = _bracket(side="right")
rr = rr.cut(chamf_cyl(cro, hd - ha, 0).translate((*ctr, ha)))
bs = EdgeLengthSelector(">0.2") - EdgeLengthSelector([wh, h], tolerance=0.02)
bs = bs - HasYCoordinateSelector(dh - 1.5, min_points=2)
bs = bs - (RadiusSelector(cro) & HasZCoordinateSelector([ha, hb, hc, hd]))
rl = rl.edges(bs).chamfer(0.5)
rr = rr.edges(bs).chamfer(0.5)
rl = rl.union(chamf_cyl(cri, hc - hb).translate((*ctr, hb)))
if not self.hinge_bolted:
rl = rl.cut(chamf_cyl(crb, hc - hb, 0).translate((*ctr, hb)))
for pt in [0, hd]:
rr = rr.union(chamf_cyl(cri, ha).translate((*ctr, pt)))
if not self.hinge_bolted:
rr = rr.union(chamf_cyl(crs, h, 0).translate((*ctr, 0)))
else:
rr = rr.cut(chamf_cyl(M3_DIAM / 2, h, 0).translate((*ctr, 0)))
rl = rl.cut(chamf_cyl(M3_CLR_DIAM / 2, h, 0).translate((*ctr, 0)))
rr = rr.cut(chamf_cyl(M3_CLR_DIAM / 2, ha, 0).translate((*ctr, h - ha)))
rr = rr.cut(
chamf_cyl(M3_CB_DIAM / 2, M3_CB_DEPTH, 0).translate(
(*ctr, h - M3_CB_DEPTH)
)
)
rx = recentre(self.hex_cut(depth=GR_HEX_D))
rh = rotate_x(rotate_z(rx, 90), 90)
xo = cl + wh + GR_HEX_D / 2
yo = GR_HINGE_H1 + GR_HEX_H / 2 - 2 * GR_HINGE_SKEW
zo = GR_HINGE_SEP / 2 + (self.hinge_width - 2) / 4
for pt in [(-GR_HEX_D / 2, yo, h / 2 - z) for z in (-zo, zo)]:
rl = rl.union(rh.translate(pt))
rh = rotate_x(rotate_z(rx, -90), 90)
for pt in [(xo, yo, h / 2 - z) for z in (-zo, zo)]:
rr = rr.union(rh.translate(pt))
if as_closed:
rl = rotate_z(rl.translate((-ctr[0], -ctr[1], 0)), 90)
rr = rotate_z(rr.translate((-ctr[0], -ctr[1], 0)), -90)
if section is not None:
r = rr if section == "outer" else rl
else:
r = rl.union(rr)
self._cq_obj = r
self._obj_label = "hinge"
return self._cq_obj
def render(self):
"""Renders the rugged box body shell."""
self.check_dimensions()
r = self.body_shell(as_lid=False)
# hollow out
rc = (
cq.Workplane("XY")
.placeSketch(rounded_rect_sketch(self.length, self.width, GR_RAD))
.extrude(self.box_height - GR_RBOX_FLOOR)
)
r = r.cut(rc.translate((0, 0, GR_RBOX_FLOOR)))
# add registration features
pts, rots = self.align_centres
for pt, rot in zip(pts, rots):
rc = chamf_rect(
GR_REG_L,
GR_REG_W,
GR_REG_H,
angle=rot,
z_offset=self.box_height,
tol=0.75,
)
r = r.union(rc.translate(pt))
rq = quarter_circle(GR_REG_R0, GR_REG_R1, GR_REG_H, "bl")
r = r.union(rq.translate(self.left_qtr_centre))
rq = quarter_circle(GR_REG_R0, GR_REG_R1, GR_REG_H, "br")
r = r.union(rq.translate(self.right_qtr_centre))
# add handle mounts
if self.front_handle and self.long_enough_for_handle:
r = r.union(
self.handle_mount(side="left").translate(self.left_handle_centre)
)
r = r.union(
self.handle_mount(side="right").translate(self.right_handle_centre)
)
# add hinge mounts
rc = self.hinge_mount()
for pt in self.hinge_centres:
r = r.cut(rc.translate(pt))
# add side handles
if self.side_handles:
w = min(GR_SIDE_HANDLE_W, self.box_width - 2 * GR_RBOX_CORNER_W)
rh = self.side_handle(width=w)
rl = rotate_z(rh, -90)
rr = rotate_z(rh, 90)
zo = self.box_height - self.lid_height
r = r.union(rl.translate((-self.box_length / 2, 0, zo)))
r = r.union(rr.translate((self.box_length / 2, 0, zo)))
hw, l2 = w / 2, self.box_length / 2
vs = HasXCoordinateSelector([-l2, l2]) & HasYCoordinateSelector([-hw, hw])
r = r.edges("|Z").edges(vs).fillet(2.5)
# add front label slot
if self.front_label:
r = r.union(self.label_slot().translate(self.label_centre))
# back feet
if self.back_feet:
rc = self.render_back_foot()
for pt in self.hinge_centres:
r = r.union(rc.translate((pt[0], pt[1], 0)))
# add baseplate
if self.inside_baseplate:
rb = GridfinityBaseplate(self.length_u, self.width_u, ext_depth=1.6)
r = r.union(rb.render().translate((0, 0, GR_RBOX_FLOOR)))
r = r.edges(FlatEdgeSelector(GR_RBOX_FLOOR)).chamfer(0.8)
else:
rb = self.extrude_profile(
rounded_rect_sketch(self.length, self.width, GR_RAD), [GR_RBOX_WALL]
)
r = r.union(rb)
self._cq_obj = r
self._obj_label = "body"
return self._cq_obj
def render_lid(self):
"""Renders the rugged box lid."""
self.check_dimensions()
r = self.body_shell(as_lid=True)
if self.lid_baseplate:
# hollow out top half
rs = rounded_rect_sketch(self.length - GR_TOL, self.width - GR_TOL, GR_RAD)
rc = self.extrude_profile(rs, [self.lid_height - 0.5, (1.0, -45)])
r = r.cut(rc)
# add topside baseplate
rb = GridfinityBaseplate(
self.length_u, self.width_u, ext_depth=0.4, straight_bottom=True
)
rb = rb.render()
r = r.union(rb.translate((0, 0, 4.7 - 0.4)))
elif self.lid_window:
# hollow out completely
rs = rounded_rect_sketch(self.length, self.width, GR_RAD)
rc = self.extrude_profile(rs, [5])
r = r.cut(rc)
# hollow out bottom
rs = rounded_rect_sketch(self.length, self.width, GR_RAD)
r = r.cut(cq.Workplane("XY").placeSketch(rs).extrude(4.6))
# add modified bottom extrusion with a looser fit
if self.lid_baseplate:
rs = self.extrude_profile(
rounded_rect_sketch(35, 35, 0.8), [(2.82, -22.1), (5, -45)]
)
rs = rs.faces(">Z").shell(-1.2)
else:
rs = self.extrude_profile(
rounded_rect_sketch(35, 35, 0.8),
[(2.82, -22.1), (4.1, -45), (9, -85), 2],
)
ra = composite_from_pts(rs, self.grid_centres)
ra = ra.translate((-self.half_l, -self.half_w, 0))
rs = rounded_rect_sketch(self.length, self.width, GR_RAD)
ra = ra.intersect(cq.Workplane("XY").placeSketch(rs).extrude(GR_LID_WINDOW_H))
r = r.union(ra)
r = r.edges(
EdgeLengthSelector(33.4) & HasZCoordinateSelector(0, min_points=2)
).chamfer(0.75)
# add optional stackable features
if self.stackable:
for k, v in self.qtr_centres(tol=0.125, at_height=self.lid_height).items():
rq = quarter_circle(GR_REG_R0, GR_REG_R1, GR_REG_H, k)
r = r.union(rq.translate(v))
if self.lid_window:
# hollow the grid apertures
ht, tp = GR_LID_WINDOW_H, 34
he = GR_LID_WINDOW_H / math.cos(math.radians(tp))
rs = (
cq.Workplane("XY")
.placeSketch(rounded_rect_sketch(30, 30, 1))
.extrude(he, taper=-tp)
)
ra = composite_from_pts(rs, self.grid_centres)
ra = ra.translate((-self.half_l, -self.half_w, 0))
r = r.cut(ra)
# window slot
ext = 20
l, w = self.lid_window_size(width_ext=-2 + ext, tol=0)
rs = rounded_rect_sketch(l, w, 0.5)
hlw = self.lid_height - GR_LID_WINDOW_H
ht = hlw - self.window_th - 0.5
rc = (
cq.Workplane("XY")
.rect(l, w)
.workplane(offset=self.window_th)
.rect(l, w)
.workplane(offset=ht)
.rect(l - 6, w - 6)
.workplane(offset=self.lid_height)
.rect(l - 6, w - 6)
.loft(ruled=True)
)
rc = rc.edges(VerticalEdgeSelector()).fillet(0.5)
# rc = self.extrude_profile(rs, [self.window_th, (ht, 60), hlw], angle=True)
r = r.cut(rc.translate((0, ext / 2, GR_LID_WINDOW_H)))
rs = rounded_rect_sketch(self.length - 5, self.width - 5, GR_RAD)
rc = self.extrude_profile(rs, [self.lid_height])
r = r.cut(rc.translate((0, 0, self.lid_height - ht)))
# add hinge mounts
rc = rotate_y(self.hinge_mount(), 180)
for pt in self.hinge_centres:
r = r.cut(rc.translate((pt[0], pt[1], 0)))
# add window retaining screw holes
if self.lid_window:
rc = (
cq.Workplane("XY")
.circle(M2_DIAM / 2)
.extrude(5)
.faces(">Z")
.wires()
.toPending()
.extrude(0.8, taper=-45)
.faces("<Z")
.chamfer(0.5)
)
for pt in self.lid_window_hole_pos(z=1):
r = r.cut(rc.translate(pt))
self._cq_obj = r
self._obj_label = "lid"
return self._cq_obj
def render_lid_window(self):
rs = rounded_rect_sketch(*self.lid_window_size(), 0.5)
r = cq.Workplane("XY").placeSketch(rs).extrude(self.window_th)
r = r.translate((0, 3, 0))
rc = cq.Workplane("XY").circle(M2_CLR_DIAM / 2).extrude(self.window_th)
for pt in self.lid_window_hole_pos(z=0):
r = r.cut(rc.translate(pt))
self._cq_obj = r
self._obj_label = "lid_window"
return self._cq_obj
def render_accessories(self):
"""Render functional accessories which are installed to main box body."""
margin = 8
latch_count = 2
if self.side_clasps:
latch_count += 4
rl = self.render_latch()
sx, sy = size_2d(rl)
pts = [(x * (sx + margin) + sx / 2, sy / 2, 0) for x in range(latch_count)]
r = composite_from_pts(rl, pts)
oy = sy + margin
if self.front_handle:
rh = recentre(rotate_x(self.render_handle(), -90))
hsx, hsy, hsz = size_3d(rh)
r = r.union(rh.translate((hsx / 2, oy + hsy / 2, hsz / 2)))
oy += hsy + margin
rh = self.render_hinge()
hsx, hsy = size_2d(rh)
r = r.union(rh.translate((margin, oy, 0)))
r = r.union(rh.translate((1.5 * hsx + margin, oy + hsy / 2, 0)))
r = r.union(rh.translate((3 * hsx + margin, oy, 0)))
r = r.union(rh.translate((4.5 * hsx + margin, oy + hsy / 2, 0)))
rl = self.render_label()
rl = rotate_x(rl, 90)
r = r.union(rl.translate((40, -20, 0.5)))
self._cq_obj = r
self._obj_label = "acc"
return self._cq_obj
def render_assembly(self):
"""Renders a CadQuery Assembly object representing the entire box with accessories"""
self.check_dimensions()
r = self.render()
a = cq.Assembly(obj=r, name="Gridfinity Rugged Box", color=self.box_color)
r = self.render_lid()
r = r.translate((0, 0, self.box_height))
a.add(r, color=self.lid_color, name="Lid")
if self.lid_window:
r = self.render_lid_window()
r = r.translate((0, 0, self.box_height + GR_LID_WINDOW_H))
a.add(r, color=self.window_color, name="Lid Window")
if self.front_handle and self.long_enough_for_handle:
r = self.render_handle()
zo = self.right_handle_centre[2] - (GR_HANDLE_SZ - M3_CB_DEPTH)
r = r.translate((0, -self.box_width / 2 - GR_HANDLE_H / 2, zo))
a.add(r, color=self.handle_color, name="Handle")
rf = rotate_x(self.render_latch(), -90)
idx = 1
yo = GR_LATCH_H / 2
zo = self.box_height - GR_RIB_CTR + yo / 2
for pt in self.front_clasp_centres:
name = "Latch %d" % (idx)
pt = (pt[0], pt[1] - yo, zo)
a.add(rf.translate(pt), color=self.latch_color, name=name)
idx += 1
if self.side_clasps:
rl = rotate_z(rotate_x(self.render_latch(), -90), -90)
rr = rotate_z(rl, 180)
for pt in self.side_clasp_centres:
name = "Latch %d" % (idx)
y = -yo if pt[0] < 0 else yo
pt = (pt[0] + y, pt[1], zo)
if pt[0] < 0:
a.add(rl.translate(pt), color=self.latch_color, name=name)
else:
a.add(rr.translate(pt), color=self.latch_color, name=name)
idx += 1
for i, section in [(a, b) for a in (0, 1) for b in ("inner", "outer")]:
r = recentre(self.render_hinge(as_closed=True, section=section), "yz")
r = rotate_y(r, 90)
name = "Right " if i else "Left "
name = name + "Hinge %s" % (section)
a.add(
r.translate(self.hinge_centres[i]),
color=self.hinge_color,
name=name,
)
if self.front_label:
r = self.render_label()
a.add(r.translate(self.label_centre), color=self.label_color, name="Label")
self._obj_label = "assembly"
self._cq_obj = a
return self._cq_obj
================================================
FILE: cqgridfinity/scripts/__init__.py
================================================
================================================
FILE: cqgridfinity/scripts/gridfinitybase.py
================================================
#! /usr/bin/env python3
"""
command line script to make a Gridfinity baseplate
"""
import argparse
import cqgridfinity
from cqgridfinity import *
title = """
_____ _ _ __ _ _ _ ____
/ ____| (_) | |/ _(_) (_) | | _ \\
| | __ _ __ _ __| | |_ _ _ __ _| |_ _ _ | |_) | __ _ ___ ___
| | |_ | '__| |/ _` | _| | '_ \\| | __| | | | | _ < / _` / __|/ _ \\
| |__| | | | | (_| | | | | | | | | |_| |_| | | |_) | (_| \\__ \\ __/
\\_____|_| |_|\\__,_|_| |_|_| |_|_|\\__|\\__, | |____/ \\__,_|___/\\___|
__/ |
|___/
"""
DESC = """
Make a customized/parameterized Gridfinity compatible simple baseplate.
"""
EPILOG = """
example usage:
6 x 3 baseplate to default STL file:
$ gridfinitybase 6 3 -f stl
"""
def main():
parser = argparse.ArgumentParser(
description=DESC,
epilog=EPILOG,
prefix_chars="-+",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"length", metavar="length", type=str, help="Box length in U (1U = 42 mm)"
)
parser.add_argument(
"width", metavar="width", type=str, help="Box width in U (1U = 42 mm)"
)
parser.add_argument(
"-f",
"--format",
default="step",
help="Output file format (STEP, STL, SVG) default=STEP",
)
parser.add_argument(
"-s",
"--screws",
default=False,
action="store_true",
help="Add screw mounting tabs to the corners (adds +5 mm to depth)",
)
parser.add_argument(
"-d",
"--depth",
default=None,
action="store",
help="Extrude extended depth under baseplate by this amount",
)
parser.add_argument(
"-hd",
"--holediam",
default=None,
action="store",
help="Corner mounting screw hole diameter (default=5)",
)
parser.add_argument(
"-hc",
"--cskdiam",
default=None,
action="store",
help="Corner mounting screw countersink diameter (default=10)",
)
parser.add_argument(
"-ca",
"--cskangle",
default=None,
action="store",
help="Corner mounting screw countersink angle (deg) (default=82)",
)
parser.add_argument(
"-o",
"--output",
default=None,
help="Output filename (inferred output file format with extension)",
)
args = parser.parse_args()
argsd = vars(args)
print(title)
print("Version: %s" % (cqgridfinity.__version__))
for k in ["depth", "holediam", "cskdiam", "cskangle"]:
if argsd[k] is not None:
argsd[k] = float(argsd[k])
base = GridfinityBaseplate(
length_u=int(argsd["length"]),
width_u=int(argsd["width"]),
ext_depth=argsd["depth"],
corner_screws=argsd["screws"],
csk_hole=argsd["holediam"],
csk_diam=argsd["cskdiam"],
csk_angle=argsd["cskangle"],
)
print(
"Gridfinity baseplate: %dU x %dU (%.1f mm x %.1f mm)"
% (
base.length_u,
base.width_u,
base.length,
base.width,
)
)
if argsd["output"] is not None:
fn = argsd["output"]
else:
fn = base.filename()
s = ["\nBaseplate generated and saved as"]
if argsd["format"].lower() == "stl" or fn.lower().endswith(".stl"):
if not fn.endswith(".stl"):
fn = fn + ".stl"
base.save_stl_file(filename=argsd["output"])
s.append("%s in STL format" % (fn))
elif argsd["format"].lower() == "svg" or fn.lower().endswith(".svg"):
if not fn.endswith(".svg"):
fn = fn + ".svg"
base.save_svg_file(filename=argsd["output"])
s.append("%s in SVG format" % (fn))
else:
if not fn.endswith(".step"):
fn = fn + ".step"
base.save_step_file(filename=argsd["output"])
s.append("%s in STEP format" % (fn))
print(" ".join(s))
if __name__ == "__main__":
main()
================================================
FILE: cqgridfinity/scripts/gridfinitybox.py
================================================
#! /usr/bin/env python3
"""
command line script to make a Gridfinity box
"""
import argparse
import cqgridfinity
from cqgridfinity import *
title = """
_____ _ _ __ _ _ _ ____
/ ____| (_) | |/ _(_) (_) | | _ \\
| | __ _ __ _ __| | |_ _ _ __ _| |_ _ _ | |_) | _____ __
| | |_ | '__| |/ _` | _| | '_ \\| | __| | | | | _ < / _ \\ \\/ /
| |__| | | | | (_| | | | | | | | | |_| |_| | | |_) | (_) > <
\\_____|_| |_|\\__,_|_| |_|_| |_|_|\\__|\\__, | |____/ \\___/_/\\_\\
__/ |
|___/
"""
DESC = """
Make a customized/parameterized Gridfinity compatible box with many optional features.
"""
EPILOG = """
example usages:
2x3x5 box with magnet holes saved to STL file with default filename:
$ gridfinitybox 2 3 5 -m -f stl
1x3x4 box with scoops, label strip, 3 internal partitions and specified name:
$ gridfinitybox 1 3 4 -s -l -ld 3 -o MyBox.step
Solid 3x3x3 box with 50% fill, unsupported magnet holes and no top lip:
$ gridfinitybox 3 3 3 -d -r 0.5 -u -n
Lite style box 3x2x3 with label strip, partitions, output to default SVG file:
$ gridfinitybox 3 2 3 -e -l -ld 2 -f svg
"""
def main():
parser = argparse.ArgumentParser(
description=DESC,
epilog=EPILOG,
prefix_chars="-+",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"length", metavar="length", type=str, help="Box length in U (1U = 42 mm)"
)
parser.add_argument(
"width", metavar="width", type=str, help="Box width in U (1U = 42 mm)"
)
parser.add_argument(
"height", metavar="height", type=str, help="Box height in U (1U = 7 mm)"
)
parser.add_argument(
"-m",
"--magnetholes",
action="store_true",
default=False,
help="Add bottom magnet/mounting holes",
)
parser.add_argument(
"-u",
"--unsupported",
action="store_true",
default=False,
help="Add bottom magnet holes with 3D printer friendly strips without support",
)
parser.add_argument(
"-n",
"--nolip",
action="store_true",
default=False,
help="Do not add mating lip to the top perimeter",
)
parser.add_argument(
"-s",
"--scoops",
action="store_true",
default=False,
help="Add finger scoops against each length-wise back wall",
)
parser.add_argument(
"-l",
"--labels",
action="store_true",
default=False,
help="Add label strips against each length-wise front wall",
)
parser.add_argument(
"-e",
"--ecolite",
action="store_true",
default=False,
help="Make economy / lite style box with no elevated floor",
)
parser.add_argument(
"-d",
"--solid",
action="store_true",
default=False,
help="Make solid (filled) box for customized storage",
)
parser.add_argument(
"-r",
"--ratio",
action="store",
default=1.0,
help="Solid box fill ratio 0.0 = minimum, 1.0 = full height",
)
parser.add_argument(
"-ld",
"--lengthdiv",
action="store",
default=0,
help="Split box length-wise with specified number of divider walls",
)
parser.add_argument(
"-wd",
"--widthdiv",
action="store",
default=0,
help="Split box width-wise with specified number of divider walls",
)
parser.add_argument(
"-wt",
"--wall",
action="store",
default=1.0,
help="Wall thickness (default=1 mm)",
)
parser.add_argument(
"-f",
"--format",
default="step",
help="Output file format (STEP, STL, SVG) default=STEP",
)
parser.add_argument(
"-o",
"--output",
default=None,
help="Output filename (inferred output file format with extension)",
)
args = parser.parse_args()
argsd = vars(args)
solid_ratio = float(argsd["ratio"])
length_div = int(argsd["lengthdiv"])
width_div = int(argsd["widthdiv"])
wall = float(argsd["wall"])
box = GridfinityBox(
length_u=int(argsd["length"]),
width_u=int(argsd["width"]),
height_u=int(argsd["height"]),
holes=argsd["magnetholes"] or argsd["unsupported"],
unsupported_holes=argsd["unsupported"],
no_lip=argsd["nolip"],
scoops=argsd["scoops"],
labels=argsd["labels"],
lite_style=argsd["ecolite"],
solid=argsd["solid"],
solid_ratio=solid_ratio,
length_div=length_div,
width_div=width_div,
wall_th=wall,
)
if argsd["ecolite"]:
bs = "lite "
elif argsd["solid"]:
bs = "solid "
else:
bs = ""
print(title)
print("Version: %s" % (cqgridfinity.__version__))
print(
"Gridfinity %sbox: %dU x %dU x %dU (%.1f mm x %.1f mm x %.1f mm), %.2f mm walls"
% (
bs,
box.length_u,
box.width_u,
box.height_u,
box.length,
box.width,
box.height,
box.wall_th,
)
)
if argsd["solid"]:
print(
" solid height ratio: %.2f top height: %.2f mm / %.2f mm"
% (solid_ratio, box.top_ref_height, box.max_height + GR_BOT_H)
)
s = []
if argsd["unsupported"]:
s.append("holes with no support")
elif argsd["magnetholes"]:
s.append("holes")
if argsd["nolip"]:
s.append("no lip")
if argsd["scoops"]:
s.append("scoops")
if argsd["labels"]:
s.append("label strips")
if length_div:
s.append("%d length-wise walls" % (length_div))
if width_div:
s.append("%d width-wise walls" % (width_div))
if len(s):
print(" with options: %s" % (", ".join(s)))
if argsd["output"] is not None:
fn = argsd["output"]
else:
fn = box.filename()
s = ["\nBox generated and saved as"]
if argsd["format"].lower() == "stl" or fn.lower().endswith(".stl"):
if not fn.endswith(".stl"):
fn = fn + ".stl"
box.save_stl_file(filename=argsd["output"])
s.append("%s in STL format" % (fn))
elif argsd["format"].lower() == "svg" or fn.lower().endswith(".svg"):
if not fn.endswith(".svg"):
fn = fn + ".svg"
box.save_svg_file(filename=argsd["output"])
s.append("%s in SVG format" % (fn))
else:
if not fn.endswith(".step"):
fn = fn + ".step"
box.save_step_file(filename=argsd["output"])
s.append("%s in STEP format" % (fn))
print(" ".join(s))
if __name__ == "__main__":
main()
================================================
FILE: cqgridfinity/scripts/ruggedbox.py
================================================
#! /usr/bin/env python3
"""
command line script to make a rugged Gridfinity box
"""
import argparse
import cqgridfinity
from cqgridfinity import *
title = """
____ _ ____
| _ \ _ _ __ _ __ _ ___ __| | __ ) _____ __
| |_) | | | |/ _` |/ _` |/ _ \\/ _` | _ \\ / _ \\ \\/ /
| _ <| |_| | (_| | (_| | __/ (_| | |_) | (_) > <
|_| \\_\\\\__,_|\\__, |\\__, |\\___|\\__,_|____/ \\___/_/\\_\\
|___/ |___/
"""
DESC = """
Make a customized/parameterized Gridfinity compatible rugged box enclosure.
The minimum box size is 3U x 3U x 4U.
"""
EPILOG = """
example usage:
5 x 4 x 6 rugged box shell and lid saved to STL files:
$ ruggedbox 5 4 6 --box --lid -f stl
"""
def save_asset(box, argsd, prefix=None):
if argsd["output"] is not None:
fn = argsd["output"]
if box._obj_label is not None:
for ext in (".stl", ".step", ".svg"):
if fn.lower().endswith(ext):
fn = fn.replace(ext, "_%s%s" % (box._obj_label, ext))
break
else:
fn = box.filename(prefix=prefix)
s = ["Component generated and saved as"]
if argsd["format"].lower() == "stl" or fn.lower().endswith(".stl"):
if not fn.endswith(".stl"):
fn = fn + ".stl"
box.save_stl_file(filename=argsd["output"], prefix=prefix)
s.append("%s in STL format" % (fn))
elif argsd["format"].lower() == "svg" or fn.lower().endswith(".svg"):
if not fn.endswith(".svg"):
fn = fn + ".svg"
box.save_svg_file(filename=argsd["output"], prefix=prefix)
s.append("%s in SVG format" % (fn))
else:
if not fn.endswith(".step"):
fn = fn + ".step"
box.save_step_file(filename=argsd["output"], prefix=prefix)
s.append("%s in STEP format" % (fn))
print(" ".join(s))
def main():
parser = argparse.ArgumentParser(
description=DESC,
epilog=EPILOG,
prefix_chars="-+",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"length", metavar="length", type=str, help="Box length in U (1U = 42 mm)"
)
parser.add_argument(
"width", metavar="width", type=str, help="Box width in U (1U = 42 mm)"
)
parser.add_argument(
"height", metavar="height", type=str, help="Box height in U (1U = 7 mm)"
)
parser.add_argument(
"+l",
"--label",
action="store_true",
default=False,
help="Add label window across the front wall",
)
parser.add_argument(
"-l",
"--nolabel",
action="store_true",
default=False,
help="Remove label window across the front wall",
)
parser.add_argument(
"+p",
"--lidbaseplate",
action="store_true",
default=False,
help="Add baseplate to top of the lid",
)
parser.add_argument(
"-p",
"--nolidbaseplate",
action="store_true",
default=False,
help="Smooth/plain lid",
)
parser.add_argument(
"+w",
"--lidwindow",
action="store_true",
default=False,
help="Add window slot to the lid",
)
parser.add_argument(
"-w",
"--nolidwindow",
action="store_true",
default=False,
help="Do not add window slot to the lid",
)
parser.add_argument(
"-wt",
"--windowthickness",
action="store",
default=None,
help="Thickness of lid windows (mm)",
)
parser.add_argument(
"+a",
"--handle",
action="store_true",
default=False,
help="Add front handle",
)
parser.add_argument(
"-a",
"--nohandle",
action="store_true",
default=False,
help="No front handle",
)
parser.add_argument(
"+c",
"--clasps",
action="store_true",
default=False,
help="Add clasps to the left and right side walls",
)
parser.add_argument(
"-c",
"--noclasps",
action="store_true",
default=False,
help="No clasps on the left and right side walls",
)
parser.add_argument(
"+s",
"--stackable",
action="store_true",
default=False,
help="Add stackable mating features to top and bottom",
)
parser.add_argument(
"-s",
"--notstackable",
action="store_true",
default=False,
help="Non-stackable box",
)
parser.add_argument(
"+v",
"--veegroove",
action="store_true",
default=False,
help="Add v-cut grooves to side walls",
)
parser.add_argument(
"-v",
"--noveegroove",
action="store_true",
default=False,
help="No v-cut grooves (plain) side walls",
)
parser.add_argument(
"+e",
"--sidehandle",
action="store_true",
default=False,
help="Add handles to side walls",
)
parser.add_argument(
"-e",
"--nosidehandle",
action="store_true",
default=False,
help="No handles on side walls",
)
parser.add_argument(
"+b",
"--backfeet",
action="store_true",
default=False,
help="Add standing feet to back wall",
)
parser.add_argument(
"-b",
"--nobackfeet",
action="store_true",
default=False,
help="No standing feet added to back wall",
)
parser.add_argument(
"-r",
"--normalstyle",
action="store_true",
default=False,
help="Make normal style box",
)
parser.add_argument(
"+r",
"--ribstyle",
action="store_true",
default=False,
help="Make rib style box with exposed vertical ribs",
)
parser.add_argument(
"-f",
"--format",
default="step",
help="Output file format (STEP, STL, SVG) default=STEP",
)
parser.add_argument(
"-o",
"--output",
default=None,
help="Output filename (inferred output file format with extension)",
)
parser.add_argument(
"-gb",
"--box",
action="store_true",
default=False,
help="Generate box",
)
parser.add_argument(
"-gl",
"--lid",
action="store_true",
default=False,
help="Generate lid",
)
parser.add_argument(
"-ga",
"--acc",
action="store_true",
default=False,
help="Generate accessory components",
)
parser.add_argument(
"-gh",
"--hinge",
action="store_true",
default=False,
help="Generate hinge element",
)
parser.add_argument(
"-ge",
"--genlabel",
action="store_true",
default=False,
help="Generate label panel insert",
)
parser.add_argument(
"-gn",
"--genhandle",
action="store_true",
default=False,
help="Generate front handle",
)
parser.add_argument(
"-gt",
"--genlatch",
action="store_true",
default=False,
help="Generate latch component",
)
parser.add_argument(
"-gw",
"--genwindow",
action="store_true",
default=False,
help="Generate lid window component",
)
args = parser.parse_args()
argsd = vars(args)
box = GridfinityRuggedBox(
length_u=int(argsd["length"]),
width_u=int(argsd["width"]),
height_u=int(argsd["height"]),
)
if argsd["lidbaseplate"]:
box.lid_baseplate = True
if argsd["nolidbaseplate"]:
box.lid_baseplate = False
if argsd["lidwindow"]:
box.lid_window = True
if argsd["nolidwindow"]:
box.lid_window = False
if argsd["handle"]:
box.front_handle = True
if argsd["nohandle"]:
box.front_handle = False
if argsd["label"]:
box.front_label = True
if argsd["nolabel"]:
box.front_label = False
if argsd["clasps"]:
box.side_clasps = True
if argsd["noclasps"]:
box.side_clasps = False
if argsd["stackable"]:
box.stackable = True
if argsd["notstackable"]:
box.stackable = False
if argsd["veegroove"]:
box.wall_vgrooves = True
if argsd["noveegroove"]:
box.wall_vgrooves = False
if argsd["sidehandle"]:
box.side_handles = True
if argsd["nosidehandle"]:
box.side_handles = False
if argsd["backfeet"]:
box.back_feet = True
if argsd["nobackfeet"]:
box.back_feet = False
if argsd["ribstyle"]:
box.rib_style = True
if argsd["normalstyle"]:
box.rib_style = False
if argsd["windowthickness"] is not None:
box.window_th = float(argsd["windowthickness"])
print(title)
print("Version: %s" % (cqgridfinity.__version__))
print(
"Gridfinity rugged box: %dU x %dU x %dU"
% (
box.length_u,
box.width_u,
box.height_u,
)
)
print(
" Exterior dim: %.1f mm x %.1f mm x %.1f mm"
% (
box.box_length + 2 * (GR_RBOX_CWALL - GR_RBOX_WALL),
box.box_width + 2 * (GR_RBOX_CWALL - GR_RBOX_WALL),
box.box_height + box.lid_height,
)
)
print(
" Interior dim: %.1f mm x %.1f mm x %.1f mm"
% (
box.length,
box.width,
box.height,
)
)
print(" Internal volume: %.3f L" % (box.length * box.width * box.height / 1e6))
if box.lid_window:
print(
" Lid window dimensions: %.2f x %.2f mm, %.2f mm thickness"
% (*box.lid_window_size(), box.window_th)
)
s = []
opts = [
"wall_vgrooves",
"front_handle",
"stackable",
"side_clasps",
"lid_baseplate",
"inside_baseplate",
"side_handles",
"front_label",
"back_feet",
"rib_style",
]
for opt in opts:
opt_name = opt.replace("_", " ").title()
val = "Y" if box.__dict__[opt] else "N"
print(" %-19s: %s" % (opt_name, val))
print(" %-19s: %s" % ("Lid Window", "Y" if box.lid_window else "N"))
if argsd["output"] is not None:
fn = argsd["output"]
else:
fn = box.filename()
g = False
if argsd["box"]:
print("Rendering box...")
box.render()
save_asset(box, argsd)
g = True
if argsd["lid"]:
print("Rendering lid...")
box.render_lid()
save_asset(box, argsd)
g = True
if argsd["acc"]:
print("Rendering accessory components...")
r = box.render_accessories()
save_asset(box, argsd)
g = True
if argsd["hinge"]:
print("Rendering hinge components...")
r = box.render_hinge()
save_asset(box, argsd)
g = True
if argsd["genlabel"]:
print("Rendering label panel...")
r = box.render_label()
save_asset(box, argsd)
g = True
if argsd["genhandle"]:
print("Rendering front handle...")
r = box.render_handle()
save_asset(box, argsd)
g = True
if argsd["genlatch"]:
print("Rendering latch component...")
r = box.render_latch()
save_asset(box, argsd)
g = True
if argsd["genwindow"]:
print(
"Rendering lid window (%.2f x %.2f mm, %.2f mm thickness)..."
% (*box.lid_window_size(), box.window_th)
)
r = box.render_lid_window()
save_asset(box, argsd)
g = True
if not g:
print("Rendering full assembly...")
a = box.render_assembly()
if argsd["output"] is not None:
fn = argsd["output"]
else:
fn = box.filename()
if not fn.endswith(".step"):
fn = fn + ".step"
a.save(fn)
if __name__ == "__main__":
main()
================================================
FILE: cqgridfinity/shims/README.md
================================================
# /pub/storage/workspace/gridfinity
Created by Zach Freedman as a versatile system of modular organization and storage modules.
This package defines the basic building blocks of the Gridfinity system.
Make use of the parameters to customize the parts to your needs.
## Parts
### baseplate
<table><tr>
<td valign=top><a href="cqgridfinity/shims/cqgi_gf_baseplate.py"><img src="../../cqgridfinity/shims/baseplate.svg" style="width: auto; height: auto; max-width: 200px; max-height: 200px;"></a></td>
<td valign=top>Parameters:<br/><ul>
<li>length_u: 2</li>
<li>width_u: 2</li>
<li>ext_depth: 0.0</li>
<li>straight_bottom: False</li>
<li>corner_screws: False</li>
<li>corner_tab_size: 21.0</li>
<li>csk_hole: 5.0</li>
<li>csk_diam: 10.0</li>
<li>csk_angle: 82.0</li>
</ul>
</td>
</tr></table>
### box
<table><tr>
<td valign=top><a href="cqgridfinity/shims/cqgi_gf_box.py"><img src="../../cqgridfinity/shims/box.svg" style="width: auto; height: auto; max-width: 200px; max-height: 200px;"></a></td>
<td valign=top>Parameters:<br/><ul>
<li>length_u: 2</li>
<li>width_u: 2</li>
<li>height_u: 2</li>
<li>length_div: 0.0</li>
<li>width_div: 0.0</li>
<li>scoops: False</li>
<li>labels: False</li>
<li>solid: False</li>
<li>holes: False</li>
<li>no_lip: False</li>
<li>solid_ratio: 1.0</li>
<li>lite_style: False</li>
<li>unsupported_holes: False</li>
<li>label_width: 12.0</li>
<li>label_height: 10.0</li>
<li>label_lip_height: 0.8</li>
<li>scoop_rad: 12.0</li>
<li>fillet_interior: True</li>
<li>wall_th: 1.0</li>
</ul>
</td>
</tr></table>
### drawerspacer
<table><tr>
<td valign=top><a href="cqgridfinity/shims/cqgi_gf_drawerspacer.py"><img src="../../cqgridfinity/shims/drawerspacer.svg" style="width: auto; height: auto; max-width: 200px; max-height: 200px;"></a></td>
<td valign=top>Parameters:<br/><ul>
<li>length_u: 2</li>
<li>width_u: 2</li>
<li>length_th: 10.0</li>
<li>width_th: 10.0</li>
<li>thickness: 5.0</li>
<li>chamf_rad: 1.0</li>
<li>show_arrows: True</li>
<li>arrow_h: 0.8</li>
<li>length_fill: 0.0</li>
<li>width_fill: 0.0</li>
<li>align_features: True</li>
<li>align_l: 16.0</li>
<li>align_tol: 0.15</li>
<li>align_min: 8.0</li>
<li>min_margin: 4.0</li>
<li>tolerance: 0.5</li>
</ul>
</td>
</tr></table>
### ruggedbox
<table><tr>
<td valign=top><a href="cqgridfinity/shims/cqgi_gf_ruggedbox.py"><img src="../../cqgridfinity/shims/ruggedbox.svg" style="width: auto; height: auto; max-width: 200px; max-height: 200px;"></a></td>
<td valign=top>Parameters:<br/><ul>
<li>length_u: 4</li>
<li>width_u: 4</li>
<li>height_u: 4</li>
<li>lid_height: 10.0</li>
<li>wall_vgrooves: True</li>
<li>front_handle: True</li>
<li>stackable: True</li>
<li>side_clasps: True</li>
<li>lid_baseplate: True</li>
<li>inside_baseplate: True</li>
<li>side_handles: True</li>
<li>front_label: True</li>
<li>label_length: 0.0</li>
<li>label_height: 0.0</li>
<li>label_th: 0.5</li>
<li>back_feet: True</li>
<li>hinge_width: 48.0</li>
<li>hinge_bolted: False</li>
</ul>
</td>
</tr></table>
<br/><br/>
*Generated by [PartCAD](https://partcad.org/)*
================================================
FILE: cqgridfinity/shims/cqgi_gf_baseplate.py
================================================
import sys
sys.path.append(".") # Relative to `partcad.yaml`
from cqgridfinity.gf_baseplate import GridfinityBaseplate
length_u = 2
width_u = 2
ext_depth = 0.0
straight_bottom = False
corner_screws = False
corner_tab_size = 21
csk_hole = 5.0
csk_diam = 10.0
csk_angle = 82
result = GridfinityBaseplate(
length_u=int(length_u),
width_u=int(width_u),
ext_depth=ext_depth,
straight_bottom=straight_bottom,
corner_screws=corner_screws,
corner_tab_size=corner_tab_size,
csk_hole=csk_hole,
csk_diam=csk_diam,
csk_angle=csk_angle,
).render().val()
show_object(result)
================================================
FILE: cqgridfinity/shims/cqgi_gf_box.py
================================================
import sys
sys.path.append(".") # Relative to `partcad.yaml`
from cqgridfinity.gf_box import GridfinityBox
length_u = 2
width_u = 2
height_u = 2
length_div = 0.0
width_div = 0.0
scoops = False
labels = False
solid = False
holes = False
no_lip = False
solid_ratio = 1.0
lite_style = False
unsupported_holes = False
label_width = 12.0 # width of the label strip
label_height = 10.0 # thickness of label overhang
label_lip_height = 0.8 # thickness of label vertical lip
scoop_rad = 12.0 # radius of optional interior scoops
fillet_interior = True
wall_th = 1.0
result = GridfinityBox(
length_u=int(length_u),
width_u=int(width_u),
height_u=int(height_u),
length_div=length_div,
width_div=width_div,
scoops=scoops,
labels=labels,
solid=solid,
holes=holes,
no_lip=no_lip,
solid_ratio=solid_ratio,
lite_style=lite_style,
unsupported_holes=unsupported_holes,
label_width=label_width,
label_height=label_height,
label_lip_height=label_lip_height,
scoop_rad=scoop_rad,
fillet_interior=fillet_interior,
wall_th=wall_th,
).render().val()
show_object(result)
================================================
FILE: cqgridfinity/shims/cqgi_gf_drawerspacer.py
================================================
import sys
sys.path.append(".") # Relative to `partcad.yaml`
from cqgridfinity.gf_drawer import GridfinityDrawerSpacer
length_u = 2
width_u = 2
length_th = 10.0
width_th = 10.0
thickness = 5.0
chamf_rad = 1.0
show_arrows = True
arrow_h = 0.8
length_fill = 0.0
width_fill = 0.0
align_features = True
align_l = 16.0
align_tol = 0.15
align_min = 8.0
min_margin = 4.0
tolerance = 0.5
result = GridfinityDrawerSpacer(
length_u=int(length_u),
width_u=int(width_u),
length_th=length_th,
width_th=width_th,
thickness=thickness,
chamf_rad=chamf_rad,
show_arrows=show_arrows,
arrow_h=arrow_h,
length_fill=length_fill,
width_fill=width_fill,
align_features=align_features,
align_l=align_l,
align_tol=align_tol,
align_min=align_min,
min_margin=min_margin,
tolerance=tolerance,
).render().val()
show_object(result)
================================================
FILE: cqgridfinity/shims/cqgi_gf_ruggedbox.py
================================================
import sys
sys.path.append(".") # Relative to `partcad.yaml`
from cqgridfinity.gf_ruggedbox import GridfinityRuggedBox
length_u = 4
width_u = 4
height_u = 4
lid_height = 10.0
wall_vgrooves = True
front_handle = True
stackable = True
side_clasps = True
lid_baseplate = True
inside_baseplate = True
side_handles = True
front_label = True
# TODO(clairbee): uncomment the below when annotations are supported by CQGI
# label_length: float = None
# label_height: float = None
label_length = 0.0
label_height = 0.0
if label_length == 0.0:
label_length = None
if label_height == 0.0:
label_height = None
label_th = 0.8
back_feet = True
hinge_width = 48.0
hinge_bolted = False
rib_style = False
result = GridfinityRuggedBox(
length_u=int(length_u),
width_u=int(width_u),
height_u=int(height_u),
lid_height=lid_height,
wall_vgrooves=wall_vgrooves,
front_handle=front_handle,
stackable=stackable,
side_clasps=side_clasps,
lid_baseplate=lid_baseplate,
inside_baseplate=inside_baseplate,
side_handles=side_handles,
front_label=front_label,
label_length=label_length,
label_height=label_height,
label_th=label_th,
back_feet=back_feet,
hinge_width=hinge_width,
hinge_bolted=hinge_bolted,
rib_style=rib_style,
).render().val()
show_object(result)
================================================
FILE: examples/demo1.assy
================================================
# This is a demo of multiple gridfinity parts put together using PartCAD.
# Use `pc show -a examples/demo1` to view it in OCP CAD Viewer or
# use `pc render -t png -a examples/demo1` to render a PNG image of this assembly.
links:
- part: baseplate
name: baseplate
- part: box
name: box
location: [[100, 0, 0], [0, 0, 1], 0]
- part: drawerspacer
name: drawerspacerbox
location: [[-200, 0, 0], [0, 0, 1], 0]
================================================
FILE: partcad.yaml
================================================
partcad: ">=0.7.16"
name: /pub/storage/workspace/gridfinity
desc: Created by Zach Freedman as a versatile system of modular organization and storage modules.
cover:
part: ruggedbox
docs:
intro: |
This package defines the basic building blocks of the Gridfinity system.
Make use of the parameters to customize the parts to your needs.
footer: |
## Implementation notes
This packages has a folder with PartCAD shims.
This folder contains wrappers for cqgridfinity main Python files
to make them compatible with CadQuery's CQGI interface that is used by PartCAD.
This is a non-intrusive alternative to refactoring cqgridfinity main Python files.
If cqgridfinity adopts CQGI, then these shims can be dropped.
parts:
baseplate:
type: cadquery
path: cqgridfinity/shims/cqgi_gf_baseplate.py
parameters:
length_u:
type: int
default: 2
width_u:
type: int
default: 2
ext_depth:
type: float
default: 0.0
straight_bottom:
type: bool
default: False
corner_screws:
type: bool
default: False
corner_tab_size:
type: float
default: 21.0
csk_hole:
type: float
default: 5.0
csk_diam:
type: float
default: 10.0
csk_angle:
type: float
default: 82.0
ruggedbox:
type: cadquery
path: cqgridfinity/shims/cqgi_gf_ruggedbox.py
parameters:
length_u:
type: int
default: 4
width_u:
type: int
default: 4
height_u:
type: int
default: 4
lid_height:
type: float
default: 10.0
wall_vgrooves:
type: bool
default: True
front_handle:
type: bool
default: True
stackable:
type: bool
default: True
side_clasps:
type: bool
default: True
lid_baseplate:
type: bool
default: True
inside_baseplate:
type: bool
default: True
side_handles:
type: bool
default: True
front_label:
type: bool
default: True
label_length:
type: float
default: 0.0
label_height:
type: float
default: 0.0
label_th:
type: float
default: 0.8
back_feet:
type: bool
default: True
hinge_width:
type: float
default: 48.0
hinge_bolted:
type: bool
default: False
rib_style:
type: bool
default: False
box:
type: cadquery
path: cqgridfinity/shims/cqgi_gf_box.py
parameters:
length_u:
type: int
default: 2
width_u:
type: int
default: 2
height_u:
type: int
default: 2
length_div:
type: float
default: 0.0
width_div:
type: float
default: 0.0
scoops:
type: bool
default: False
labels:
type: bool
default: False
solid:
type: bool
default: False
holes:
type: bool
default: False
no_lip:
type: bool
default: False
solid_ratio:
type: float
default: 1.0
lite_style:
type: bool
default: False
unsupported_holes:
type: bool
default: False
label_width:
type: float
default: 12.0 # width of the label strip
label_height:
type: float
default: 10.0 # thickness of label overhang
label_lip_height:
type: float
default: 0.8 # thickness of label vertical lip
scoop_rad:
type: float
default: 12.0 # radius of optional interior scoops
fillet_interior:
type: bool
default: True
wall_th:
type: float
default: 1.0
drawerspacer:
type: cadquery
path: cqgridfinity/shims/cqgi_gf_drawerspacer.py
parameters:
length_u:
type: int
default: 2
width_u:
type: int
default: 2
length_th:
type: float
default: 10.0
width_th:
type: float
default: 10.0
thickness:
type: float
default: 5.0
chamf_rad:
type: float
default: 1.0
show_arrows:
type: bool
default: True
arrow_h:
type: float
default: 0.8
length_fill:
type: float
default: 0.0
width_fill:
type: float
default: 0.0
align_features:
type: bool
default: True
align_l:
type: float
default: 16.0
align_tol:
type: float
default: 0.15
align_min:
type: float
default: 8.0
min_margin:
type: float
default: 4.0
tolerance:
type: float
default: 0.5
assemblies:
examples/demo1:
type: assy
render:
svg:
prefix: cqgridfinity/shims
exclude:
- assemblies
readme:
path: cqgridfinity/shims/README.md
exclude:
- assemblies
================================================
FILE: requirements.in
================================================
cadquery
cqkit>=0.5.6
================================================
FILE: requirements.txt
================================================
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile requirements.in
#
cadquery==2.4.0
# via -r requirements.in
cadquery-ocp==7.7.2
# via cadquery
casadi==3.6.7
# via cadquery
cqkit==0.5.8
# via -r requirements.in
ezdxf==1.3.4
# via cadquery
fonttools==4.55.2
# via ezdxf
multimethod==1.9.1
# via cadquery
nlopt==2.9.0
# via cadquery
nptyping==2.0.1
# via cadquery
numpy==2.1.3
# via
# casadi
# ezdxf
# nlopt
# nptyping
path==17.0.0
# via cadquery
pyparsing==3.2.0
# via ezdxf
typing-extensions==4.12.2
# via ezdxf
typish==1.9.3
# via cadquery
================================================
FILE: setup.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import os.path
from pathlib import Path
import sys
import setuptools
PACKAGE_NAME = "cqgridfinity"
required = ["cadquery", "cqkit>=0.5.6"]
dependency_links = []
def read_package_variable(key, filename="__init__.py"):
"""Read the value of a variable from the package without importing."""
module_path = os.path.join(PACKAGE_NAME, filename)
with open(module_path) as module:
for line in module:
parts = line.strip().split(" ", 2)
if parts[:-1] == [key, "="]:
return parts[-1].strip("'")
sys.exit("'{0}' not found in '{1}'".format(key, module_path))
this_directory = Path(__file__).parent
long_description = (this_directory / "README.md").read_text()
setuptools.setup(
name=read_package_variable("__project__"),
version=read_package_variable("__version__"),
description="A python library to make Gridfinity compatible objects with CadQuery.",
url="https://github.com/michaelgale/cq-gridfinity",
author="Michael Gale",
author_email="michael@fxbricks.com",
python_requires=">=3.9",
packages=setuptools.find_packages(),
long_description=long_description,
long_description_content_type="text/markdown",
license="MIT",
classifiers=[
"Development Status :: 4 - Beta",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.9",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
],
install_requires=required,
dependency_links=dependency_links,
entry_points={
"console_scripts": [
"gridfinitybox=cqgridfinity.scripts.gridfinitybox:main",
"gridfinitybase=cqgridfinity.scripts.gridfinitybase:main",
"ruggedbox=cqgridfinity.scripts.ruggedbox:main",
],
},
)
================================================
FILE: tests/common_test.py
================================================
import os
EXPORT_STEP_FILE_PATH = "./tests/testfiles"
env = dict(os.environ)
SKIP_TEST_BOX = "SKIP_TEST_BOX" in env
SKIP_TEST_RBOX = "SKIP_TEST_RBOX" in env
SKIP_TEST_SPACER = "SKIP_TEST_SPACER" in env
SKIP_TEST_BASEPLATE = "SKIP_TEST_BASEPLATE" in env
def INCHES(x):
return x * 25.4
def _faces_match(obj, face, n):
nf = len(obj.faces(face).vals())
return nf == n
def _edges_match(obj, face, n):
nf = len(obj.faces(face).edges().vals())
return abs(nf - n) < 3
def _almost_same(x, y, tol=1e-3):
if isinstance(x, (list, tuple)):
return all((abs(xe - ye) < tol for xe, ye in zip(x, y)))
return abs(x - y) < tol
def _export_files(spec="all"):
if "EXPORT_STEP_FILES" in env:
exp_var = env["EXPORT_STEP_FILES"].lower()
if exp_var == "all":
return True
elif exp_var == spec.lower():
return True
return False
return False
================================================
FILE: tests/test_baseplate.py
================================================
# Gridfinity tests
import pytest
# my modules
from cqgridfinity import *
from cqkit import FlatEdgeSelector
from cqkit.cq_helpers import size_3d
from common_test import (
EXPORT_STEP_FILE_PATH,
_almost_same,
_faces_match,
_export_files,
SKIP_TEST_BASEPLATE,
)
@pytest.mark.skipif(
SKIP_TEST_BASEPLATE,
reason="Skipped intentionally by test scope environment variable",
)
def test_make_baseplate():
bp = GridfinityBaseplate(4, 3)
r = bp.render()
if _export_files("baseplate"):
bp.save_step_file(path=EXPORT_STEP_FILE_PATH)
assert bp.filename() == "gf_baseplate_4x3"
assert _almost_same(size_3d(r), (168, 126, 4.75))
assert _faces_match(r, ">Z", 16)
assert _faces_match(r, "<Z", 1)
edge_diff = abs(len(r.edges(FlatEdgeSelector(0)).vals()) - 104)
assert edge_diff < 3
@pytest.mark.skipif(
SKIP_TEST_BASEPLATE,
reason="Skipped intentionally by test scope environment variable",
)
def test_make_ext_baseplate():
bp = GridfinityBaseplate(5, 4, ext_depth=5, corner_screws=True)
r = bp.render()
assert _almost_same(size_3d(r), (210, 168, 9.75))
edge_diff = abs(len(r.edges(FlatEdgeSelector(0)).vals()) - 188)
assert edge_diff < 3
================================================
FILE: tests/test_box.py
================================================
# Gridfinity tests
import pytest
# my modules
from cqgridfinity import *
from cqkit.cq_helpers import *
from cqkit import *
from common_test import (
EXPORT_STEP_FILE_PATH,
_almost_same,
_edges_match,
_faces_match,
_export_files,
SKIP_TEST_BOX,
)
@pytest.mark.skipif(
SKIP_TEST_BOX, reason="Skipped intentionally by test scope environment variable"
)
def test_basic_box():
b1 = GridfinityBox(2, 3, 5, no_lip=True)
r = b1.render()
assert _almost_same(size_3d(r), (83.5, 125.5, 38.8))
assert _faces_match(r, ">Z", 1)
assert _faces_match(r, "<Z", 6)
assert _edges_match(r, ">Z", 16)
assert _edges_match(r, "<Z", 48)
assert b1.filename() == "gf_box_2x3x5_basic"
if _export_files("box"):
b1.save_step_file(path=EXPORT_STEP_FILE_PATH)
b1 = GridfinityBox(2, 3, 5, no_lip=True)
if _export_files("box"):
b1.wall_th = 1.5
r = b1.render()
b1.save_step_file(path=EXPORT_STEP_FILE_PATH)
@pytest.mark.skipif(
SKIP_TEST_BOX, reason="Skipped intentionally by test scope environment variable"
)
def test_invalid_box():
with pytest.raises(ValueError):
b1 = GridfinityBox(2, 3, 5, lite_style=True, solid=True)
b1.render()
with pytest.raises(ValueError):
b1 = GridfinityBox(2, 3, 5, lite_style=True, holes=True)
b1.render()
with pytest.raises(ValueError):
b1 = GridfinityBox(2, 3, 5, lite_style=True, wall_th=2.0)
b1.render()
with pytest.raises(ValueError):
b1 = GridfinityBox(2, 3, 5, wall_th=0.4)
b1.render()
with pytest.raises(ValueError):
b1 = GridfinityBox(2, 3, 5, wall_th=3.0)
b1.render()
@pytest.mark.skipif(
SKIP_TEST_BOX, reason="Skipped intentionall
gitextract_j4oewo7c/
├── .devcontainer/
│ ├── Dockerfile
│ ├── devcontainer.json
│ └── entrypoint.sh
├── .github/
│ └── workflows/
│ └── checks.yaml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── cqgridfinity/
│ ├── __init__.py
│ ├── constants.py
│ ├── gf_baseplate.py
│ ├── gf_box.py
│ ├── gf_drawer.py
│ ├── gf_helpers.py
│ ├── gf_obj.py
│ ├── gf_ruggedbox.py
│ ├── scripts/
│ │ ├── __init__.py
│ │ ├── gridfinitybase.py
│ │ ├── gridfinitybox.py
│ │ └── ruggedbox.py
│ └── shims/
│ ├── README.md
│ ├── cqgi_gf_baseplate.py
│ ├── cqgi_gf_box.py
│ ├── cqgi_gf_drawerspacer.py
│ └── cqgi_gf_ruggedbox.py
├── examples/
│ └── demo1.assy
├── partcad.yaml
├── requirements.in
├── requirements.txt
├── setup.py
└── tests/
├── common_test.py
├── test_baseplate.py
├── test_box.py
├── test_rbox.py
├── test_spacer.py
└── testfiles/
└── .gitkeep
SYMBOL INDEX (159 symbols across 15 files)
FILE: cqgridfinity/gf_baseplate.py
class GridfinityBaseplate (line 38) | class GridfinityBaseplate(GridfinityObject):
method __init__ (line 56) | def __init__(self, length_u, width_u, **kwargs):
method _corner_pts (line 73) | def _corner_pts(self):
method render (line 81) | def render(self):
FILE: cqgridfinity/gf_box.py
class GridfinityBox (line 34) | class GridfinityBox(GridfinityObject):
method __init__ (line 68) | def __init__(self, length_u, width_u, height_u, **kwargs):
method __str__ (line 96) | def __str__(self):
method render (line 146) | def render(self):
method top_ref_height (line 215) | def top_ref_height(self):
method bin_height (line 225) | def bin_height(self):
method safe_label_height (line 228) | def safe_label_height(self, backwall=False, from_bottom=False):
method has_dividers (line 249) | def has_dividers(self):
method interior_solid (line 253) | def interior_solid(self):
method render_interior (line 259) | def render_interior(self, force_solid=False):
method solid_shell (line 291) | def solid_shell(self):
method mask_with_obj (line 299) | def mask_with_obj(self, obj):
method base_interior (line 303) | def base_interior(self):
method render_shell (line 318) | def render_shell(self, as_solid=False):
method render_dividers (line 344) | def render_dividers(self):
method render_scoops (line 379) | def render_scoops(self):
method render_labels (line 408) | def render_labels(self):
method render_holes (line 452) | def render_holes(self, obj):
method render_hole_fillers (line 465) | def render_hole_fillers(self, obj):
class GridfinitySolidBox (line 477) | class GridfinitySolidBox(GridfinityBox):
method __init__ (line 480) | def __init__(self, length_u, width_u, height_u, **kwargs):
FILE: cqgridfinity/gf_drawer.py
class GridfinityDrawerSpacer (line 34) | class GridfinityDrawerSpacer(GridfinityObject):
method __init__ (line 46) | def __init__(self, dr_width=None, dr_depth=None, **kwargs):
method best_fit_to_dim (line 72) | def best_fit_to_dim(self, length, width, verbose=False):
method fillet_rad (line 145) | def fillet_rad(self):
method safe_chamfer_rad (line 154) | def safe_chamfer_rad(self):
method wide_enough (line 163) | def wide_enough(self):
method deep_enough (line 167) | def deep_enough(self):
method fb_length_th (line 171) | def fb_length_th(self):
method check_dimensions (line 176) | def check_dimensions(self):
method render (line 191) | def render(self, arrows_top=True, arrows_bottom=True, front_and_back=T...
method alignment_feature (line 241) | def alignment_feature(self, as_cutter=False, horz=True):
method orientation_arrows (line 270) | def orientation_arrows(self, obj, x, y, up=True, down=True, top=True, ...
method render_length_filler (line 301) | def render_length_filler(self, alignment_type="peg"):
method render_width_filler (line 326) | def render_width_filler(self, arrows_top=True, arrows_bottom=True):
method render_full_set (line 347) | def render_full_set(self, include_baseplate=False):
method render_half_set (line 401) | def render_half_set(self):
FILE: cqgridfinity/gf_helpers.py
function quarter_circle (line 30) | def quarter_circle(
function chamf_cyl (line 61) | def chamf_cyl(rad, height, chamf=0.5):
function chamf_rect (line 69) | def chamf_rect(length, width, height, angle=0, tol=0.5, z_offset=0):
FILE: cqgridfinity/gf_obj.py
class GridfinityObject (line 48) | class GridfinityObject:
method __init__ (line 55) | def __init__(self, **kwargs):
method cq_obj (line 66) | def cq_obj(self):
method length (line 72) | def length(self):
method width (line 76) | def width(self):
method height (line 80) | def height(self):
method int_height (line 84) | def int_height(self):
method max_height (line 91) | def max_height(self):
method floor_h (line 95) | def floor_h(self):
method lip_width (line 101) | def lip_width(self):
method outer_l (line 107) | def outer_l(self):
method outer_w (line 111) | def outer_w(self):
method outer_dim (line 115) | def outer_dim(self):
method inner_l (line 119) | def inner_l(self):
method inner_w (line 123) | def inner_w(self):
method inner_dim (line 127) | def inner_dim(self):
method half_l (line 131) | def half_l(self):
method half_w (line 135) | def half_w(self):
method half_dim (line 139) | def half_dim(self):
method half_in (line 143) | def half_in(self):
method outer_rad (line 147) | def outer_rad(self):
method inner_rad (line 151) | def inner_rad(self):
method under_h (line 155) | def under_h(self):
method safe_fillet_rad (line 159) | def safe_fillet_rad(self):
method grid_centres (line 165) | def grid_centres(self):
method hole_centres (line 173) | def hole_centres(self):
method safe_fillet (line 182) | def safe_fillet(self, obj, selector, rad):
method filename (line 187) | def filename(self, prefix=None, path=None):
method save_step_file (line 272) | def save_step_file(self, filename=None, path=None, prefix=None):
method save_stl_file (line 285) | def save_stl_file(
method save_svg_file (line 301) | def save_svg_file(self, filename=None, path=None, prefix=None):
method extrude_profile (line 324) | def extrude_profile(self, sketch, profile, workplane="XY", angle=None):
method to_step_file (line 349) | def to_step_file(
method to_stl_file (line 365) | def to_stl_file(
method as_obj (line 381) | def as_obj(cls, length_u=None, width_u=None, height_u=None, **kwargs):
FILE: cqgridfinity/gf_ruggedbox.py
class GridfinityRuggedBox (line 57) | class GridfinityRuggedBox(GridfinityObject):
method __init__ (line 58) | def __init__(self, length_u, width_u, height_u, **kwargs):
method check_dimensions (line 92) | def check_dimensions(self):
method box_length (line 99) | def box_length(self):
method int_length (line 103) | def int_length(self):
method box_width (line 107) | def box_width(self):
method int_width (line 111) | def int_width(self):
method clasp_pos (line 115) | def clasp_pos(self):
method box_height (line 119) | def box_height(self):
method clasp_heights (line 123) | def clasp_heights(self):
method side_clasp_centres (line 129) | def side_clasp_centres(self):
method front_clasp_centres (line 135) | def front_clasp_centres(self):
method clasp_notch_points (line 141) | def clasp_notch_points(self):
method hinge_centres (line 152) | def hinge_centres(self):
method align_centres (line 159) | def align_centres(self):
method right_qtr_centre (line 175) | def right_qtr_centre(self):
method left_qtr_centre (line 183) | def left_qtr_centre(self):
method bottom_qtr_centres (line 187) | def bottom_qtr_centres(self):
method qtr_centres (line 190) | def qtr_centres(self, tol=0.25, at_height=0, front=True, back=True):
method long_enough_for_handle (line 203) | def long_enough_for_handle(self):
method right_handle_centre (line 207) | def right_handle_centre(self):
method left_handle_centre (line 214) | def left_handle_centre(self):
method back_corner_centres (line 218) | def back_corner_centres(self):
method front_corner_centres (line 224) | def front_corner_centres(self):
method label_centre (line 230) | def label_centre(self):
method lid_window (line 239) | def lid_window(self):
method lid_window (line 243) | def lid_window(self, enable):
method lid_window_size (line 248) | def lid_window_size(self, width_ext=None, tol=None):
method lid_window_hole_pos (line 253) | def lid_window_hole_pos(self, z=0):
method label_size (line 262) | def label_size(self, as_insert=False, as_aperture=False, tol=0):
method body_shell (line 286) | def body_shell(self, as_lid=False):
method render_vcut (line 368) | def render_vcut(self):
method rib_style_cut (line 401) | def rib_style_cut(self):
method lid_handle (line 461) | def lid_handle(self, width=None):
method side_handle (line 482) | def side_handle(self, width=None):
method label_slot (line 536) | def label_slot(self):
method render_label (line 573) | def render_label(self):
method clasp_cut (line 581) | def clasp_cut(self, as_lid=False):
method clasp_rib (line 594) | def clasp_rib(self, chamfered=False):
method clasp_ribs (line 613) | def clasp_ribs(self, side="left", as_lid=False):
method handle_mount (line 636) | def handle_mount(self, side="left"):
method render_handle (line 678) | def render_handle(self):
method render_back_foot (line 701) | def render_back_foot(self):
method hinge_mount (line 708) | def hinge_mount(self):
method hex_cut (line 730) | def hex_cut(self, depth=None):
method render_latch (line 752) | def render_latch(self):
method render_hinge (line 817) | def render_hinge(self, as_closed=False, section=None):
method render (line 894) | def render(self):
method render_lid (line 976) | def render_lid(self):
method render_lid_window (line 1090) | def render_lid_window(self):
method render_accessories (line 1101) | def render_accessories(self):
method render_assembly (line 1134) | def render_assembly(self):
FILE: cqgridfinity/scripts/gridfinitybase.py
function main (line 33) | def main():
FILE: cqgridfinity/scripts/gridfinitybox.py
function main (line 42) | def main():
FILE: cqgridfinity/scripts/ruggedbox.py
function save_asset (line 32) | def save_asset(box, argsd, prefix=None):
function main (line 61) | def main():
FILE: setup.py
function read_package_variable (line 17) | def read_package_variable(key, filename="__init__.py"):
FILE: tests/common_test.py
function INCHES (line 12) | def INCHES(x):
function _faces_match (line 16) | def _faces_match(obj, face, n):
function _edges_match (line 21) | def _edges_match(obj, face, n):
function _almost_same (line 26) | def _almost_same(x, y, tol=1e-3):
function _export_files (line 32) | def _export_files(spec="all"):
FILE: tests/test_baseplate.py
function test_make_baseplate (line 21) | def test_make_baseplate():
function test_make_ext_baseplate (line 38) | def test_make_ext_baseplate():
FILE: tests/test_box.py
function test_basic_box (line 23) | def test_basic_box():
function test_invalid_box (line 44) | def test_invalid_box():
function test_lite_box (line 65) | def test_lite_box():
function test_empty_box (line 98) | def test_empty_box():
function test_solid_box (line 132) | def test_solid_box():
function test_divided_box (line 153) | def test_divided_box():
function test_all_features_box (line 172) | def test_all_features_box():
FILE: tests/test_rbox.py
function _rugged_box (line 18) | def _rugged_box():
function test_rugged_box (line 36) | def test_rugged_box():
function test_rugged_box_lid (line 49) | def test_rugged_box_lid():
function test_rugged_box_acc (line 62) | def test_rugged_box_acc():
function test_rugged_box_parts (line 74) | def test_rugged_box_parts():
function test_rugged_box_assembly (line 104) | def test_rugged_box_assembly():
FILE: tests/test_spacer.py
function test_spacer (line 22) | def test_spacer():
function test_spacer_render (line 68) | def test_spacer_render():
function test_back_only_spacer (line 112) | def test_back_only_spacer():
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (219K chars).
[
{
"path": ".devcontainer/Dockerfile",
"chars": 1705,
"preview": "FROM condaforge/mambaforge:23.3.1-1\n\n# Create non-root user\nARG USERNAME=vscode\nARG USER_UID=1000\nARG USER_GID=$USER_UID"
},
{
"path": ".devcontainer/devcontainer.json",
"chars": 928,
"preview": "{\n \"name\": \"CQ Gridfinity Development\",\n \"build\": {\n \"dockerfile\": \"Dockerfile\",\n \"context\": \"..\"\n "
},
{
"path": ".devcontainer/entrypoint.sh",
"chars": 125,
"preview": "#!/bin/bash\nsource /opt/conda/etc/profile.d/conda.sh\nconda activate cqdev\nXvfb :99 -screen 0 1024x768x16 &\nsleep 1\nexec "
},
{
"path": ".github/workflows/checks.yaml",
"chars": 1020,
"preview": "name: Run Tests\n\non:\n pull_request:\n branches:\n - main # Runs on pull requests targeting the main branch\n\njobs"
},
{
"path": ".gitignore",
"chars": 561,
"preview": "# python intermediate files\n*.py[cod]\n\n# intermediate and cached 3D solid files\n/cache/*\n/tests/testfiles/*.step\n/tests/"
},
{
"path": "CHANGELOG.md",
"chars": 1554,
"preview": "## Changelog\n\n- v.0.1.0 - Initial release\n- v.0.1.1 - fixed release\n- v.0.2.0 - Added new \"lite\" style box\n- v.0.2.1 - A"
},
{
"path": "LICENSE",
"chars": 1079,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2023 Michael Gale\n\nPermission is hereby granted, free of charge, to any person obta"
},
{
"path": "Makefile",
"chars": 2565,
"preview": ".PHONY: clean clean-test clean-pyc clean-build test\n.DEFAULT_GOAL := help\n\ndefine PRINT_HELP_PYSCRIPT\nimport re, sys\n\nfo"
},
{
"path": "README.md",
"chars": 32873,
"preview": "<!-- <img src=./images/logo.png width=320> -->\n\n\n# cq-gridfinity\n\n[ 2023 Michael Gale\n# This file is part of the cq-gridfinity python module.\n# P"
},
{
"path": "cqgridfinity/gf_baseplate.py",
"chars": 4339,
"preview": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023 Michael Gale\n# This file is part of the cq-gridfinity python module.\n# P"
},
{
"path": "cqgridfinity/gf_box.py",
"chars": 19614,
"preview": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023 Michael Gale\n# This file is part of the cq-gridfinity python module.\n# P"
},
{
"path": "cqgridfinity/gf_drawer.py",
"chars": 19299,
"preview": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023 Michael Gale\n# This file is part of the cq-gridfinity python module.\n# P"
},
{
"path": "cqgridfinity/gf_helpers.py",
"chars": 2875,
"preview": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023 Michael Gale\n# This file is part of the cq-gridfinity python module.\n# P"
},
{
"path": "cqgridfinity/gf_obj.py",
"chars": 12674,
"preview": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023 Michael Gale\n# This file is part of the cq-gridfinity python module.\n# P"
},
{
"path": "cqgridfinity/gf_ruggedbox.py",
"chars": 47397,
"preview": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023 Michael Gale\n# This file is part of the cq-gridfinity python module.\n# P"
},
{
"path": "cqgridfinity/scripts/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "cqgridfinity/scripts/gridfinitybase.py",
"chars": 4109,
"preview": "#! /usr/bin/env python3\n\"\"\"\ncommand line script to make a Gridfinity baseplate\n\"\"\"\nimport argparse\n\nimport cqgridfinity\n"
},
{
"path": "cqgridfinity/scripts/gridfinitybox.py",
"chars": 6884,
"preview": "#! /usr/bin/env python3\n\"\"\"\ncommand line script to make a Gridfinity box\n\"\"\"\nimport argparse\n\nimport cqgridfinity\nfrom c"
},
{
"path": "cqgridfinity/scripts/ruggedbox.py",
"chars": 12128,
"preview": "#! /usr/bin/env python3\n\"\"\"\ncommand line script to make a rugged Gridfinity box\n\"\"\"\nimport argparse\n\nimport cqgridfinity"
},
{
"path": "cqgridfinity/shims/README.md",
"chars": 3039,
"preview": "# /pub/storage/workspace/gridfinity\n\nCreated by Zach Freedman as a versatile system of modular organization and storage "
},
{
"path": "cqgridfinity/shims/cqgi_gf_baseplate.py",
"chars": 600,
"preview": "import sys\nsys.path.append(\".\") # Relative to `partcad.yaml`\n\nfrom cqgridfinity.gf_baseplate import GridfinityBaseplate\n"
},
{
"path": "cqgridfinity/shims/cqgi_gf_box.py",
"chars": 1130,
"preview": "import sys\nsys.path.append(\".\") # Relative to `partcad.yaml`\n\nfrom cqgridfinity.gf_box import GridfinityBox\n\nlength_u = "
},
{
"path": "cqgridfinity/shims/cqgi_gf_drawerspacer.py",
"chars": 869,
"preview": "import sys\nsys.path.append(\".\") # Relative to `partcad.yaml`\n\nfrom cqgridfinity.gf_drawer import GridfinityDrawerSpacer\n"
},
{
"path": "cqgridfinity/shims/cqgi_gf_ruggedbox.py",
"chars": 1320,
"preview": "import sys\nsys.path.append(\".\") # Relative to `partcad.yaml`\n\nfrom cqgridfinity.gf_ruggedbox import GridfinityRuggedBox\n"
},
{
"path": "examples/demo1.assy",
"chars": 432,
"preview": "# This is a demo of multiple gridfinity parts put together using PartCAD.\n# Use `pc show -a examples/demo1` to view it i"
},
{
"path": "partcad.yaml",
"chars": 5164,
"preview": "partcad: \">=0.7.16\"\n\nname: /pub/storage/workspace/gridfinity\ndesc: Created by Zach Freedman as a versatile system of mod"
},
{
"path": "requirements.in",
"chars": 22,
"preview": "cadquery\ncqkit>=0.5.6\n"
},
{
"path": "requirements.txt",
"chars": 681,
"preview": "#\n# This file is autogenerated by pip-compile with Python 3.12\n# by the following command:\n#\n# pip-compile requiremen"
},
{
"path": "setup.py",
"chars": 1944,
"preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nimport os\nimport os.path\nfrom pathlib import Path\nimport sys\nimport setup"
},
{
"path": "tests/common_test.py",
"chars": 926,
"preview": "import os\n\nEXPORT_STEP_FILE_PATH = \"./tests/testfiles\"\n\nenv = dict(os.environ)\nSKIP_TEST_BOX = \"SKIP_TEST_BOX\" in env\nSK"
},
{
"path": "tests/test_baseplate.py",
"chars": 1227,
"preview": "# Gridfinity tests\nimport pytest\n\n# my modules\nfrom cqgridfinity import *\nfrom cqkit import FlatEdgeSelector\nfrom cqkit."
},
{
"path": "tests/test_box.py",
"chars": 7230,
"preview": "# Gridfinity tests\nimport pytest\n\n# my modules\nfrom cqgridfinity import *\n\nfrom cqkit.cq_helpers import *\nfrom cqkit imp"
},
{
"path": "tests/test_rbox.py",
"chars": 3217,
"preview": "# Gridfinity tests\nimport pytest\n\n# my modules\nfrom cqgridfinity import *\n\nfrom cqkit.cq_helpers import *\nfrom cqkit imp"
},
{
"path": "tests/test_spacer.py",
"chars": 4451,
"preview": "# Gridfinity tests\nimport pytest\n\n# my modules\nfrom cadquery import exporters\nfrom cqgridfinity import *\nfrom cqkit.cq_h"
},
{
"path": "tests/testfiles/.gitkeep",
"chars": 0,
"preview": ""
}
]
About this extraction
This page contains the full source code of the michaelgale/cq-gridfinity GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (203.8 KB), approximately 59.0k tokens, and a symbol index with 159 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.