Full Code of michaelgale/cq-gridfinity for AI

main d317286ec15d cached
37 files
203.8 KB
59.0k tokens
159 symbols
1 requests
Download .txt
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 Logo](./images/logo.png)

# cq-gridfinity

[![](https://img.shields.io/pypi/v/cqgridfinity.svg)](https://pypi.org/project/cqgridfinity/)
![python version](https://img.shields.io/static/v1?label=python&message=3.9%2B&color=blue&style=flat&logo=python)
[![](https://img.shields.io/static/v1?label=dependencies&message=CadQuery%202.0%2B&color=blue&style=flat)](https://github.com/CadQuery/cadquery)
[![](https://img.shields.io/badge/CQ--kit-blue)](https://github.com/michaelgale/cq-kit)
![https://github.com/michaelgale/cq-kit/blob/master/LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)
[![](https://img.shields.io/badge/code%20style-black-black.svg)](http://github.com/psf/black)
[![Run Tests](https://github.com/michaelgale/cq-gridfinity/actions/workflows/checks.yaml/badge.svg)](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
Download .txt
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
Download .txt
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![cq-gridfinity Logo](./images/logo.png)\n\n# cq-gridfinity\n\n[![](https://i"
  },
  {
    "path": "cqgridfinity/__init__.py",
    "chars": 513,
    "preview": "\"\"\"cqgridfinity - A python library to make Gridfinity compatible objects with CadQuery.\"\"\"\n\nimport os\n\n# fmt: off\n__proj"
  },
  {
    "path": "cqgridfinity/constants.py",
    "chars": 4199,
    "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_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.

Copied to clipboard!