[
  {
    "path": ".devcontainer/Dockerfile",
    "content": "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\nRUN groupadd --gid $USER_GID $USERNAME \\\n    && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \\\n    && apt-get update \\\n    && apt-get install -y sudo \\\n    && echo $USERNAME ALL=\\(root\\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \\\n    && chmod 0440 /etc/sudoers.d/$USERNAME\n\n# Install system dependencies\nRUN export DEBIAN_FRONTEND=noninteractive \\\n    && apt-get -y install --no-install-recommends \\\n    git=1:2.* \\\n    libgl1-mesa-dev \\\n    xvfb \\\n    && apt-get clean -y \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Create conda environment and install packages\nRUN mamba create -n cqdev python=3.12 -y \\\n    && mamba init bash \\\n    && mamba install -n cqdev -c conda-forge -y \\\n    cadquery=2.4 \\\n    pytest=8.* \\\n    black=24.* \\\n    flake8=7.* \\\n    isort=5.* \\\n    && mamba clean --all -f -y\n\n# Install pip dependencies\nSHELL [\"/bin/bash\", \"-c\"]\nRUN source /opt/conda/etc/profile.d/conda.sh \\\n    && conda activate cqdev \\\n    && pip install --no-cache-dir \\\n    ezdxf==1.* \\\n    cqkit \\\n    importlib_metadata\n\n# Set up X11 and OpenGL\nRUN mkdir -p /tmp/runtime-root \\\n    && chown ${USERNAME}:${USERNAME} /tmp/runtime-root\nENV XDG_RUNTIME_DIR=/tmp/runtime-root\nENV DISPLAY=:99\nENV LIBGL_ALWAYS_INDIRECT=1\n\n# Configure conda environment\nENV PATH=/opt/conda/envs/cqdev/bin:$PATH\nRUN echo \"conda activate cqdev\" >> /home/${USERNAME}/.bashrc\n\n# Set up virtual framebuffer\nCOPY .devcontainer/entrypoint.sh /usr/local/bin/\nRUN chmod +x /usr/local/bin/entrypoint.sh\n\n# Switch to non-root user\nUSER ${USERNAME}\n\nENTRYPOINT [\"/usr/local/bin/entrypoint.sh\"]\nCMD [\"sleep\", \"infinity\"] "
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n    \"name\": \"CQ Gridfinity Development\",\n    \"build\": {\n        \"dockerfile\": \"Dockerfile\",\n        \"context\": \"..\"\n    },\n    \"workspaceFolder\": \"/workspaces/cq-gridfinity\",\n    \"workspaceMount\": \"source=${localWorkspaceFolder},target=/workspaces/cq-gridfinity,type=bind\",\n    \"customizations\": {\n        \"vscode\": {\n            \"extensions\": [\n                \"ms-python.python\",\n                \"ms-python.vscode-pylance\",\n                \"ms-python.black-formatter\",\n                \"ms-python.flake8\",\n                \"ms-python.isort\"\n            ],\n            \"settings\": {\n                \"python.defaultInterpreterPath\": \"/opt/conda/envs/cqdev/bin/python\",\n                \"python.testing.pytestEnabled\": true,\n                \"python.condaPath\": \"/opt/conda/bin/conda\"\n            }\n        }\n    },\n    \"forwardPorts\": [\n        8080\n    ],\n    \"postCreateCommand\": \"pip install -e .\",\n    \"remoteUser\": \"vscode\"\n}"
  },
  {
    "path": ".devcontainer/entrypoint.sh",
    "content": "#!/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",
    "content": "name: Run Tests\n\non:\n  pull_request:\n    branches:\n      - main  # Runs on pull requests targeting the main branch\n\njobs:\n  tests:\n    name: Test (${{ matrix.python-version }}, ${{ matrix.os }})\n    runs-on: ${{ matrix.os }}\n    defaults:\n      run:\n        shell: bash -l {0}\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [\"ubuntu-latest\"]\n        python-version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\"]\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n      - uses: conda-incubator/setup-miniconda@v2\n        with:\n          python-version: ${{ matrix.python-version }}\n          mamba-version: \"*\"\n          channels: conda-forge,defaults\n          channel-priority: true\n          activate-environment: cq\n      - name: Install cadquery\n        run: mamba install cadquery\n      - name: Install pip dependencies\n        run: pip install cqkit pytest\n      - name: Install current version of cq-gridfinity\n        run: pip install .\n      - name: Run tests\n        run: pytest tests\n"
  },
  {
    "path": ".gitignore",
    "content": "# python intermediate files\n*.py[cod]\n\n# intermediate and cached 3D solid files\n/cache/*\n/tests/testfiles/*.step\n/tests/testfiles/*.iges\n/tests/testfiles/*.stl\n*.graffle\n\n# editors\n/.vscode\n\n# C extensions\n*.so\n\n# OS litter\n.DS_Store\nDesktop.ini\n._*\nThumbs.db\n.Trashes\n\n# Packages\n*.egg\n*.egg-info\ndist\nbuild\neggs\nparts\nbin\nvar\nsdist\ndevelop-eggs\n.installed.cfg\nlib\nlib64\n__pycache__\n\n# Installer logs\npip-log.txt\n\n# Unit test / coverage reports\n.coverage\n.tox\nnosetests.xml\n\n# Translations\n*.mo\n\n# Mr Developer\n.mr.developer.cfg\n.project\n.pydevproject\n\n.venv/\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## 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 - Added new unsupported magnet hole types\n- v.0.2.2 - Added SVG export and integrated STL exporter\n- v.0.2.3 - Updated to python build tools to make distribution\n- v.0.3.0 - Added console generator scripts: gridfinitybox and gridfinitybase\n- v.0.4.0 - Added `GridfinityRuggedBox` class and `ruggedbox` console script. Various other improvements.\n- v.0.4.1 - Fixed docstring in `__init__.py`\n- v.0.4.2 - Improved script automatic renaming\n- v.0.4.3 - Fixed regression bug with using multilevel extrusion functions from cq-kit\n- 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.\n- v.0.4.5 - IMPORTANT FIX: fixes error in v.0.4.4 for extrusion angle\n- v.0.5.0 - Improved rugged box to make viable boxes down to 3U x 3U x 4U\n- v.0.5.1 - Increased the resolution of the gridfinity extruded base profile\n- v.0.5.2 - Adjusted geometry of box/bin floor/lip heights to exactly 7.00 mm intervals\n- v.0.5.3 - Removed a potential namespace collision for computing the height of boxes\n- v.0.5.4 - Optimized the geometry of the baseplate top height\n- v.0.5.5 - Added underside bin clearance and variable wall thickness interior radiusing\n- v.0.5.6 - Added adjustable magnet hole diameter to box. Prevent drawer spacers being rendered which fall below minimum size\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2023 Michael Gale\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: clean clean-test clean-pyc clean-build test\n.DEFAULT_GOAL := help\n\ndefine PRINT_HELP_PYSCRIPT\nimport re, sys\n\nfor line in sys.stdin:\n\tmatch = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)\n\tif match:\n\t\ttarget, help = match.groups()\n\t\tprint(\"%-20s %s\" % (target, help))\nendef\nexport PRINT_HELP_PYSCRIPT\n\nhelp:\n\t@python -c \"$$PRINT_HELP_PYSCRIPT\" < $(MAKEFILE_LIST)\n\nclean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts\n\nclean-build: ## remove build artifacts\n\t@rm -fr build/\n\t@rm -fr dist/\n\t@rm -fr .eggs/\n\t@find . -name '*.egg-info' -exec rm -fr {} +\n\t@find . -name '*.egg' -exec rm -f {} +\t\n\nclean-pyc: ## remove Python file artifacts\n\t@find . -name '*.pyc' -exec rm -f {} +\n\t@find . -name '*.pyo' -exec rm -f {} +\n\t@find . -name '*~' -exec rm -f {} +\n\t@find . -name '__pycache__' -exec rm -fr {} +\n\nclean-test: ## remove test and coverage artifacts\n\t@find . -name '*.step' -exec rm -f {} +\n\t@find . -name '*.stl' -exec rm -f {} +\n\t@find . -name '*.svg' -exec rm -f {} +\n\t@rm -f .coverage\n\t@rm -fr htmlcov/\n\nlint: ## check style with black\n\t@black cqgridfinity/*.py\n\t@black cqgridfinity/scripts/*.py\n\t@black tests/*.py\n\nlint-check: ## check if lint status is consistent between commits\n\t@black --diff --check cqgridfinity/*.py\n\t@black --diff --check cqgridfinity/scripts/*.py\n\t@black --diff --check tests/*.py\n\ntest: ## run tests quickly with the default Python\n\tpy.test -s -v --cov -W ignore::DeprecationWarning:nptyping.typing_\n\n# @export SKIP_TEST_BOX=\"all\" && \\\n# export SKIP_TEST_RBOX=\"all\" && \\\n# export SKIP_TEST_SPACER=\"all\" && \\\n# export SKIP_TEST_BASEPLATE=\"all\" && \\\n# export EXPORT_STEP_FILES=\"all\" && \\\n\ntest-some: ## run selective tests quickly with the default Python\n\t@export SKIP_TEST_BOX=\"all\" && \\\n\texport SKIP_TEST_RBOX=\"all\" && \\\n\texport SKIP_TEST_BASEPLATE=\"all\" && \\\n\texport EXPORT_STEP_FILES=\"all\" && \\\n\tpy.test -s -v --cov -W ignore::DeprecationWarning:nptyping.typing_\n\ntest-files: ## run tests and export test files artifacts\n\t@export EXPORT_STEP_FILES=\"all\" && \\\n\tpy.test -s -v -W ignore::DeprecationWarning:nptyping.typing_\n\t\ncoverage: ## check code coverage quickly with the default Python\n\tcoverage run --source cqgridfinity -m pytest\n\tcoverage report -m\n\tcoverage html\n\topen htmlcov/index.html\n\nrelease: clean dist ## package and upload a release\n\ttwine check dist/*\n\ttwine upload dist/*\n\ndist: clean ## builds source and wheel package\n\t@python -m build\n\t@twine check dist/*\n\t@ls -l dist\n\ninstall: clean ## install the package to the active Python's site-packages\n\t@pip install .\n"
  },
  {
    "path": "README.md",
    "content": "<!-- <img src=./images/logo.png width=320> -->\n![cq-gridfinity Logo](./images/logo.png)\n\n# cq-gridfinity\n\n[![](https://img.shields.io/pypi/v/cqgridfinity.svg)](https://pypi.org/project/cqgridfinity/)\n![python version](https://img.shields.io/static/v1?label=python&message=3.9%2B&color=blue&style=flat&logo=python)\n[![](https://img.shields.io/static/v1?label=dependencies&message=CadQuery%202.0%2B&color=blue&style=flat)](https://github.com/CadQuery/cadquery)\n[![](https://img.shields.io/badge/CQ--kit-blue)](https://github.com/michaelgale/cq-kit)\n![https://github.com/michaelgale/cq-kit/blob/master/LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)\n[![](https://img.shields.io/badge/code%20style-black-black.svg)](http://github.com/psf/black)\n[![Run Tests](https://github.com/michaelgale/cq-gridfinity/actions/workflows/checks.yaml/badge.svg)](https://github.com/michaelgale/cq-gridfinity/actions/workflows/checks.yaml)\n\n\nThis 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.\n\nExamples of how I am starting to use Gridfinity to organize my tools are shown below using components built with this python library:\n\n<img src=./images/examples.png width=800>\n\n# Quick Links\n\n- [Installation / Usage](#installation)\n- [Shell Command Scripts](#shell-command-scripts)\n  - [gridfinitybox](#gridfinitybox)\n  - [gridfinitybase](#gridfinitybase)\n  - [ruggedbox](#ruggedbox)\n- [Classes](#classes)\n  - [GridfinityBaseplate](#gridfinitybaseplate)\n  - [GridfinityBox](#gridfinitybox-1)\n  - [GridfinityDrawerSpacer](#gridfinitydrawerspacer)\n  - [GridfinityRuggedBox](#gridfinityruggedbox)\n  - [GridfinityObject](#gridfinityobject)\n- [References](#references)\n\n## Installation\n\n**cq-gridfinity** has the following installation dependencies:\n- [CadQuery](https://github.com/CadQuery/cadquery)\n- [cq-kit](https://github.com/michaelgale/cq-kit)\n\nAssuming these dependencie are installed, you can install **cq-gridfinity** using a [PyPI package](https://pypi.org/project/cqgridfinity/) as follows:\n\n```bash\n$ pip install cqgridfinity\n```\n\nAlternatively, the **cq-gridfinity** package can be installed directly from the source code:\n\n```bash\n$ git clone https://github.com/michaelgale/cq-gridfinity.git\n$ cd cq-gridfinity\n$ pip install .\n```\n\n## Development with VS Code Dev Container\n\nThis project includes a development container configuration that provides a consistent development environment with all required dependencies pre-installed.\n\n### Prerequisites\n\n1. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/)\n2. Install [Visual Studio Code](https://code.visualstudio.com/)\n3. Install the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) in VS Code\n\n### Getting Started\n\n1. Clone this repository:\n   ```bash\n   git clone https://github.com/michaelgale/cq-gridfinity.git\n   cd cq-gridfinity\n   ```\n\n2. Open the project in VS Code:\n   ```bash\n   code .\n   ```\n\n3. When VS Code detects the dev container configuration, it will prompt you to \"Reopen in Container\". Click this prompt, or:\n   - Press `F1` or `Ctrl+Shift+P` (Cmd+Shift+P on Mac)\n   - Type \"Dev Containers: Reopen in Container\" and select it\n\n4. Wait for the container to build (this may take a few minutes the first time)\n\n### What's Included\n\nThe development container comes with:\n- Python 3.12\n- CadQuery 2.4\n- All required dependencies (pytest, black, flake8, etc.)\n- A pre-configured environment for CAD development\n\n### Troubleshooting\n\nIf you encounter issues:\n1. Ensure Docker is running\n2. Try rebuilding the container:\n   - Press `F1` or `Ctrl+Shift+P`\n   - Select \"Dev Containers: Rebuild Container\"\n\n## Basic Usage\n\nAfter installation, the package can imported:\n\n```shell\n$ python\n>>> import cqgridfinity\n>>> cqgridfinity.__version__\n```\n\nAn example of the package can be seen below:\n\n```python\nfrom cqgridfinity import *\n\n# make a simple box\nbox = GridfinityBox(3, 2, 5, holes=True, no_lip=False, scoops=True, labels=True)\nbox.save_stl_file()\n# Output a STL file of box:\n#   gf_box_3x2x5_holes_scoops_labels.stl\n```\n\n# Shell Command Scripts\n\n- [gridfinitybox](#gridfinitybox)\n- [gridfinitybase](#gridfinitybase)\n- [ruggedbox](#ruggedbox)\n\nThis 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.\n\nHowever, 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.\n\n## `gridfinitybox`\n\n<img src=./images/box_script.png width=600>\n\nMake a customized/parameterized Gridfinity compatible box with many optional features.\n\n```\nusage: gridfinitybox [-h] [-m] [-u] [-n] [-s] [-l] [-e] [-d] [-r RATIO] [-ld LENGTHDIV] [-wd WIDTHDIV] [-wt WALL]\n                     [-f FORMAT] [-o OUTPUT]\n                     length width height\n\nMake a customized/parameterized Gridfinity compatible box with many optional features.\n\npositional arguments:\n  length                Box length in U (1U = 42 mm)\n  width                 Box width in U (1U = 42 mm)\n  height                Box height in U (1U = 7 mm)\n\noptions:\n  -h, --help            show this help message and exit\n  -m, --magnetholes     Add bottom magnet/mounting holes\n  -u, --unsupported     Add bottom magnet holes with 3D printer friendly strips without support\n  -n, --nolip           Do not add mating lip to the top perimeter\n  -s, --scoops          Add finger scoops against each length-wise back wall\n  -l, --labels          Add label strips against each length-wise front wall\n  -e, --ecolite         Make economy / lite style box with no elevated floor\n  -d, --solid           Make solid (filled) box for customized storage\n  -r RATIO, --ratio RATIO\n                        Solid box fill ratio 0.0 = minimum, 1.0 = full height\n  -ld LENGTHDIV, --lengthdiv LENGTHDIV\n                        Split box length-wise with specified number of divider walls\n  -wd WIDTHDIV, --widthdiv WIDTHDIV\n                        Split box width-wise with specified number of divider walls\n  -wt WALL, --wall WALL\n                        Wall thickness (default=1 mm)\n  -f FORMAT, --format FORMAT\n                        Output file format (STEP, STL, SVG) default=STEP\n  -o OUTPUT, --output OUTPUT\n                        Output filename (inferred output file format with extension)\n\n```\n\nExamples:\n\n```shell\n# 2x3x5 box with magnet holes saved to STL file with default filename:\n$ gridfinitybox 2 3 5 -m -f stl\n# gf_box_2x3x5_holes.stl\n\n# 1x3x4 box with scoops, label strip, 3 internal partitions and specified name:\n$ gridfinitybox 1 3 4 -s -l -ld 3 -o MyBox.step\n# MyBox.step\n\n# Solid 3x3x3 box with 50% fill, unsupported magnet holes and no top lip:\n$ gridfinitybox 3 3 3 -d -r 0.5 -u -n\n# gf_box_3x3x3_basic_holes_solid.step\n\n# Lite style box 3x2x3 with label strip, partitions, output to default SVG file:\n$ gridfinitybox 3 2 3 -e -l -ld 2 -f svg\n# gf_box_lite_3x2x3_div2_labels.svg\n```\n\n## `gridfinitybase`\n\n<img src=./images/base_script.png width=600>\n\nMake a customized/parameterized Gridfinity compatible simple baseplate.\n\n```\nusage: gridfinitybase [-h] [-f FORMAT] [-s] [-d DEPTH] [-hd HOLEDIAM] [-hc CSKDIAM] [-ca CSKANGLE] [-o OUTPUT]\n                      length width\n\nMake a customized/parameterized Gridfinity compatible simple baseplate.\n\npositional arguments:\n  length                Box length in U (1U = 42 mm)\n  width                 Box width in U (1U = 42 mm)\n\noptions:\n  -h, --help            show this help message and exit\n  -f FORMAT, --format FORMAT\n                        Output file format (STEP, STL, SVG) default=STEP\n  -s, --screws          Add screw mounting tabs to the corners (adds +5 mm to depth)\n  -d DEPTH, --depth DEPTH\n                        Extrude extended depth under baseplate by this amount\n  -hd HOLEDIAM, --holediam HOLEDIAM\n                        Corner mounting screw hole diameter (default=5)\n  -hc CSKDIAM, --cskdiam CSKDIAM\n                        Corner mounting screw countersink diameter (default=10)\n  -ca CSKANGLE, --cskangle CSKANGLE\n                        Corner mounting screw countersink angle (deg) (default=82)\n  -o OUTPUT, --output OUTPUT\n                        Output filename (inferred output file format with extension)\n```\n\nExamples:\n\n```shell\n# 7 x 4 baseplate with screw corners to default STL file:\n$ gridfinitybase 7 4 -s -f stl\n# gf_baseplate_7x4x5.0_screwtabs.stl\n```\n\n## `ruggedbox`\n\n<img src=./images/rugged_box.png width=600>\n\nMake 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.\n\n```\nusage: ruggedbox [-h] [+l] [-l] [+p] [-p] [+w] [-w] [-wt WINDOWTHICKNESS] [+a] [-a] [+c] [-c] [+s] [-s] [+v] [-v]\n                 [+e] [-e] [+b] [-b] [-r] [+r] [-f FORMAT] [-o OUTPUT] [-gb] [-gl] [-ga] [-gh] [-ge] [-gn] [-gt]\n                 [-gw]\n                 length width height\n\nMake a customized/parameterized Gridfinity compatible rugged box enclosure.\nThe minimum box size is 3U x 3U x 4U.\n\npositional arguments:\n  length                Box length in U (1U = 42 mm)\n  width                 Box width in U (1U = 42 mm)\n  height                Box height in U (1U = 7 mm)\n\noptions:\n  -h, --help            show this help message and exit\n  +l, --label           Add label window across the front wall\n  -l, --nolabel         Remove label window across the front wall\n  +p, --lidbaseplate    Add baseplate to top of the lid\n  -p, --nolidbaseplate  Smooth/plain lid\n  +w, --lidwindow       Add window slot to the lid\n  -w, --nolidwindow     Do not add window slot to the lid\n  -wt WINDOWTHICKNESS, --windowthickness WINDOWTHICKNESS\n                        Thickness of lid windows (mm)\n  +a, --handle          Add front handle\n  -a, --nohandle        No front handle\n  +c, --clasps          Add clasps to the left and right side walls\n  -c, --noclasps        No clasps on the left and right side walls\n  +s, --stackable       Add stackable mating features to top and bottom\n  -s, --notstackable    Non-stackable box\n  +v, --veegroove       Add v-cut grooves to side walls\n  -v, --noveegroove     No v-cut grooves (plain) side walls\n  +e, --sidehandle      Add handles to side walls\n  -e, --nosidehandle    No handles on side walls\n  +b, --backfeet        Add standing feet to back wall\n  -b, --nobackfeet      No standing feet added to back wall\n  -r, --normalstyle     Make normal style box\n  +r, --ribstyle        Make rib style box with exposed vertical ribs\n  -f FORMAT, --format FORMAT\n                        Output file format (STEP, STL, SVG) default=STEP\n  -o OUTPUT, --output OUTPUT\n                        Output filename (inferred output file format with extension)\n  -gb, --box            Generate box\n  -gl, --lid            Generate lid\n  -ga, --acc            Generate accessory components\n  -gh, --hinge          Generate hinge element\n  -ge, --genlabel       Generate label panel insert\n  -gn, --genhandle      Generate front handle\n  -gt, --genlatch       Generate latch component\n  -gw, --genwindow      Generate lid window component\n\nexample usage:\n\n  5 x 4 x 6 rugged box shell and lid saved to STL files:\n  $ ruggedbox 5 4 6 --box --lid -f stl\n```\nExamples:\n\n5 x 4 x 6 rugged box component saved to STL file:\n\n```shell\n$ ruggedbox 5 4 6 -gb -f stl\n ____                             _ ____\n|  _ \\ _   _  __ _  __ _  ___  __| | __ )  _____  __\n| |_) | | | |/ _` |/ _` |/ _ \\/ _` |  _ \\ / _ \\ \\/ /\n|  _ <| |_| | (_| | (_| |  __/ (_| | |_) | (_) >  <\n|_| \\_\\\\__,_|\\__, |\\__, |\\___|\\__,_|____/ \\___/_/\\_\\\n             |___/ |___/\n\nVersion: 0.5.7\nGridfinity rugged box: 5U x 4U x 6U\n  Exterior dim: 230.0 mm x 188.0 mm x 55.0 mm\n  Interior dim: 210.0 mm x 168.0 mm x 45.8 mm\n  Internal volume: 1.616 L\n  Wall Vgrooves      : Y\n  Front Handle       : Y\n  Stackable          : Y\n  Side Clasps        : Y\n  Lid Baseplate      : Y\n  Inside Baseplate   : Y\n  Side Handles       : Y\n  Front Label        : Y\n  Back Feet          : Y\n  Rib Style          : N\n  Lid Window         : N\nRendering box...\nComponent generated and saved as gf_ruggedbox_5x4x6_body_fr-hl_sd-hc_stack_lidbp.stl in STL format\n$\n```\n\n```shell\n# same 5 x 4 x 6 rugged box with the lid saved to STL file:\n$ ruggedbox 5 4 6 --lid -f stl\n# gf_ruggedbox_5x4x6_lid_fr-hl_sd-hc_stack_lidbp.stl\n\n# 5 x 5 x 9 rugged box, smooth lid, non-stackable, and no handle; full assembly saved to STEP file\n$ ruggedbox 5 5 9 --nohandle --nolidbaseplate --notstackable\n# gf_ruggedbox_5x5x9_fr-l_sd-hc.step\n\n# Render the box, lid, and hinge for a 5x4x6 rugged box all at once:\n$ ruggedbox 5 4 6 --box --lid --hinge\n# gf_ruggedbox_5x4x6_fr-hl_sd-hc_stack_lidbp.step\n# gf_ruggedbox_5x4x6_lid_fr-hl_sd-hc_stack_lidbp.step\n# gf_ruggedbox_5x4x6_hinge_fr-hl_sd-hc_stack_lidbp.step\n\n# Then render the latches and handle components for the same box:\n$ ruggedbox 5 4 6 --acc\n# gf_ruggedbox_5x4x6_acc_fr-hl_sd-hc_stack_lidbp.step\n\n# Or render individual components as STL files with your preferred name:\n$ ruggedbox 5 4 6 --genhandle --genlatch -o orange.stl\n# orange_handle.stl\n# orange_latch.stl\n```\n\n# Classes\n\n- [GridfinityBaseplate](#gridfinitybaseplate)\n- [GridfinityBox](#gridfinitybox-1)\n- [GridfinityDrawerSpacer](#gridfinitydrawerspacer)\n- [GridfinityRuggedBox](#gridfinityruggedbox)\n- [GridfinityObject](#gridfinityobject)\n  \n\n## `GridfinityBaseplate`\n\nGridfinity 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:\n\n```python\n# Create 4 x 3 baseplate\nbaseplate = GridfinityBaseplate(4, 3)\nbaseplate.save_step_file()\n# gf_baseplate_4x3.step\n```\n<img src=./images/baseplate4x3.png width=512>\n\nBaseplates 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.\n\n<img src=./images/baseplate6x3.png width=512>\n\n### Optional Keyword Arguments\n\n```python\next_depth = 0            # extended depth to extrude below baseplate\nstraight_bottom = False  # add/remove 0.8 mm chamfer on bottom of baseplate\ncorner_screws = False    # add corner mounting screw tabs\ncorner_tab_size = 21     # size of screw mounting tab (mm)\ncsk_hole = 5.0           # hole diameter of countersink mounting screw (mm)\ncsk_diam = 10.0          # countersink diameter (mm)\ncsk_angle = 82           # countersink angle (deg)\n```\n\n## `GridfinityBox`\n\nGridfinity 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.\n\n### Simple Box\n\n```python\n# Create a simple 3 x 2 box, 5U high\nbox = GridfinityBox(3, 2, 5)\nbox.save_step_file()\n# Output a STEP file of box named:\n#   gf_box_3x2x5.step\n``` \n<img src=./images/basic_box.png width=512>\n\n### Lite Style Box\n\n\"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.\n\n```python\n# Create a \"lite\" style 3 x 2 box, 5U high\nbox = GridfinityBox(3, 2, 5, lite_style=True)\nbox.save_step_file()\n# Output a STEP file of box named:\n#   gf_box_lite_3x2x5.step\n``` \n<img src=./images/box_lite.png width=512>\n\n### Magnet Holes\n\n```python\n# add magnet holes to the box\nbox = GridfinityBox(3, 2, 5, holes=True)\nbox.save_step_file()\n# gf_box_3x2x5_holes.step\n```\n<img src=./images/box_holes.png width=512>\n\nThe `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.\n\n```python\n# add magnet holes to the box\nbox = GridfinityBox(1, 1, 5, holes=True, unsupported_holes=True)\nbox.save_step_file()\n# gf_box_1x1x5_holes.step\n```\n<img src=./images/box_holetypes.png width=512>\n\n### Simple Box with No Top Lip\n\n```python\n# remove top mounting lip\nbox = GridfinityBox(3, 2, 5, no_lip=True)\nbox.save_step_file()\n# gf_box_3x2x5_basic.step\n```\n<img src=./images/box_nolip.png width=512>\n\n### Scoops and Labels\n\n```python\n# add finger scoops and label top flange\nbox = GridfinityBox(3, 2, 5, scoops=True, labels=True)\nbox.save_step_file()\n# gf_box_3x2x5_scoops_labels.step\n```\n<img src=./images/box_options.png width=512>\n\n### Dividing Walls\n\n```python\n# add dividing walls\nbox = GridfinityBox(3, 2, 5, length_div=2, width_div=1, scoops=True, labels=True)\nbox.save_step_file()\n# gf_box_3x2x5_div2x1_scoops_labels.step\n```\n<img src=./images/box_div.png width=512>\n\n### Solid Box\n\n```python\n# make a partially solid box\nbox = GridfinityBox(3, 2, 5, solid=True, solid_ratio=0.7)\nbox.save_step_file()\n# gf_box_3x2x5_solid.step\n```\n<img src=./images/box_solid.png width=512>\n\n### Optional Keyword Arguments\n\n```python\nlength_div=0            # add dividing walls along length\nwidth_div=0             # add dividing walls along width\nholes=False             # add magnet holes to bottom\nunsupported_holes=False # 3D-printer friendly hole style requiring no supports\nno_lip=False            # remove top mating lip feature\nscoops=False            # add finger scoops\nscoop_rad=11            # radius of optional interior scoops\nlabels=False            # add a label flange to the top\nlabel_width=12          # width of the label strip\nlabel_height=10         # thickness height of label overhang\nlabel_lip_height=0.8    # thickness of label vertical lip\nlite_style=False        # make a \"lite\" version of box without elevated floor\nsolid=False             # make a solid box\nsolid_ratio=1.0         # ratio of solid height range 0.0 to 1.0 (max height)\nwall_th=1.0             # wall thickness (0.5-2.5 mm)\nfillet_interior=True    # enable/disable internal fillet edges\n```\n\n## `GridfinityDrawerSpacer`\n\nThe `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.\n\nIf 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.\n\n```python\n# make drawer spacers for Craftsman tool chest drawer 23\" wide x 19\" deep\nspacer = GridfinityDrawerSpacer(582, 481, verbose=True)\n# Best fit for 582.00 x 481.00 mm is 13U x 11U\n# with 18.00 mm margin each side and 9.50 mm margin front and back\n# Corner spacers     : 4U wide x 3U deep\n# Front/back spacers : 5U wide x 9.25 mm +0.25 mm tolerance\n# Left/right spacers : 5U deep x 17.75 mm +0.25 mm tolerance\n```\n<img src=./images/drawer_photo.png width=600>\n\n\nA 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.\n\n<img src=./images/full_set.png width=600>\n\n\nNormally, 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.\n\n<img src=./images/half_set.png width=600>\n\n### Optional Keyword Arguments\n\n```python\nthickness=GR_BASE_HEIGHT # thickness of spacers, default=5 mm\nchamf_rad=1.0            # chamfer radius of spacer top/bottom edges\nshow_arrows=True         # show orientation arrows indicating drawer in/out direction\nalign_features=True      # add \"jigsaw\" interlocking feautures\nalign_tol=0.15           # tolerance of the interlocking joint\nalign_min=8              # minimum spacer width for adding interlocking feature\nmin_margin=4             # minimum size to make a spacer, nothing is made for smaller gaps\ntolerance=GR_TOL         # overall tolerance for spacer components, default=0.5 mm\n```\n### Example with IKEA ALEX narrow drawer\n\nAn example use case to make a set of spacer components for a typical IKEA narrow ALEX drawer is as follows:\n\n```python\nspacers = GridfinityDrawerSpacer(INCHES(11.5), INCHES(20.5), verbose=True)\nspacers.render_full_set(include_baseplate=True)\nspacers.save_step_file(\"ikea_alex_full_set.step\")\n# make a half set for 3D printing\nspacers.render_half_set()\nspacers.save_stl_file(\"ikea_alex_half_set.stl\")\n```\n\n<img src=./images/alexdrawer.png width=600>\n\n## `GridfinityRuggedBox`\n\n<img src=./images/rugged_box.png width=600>\n\nThe `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).\n\nThe **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:\n\n<img src=./images/min_rugged_box.png width=600>\n\nThe desired box size and features are specified with keyword arguments/attributes such as the ones illustrated below:\n\n<img src=./images/rugged_box_features.png width=600>\n\nA 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.\n\n<img src=./images/ribstylebox.png width=600>\n\nLastly, 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.\n\n<img src=./images/lid_window.png width=600>\n\nAfter the lid has been printed the process to install the lid window is as follows:\n1. 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.\n2. Slide the window into the lid starting from the back and along the tapered window groove slot around the inside perimeter of the lid.\n3. The window should be inserted just past the retention slots for the hinges.\n4. 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.\n5. Install the lid hinges.  The hinges must be installed last since they act as a physical retainer along the back edge of the window.\n  \nThe 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.\n\nThe 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:\n\n`render()` - renders just the main box body shell:\n\n<img src=./images/rugged_box_shell.png width=600>\n\n`render_lid()` - renders the lid:\n\n<img src=./images/rugged_box_lid.png width=600>\n\n`render_accessories()` - renders the accessory component elements as a group in the quantities required for the desired box:\n\n<img src=./images/rugged_box_acc.png width=600>\n\nLastly, each individual component has an individual render method.\n\n- `render_hinge()`\n- `render_latch()`\n- `render_label()`\n- `render_handle()`\n  \n### Optional Keyword Arguments\n\n```python\nlid_height = 10            # lid height (should be multiple of 10 mm for stacking)\nwall_vgrooves = True       # enable horizontal v-grooves to body shell\nfront_handle = True        # enable front handle\nstackable = True           # add mating stackable features\nside_clasps = True         # add extra side latching clasps\nlid_baseplate = True       # enable top/lid baseplate\ninside_baseplate = True    # enable interior baseplate\nside_handles = True        # enable side handles to box\nfront_label = True         # enable front label panel\nlabel_length = None        # length of front label panel, None=auto size\nlabel_height = None        # height of front label panel, None=auto size\nlabel_th = GR_LABEL_TH     # thickness of label panel, default=0.5 mm\nback_feet = True           # add rear back feet matching hinges to allow the stand box vertically\nhinge_width = GR_HINGE_SZ  # Size of hinge, default=32 mm\nhinge_bolted = False       # printed or bolted hinge construction\nbox_color = cq.Color(0.25, 0.25, 0.25)    # colors for the assembly STEP file\nlid_color = cq.Color(0.25, 0.5, 0.75)\nhandle_color = cq.Color(0.75, 0.5, 0.25)\nlatch_color = cq.Color(0.75, 0.5, 0.25)\nhinge_color = cq.Color(0.75, 0.5, 0.25)\nlabel_color = cq.Color(0.7, 0.7, 0.7)\n```\n\n## `GridfinityObject`\n\nThe `GridfinityObject` is the base class for `GridfinityBox`, `GridfinityBaseplate`, etc. It has several useful methods and attributes including:\n\n### File export and naming\n\n`obj.filename(self, prefix=None, path=None)` returns a filename string with descriptive attributes such as the object size and enabled features.\n\n```python\nbox = GridfinityBox(3, 2, 5, holes=True)\nbox.filename()\n# gf_box_3x2x5_holes\nbox.filename(prefix=\"MyBox\")\n# MyBox_3x2x5_holes\nbox.filename(path=\"./outputfiles\")\n# ./outputfiles/gf_box_3x2x5_holes\nbox2 = GridfinityBox(4, 3, 3, holes=True, length_div=2, width_div=1)\nbox2.filename()\n# gf_box_4x3x3_holes_div2x1\n```\n\n```python\n# Export object to STEP, STL, or SVG file\nobj.save_step_file(filename=None, path=None, prefix=None)\nobj.save_stl_file(filename=None, path=None, prefix=None)\nobj.save_svg_file(filename=None, path=None, prefix=None)\n```\n\nThe 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:\n\n```python\nb1 = GridfinityRuggedBox(5, 4, 6)\nb1.render_accessories()\nb1.save_step_file()\n# saved as \"gf_ruggedbox_5x4x6_acc_fr-hl_sd-hc_stack_lidbp.step\"\nb1.render_handle()\nb1.save_stl_file()\n# saved as \"gf_ruggedbox_5x4x6_handle_fr-hl_sd-hc_stack_lidbp.stl\"\nb1.render_hinge()\nb1.save_svg_file(path=\"./mystuff\")\n# saved as \"./mystuff/gf_ruggedbox_5x4x6_hinge_fr-hl_sd-hc_stack_lidbp.svg\"\nb1.render_assembly()\nb1.save_step_file()\n# saved as \"gf_ruggedbox_5x4x6_assembly_fr-hl_sd-hc_stack_lidbp.step\"\n```\n\n### Useful properties\n\n```obj.cq_obj``` returns a rendered CadQuery Workplane object  \n```obj.length``` returns length in mm  \n```obj.width``` returns width in mm  \n```obj.height``` returns height in mm  \n```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.\n\n# To-do\n\n- add more example scripts\n- improve documentation\n\n# Releases\n\n- v.0.1.0 - First release on PyPI\n- v.0.1.1 - Fixed release\n- v.0.2.0 - Added new \"lite\" style box\n- v.0.2.1 - Added new unsupported magnet hole types\n- v.0.2.2 - Added SVG export and integrated STL exporter\n- v.0.2.3 - Updated to python build tools to make distribution\n- v.0.3.0 - Added console generator scripts: `gridfinitybox` and `gridfinitybase`\n- v.0.4.0 - Added `GridfinityRuggedBox` class and `ruggedbox` console script. Various other improvements.\n- v.0.4.1 - Fixed docstring in `__init__.py`\n- v.0.4.2 - Improved script automatic renaming\n- v.0.4.3 - Fixed regression bug with using multilevel extrusion functions from cq-kit\n- 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.\n- v.0.4.5 - IMPORTANT FIX: fixes error in v.0.4.4 for extrusion angle\n- v.0.5.0 - Improved rugged box to make viable boxes down to 3U x 3U x 4U\n- v.0.5.1 - Increased the resolution of the gridfinity extruded base profile\n- v.0.5.2 - Adjusted geometry of box/bin floor/lip heights to exactly 7.00 mm intervals\n- v.0.5.3 - Removed a potential namespace collision for computing the height of boxes\n- v.0.5.4 - Optimized the geometry of the baseplate top height\n- v.0.5.5 - Added underside bin clearance and variable wall thickness interior radiusing\n- v.0.5.6 - Added adjustable magnet hole diameter to box. Prevent drawer spacers being rendered which fall below minimum size\n- 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.\n\n# References\n\n- [Zach Freedman's YouTube Channel](https://www.youtube.com/c/ZackFreedman)\n- [The video that started it all!](https://youtu.be/ra_9zU-mnl8?si=EOT1LFV65VZfiepi)\n- [Gridfinity Documentation repo](https://github.com/Stu142/Gridfinity-Documentation)\n- [Gridfinity Unofficial wiki](https://gridfinity.xyz)\n- Catalogs\n  - [gridfinity-catalog](https://github.com/jeffbarr/gridfinity-catalog)\n  - [Master Collection on Printables](https://www.printables.com/model/242711-gridfinity-master-collection)\n- Software/Tools\n  - [Online Gridfinity Creator](https://gridfinity.bouwens.co)\n  - [Gridfinity rebuilt OpenSCAD library](https://github.com/kennetek/gridfinity-rebuilt-openscad)\n  - [Gridfinity Fusion360 generator plugin](https://github.com/Le0Michine/FusionGridfinityGenerator)\n  - [FreeCAD Gridfinity Parametric Files (on Printables)](https://www.printables.com/@Stu142_524934/collections/969910)\n  - [Gridfinity eco (low-cost Gridfinity resources)](https://github.com/jrymk/gridfinity-eco)\n  - [Another CadQuery based Gridfinity script](https://github.com/kmeisthax/gridfinity-cadquery)\n- Videos\n  - [Zach Freedman's follow-up Jul 2022](https://youtu.be/Bd4NnHvTRAY?si=rvgb9geXnq83mhOv)\n  - [Zach Freedman's follow-up Dec 2022](https://youtu.be/7FCwMq-rVsY?si=tdqAe8MthGjfWEbR)\n  - [The Next Layer tips video](https://youtu.be/KtbKwAuwv9s?si=1hYPjOvqf8tb5NO9)\n\n## Authors\n\n**cq-gridfinity** was written by [Michael Gale](https://github.com/michaelgale)\n\n"
  },
  {
    "path": "cqgridfinity/__init__.py",
    "content": "\"\"\"cqgridfinity - A python library to make Gridfinity compatible objects with CadQuery.\"\"\"\n\nimport os\n\n# fmt: off\n__project__ = 'cqgridfinity'\n__version__ = '0.5.7'\n# fmt: on\n\nVERSION = __project__ + \"-\" + __version__\n\nscript_dir = os.path.dirname(__file__)\n\nfrom .constants import *\nfrom .gf_obj import GridfinityObject\nfrom .gf_baseplate import GridfinityBaseplate\nfrom .gf_box import GridfinityBox, GridfinitySolidBox\nfrom .gf_drawer import GridfinityDrawerSpacer\nfrom .gf_ruggedbox import GridfinityRuggedBox\n"
  },
  {
    "path": "cqgridfinity/constants.py",
    "content": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023  Michael Gale\n# This file is part of the cq-gridfinity python module.\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation\n# files (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\n# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\n# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n#\n# Globally useful constants representing Gridfinity geometry\n\nfrom math import sqrt\n\nSQRT2 = sqrt(2)\nEPS = 1e-5\nM2_DIAM = 1.8\nM2_CLR_DIAM = 2.5\nM3_DIAM = 3\nM3_CLR_DIAM = 3.5\nM3_CB_DIAM = 5.5\nM3_CB_DEPTH = 3.5\n\nGRU = 42\nGRU2 = GRU / 2\nGRHU = 7\n\nGRU_CUT = 42.2  # base extrusion width\nGR_WALL = 1.0  # nominal exterior wall thickness\nGR_DIV_WALL = 1.2  # width of dividing walls\nGR_TOL = 0.5  # nominal tolerance\n\nGR_RAD = 4  # nominal exterior filleting radius\nGR_BASE_CLR = 0.25  # clearance above the nominal base height\nGR_BASE_HEIGHT = 4.75  # nominal base height\n\n# baseplate extrusion profile\nGR_BASE_CHAMF_H = 0.98994949 / SQRT2\nGR_STR_H = 1.8\nGR_BASE_TOP_CHAMF = GR_BASE_HEIGHT - GR_BASE_CHAMF_H - GR_STR_H\nGR_BASE_PROFILE = (\n    (GR_BASE_TOP_CHAMF * SQRT2, 45),\n    GR_STR_H,\n    (GR_BASE_CHAMF_H * SQRT2, 45),\n)\nGR_STR_BASE_PROFILE = (\n    (GR_BASE_TOP_CHAMF * SQRT2, 45),\n    GR_STR_H + GR_BASE_CHAMF_H,\n)\n\nGR_BOT_H = 7  # bin nominal floor height\nGR_FILLET = 1.1  # inside filleting radius\nGR_FLOOR = GR_BOT_H - GR_BASE_HEIGHT  # floor offset\n\n# box/bin extrusion profile\nGR_BOX_CHAMF_H = 1.1313708 / SQRT2\nGR_BOX_TOP_CHAMF = GR_BASE_HEIGHT - GR_BOX_CHAMF_H - GR_STR_H + GR_BASE_CLR\nGR_BOX_PROFILE = (\n    (GR_BOX_TOP_CHAMF * SQRT2, 45),\n    GR_STR_H,\n    (GR_BOX_CHAMF_H * SQRT2, 45),\n)\n\n# bin mating lip extrusion profile\nGR_UNDER_H = 1.6\nGR_TOPSIDE_H = 1.2\nGR_LIP_PROFILE = (\n    (GR_UNDER_H * SQRT2, 45),\n    GR_TOPSIDE_H,\n    (0.7 * SQRT2, -45),\n    1.8,\n    (1.3 * SQRT2, -45),\n)\nGR_LIP_H = 0\nfor h in GR_LIP_PROFILE:\n    if isinstance(h, tuple):\n        GR_LIP_H += h[0] / SQRT2\n    else:\n        GR_LIP_H += h\nGR_NO_PROFILE = (GR_LIP_H,)\n\n# bottom hole nominal dimensions\nGR_HOLE_D = 6.5\nGR_HOLE_H = 2.4\nGR_BOLT_D = 3.0\nGR_BOLT_H = 3.6 + GR_HOLE_H\nGR_HOLE_DIST = 26 / 2\nGR_HOLE_SLICE = 0.25\n\n# Rugged Box constant parameters\nGR_RBOX_WALL = 2.5\nGR_RBOX_FLOOR = 1.2\nGR_RBOX_CWALL = 10.0\nGR_RBOX_CORNER_W = 56\nGR_RBOX_BACK_L = 66\nGR_RBOX_FRONT_L = 56\nGR_RBOX_RAD = 3.745\nGR_RBOX_CRAD = 14\n\nGR_RBOX_CHAN_W = 20\nGR_RBOX_CHAN_D = GR_RBOX_CWALL - GR_RBOX_WALL\nGR_RBOX_VCUT_D = 1\n\nGR_CLASP_SLIDE_D = 39\nGR_CLASP_SLIDE_W = 4\n\nGR_RIB_W = 2\nGR_RIB_L = 5\nGR_RIB_GAP = 1\nGR_RIB_H = 3.5\nGR_RIB_SEP = 4\nGR_RIB_CTR = 10\n\nGR_REG_L = 5\nGR_REG_W = 2.5\nGR_REG_H = 2.5\nGR_REG_R0 = 10.75\nGR_REG_R1 = 8.25\nGR_BREG_R0 = GR_REG_R0 + 0.25\nGR_BREG_R1 = GR_REG_R1 - 0.25\n\nGR_HANDLE_L1 = 12\nGR_HANDLE_L2 = 28\nGR_HANDLE_H = 7.5\nGR_HANDLE_W = 5\nGR_HANDLE_SEP = 12.5\nGR_HANDLE_OFS = 61.5\nGR_HANDLE_SZ = 30\nGR_HANDLE_TH = 7\nGR_HANDLE_RAD = 11\n\nGR_LID_HANDLE_W = 70\nGR_SIDE_HANDLE_W = 60\n\nGR_HINGE_SZ = 32\nGR_HINGE_D = 3\nGR_HINGE_W1 = 5.5\nGR_HINGE_H1 = 2.7\nGR_HINGE_W2 = 2.1\nGR_HINGE_H2 = 9\nGR_HINGE_CTR = 30.625\nGR_HINGE_W3 = 2\nGR_HINGE_SEP = 1\nGR_HINGE_OFFS = 2.65\nGR_HINGE_SKEW = 0.15\nGR_HINGE_RAD = 3.5\nGR_HINGE_TOL = 0.4\nGR_HEX_H = 3\nGR_HEX_W = 4\nGR_HEX_D = 1.3\nGR_LID_WINDOW_H = 6.5\n\nGR_LABEL_SLOT_TH = 2.5\nGR_LABEL_TH = 0.8\nGR_LABEL_H = 31\n\nGR_LATCH_L = 32.5\nGR_LATCH_W = 19.6\nGR_LATCH_H = 7\nGR_LATCH_IW = 14.75\nGR_LATCH_IL = 5.2\n"
  },
  {
    "path": "cqgridfinity/gf_baseplate.py",
    "content": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023  Michael Gale\n# This file is part of the cq-gridfinity python module.\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation\n# files (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\n# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\n# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n#\n# Gridfinity Baseplates\n\nimport cadquery as cq\n\nfrom cqgridfinity import *\nfrom cqkit.cq_helpers import (\n    rounded_rect_sketch,\n    composite_from_pts,\n    rotate_x,\n    recentre,\n)\nfrom cqkit import VerticalEdgeSelector, HasZCoordinateSelector\n\n\nclass GridfinityBaseplate(GridfinityObject):\n    \"\"\"Gridfinity Baseplate\n\n    This class represents a basic Gridfinity baseplate object. This baseplate\n    more or less conforms to the original simple baseplate released by\n    Zach Freedman. As such, it does not include features such as mounting\n    holes, magnet holes, weight slots, etc.\n      length_u - length in U (42 mm / U)\n      width_u - width in U (42 mm / U)\n      ext_depth - extrude bottom face by an extra amount in mm\n      straight_bottom - remove bottom chamfer and replace with straight side\n      corner_screws - add countersink mounting screws to the inside corners\n      corner_tab_size - size of mounting screw corner tabs\n      csk_hole - mounting screw hole diameter\n      csk_diam - mounting screw countersink diameter\n      csk_angle - mounting screw countersink angle\n    \"\"\"\n\n    def __init__(self, length_u, width_u, **kwargs):\n        super().__init__()\n        self.length_u = length_u\n        self.width_u = width_u\n        self.ext_depth = 0\n        self.straight_bottom = False\n        self.corner_screws = False\n        self.corner_tab_size = 21\n        self.csk_hole = 5.0\n        self.csk_diam = 10.0\n        self.csk_angle = 82\n        for k, v in kwargs.items():\n            if k in self.__dict__ and v is not None:\n                self.__dict__[k] = v\n        if self.corner_screws:\n            self.ext_depth = max(self.ext_depth, 5.0)\n\n    def _corner_pts(self):\n        oxy = self.corner_tab_size / 2\n        return [\n            (i * (self.length / 2 - oxy), j * (self.width / 2 - oxy), 0)\n            for i in (-1, 1)\n            for j in (-1, 1)\n        ]\n\n    def render(self):\n        profile = GR_BASE_PROFILE if not self.straight_bottom else GR_STR_BASE_PROFILE\n        if self.ext_depth > 0:\n            profile = [*profile, self.ext_depth]\n        rc = self.extrude_profile(\n            rounded_rect_sketch(GRU_CUT, GRU_CUT, GR_RAD), profile\n        )\n        rc = rotate_x(rc, 180).translate((GRU2, GRU2, GR_BASE_HEIGHT + self.ext_depth))\n        rc = recentre(composite_from_pts(rc, self.grid_centres), \"XY\")\n        r = (\n            cq.Workplane(\"XY\")\n            .rect(self.length, self.width)\n            .extrude(GR_BASE_HEIGHT + self.ext_depth)\n            .edges(\"|Z\")\n            .fillet(GR_RAD)\n            .faces(\">Z\")\n            .cut(rc)\n        )\n        if self.corner_screws:\n            rs = cq.Sketch().rect(self.corner_tab_size, self.corner_tab_size)\n            rs = cq.Workplane(\"XY\").placeSketch(rs).extrude(self.ext_depth)\n            rs = rs.faces(\">Z\").cskHole(\n                self.csk_hole, cskDiameter=self.csk_diam, cskAngle=self.csk_angle\n            )\n            r = r.union(recentre(composite_from_pts(rs, self._corner_pts()), \"XY\"))\n            bs = VerticalEdgeSelector(self.ext_depth) & HasZCoordinateSelector(0)\n            r = r.edges(bs).fillet(GR_RAD)\n        return r\n"
  },
  {
    "path": "cqgridfinity/gf_box.py",
    "content": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023  Michael Gale\n# This file is part of the cq-gridfinity python module.\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation\n# files (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\n# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\n# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n#\n# Gridfinity Boxes\n\nimport math\n\nimport cadquery as cq\nfrom cqkit import HasZCoordinateSelector, VerticalEdgeSelector, FlatEdgeSelector\nfrom cqkit.cq_helpers import rounded_rect_sketch, composite_from_pts\nfrom cqgridfinity import *\n\n\nclass GridfinityBox(GridfinityObject):\n    \"\"\"Gridfinity Box\n\n    This class represents a Gridfinity compatible box module. As a minimum,\n    this class is initialized with basic 3D unit dimensions for length,\n    width, and height.  length and width are multiples of 42 mm Gridfinity\n    intervals and height represents multiples of 7 mm.\n\n    Many box features can be enabled with attributes provided either as\n    keywords or direct dotted access.  These attributes include:\n    - solid :   renders the box without an interior, i.e. a solid block. This\n                is useful for making custom Gridfinity modules by subtracting\n                out shapes from the solid interior. Normally, the box is\n                rendered solid up to its maximum size; however, the\n                solid_ratio attribute can specify a solid fill of between\n                0.0 to 1.0, i.e. 0 to 100% fill.\n    - holes : adds bottom mounting holes for magnets or screws\n    - scoops : adds a radiused bottom edge to the interior to help fetch\n               parts from the box\n    - labels : adds a flat flange along each compartment for adding a label\n    - no_lip : removes the contoured lip on the top module used for stacking\n    - length_div, width_div : subdivides the box into sub-compartments in\n                 length and/or width.\n    - lite_style : render box as an economical shell without elevated floor\n    - unsupported_holes : render bottom holes as 3D printer friendly versions\n                          which can be printed without supports\n    - label_width : width of top label ledge face overhang\n    - label_height : height of label ledge overhang\n    - scoop_rad : radius of the bottom scoop feature\n    - wall_th : wall thickness\n    - hole_diam : magnet/counterbore bolt hole diameter\n\n    \"\"\"\n\n    def __init__(self, length_u, width_u, height_u, **kwargs):\n        super().__init__()\n        self.length_u = length_u\n        self.width_u = width_u\n        self.height_u = height_u\n        self.length_div = 0\n        self.width_div = 0\n        self.scoops = False\n        self.labels = False\n        self.solid = False\n        self.holes = False\n        self.no_lip = False\n        self.solid_ratio = 1.0\n        self.lite_style = False\n        self.unsupported_holes = False\n        self.label_width = 12  # width of the label strip\n        self.label_height = 10  # thickness of label overhang\n        self.label_lip_height = 0.8  # thickness of label vertical lip\n        self.scoop_rad = 14  # radius of optional interior scoops\n        self.fillet_interior = True\n        self.wall_th = GR_WALL\n        self.hole_diam = GR_HOLE_D  # magnet/bolt hole diameter\n        for k, v in kwargs.items():\n            if k in self.__dict__:\n                self.__dict__[k] = v\n        self._int_shell = None\n        self._ext_shell = None\n\n    def __str__(self):\n        s = []\n        s.append(\n            \"Gridfinity Box %dU x %dU x %dU (%.2f x %.2f x %.2f mm)\"\n            % (\n                self.length_u,\n                self.width_u,\n                self.height_u,\n                self.length - GR_TOL,\n                self.width - GR_TOL,\n                self.height,\n            )\n        )\n        sl = \"no mating top lip\" if self.no_lip else \"with mating top lip\"\n        ss = \"Lite style box  \" if self.lite_style else \"\"\n        s.append(\"  %sWall thickness: %.2f mm  %s\" % (ss, self.wall_th, sl))\n        s.append(\n            \"  Floor height  : %.2f mm  Inside height: %.2f mm  Top reference height: %.2f mm\"\n            % (self.floor_h + GR_BASE_HEIGHT, self.int_height, self.top_ref_height)\n        )\n        if self.solid:\n            s.append(\"  Solid filled box with fill ratio %.2f\" % (self.solid_ratio))\n        if self.holes:\n            s.append(\"  Bottom mounting holes with %.2f mm diameter\" % (self.hole_diam))\n            if self.unsupported_holes:\n                s.append(\"  Holes are 3D printer friendly and can be unsupported\")\n        if self.scoops:\n            s.append(\"  Lengthwise scoops with %.2f mm radius\" % (self.scoop_rad))\n        if self.labels:\n            s.append(\n                \"  Lengthwise label shelf %.2f mm wide with %.2f mm overhang\"\n                % (self.label_width, self.label_height)\n            )\n        if self.length_div:\n            xl = (self.inner_l - GR_DIV_WALL * (self.length_div)) / (\n                self.length_div + 1\n            )\n            s.append(\n                \"  %dx lengthwise divisions for %.2f mm compartment lengths\"\n                % (self.length_div, xl)\n            )\n        if self.width_div:\n            yl = (self.inner_w - GR_DIV_WALL * (self.width_div)) / (self.width_div + 1)\n            s.append(\n                \"  %dx widthwise divisions for %.2f mm compartment widths\"\n                % (self.width_div, yl)\n            )\n        s.append(\"  Auto filename: %s\" % (self.filename()))\n        return \"\\n\".join(s)\n\n    def render(self):\n        \"\"\"Returns a CadQuery Workplane object representing this Gridfinity box.\"\"\"\n        self._int_shell = None\n        if self.lite_style:\n            # just force the dividers to the desired quantity in both dimensions\n            # rather than raise a exception\n            if self.length_div:\n                self.length_div = self.length_u - 1\n            if self.width_div:\n                self.width_div = self.width_u - 1\n            if self.solid:\n                raise ValueError(\n                    \"Cannot select both solid and lite box styles together\"\n                )\n            if self.holes:\n                raise ValueError(\n                    \"Cannot select both holes and lite box styles together\"\n                )\n            if self.wall_th > 1.5:\n                raise ValueError(\n                    \"Wall thickness cannot exceed 1.5 mm for lite box style\"\n                )\n        if self.wall_th > 2.5:\n            raise ValueError(\"Wall thickness cannot exceed 2.5 mm\")\n        if self.wall_th < 0.5:\n            raise ValueError(\"Wall thickness must be at least 0.5 mm\")\n        r = self.render_shell()\n        rd = self.render_dividers()\n        rs = self.render_scoops()\n        rl = self.render_labels()\n        for e in (rd, rl, rs):\n            if e is not None:\n                r = r.union(e)\n        if not self.solid and self.fillet_interior:\n            heights = [GR_FLOOR]\n            if self.labels:\n                heights.append(self.safe_label_height(backwall=True, from_bottom=True))\n                heights.append(self.safe_label_height(backwall=False, from_bottom=True))\n            bs = (\n                HasZCoordinateSelector(heights, min_points=1, tolerance=0.5)\n                + VerticalEdgeSelector(\">5\")\n                - HasZCoordinateSelector(\"<%.2f\" % (self.floor_h))\n            )\n            if self.lite_style and self.scoops:\n                bs = bs - HasZCoordinateSelector(\"<=%.2f\" % (self.floor_h))\n                bs = bs - VerticalEdgeSelector()\n            r = self.safe_fillet(r, bs, self.safe_fillet_rad)\n\n            if self.lite_style and not self.has_dividers:\n                bs = FlatEdgeSelector(self.floor_h)\n                if self.wall_th < 1.2:\n                    r = self.safe_fillet(r, bs, 0.5)\n                elif self.wall_th < 1.25:\n                    r = self.safe_fillet(r, bs, 0.25)\n\n            if not self.labels and self.has_dividers:\n                bs = VerticalEdgeSelector(\n                    GR_TOPSIDE_H, tolerance=0.05\n                ) & HasZCoordinateSelector(GRHU * self.height_u - GR_BASE_HEIGHT)\n                r = self.safe_fillet(r, bs, GR_TOPSIDE_H - EPS)\n\n        if self.holes:\n            r = self.render_holes(r)\n        r = r.translate((-self.half_l, -self.half_w, GR_BASE_HEIGHT))\n        if self.unsupported_holes:\n            r = self.render_hole_fillers(r)\n        return r\n\n    @property\n    def top_ref_height(self):\n        \"\"\"The height of the top surface of a solid box or the floor\n        height of an empty box.\"\"\"\n        if self.solid:\n            return self.max_height * self.solid_ratio + GR_BOT_H\n        if self.lite_style:\n            return self.floor_h\n        return GR_BOT_H\n\n    @property\n    def bin_height(self):\n        return self.height - GR_BASE_HEIGHT\n\n    def safe_label_height(self, backwall=False, from_bottom=False):\n        lw = self.label_width\n        if backwall:\n            lw += self.lip_width\n        lh = self.label_height * (lw / self.label_width)\n        yl = self.max_height - self.label_height + self.wall_th\n        if backwall:\n            yl -= self.lip_width\n        if yl < 0:\n            lh = self.max_height - 1.5 * GR_FILLET - 0.1\n        elif yl < 1.5 * GR_FILLET:\n            lh -= 1.5 * GR_FILLET - yl + 0.1\n        if from_bottom:\n            ws = math.sin(math.atan2(self.label_height, self.label_width))\n            if backwall:\n                lh = self.max_height + GR_FLOOR - lh + ws * self.wall_th\n            else:\n                lh = self.max_height + GR_FLOOR - lh + ws * GR_DIV_WALL\n        return lh\n\n    @property\n    def has_dividers(self):\n        return self.length_div > 0 or self.width_div > 0\n\n    @property\n    def interior_solid(self):\n        if self._int_shell is not None:\n            return self._int_shell\n        self._int_shell = self.render_interior()\n        return self._int_shell\n\n    def render_interior(self, force_solid=False):\n        \"\"\"Renders the interior cutting solid of the box.\"\"\"\n        wall_u = self.wall_th - GR_WALL\n        wall_h = self.int_height + wall_u\n        under_h = ((GR_UNDER_H - wall_u) * SQRT2, 45)\n        profile = GR_NO_PROFILE if self.no_lip else [under_h, *GR_LIP_PROFILE[1:]]\n        profile = [wall_h, *profile]\n        if self.int_height < 0:\n            profile = [self.height - GR_BOT_H]\n        rci = self.extrude_profile(\n            rounded_rect_sketch(*self.inner_dim, self.inner_rad), profile\n        )\n        rci = rci.translate((*self.half_dim, self.floor_h))\n        if self.solid or force_solid:\n            hs = self.max_height * self.solid_ratio\n            ri = rounded_rect_sketch(*self.inner_dim, self.inner_rad)\n            rf = cq.Workplane(\"XY\").placeSketch(ri).extrude(hs)\n            rf = rf.translate((*self.half_dim, self.floor_h))\n            rci = rci.cut(rf)\n        if self.scoops and not self.no_lip and not self.lite_style:\n            rf = (\n                cq.Workplane(\"XY\")\n                .rect(self.inner_l, 2 * self.under_h)\n                .extrude(self.max_height)\n                .translate((self.half_l, -self.half_in, self.floor_h))\n            )\n            rci = rci.cut(rf)\n        if self.lite_style:\n            r = composite_from_pts(self.base_interior(), self.grid_centres)\n            rci = rci.union(r)\n        return rci\n\n    def solid_shell(self):\n        \"\"\"Returns a completely solid box object useful for intersecting with other solids.\"\"\"\n        if self._ext_shell is not None:\n            return self._ext_shell\n        r = self.render_shell(as_solid=True)\n        self._ext_shell = r.cut(self.render_interior(force_solid=True))\n        return self._ext_shell\n\n    def mask_with_obj(self, obj):\n        \"\"\"Intersects a solid object with this box.\"\"\"\n        return obj.intersect(self.solid_shell())\n\n    def base_interior(self):\n        profile = [GR_BASE_HEIGHT, *GR_BOX_PROFILE]\n        zo = GR_BASE_HEIGHT + GR_BASE_CLR\n        if self.int_height < 0:\n            h = self.bin_height - GR_BASE_HEIGHT\n            profile = [h, *profile]\n            zo += h\n        r = self.extrude_profile(\n            rounded_rect_sketch(GRU - GR_TOL, GRU - GR_TOL, self.outer_rad),\n            profile,\n        )\n        rx = r.faces(\"<Z\").shell(-self.wall_th)\n        r = r.cut(rx).mirror(mirrorPlane=\"XY\").translate((0, 0, zo))\n        return r\n\n    def render_shell(self, as_solid=False):\n        \"\"\"Renders the box shell without any added features.\"\"\"\n        r = self.extrude_profile(\n            rounded_rect_sketch(GRU, GRU, self.outer_rad + GR_BASE_CLR), GR_BOX_PROFILE\n        )\n        r = r.translate((0, 0, -GR_BASE_CLR))\n        r = r.mirror(mirrorPlane=\"XY\")\n        r = composite_from_pts(r, self.grid_centres)\n        rs = rounded_rect_sketch(*self.outer_dim, self.outer_rad)\n        rw = (\n            cq.Workplane(\"XY\")\n            .placeSketch(rs)\n            .extrude(self.bin_height - GR_BASE_CLR)\n            .translate((*self.half_dim, GR_BASE_CLR))\n        )\n        rc = (\n            cq.Workplane(\"XY\")\n            .placeSketch(rs)\n            .extrude(-GR_BASE_HEIGHT - 1)\n            .translate((*self.half_dim, 0.5))\n        )\n        rc = rc.intersect(r).union(rw)\n        if not as_solid:\n            return rc.cut(self.interior_solid)\n        return rc\n\n    def render_dividers(self):\n        r = None\n        if self.length_div > 0 and not self.solid:\n            wall_w = (\n                cq.Workplane(\"XY\")\n                .rect(GR_DIV_WALL, self.outer_w)\n                .extrude(self.max_height)\n                .translate((0, 0, self.floor_h))\n            )\n            xl = self.inner_l / (self.length_div + 1)\n            pts = [\n                ((x + 1) * xl - self.half_in, self.half_w)\n                for x in range(self.length_div)\n            ]\n            r = composite_from_pts(wall_w, pts)\n\n        if self.width_div > 0 and not self.solid:\n            wall_l = (\n                cq.Workplane(\"XY\")\n                .rect(self.outer_l, GR_DIV_WALL)\n                .extrude(self.max_height)\n                .translate((0, 0, self.floor_h))\n            )\n            yl = self.inner_w / (self.width_div + 1)\n            pts = [\n                (self.half_l, (y + 1) * yl - self.half_in)\n                for y in range(self.width_div)\n            ]\n            rw = composite_from_pts(wall_l, pts)\n            if r is not None:\n                r = r.union(rw)\n            else:\n                r = rw\n        return r\n\n    def render_scoops(self):\n        if not self.scoops or self.solid:\n            return None\n        # front wall scoop\n        # prevent the scoop radius exceeding the internal height\n        srad = min(self.scoop_rad, self.int_height - 0.1)\n        rs = cq.Sketch().rect(srad, srad).vertices(\">X and >Y\").circle(srad, mode=\"s\")\n        rsc = cq.Workplane(\"YZ\").placeSketch(rs).extrude(self.inner_l)\n        rsc = rsc.translate((0, 0, srad / 2 + GR_FLOOR))\n        yo = -self.half_in + srad / 2\n        # offset front wall scoop by top lip overhang if applicable\n        if not self.no_lip and not self.lite_style:\n            yo += self.under_h\n        zo = -GR_BOT_H + self.wall_th if self.lite_style else 0\n        rs = rsc.translate((-self.half_in, yo, zo))\n        # intersect to prevent solids sticking out of rounded corners\n        r = rs.intersect(self.interior_solid)\n        if self.width_div > 0:\n            # add scoops along each internal dividing wall in the width dimension\n            yl = self.inner_w / (self.width_div + 1)\n            pts = [\n                (-self.half_in, (y + 1) * yl - self.half_in)\n                for y in range(self.width_div)\n            ]\n            rs = composite_from_pts(rsc, pts)\n            r = r.union(rs.translate((0, GR_DIV_WALL / 2 + srad / 2, zo)))\n            r = r.intersect(self.render_shell(as_solid=True))\n        return r\n\n    def render_labels(self):\n        if not self.labels or self.solid:\n            return None\n        # back wall label flange with compensated width and height\n        lw = self.label_width + self.lip_width\n        rs = (\n            cq.Sketch()\n            .segment((0, 0), (lw, 0))\n            .segment((lw, -self.safe_label_height(backwall=True)))\n            .segment((0, -self.label_lip_height))\n            .close()\n            .assemble()\n            .vertices(\"<X\")\n            .vertices(\"<Y\")\n            .fillet(self.label_lip_height / 2)\n        )\n        rsc = cq.Workplane(\"YZ\").placeSketch(rs).extrude(self.inner_l)\n        yo = -lw + self.outer_w / 2 + self.half_w + self.wall_th / 4\n        rs = rsc.translate((-self.half_in, yo, self.floor_h + self.max_height))\n        # intersect to prevent solids sticking out of rounded corners\n        r = rs.intersect(self.interior_solid)\n        if self.width_div > 0:\n            # add label flanges along each dividing wall\n            rs = (\n                cq.Sketch()\n                .segment((0, 0), (self.label_width, 0))\n                .segment((self.label_width, -self.safe_label_height(backwall=False)))\n                .segment((0, -self.label_lip_height))\n                .close()\n                .assemble()\n                .vertices(\"<X\")\n                .vertices(\"<Y\")\n                .fillet(self.label_lip_height / 2)\n            )\n            rsc = cq.Workplane(\"YZ\").placeSketch(rs).extrude(self.inner_l)\n            rsc = rsc.translate((0, -self.label_width, self.floor_h + self.max_height))\n            yl = self.inner_w / (self.width_div + 1)\n            pts = [\n                (-self.half_in, (y + 1) * yl - self.half_in + GR_DIV_WALL / 2)\n                for y in range(self.width_div)\n            ]\n            r = r.union(composite_from_pts(rsc, pts))\n        return r\n\n    def render_holes(self, obj):\n        if not self.holes:\n            return obj\n        h = GR_HOLE_H\n        if self.unsupported_holes:\n            h += GR_HOLE_SLICE\n        return (\n            obj.faces(\"<Z\")\n            .workplane()\n            .pushPoints(self.hole_centres)\n            .cboreHole(GR_BOLT_D, self.hole_diam, h, depth=GR_BOLT_H)\n        )\n\n    def render_hole_fillers(self, obj):\n        rc = (\n            cq.Workplane(\"XY\")\n            .rect(self.hole_diam / 2, self.hole_diam)\n            .extrude(GR_HOLE_SLICE)\n        )\n        xo = self.hole_diam / 2\n        rs = composite_from_pts(rc, [(-xo, 0, GR_HOLE_H), (xo, 0, GR_HOLE_H)])\n        rs = composite_from_pts(rs, self.hole_centres)\n        return obj.union(rs.translate((-self.half_l, self.half_w, 0)))\n\n\nclass GridfinitySolidBox(GridfinityBox):\n    \"\"\"Convenience class to represent a solid Gridfinity box.\"\"\"\n\n    def __init__(self, length_u, width_u, height_u, **kwargs):\n        super().__init__(length_u, width_u, height_u, **kwargs, solid=True)\n"
  },
  {
    "path": "cqgridfinity/gf_drawer.py",
    "content": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023  Michael Gale\n# This file is part of the cq-gridfinity python module.\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation\n# files (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\n# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\n# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n#\n# Gridfinity Drawer Spacers\n\nimport math\n\nimport cadquery as cq\n\nfrom cqgridfinity import *\nfrom cqkit.cq_helpers import rotate_x, rotate_y, rotate_z\n\n\nclass GridfinityDrawerSpacer(GridfinityObject):\n    \"\"\"Gridfinity Drawer Spacers\n    This class is used for making spacer elements which help fit Gridfinity baseplates\n    snugly into a drawer.  The spacers consist of 4x corner elements plus a left/right\n    pair and front/back pair. If the spacers are wide enough, they will include\n    interlocking alignment pegs/holes.\n    Normally spacers are made for the front and back of the drawer of the same size.\n    However, if front_and_back is False, then only a back spacer (2x thicker) is made.\n    This will place the gridfinity grid flush behind the drawer face, rather than\n    equally spaced between the face and back wall.\n    \"\"\"\n\n    def __init__(self, dr_width=None, dr_depth=None, **kwargs):\n        super().__init__()\n        self.length_u = 1\n        self.width_u = 1\n        self.length_th = 10\n        self.width_th = 10\n        self.thickness = GR_BASE_HEIGHT\n        self.chamf_rad = 1.0\n        self.show_arrows = True\n        self.arrow_h = 0.8\n        self.length_fill = 0\n        self.width_fill = 0\n        self.align_features = True\n        self.align_l = 16\n        self.align_tol = 0.15\n        self.align_min = 8\n        self.min_margin = 4\n        self.tolerance = GR_TOL\n        self.front_and_back = True\n        for k, v in kwargs.items():\n            if k in self.__dict__:\n                self.__dict__[k] = v\n        if dr_width is not None and dr_depth is not None:\n            verbose = kwargs[\"verbose\"] if \"verbose\" in kwargs else False\n            self.best_fit_to_dim(dr_width, dr_depth, verbose=verbose)\n\n    def best_fit_to_dim(self, length, width, verbose=False):\n        \"\"\"Computes the best fit of Gridfinity units to fill a drawer dimensions.\n        The geometry of all the spacer elements is then computed to securely\n        centre the Gridfinity baseplate(s) inside the drawer footprint.\"\"\"\n        self.size = length, width\n        lu, wu = (math.floor(x / GRU) for x in (length, width))\n        lg, wg = (x * GRU for x in (lu, wu))\n        lm, wm = (length - lg) / 2, (width - wg) / 2\n        self.size_u = lu, wu\n        self.width_th, self.length_th = lm - self.tolerance, wm - self.tolerance\n        self.length_u, self.width_u = math.floor(lu / 3), math.floor(wu / 3)\n        self.length_fill, self.width_fill = lg - 2 * self.length, wg - 2 * self.width\n        if self.wide_enough:\n            self.align_l = 1.5 * self.width_th\n        if self.deep_enough:\n            self.align_l = min(self.align_l, 1.5 * self.length_th)\n        self.align_l = min(self.align_l, 16)\n        if verbose:\n            print(\"Best fit for %.2f x %.2f mm is %dU x %dU\" % (length, width, lu, wu))\n            if self.front_and_back:\n                print(\n                    \"with %.2f mm margin each side and %.2f mm margin front and back\"\n                    % (lm, wm)\n                )\n            else:\n                print(\n                    \"with %.2f mm margin each side and %.2f mm back (or front) margin\"\n                    % (lm, 2 * wm)\n                )\n            if not self.front_and_back:\n                print(\"Corner spacers only generated for either front or back wall\")\n            if self.wide_enough and self.deep_enough:\n                print(\n                    \"Corner spacers     : %dU wide x %dU deep\"\n                    % (self.length_u, self.width_u)\n                )\n            elif self.wide_enough:\n                print(\n                    \"Corner spacers     : %dU deep x %.2f mm\"\n                    % (self.width_u, self.width_th)\n                )\n            elif self.deep_enough:\n                print(\n                    \"Corner spacers     : %dU wide x %.2f mm\"\n                    % (self.length_u, self.fb_length_th)\n                )\n\n            if self.deep_enough:\n                if self.front_and_back:\n                    print(\n                        \"Front/back spacers : %dU wide x %.2f mm +%.2f mm tolerance\"\n                        % (self.length_fill / GRU, self.length_th, self.tolerance)\n                    )\n                else:\n                    print(\n                        \"Back spacer        : %dU wide x %.2f mm +%.2f mm tolerance\"\n                        % (self.length_fill / GRU, self.fb_length_th, self.tolerance)\n                    )\n            else:\n                print(\"Front/back spacers : not required\")\n            if self.wide_enough:\n                print(\n                    \"Left/right spacers : %dU deep x %.2f mm +%.2f mm tolerance\"\n                    % (self.width_fill / GRU, self.width_th, self.tolerance)\n                )\n                if not self.front_and_back:\n                    print(\n                        \"Extra left/right spacers generated %dU deep\" % (self.width_u)\n                    )\n            else:\n                print(\"Left/right spacers : not required\")\n\n    @property\n    def fillet_rad(self):\n        rads = [GR_RAD]\n        if self.wide_enough:\n            rads.append(self.width_th / 6)\n        if self.deep_enough:\n            rads.append(self.length_th / 6)\n        return min(rads)\n\n    @property\n    def safe_chamfer_rad(self):\n        rads = [self.chamf_rad]\n        if self.wide_enough:\n            rads.append(self.width_th / 6)\n        if self.deep_enough:\n            rads.append(self.length_th / 6)\n        return min(rads)\n\n    @property\n    def wide_enough(self):\n        return self.width_th > self.min_margin\n\n    @property\n    def deep_enough(self):\n        return self.length_th > self.min_margin\n\n    @property\n    def fb_length_th(self):\n        if not self.front_and_back:\n            return 2 * self.length_th\n        return self.length_th\n\n    def check_dimensions(self):\n        \"\"\"Check required size does not fall below specified minimum margin.\"\"\"\n        if not self.wide_enough and not self.deep_enough:\n            print(\"Drawer spacers NOT required since resulting margins are:\")\n            print(\n                \"  %.2f mm +/-%.2f mm (tolerance) widthwise which is not above the %.2f margin threshold\"\n                % (self.length_th, self.tolerance, self.min_margin)\n            )\n            print(\n                \"  %.2f mm +/-%.2f mm (tolerance) depthwise which is not above the %.2f margin threshold\"\n                % (self.width_th, self.tolerance, self.min_margin)\n            )\n            return False\n        return True\n\n    def render(self, arrows_top=True, arrows_bottom=True, front_and_back=True):\n        \"\"\"Renders a corner spacer component. This component can be used for any of\n        the four corners due to symmetry.  Optional arrows can be cut into the\n        component on the top or bottom to show the drawer sliding/depth-wise direction\n        \"\"\"\n        if not self.check_dimensions():\n            return None\n        sp_length = self.length + self.width_th + self.tolerance\n        sp_width = self.width + self.fb_length_th + self.tolerance\n        r, rd = None, None\n        if self.deep_enough and front_and_back:\n            r = (\n                cq.Workplane(\"XY\")\n                .rect(sp_length, self.fb_length_th)\n                .extrude(self.thickness)\n            )\n            er = min(GR_RAD, max(self.length_th, self.width_th) / 4)\n            r = r.translate((sp_length / 2, self.fb_length_th / 2, 0))\n            r = r.edges(\"|Z\").edges(\"<XY\").fillet(er)\n            r = r.edges(\"|Z\").fillet(self.fillet_rad)\n\n        if self.wide_enough:\n            if not front_and_back:\n                sp_width -= self.fb_length_th\n            rd = (\n                cq.Workplane(\"XY\").rect(self.width_th, sp_width).extrude(self.thickness)\n            )\n            er = min(GR_RAD, max(self.length_th, self.width_th) / 4)\n            rd = rd.translate((self.width_th / 2, sp_width / 2, 0))\n            rd = rd.edges(\"|Z\").edges(\"<Y\").fillet(er)\n            rd = rd.edges(\"|Z\").fillet(self.fillet_rad)\n\n        if r is not None and rd is not None:\n            r = r.union(rd)\n        elif r is None and rd is not None:\n            r = rd\n        r = r.faces(\">Z or <Z\").chamfer(self.safe_chamfer_rad)\n        r = self.orientation_arrows(\n            r, self.width_th / 2, sp_width / 2, top=arrows_top, bottom=arrows_bottom\n        )\n        if self.align_features and self.fb_length_th > self.align_min:\n            rc = self.alignment_feature(as_cutter=True)\n            r = r.cut(rc.translate((sp_length, self.fb_length_th / 2, 0)))\n        if self.align_features and self.width_th > self.align_min:\n            rc = self.alignment_feature(as_cutter=False, horz=False)\n            r = r.union(rc.translate((self.width_th / 2, sp_width, 0)))\n        self._cq_obj = r\n        self._obj_label = \"corner_spacer\"\n        return r\n\n    def alignment_feature(self, as_cutter=False, horz=True):\n        \"\"\"Renders optional mating alignment pegs/holes for connecting the spacer components.\"\"\"\n        x, y = self.align_l, self.fb_length_th / 2\n        if not horz:\n            y = self.width_th / 2\n        fr = min(GR_RAD / 2, y / 3)\n        if as_cutter:\n            x += 2 * self.align_tol\n            y += 2 * self.align_tol\n            fr += self.align_tol\n        rs = (\n            cq.Sketch()\n            .segment((0, y / 3), (x / 2, y / 2))\n            .segment((x / 2, -y / 2))\n            .segment((0, -y / 3))\n            .segment((-x / 2, -y / 2))\n            .segment((-x / 2, y / 2))\n            .close()\n            .assemble()\n            .vertices()\n            .fillet(fr)\n        )\n        r = cq.Workplane(\"XY\").placeSketch(rs).extrude(self.thickness)\n        if not horz:\n            r = rotate_z(r, 90)\n        if not as_cutter:\n            r = r.faces(\">Z or <Z\").chamfer(self.safe_chamfer_rad)\n        return r\n\n    def orientation_arrows(self, obj, x, y, up=True, down=True, top=True, bottom=True):\n        \"\"\"Renders optional orientation arrows which show the sliding (depth-wise)\n        direction of the drawer.\"\"\"\n        if self.show_arrows and self.wide_enough:\n            la = self.width_th / 2\n            ra = (\n                cq.Sketch()\n                .segment((0, 0), (la / 2, la))\n                .segment((la, 0))\n                .close()\n                .assemble()\n            )\n            ru = (\n                cq.Workplane(\"XY\")\n                .placeSketch(ra)\n                .extrude(self.arrow_h)\n                .translate((-la / 2, -la / 2, 0))\n            )\n            rd = ru.rotate((0, 0, 0), (0, 0, 1), 180)\n            th = self.thickness - self.arrow_h\n            yo = 10 * self.width_th / 15 if up and down else 0\n            if up and top:\n                obj = obj.cut(ru.translate((x, y + yo, th)))\n            if up and bottom:\n                obj = obj.cut(ru.translate((x, y + yo, 0)))\n            if down and top:\n                obj = obj.cut(rd.translate((x, y - yo, th)))\n            if down and bottom:\n                obj = obj.cut(rd.translate((x, y - yo, 0)))\n        return obj\n\n    def render_length_filler(self, alignment_type=\"peg\"):\n        \"\"\"Renders the centre filler element used along the front/back walls\n        of the drawer.\"\"\"\n        if not self.deep_enough:\n            return None\n        r = (\n            cq.Workplane(\"XY\")\n            .rect(self.length_fill, self.fb_length_th)\n            .extrude(self.thickness)\n        )\n        r = r.edges(\"|Z\").fillet(self.fillet_rad)\n        r = r.faces(\">Z or <Z\").chamfer(self.safe_chamfer_rad)\n        if self.align_features and self.fb_length_th > self.align_min:\n            if alignment_type == \"hole\":\n                ra = self.alignment_feature(as_cutter=True)\n                r = r.cut(ra.translate((self.length_fill / 2, 0, 0)))\n                r = r.cut(ra.translate((-self.length_fill / 2, 0, 0)))\n            else:\n                ra = self.alignment_feature(as_cutter=False)\n                r = r.union(ra.translate((self.length_fill / 2, 0, 0)))\n                r = r.union(ra.translate((-self.length_fill / 2, 0, 0)))\n        self._cq_obj = r\n        self._obj_label = \"length_spacer\"\n        return r\n\n    def render_width_filler(self, arrows_top=True, arrows_bottom=True):\n        \"\"\"Renders the centre filler element used along the left/right walls\n        of the drawer.\"\"\"\n        if not self.wide_enough:\n            return None\n        r = (\n            cq.Workplane(\"XY\")\n            .rect(self.width_th, self.width_fill)\n            .extrude(self.thickness)\n        )\n        r = r.edges(\"|Z\").fillet(self.fillet_rad)\n        r = r.faces(\">Z or <Z\").chamfer(self.safe_chamfer_rad)\n        r = self.orientation_arrows(r, 0, 0, top=arrows_top, bottom=arrows_bottom)\n        if self.align_features and self.width_th > self.align_min:\n            ra = self.alignment_feature(horz=False, as_cutter=True)\n            r = r.cut(ra.translate((0, self.width_fill / 2, 0)))\n            r = r.cut(ra.translate((0, -self.width_fill / 2, 0)))\n        self._cq_obj = r\n        self._obj_label = \"width_spacer\"\n        return r\n\n    def render_full_set(self, include_baseplate=False):\n        \"\"\"Renders a complete set of spacer components including the four corners plus\n        left/right and front/back spacer pairs.  The components are placed in their\n        respective installed position in the drawer so that the resulting object can\n        be used to preview final composition of components.\"\"\"\n        # Four corners top/bottom left + top/bottom right\n        if not self.check_dimensions():\n            return None\n        if self.front_and_back:\n            bl = self.render()\n            tl = rotate_x(bl, 180).translate((0, self.size[1], self.thickness))\n            br = rotate_y(bl, 180).translate((self.size[0], 0, self.thickness))\n            tr = rotate_z(bl, 180).translate((*self.size, 0))\n        else:\n            bl = self.render(arrows_bottom=False)\n            br = self.render(arrows_top=False)\n            br = rotate_y(br, 180).translate((self.size[0], 0, self.thickness))\n            tl = self.render(arrows_bottom=False, front_and_back=False)\n            tl = rotate_z(tl, 180).translate((self.width_th, self.size[1], 0))\n            tr = self.render(arrows_top=False, front_and_back=False)\n            tr = rotate_y(tr, 180)\n            tr = rotate_z(tr, 180)\n            tr = tr.translate((*self.size, 0))\n            tr = tr.translate((-self.width_th, 0, self.thickness))\n\n        r = bl.union(tl).union(br).union(tr)\n        # 2x length-wise (drawer width) fillers\n        if self.deep_enough:\n            lf = self.render_length_filler()\n            r = r.union(lf.translate((self.size[0] / 2, self.fb_length_th / 2, 0)))\n            if self.front_and_back:\n                r = r.union(\n                    lf.translate(\n                        (self.size[0] / 2, self.size[1] - self.fb_length_th / 2, 0)\n                    )\n                )\n        # 2x width-wise (drawer depth) fillers\n        if self.wide_enough:\n            wf = self.render_width_filler()\n            yo = self.size[1] / 2\n            if not self.front_and_back:\n                yo += self.fb_length_th / 2\n            r = r.union(wf.translate((self.width_th / 2, yo, 0)))\n            r = r.union(wf.translate((self.size[0] - self.width_th / 2, yo, 0)))\n        if include_baseplate:\n            bp = GridfinityBaseplate(*self.size_u)\n            rb = bp.render().translate((self.size[0] / 2, self.size[1] / 2, 0))\n            if not self.front_and_back:\n                rb = rb.translate((0, self.fb_length_th / 2, 0))\n            r = r.union(rb)\n        self._cq_obj = r\n        self._obj_label = \"full_set\"\n        return r\n\n    def render_half_set(self):\n        \"\"\"Renders half of the full set of spacer components arranged for convenience\n        for 3D printing.  This resulting compound object can then be printed twice to\n        yield a complete set of spacer components for a drawer.\n        If front_and_back is False, then this function will render all of the\n        components to fill the drawer since only one set of corner spacers is\n        required and the remaining spacers are typically slim enough to fit together\n        on a build plate.\"\"\"\n        # one of each corner\n        if not self.check_dimensions():\n            return None\n        bl = self.render(arrows_bottom=False)\n        br = self.render(arrows_top=False)\n        if self.deep_enough:\n            xo = self.length + 2.5 * self.width_th\n            yo = 1.5 * self.fb_length_th\n        else:\n            xo = 2.5 * self.width_th\n            yo = 0\n        br = rotate_y(br, 180).translate((xo, yo, self.thickness))\n        r = bl.union(br)\n        # length-wise (drawer width) filler\n        if self.deep_enough:\n            xl = self.length_fill / 2 - (\n                self.length_fill - (self.length + self.width_th)\n            )\n            if self.fb_length_th > self.align_min:\n                xl -= self.align_l / 2\n            if self.wide_enough:\n                yt = self.width + self.fb_length_th\n                if self.width_th > self.align_min:\n                    yt += self.align_l / 2\n                yl = max(yt, self.width_fill)\n                yl += max(self.fb_length_th, self.align_l / 2)\n            else:\n                yl = 3.5 * self.fb_length_th\n            r = r.union(self.render_length_filler().translate((xl, yl, 0)))\n        # width-wise (drawer depth) filler\n        if self.wide_enough:\n            wf = self.render_width_filler(arrows_bottom=False)\n            r = r.union(wf.translate((-self.width_th, self.width_fill / 2, 0)))\n            if not self.front_and_back:\n                r = r.union(\n                    wf.translate((-2.5 * self.width_th, self.width_fill / 2, 0))\n                )\n                fb = self.render(arrows_bottom=False, front_and_back=False)\n                r = r.union(fb.translate((-4.5 * self.width_th, 0, 0)))\n                r = r.union(fb.translate((-6 * self.width_th, 0, 0)))\n\n        self._cq_obj = r\n        self._obj_label = \"half_set\"\n        return r\n"
  },
  {
    "path": "cqgridfinity/gf_helpers.py",
    "content": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023  Michael Gale\n# This file is part of the cq-gridfinity python module.\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation\n# files (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\n# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\n# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n#\n# Gridfinity Helper Functions\n\nimport cadquery as cq\nfrom cqkit import rotate_z\n\n\ndef quarter_circle(\n    outer_rad, inner_rad, height, quad=\"tr\", chamf=0.5, chamf_face=\">Z\", ext=0\n):\n    \"\"\"Renders a quarter circle shaped slot in any of 4 quadrants\"\"\"\n    r = cq.Workplane(\"XY\").circle(outer_rad).extrude(height)\n    rc = cq.Workplane(\"XY\").circle(inner_rad).extrude(height)\n    r = r.cut(rc)\n    rc = cq.Workplane(\"XY\").rect(outer_rad, outer_rad).extrude(height)\n    pos = {\n        \"tr\": (outer_rad / 2, outer_rad / 2, 0),\n        \"tl\": (-outer_rad / 2, outer_rad / 2, 0),\n        \"br\": (outer_rad / 2, -outer_rad / 2, 0),\n        \"bl\": (-outer_rad / 2, -outer_rad / 2, 0),\n    }\n    pt = pos[quad]\n    r = r.intersect(rc.translate(pt))\n    r = r.translate((-pt[0], -pt[1], 0))\n    if ext > 0:\n        faces = {\n            \"tl\": \"<Y >X\",\n            \"tr\": \"<X <Y\",\n            \"br\": \"<X >Y\",\n            \"bl\": \">Y >X\",\n        }\n        for face in faces[quad].split():\n            r = r.faces(face).wires().toPending().workplane().extrude(ext, combine=True)\n    if chamf > 0:\n        r = r.faces(chamf_face).chamfer(chamf)\n    return r\n\n\ndef chamf_cyl(rad, height, chamf=0.5):\n    \"\"\"Chamfered cylinder.\"\"\"\n    r = cq.Workplane(\"XY\").circle(rad).extrude(height)\n    if chamf > 0:\n        return r.faces(\"<Z or >Z\").chamfer(chamf)\n    return r\n\n\ndef chamf_rect(length, width, height, angle=0, tol=0.5, z_offset=0):\n    \"\"\"Chamfer rectangular box\"\"\"\n    if not z_offset > 0:\n        length += tol\n        width += tol\n        height += tol\n    r = cq.Workplane(\"XY\").rect(length, width).extrude(height)\n    r = r.faces(\">Z\").chamfer(0.5).translate((0, 0, z_offset))\n    return rotate_z(r, angle)\n"
  },
  {
    "path": "cqgridfinity/gf_obj.py",
    "content": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023  Michael Gale\n# This file is part of the cq-gridfinity python module.\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation\n# files (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\n# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\n# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n#\n# Gridfinity base object class\n\nimport math\nimport os\n\nfrom OCP.BRepMesh import BRepMesh_IncrementalMesh\nfrom OCP.StlAPI import StlAPI_Writer\nimport cadquery as cq\nfrom cadquery import exporters\n\nfrom cqgridfinity import *\nfrom cqkit import export_step_file\n\n# Special test to see which version of CadQuery is installed and\n# therefore if any compensation is required for extruded zlen\n# CQ versions < 2.4.0 typically require zlen correction, i.e.\n# scaling the vertical extrusion extent by 1/cos(taper)\nZLEN_FIX = True\n_r = cq.Workplane(\"XY\").rect(2, 2).extrude(1, taper=45)\n_bb = _r.vals()[0].BoundingBox()\nif abs(_bb.zlen - 1.0) < 1e-3:\n    ZLEN_FIX = False\n\n\nclass GridfinityObject:\n    \"\"\"Base Gridfinity object class\n\n    This class bundles glabally relevant constants, properties, and methods\n    for derived Gridfinity object classes.\n    \"\"\"\n\n    def __init__(self, **kwargs):\n        self.length_u = 1\n        self.width_u = 1\n        self.height_u = 1\n        self._cq_obj = None\n        self._obj_label = None\n        for k, v in kwargs.items():\n            if k in self.__dict__:\n                self.__dict__[k] = v\n\n    @property\n    def cq_obj(self):\n        if self._cq_obj is None:\n            return self.render()\n        return self._cq_obj\n\n    @property\n    def length(self):\n        return self.length_u * GRU\n\n    @property\n    def width(self):\n        return self.width_u * GRU\n\n    @property\n    def height(self):\n        return 3.8 + GRHU * self.height_u\n\n    @property\n    def int_height(self):\n        h = self.height - GR_LIP_H - GR_BOT_H\n        if self.lite_style:\n            return h + self.wall_th\n        return h\n\n    @property\n    def max_height(self):\n        return self.int_height + GR_UNDER_H + GR_TOPSIDE_H\n\n    @property\n    def floor_h(self):\n        if self.lite_style:\n            return GR_FLOOR - self.wall_th\n        return GR_FLOOR\n\n    @property\n    def lip_width(self):\n        if self.no_lip:\n            return self.wall_th\n        return GR_UNDER_H + self.wall_th\n\n    @property\n    def outer_l(self):\n        return self.length_u * GRU - GR_TOL\n\n    @property\n    def outer_w(self):\n        return self.width_u * GRU - GR_TOL\n\n    @property\n    def outer_dim(self):\n        return self.outer_l, self.outer_w\n\n    @property\n    def inner_l(self):\n        return self.outer_l - 2 * self.wall_th\n\n    @property\n    def inner_w(self):\n        return self.outer_w - 2 * self.wall_th\n\n    @property\n    def inner_dim(self):\n        return self.inner_l, self.inner_w\n\n    @property\n    def half_l(self):\n        return (self.length_u - 1) * GRU2\n\n    @property\n    def half_w(self):\n        return (self.width_u - 1) * GRU2\n\n    @property\n    def half_dim(self):\n        return self.half_l, self.half_w\n\n    @property\n    def half_in(self):\n        return GRU2 - self.wall_th - GR_TOL / 2\n\n    @property\n    def outer_rad(self):\n        return GR_RAD - GR_TOL / 2\n\n    @property\n    def inner_rad(self):\n        return self.outer_rad - self.wall_th\n\n    @property\n    def under_h(self):\n        return GR_UNDER_H - (self.wall_th - GR_WALL)\n\n    @property\n    def safe_fillet_rad(self):\n        if not any([self.scoops, self.labels, self.length_div, self.width_div]):\n            return GR_FILLET\n        return min(GR_FILLET, (GR_UNDER_H + GR_WALL) - self.wall_th - 0.05)\n\n    @property\n    def grid_centres(self):\n        return [\n            (x * GRU, y * GRU)\n            for x in range(self.length_u)\n            for y in range(self.width_u)\n        ]\n\n    @property\n    def hole_centres(self):\n        return [\n            (x * GRU - GR_HOLE_DIST * i, -(y * GRU - GR_HOLE_DIST * j))\n            for x in range(self.length_u)\n            for y in range(self.width_u)\n            for i in (-1, 1)\n            for j in (-1, 1)\n        ]\n\n    def safe_fillet(self, obj, selector, rad):\n        if len(obj.edges(selector).vals()) > 0:\n            return obj.edges(selector).fillet(rad)\n        return obj\n\n    def filename(self, prefix=None, path=None):\n        \"\"\"Returns a descriptive readable filename which represents a Gridfinity object.\n        The filename can be optionally prefixed with arbitrary text and\n        an optional path prefix can also be specified.\"\"\"\n        from cqgridfinity import (\n            GridfinityBaseplate,\n            GridfinityBox,\n            GridfinityDrawerSpacer,\n            GridfinityRuggedBox,\n        )\n\n        if prefix is not None:\n            prefix = prefix\n        elif isinstance(self, GridfinityBaseplate):\n            prefix = \"gf_baseplate_\"\n        elif isinstance(self, GridfinityBox):\n            prefix = \"gf_box_\"\n            if self.lite_style:\n                prefix = prefix + \"lite_\"\n        elif isinstance(self, GridfinityDrawerSpacer):\n            prefix = \"gf_drawer_\"\n        elif isinstance(self, GridfinityRuggedBox):\n            prefix = \"gf_ribbox_\" if self.rib_style else \"gf_ruggedbox_\"\n        else:\n            prefix = \"\"\n        fn = \"\"\n        if path is not None:\n            fn = fn.replace(os.sep, \"\")\n            fn = path + os.sep\n        fn = fn + prefix\n        fn = fn + \"%dx%d\" % (self.length_u, self.width_u)\n        if isinstance(self, GridfinityBox):\n            fn = fn + \"x%d\" % (self.height_u)\n            if self.length_div and not self.solid:\n                fn = fn + \"_div%d\" % (self.length_div)\n            if self.width_div and not self.solid:\n                if self.length_div:\n                    fn = fn + \"x%d\" % (self.width_div)\n                else:\n                    fn = fn + \"_div_x%d\" % (self.width_div)\n            if abs(self.wall_th - GR_WALL) > 1e-3:\n                fn = fn + \"_%.2f\" % (self.wall_th)\n            if self.no_lip:\n                fn = fn + \"_basic\"\n            if self.holes:\n                fn = fn + \"_holes\"\n            if self.solid:\n                fn = fn + \"_solid\"\n            else:\n                if self.scoops:\n                    fn = fn + \"_scoops\"\n                if self.labels:\n                    fn = fn + \"_labels\"\n        elif isinstance(self, GridfinityRuggedBox):\n            fn = fn + \"x%d\" % (self.height_u)\n            if self._obj_label is not None:\n                fn = fn + \"_%s\" % (self._obj_label)\n            if self.front_handle or self.front_label:\n                fn = fn + \"_fr-\"\n                if self.front_handle:\n                    fn = fn + \"h\"\n                if self.front_label:\n                    fn = fn + \"l\"\n            if self.side_handles or self.side_clasps:\n                fn = fn + \"_sd-\"\n                if self.side_handles:\n                    fn = fn + \"h\"\n                if self.side_clasps:\n                    fn = fn + \"c\"\n            if self.stackable:\n                fn = fn + \"_stack\"\n            if self.lid_baseplate:\n                fn = fn + \"_lidbp\"\n            if self.lid_window:\n                fn = fn + \"_win\"\n        elif isinstance(self, GridfinityDrawerSpacer):\n            if self._obj_label is not None:\n                fn = fn + \"_%s\" % (self._obj_label)\n        elif isinstance(self, GridfinityBaseplate):\n            if self.ext_depth > 0:\n                fn = fn + \"x%.1f\" % (self.ext_depth)\n            if self.corner_screws:\n                fn = fn + \"_screwtabs\"\n        return fn\n\n    def save_step_file(self, filename=None, path=None, prefix=None):\n        fn = (\n            filename\n            if filename is not None\n            else self.filename(path=path, prefix=prefix)\n        )\n        if not fn.lower().endswith(\".step\"):\n            fn = fn + \".step\"\n        if isinstance(self.cq_obj, cq.Assembly):\n            self.cq_obj.save(fn)\n        else:\n            export_step_file(self.cq_obj, fn)\n\n    def save_stl_file(\n        self, filename=None, path=None, prefix=None, tol=1e-2, ang_tol=0.1\n    ):\n        fn = (\n            filename\n            if filename is not None\n            else self.filename(path=path, prefix=prefix)\n        )\n        if not fn.lower().endswith(\".stl\"):\n            fn = fn + \".stl\"\n        obj = self.cq_obj.val().wrapped\n        mesh = BRepMesh_IncrementalMesh(obj, tol, True, ang_tol, True)\n        mesh.Perform()\n        writer = StlAPI_Writer()\n        writer.Write(obj, fn)\n\n    def save_svg_file(self, filename=None, path=None, prefix=None):\n        fn = (\n            filename\n            if filename is not None\n            else self.filename(path=path, prefix=prefix)\n        )\n        if not fn.lower().endswith(\".svg\"):\n            fn = fn + \".svg\"\n        r = self.cq_obj.rotate((0, 0, 0), (0, 0, 1), 75)\n        r = r.rotate((0, 0, 0), (1, 0, 0), -90)\n        exporters.export(\n            r,\n            fn,\n            opt={\n                \"width\": 600,\n                \"height\": 400,\n                \"showAxes\": False,\n                \"marginTop\": 20,\n                \"marginLeft\": 20,\n                \"projectionDir\": (1, 1, 1),\n            },\n        )\n\n    def extrude_profile(self, sketch, profile, workplane=\"XY\", angle=None):\n        taper = profile[0][1] if isinstance(profile[0], (list, tuple)) else 0\n        zlen = profile[0][0] if isinstance(profile[0], (list, tuple)) else profile[0]\n        if abs(taper) > 0:\n            if angle is None:\n                zlen = zlen if ZLEN_FIX else zlen / SQRT2\n            else:\n                zlen = zlen / math.cos(math.radians(taper)) if ZLEN_FIX else zlen\n        r = cq.Workplane(workplane).placeSketch(sketch).extrude(zlen, taper=taper)\n        for level in profile[1:]:\n            if isinstance(level, (tuple, list)):\n                if angle is None:\n                    zlen = level[0] if ZLEN_FIX else level[0] / SQRT2\n                else:\n                    zlen = (\n                        level[0] / math.cos(math.radians(level[1]))\n                        if ZLEN_FIX\n                        else level[0]\n                    )\n                r = r.faces(\">Z\").wires().toPending().extrude(zlen, taper=level[1])\n            else:\n                r = r.faces(\">Z\").wires().toPending().extrude(level)\n        return r\n\n    @classmethod\n    def to_step_file(\n        cls,\n        length_u,\n        width_u,\n        height_u=None,\n        filename=None,\n        prefix=None,\n        path=None,\n        **kwargs\n    ):\n        \"\"\"Convenience method to create, render and save a STEP file representation\n        of a Gridfinity object.\"\"\"\n        obj = GridfinityObject.as_obj(cls, length_u, width_u, height_u, **kwargs)\n        obj.save_step_file(filename=filename, path=path, prefix=prefix)\n\n    @classmethod\n    def to_stl_file(\n        cls,\n        length_u,\n        width_u,\n        height_u=None,\n        filename=None,\n        prefix=None,\n        path=None,\n        **kwargs\n    ):\n        \"\"\"Convenience method to create, render and save a STEP file representation\n        of a Gridfinity object.\"\"\"\n        obj = GridfinityObject.as_obj(cls, length_u, width_u, height_u, **kwargs)\n        obj.save_stl_file(filename=filename, path=path, prefix=prefix)\n\n    @staticmethod\n    def as_obj(cls, length_u=None, width_u=None, height_u=None, **kwargs):\n        if \"GridfinityBox\" in cls.__name__:\n            obj = GridfinityBox(length_u, width_u, height_u, **kwargs)\n            if \"GridfinitySolidBox\" in cls.__name__:\n                obj.solid = True\n        elif \"GridfinityBaseplate\" in cls.__name__:\n            obj = GridfinityBaseplate(length_u, width_u, **kwargs)\n        elif \"GridfinityDrawerSpacer\" in cls.__name__:\n            obj = GridfinityDrawerSpacer(**kwargs)\n        return obj\n"
  },
  {
    "path": "cqgridfinity/gf_ruggedbox.py",
    "content": "#! /usr/bin/env python3\n#\n# Copyright (C) 2023  Michael Gale\n# This file is part of the cq-gridfinity python module.\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation\n# files (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\n# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\n# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n#\n# Gridfinity Rugged Boxes\n\nimport math\n\nimport cadquery as cq\nfrom cadquery.selectors import StringSyntaxSelector\nfrom cqkit import (\n    HasXCoordinateSelector,\n    HasYCoordinateSelector,\n    HasZCoordinateSelector,\n    VerticalEdgeSelector,\n    EdgeLengthSelector,\n    RadiusSelector,\n    FlatEdgeSelector,\n    rounded_rect_sketch,\n    recentre,\n    composite_from_pts,\n    rotate_x,\n    rotate_y,\n    rotate_z,\n    size_2d,\n    size_3d,\n    bounds_3d,\n    inverse_fillet,\n    inverse_chamfer,\n    Ribbon,\n)\n\n# from cqkit import Ribbon\nfrom cqgridfinity import *\nfrom .gf_helpers import *\n\n\nclass GridfinityRuggedBox(GridfinityObject):\n    def __init__(self, length_u, width_u, height_u, **kwargs):\n        super().__init__()\n        self.length_u = length_u\n        self.width_u = width_u\n        self.height_u = height_u\n        self.lid_height = 10\n        self.wall_vgrooves = True\n        self.front_handle = True\n        self.stackable = True\n        self.side_clasps = True\n        self.lid_baseplate = True\n        self.inside_baseplate = True\n        self.side_handles = True\n        self.front_label = True\n        self.label_length = None\n        self.label_height = None\n        self.label_th = GR_LABEL_TH\n        self.back_feet = True\n        self.hinge_width = GR_HINGE_SZ\n        self.hinge_bolted = False\n        self.rib_style = False\n        self._lid_window = False\n        self.window_th = 1.0\n        self.box_color = cq.Color(0.25, 0.25, 0.25)\n        self.lid_color = cq.Color(0.25, 0.5, 0.75)\n        self.handle_color = cq.Color(0.75, 0.5, 0.25)\n        self.latch_color = cq.Color(0.75, 0.5, 0.25)\n        self.hinge_color = cq.Color(0.75, 0.5, 0.25)\n        self.label_color = cq.Color(0.7, 0.7, 0.7)\n        self.window_color = cq.Color(0.9, 0.9, 0.9, 0.25)\n        for k, v in kwargs.items():\n            if k in self.__dict__:\n                self.__dict__[k] = v\n\n    def check_dimensions(self):\n        \"\"\"Verifies that the specified box dimensions are within specification.\"\"\"\n        assert self.length_u >= 3\n        assert self.width_u >= 3\n        assert self.height_u >= 4\n\n    @property\n    def box_length(self):\n        return self.length_u * GRU + 2 * GR_RBOX_WALL\n\n    @property\n    def int_length(self):\n        return self.length_u * GRU\n\n    @property\n    def box_width(self):\n        return self.width_u * GRU + 2 * GR_RBOX_WALL\n\n    @property\n    def int_width(self):\n        return self.width_u * GRU\n\n    @property\n    def clasp_pos(self):\n        return self.int_length / 2 - GRU2, self.int_width / 2 - GRU2\n\n    @property\n    def box_height(self):\n        return self.height_u * GRHU + 3\n\n    @property\n    def clasp_heights(self):\n        h0 = GR_RIB_CTR / 2 + GR_RIB_L / 2\n        h1 = h0 + GR_RIB_CTR\n        return [GR_RIB_L / 2, self.box_height - h0, self.box_height - h1]\n\n    @property\n    def side_clasp_centres(self):\n        xo = self.box_length / 2 + GR_RBOX_CHAN_D / 2\n        yo = self.clasp_pos[1]\n        return [(-xo, yo, 0), (xo, yo, 0), (-xo, -yo, 0), (xo, -yo, 0)]\n\n    @property\n    def front_clasp_centres(self):\n        xo = self.clasp_pos[0]\n        yo = self.box_width / 2 + GR_RBOX_CHAN_D / 2\n        return [(-xo, -yo, 0), (xo, -yo, 0)]\n\n    @property\n    def clasp_notch_points(self):\n        return [\n            (\n                x * GR_RBOX_CHAN_W / 2,\n                -GR_RBOX_CHAN_D / 2,\n                self.box_height - self.lid_height,\n            )\n            for x in (-1, 1)\n        ]\n\n    @property\n    def hinge_centres(self):\n        xo = self.box_length / 2 - GR_HINGE_CTR\n        yo = self.box_width / 2 + GR_RBOX_CWALL - GR_RBOX_WALL\n        zo = self.box_height\n        return [(-xo, yo, zo), (xo, yo, zo)]\n\n    @property\n    def align_centres(self):\n        ro = GR_RBOX_CHAN_D / 2 - GR_REG_W / 2\n        xo, xc = self.box_length / 2 - GRU, self.box_length / 2 + ro\n        yo, yc = self.box_width / 2 - GRU, self.box_width / 2 + ro\n        pts = [\n            (-xo, -yc, 0),\n            (xo, -yc, 0),\n            (-xc, -yo, 0),\n            (xc, -yo, 0),\n            (-xc, yo, 0),\n            (xc, yo, 0),\n        ]\n        rots = [0, 0, 90, 90, 90, 90]\n        return pts, rots\n\n    @property\n    def right_qtr_centre(self):\n        return (\n            self.box_length / 2 - GR_RBOX_WALL / 2 + 0.125,\n            -self.box_width / 2 + GR_RBOX_WALL / 2 - 0.125,\n            self.box_height,\n        )\n\n    @property\n    def left_qtr_centre(self):\n        return -self.right_qtr_centre[0], *self.right_qtr_centre[1:]\n\n    @property\n    def bottom_qtr_centres(self):\n        return self.qtr_centres(tol=0.25)\n\n    def qtr_centres(self, tol=0.25, at_height=0, front=True, back=True):\n        xo = self.box_length / 2 - GR_RBOX_WALL / 2 + tol\n        yo = self.box_width / 2 - GR_RBOX_WALL / 2 + tol\n        qd = {}\n        if front:\n            qd[\"br\"] = (xo, -yo, at_height)\n            qd[\"bl\"] = (-xo, -yo, at_height)\n        if back:\n            qd[\"tr\"] = (xo, yo, at_height)\n            qd[\"tl\"] = (-xo, yo, at_height)\n        return qd\n\n    @property\n    def long_enough_for_handle(self):\n        return self.right_handle_centre[0] > GRU / 2\n\n    @property\n    def right_handle_centre(self):\n        zo = (self.box_height + self.lid_height) / 2\n        if (zo + GR_HANDLE_SZ / 2) > self.box_height:\n            zo = self.box_height / 2\n        return self.box_length / 2 - GR_HANDLE_OFS, -self.box_width / 2, zo\n\n    @property\n    def left_handle_centre(self):\n        return -self.right_handle_centre[0], *self.right_handle_centre[1:]\n\n    @property\n    def back_corner_centres(self):\n        xo = self.box_length / 2 - GR_RBOX_BACK_L / 2 + GR_RBOX_CWALL - GR_RBOX_WALL\n        yo = self.box_width / 2 - GR_RBOX_CORNER_W / 2 + GR_RBOX_CWALL - GR_RBOX_WALL\n        return [(-xo, yo, 0), (xo, yo, 0)]\n\n    @property\n    def front_corner_centres(self):\n        xo = self.box_length / 2 - GR_RBOX_FRONT_L / 2 + GR_RBOX_CWALL - GR_RBOX_WALL\n        yo = -self.back_corner_centres[0][1]\n        return [(-xo, yo, 0), (xo, yo, 0)]\n\n    @property\n    def label_centre(self):\n        zo = self.left_handle_centre[2]\n        zt = zo + self.label_size()[1] / 2\n        # ensure the front label fits vertically\n        if zt > self.box_height:\n            zo = self.box_height / 2\n        return (0, -self.box_width / 2, zo)\n\n    @property\n    def lid_window(self):\n        return self._lid_window\n\n    @lid_window.setter\n    def lid_window(self, enable):\n        self._lid_window = enable\n        if self._lid_window:\n            self.lid_baseplate = False\n\n    def lid_window_size(self, width_ext=None, tol=None):\n        tol = tol if tol is not None else GR_TOL\n        width_ext = width_ext if width_ext is not None else 4\n        return self.length - 2 - tol, self.width + width_ext - tol\n\n    def lid_window_hole_pos(self, z=0):\n        pts = [\n            (-x * (self.box_length / 2 - GR_RBOX_CORNER_W), self.width / 2 + 2, z)\n            for x in (-1, 1)\n        ]\n        if self.rib_style:\n            pts.append((0, self.width / 2 + 2, z))\n        return pts\n\n    def label_size(self, as_insert=False, as_aperture=False, tol=0):\n        # use provided label size if applicable otherwise auto size\n        if self.label_length is not None:\n            length = self.label_length\n        else:\n            length = self.box_length - 2 * GR_RBOX_CORNER_W + (GR_RBOX_CWALL) / 2\n        if self.label_height is not None:\n            height = self.label_height\n        else:\n            height = GR_LABEL_H\n        # ensure the label is not too tall\n        if height >= self.box_height:\n            height = self.box_height - 5\n        # trim label size if handles are enabled\n        if self.front_handle and self.long_enough_for_handle:\n            length = length - 2 * (GR_HANDLE_SEP + GR_HANDLE_W)\n        # return the desired size variant\n        if as_insert:\n            length -= 5\n        if as_aperture:\n            length -= 8\n            height -= 8\n        return length - 2 * tol, height - 2 * tol\n\n    def body_shell(self, as_lid=False):\n        \"\"\"General purpose render function for both the box and the lid.\"\"\"\n        height = self.box_height if not as_lid else self.lid_height\n        # render overall box shape\n        rs = rounded_rect_sketch(self.box_length, self.box_width, GR_RAD)\n        r = cq.Workplane(\"XY\").placeSketch(rs).extrude(height)\n        # back corners\n        if self.rib_style:\n            lb = self.box_length + 2 * (GR_RBOX_CWALL - GR_RBOX_WALL)\n            yo = self.back_corner_centres[0]\n            rc = cq.Workplane(\"XY\").rect(lb, GR_RBOX_CORNER_W).extrude(height)\n            r = r.union(rc.translate((0, yo[1], 0)))\n            if not as_lid or (as_lid and not self.side_handles):\n                h = height / 2 if self.side_handles else height\n                wb = self.box_width - GR_RBOX_CORNER_W\n                rc = cq.Workplane(\"XY\").rect(lb, wb).extrude(h)\n                r = r.union(rc)\n        else:\n            rc = (\n                cq.Workplane(\"XY\")\n                .rect(GR_RBOX_BACK_L, GR_RBOX_CORNER_W)\n                .extrude(height)\n            )\n            r = r.union(composite_from_pts(rc, self.back_corner_centres))\n        # front corners\n        rc = cq.Workplane(\"XY\").rect(GR_RBOX_FRONT_L, GR_RBOX_CORNER_W).extrude(height)\n        r = r.union(composite_from_pts(rc, self.front_corner_centres))\n        # fillet external edges\n        vs = VerticalEdgeSelector()\n        cs = StringSyntaxSelector(\"(<XY) or (>X and <Y) or (<X and >Y) or (>XY)\")\n        r = r.edges(vs - cs).fillet(GR_RBOX_RAD).edges(cs).fillet(GR_RBOX_CRAD)\n\n        if self.stackable or as_lid:\n            # bottom stacking mates\n            for k, v in self.qtr_centres(back=not as_lid).items():\n                rq = quarter_circle(\n                    GR_BREG_R0, GR_BREG_R1, GR_REG_H + 0.5, k, chamf=0, ext=0.25\n                )\n                r = r.cut(rq.translate(v))\n            pts, rots = self.align_centres\n            for pt, rot in zip(pts, rots):\n                rc = chamf_rect(GR_REG_L, GR_REG_W, GR_REG_H, angle=rot)\n                r = r.cut(rc.translate(pt))\n\n        # chamfer top edges\n        r = r.edges(\">Z\").chamfer(GR_RBOX_VCUT_D)\n\n        # front lid overhang\n        if as_lid:\n            w = min(GR_LID_HANDLE_W, self.box_length - 2 * GR_RBOX_FRONT_L)\n            r = r.union(self.lid_handle(width=w).translate((0, -self.box_width / 2, 0)))\n            hw = w / 2\n            vs = VerticalEdgeSelector([9]) & HasXCoordinateSelector([-hw, hw])\n            r = r.edges(vs).fillet(2.5 - EPS)\n\n        # chamfer cuts\n        if self.wall_vgrooves:\n            if self.rib_style:\n                r = r.cut(self.render_vcut())\n            else:\n                r = r.intersect(self.render_vcut())\n\n        # chamfer bottom edges\n        r = r.edges(\"<Z\").chamfer(GR_RBOX_VCUT_D)\n\n        # apply rib style cutouts if applicable\n        if self.rib_style and not as_lid:\n            r = r.intersect(self.rib_style_cut())\n\n        # add clasp features\n        rc = self.clasp_cut(as_lid=as_lid)\n        if self.side_clasps:\n            for pt in self.side_clasp_centres:\n                r = r.cut(rc.translate(pt))\n                side = \"left\" if pt[0] < 0 else \"right\"\n                r = r.union(self.clasp_ribs(side=side, as_lid=as_lid).translate(pt))\n        rc = rotate_z(rc, 90)\n        for pt in self.front_clasp_centres:\n            r = r.cut(rc.translate(pt))\n            r = r.union(self.clasp_ribs(side=\"front\", as_lid=as_lid).translate(pt))\n        return r\n\n    def render_vcut(self):\n        \"\"\"Renders a matching box shape with side v-cuts to intersect with main box.\"\"\"\n        # rib style implements v-notches along the clasp channels\n        if self.rib_style:\n            rc = cq.Workplane(\"XY\").rect(2, 2).extrude(SQRT2, taper=45)\n            rc = rc.rotate_x(-90)\n            rc = composite_from_pts(rc, self.clasp_notch_points)\n            r = composite_from_pts(rc, self.front_clasp_centres)\n            if self.side_clasps:\n                for pt in self.side_clasp_centres:\n                    if pt[0] < 0:\n                        r = r.union(rc.rotate_z(-90).translate(pt))\n                    else:\n                        r = r.union(rc.rotate_z(90).translate(pt))\n            return r\n        else:\n            xl = self.box_length + 2 * GR_RBOX_CWALL - 2 * GR_RBOX_WALL\n            yl = self.box_width + 2 * GR_RBOX_CWALL - 2 * GR_RBOX_WALL\n            lead_height = self.lid_height - GR_RBOX_VCUT_D\n            mid_height = self.box_height - 2 * (self.lid_height + GR_RBOX_VCUT_D)\n            cut_half = GR_RBOX_VCUT_D * SQRT2\n            profile = [\n                lead_height,\n                (cut_half, 45),\n                (cut_half, -45),\n                mid_height,\n                (cut_half, 45),\n                (cut_half, -45),\n                lead_height,\n            ]\n            rs = rounded_rect_sketch(xl, yl, GR_RBOX_CRAD)\n            return self.extrude_profile(rs, profile)\n\n    def rib_style_cut(self):\n        \"\"\"Render cutouts for a rib style box\"\"\"\n        xl = self.box_length + 2 * GR_RBOX_CWALL - 2 * GR_RBOX_WALL\n        yl = self.box_width + 2 * GR_RBOX_CWALL - 2 * GR_RBOX_WALL\n        wd = GR_RBOX_CWALL - GR_RBOX_WALL\n        lead_height = self.lid_height - GR_RBOX_VCUT_D\n        cut_half = wd * SQRT2\n        mid_height = self.box_height - 2 * (lead_height + wd)\n        profile = [\n            lead_height,\n            (cut_half, 45),\n            mid_height,\n            (cut_half, -45),\n            lead_height,\n        ]\n        rs = rounded_rect_sketch(xl, yl, GR_RBOX_CRAD)\n        r = self.extrude_profile(rs, profile)\n        w = GR_RBOX_CHAN_W + 3 * GR_RBOX_WALL\n        rc = cq.Workplane(\"XY\").rect(GR_RBOX_CHAN_D, w).extrude(self.box_height)\n        if self.side_clasps:\n            for pt in self.side_clasp_centres:\n                r = r.union(rc.translate(pt))\n        else:\n            rd = (\n                cq.Workplane(\"XY\")\n                .rect(GR_RBOX_CHAN_D, 1.5 * GR_RBOX_WALL)\n                .extrude(self.box_height)\n            )\n            xo = self.box_length / 2 + GR_RBOX_CHAN_D / 2\n            yo = self.clasp_pos[1] + GR_RBOX_CHAN_W / 2 + 1.5 * GR_RBOX_WALL / 2\n            for pt in [(x * xo, y * yo, 0) for x in (-1, 1) for y in (-1, 1)]:\n                r = r.union(rd.translate(pt))\n        rc = rotate_z(rc, 90)\n        for pt in self.front_clasp_centres:\n            r = r.union(rc.translate(pt))\n        w = 1.5 * GR_RBOX_WALL\n        rc = cq.Workplane(\"XY\").rect(w, wd).extrude(self.box_height)\n        for pt in self.hinge_centres:\n            r = r.union(rc.translate((pt[0] - GR_HINGE_SZ / 2 - w, pt[1] - wd / 2, 0)))\n            r = r.union(rc.translate((pt[0] + GR_HINGE_SZ / 2 + w, pt[1] - wd / 2, 0)))\n        yo = self.box_width / 2 + GR_RBOX_CWALL - GR_RBOX_WALL - wd / 2\n        for x in range(self.length_u):\n            xo = -self.int_length / 2 + x * GRU\n            if abs(xo) < (self.box_length / 2 - GR_RBOX_BACK_L):\n                r = r.union(rc.translate((xo, yo, 0)))\n        if not self.side_handles:\n            xo = self.box_length / 2 + GR_RBOX_CWALL - GR_RBOX_WALL - wd / 2\n            rc = rotate_z(rc, 90)\n            ylim = self.int_width / 2\n            if self.side_clasps:\n                ylim -= GR_RBOX_CORNER_W\n            for y in range(self.width_u):\n                yo = -self.int_width / 2 + y * GRU\n                if abs(yo) < ylim:\n                    r = r.union(rc.translate((xo, yo, 0)))\n                    r = r.union(rc.translate((-xo, yo, 0)))\n        hm = self.box_height - 2 * lead_height\n        r = r.edges(VerticalEdgeSelector([mid_height, hm])).fillet(1)\n        return r\n\n    def lid_handle(self, width=None):\n        \"\"\"Renders the front overhanging handle lip for the lid.\"\"\"\n        width = width if width is not None else GR_LID_HANDLE_W\n        l0, l1, h1, h2, hw = 3, 5, 4, self.lid_height - GR_RBOX_VCUT_D, width / 2\n        rs = (\n            cq.Sketch()\n            .segment((l0, 0), (-l1, 0))\n            .segment((-l1, h1))\n            .segment((l0, h2 + l0))\n            .close()\n            .assemble()\n        )\n        r = cq.Workplane(\"YZ\").placeSketch(rs).extrude(width).translate((-hw, 0, 0))\n        vs = VerticalEdgeSelector([h1]) & HasXCoordinateSelector([-hw, hw])\n        r = r.edges(vs).fillet(2.45).faces(\"<Z\").shell(-2.5)\n        vs = VerticalEdgeSelector(3) & HasYCoordinateSelector(-l1 + 2.5)\n        r = r.edges(vs).fillet(1)\n        rc = cq.Workplane(\"XY\").rect(4 * hw, 4 * hw).extrude(self.lid_height)\n        r = r.intersect(rc)\n        return r\n\n    def side_handle(self, width=None):\n        \"\"\"Renders the handles for the left and right box sides.\"\"\"\n        width = width if width is not None else GR_LID_HANDLE_W\n        l0, l1, h1, hw = GR_RBOX_WALL, 7, 4, width / 2\n        l2 = GR_RBOX_WALL / 2\n        h2 = self.lid_height - GR_RBOX_VCUT_D + 2\n        # handle shape\n        rs = (\n            cq.Sketch()\n            .segment((l0, 0), (-l1, 0))\n            .segment((-l1, h1))\n            .segment((l0, h2 + l0))\n            .close()\n            .assemble()\n        )\n        r = cq.Workplane(\"YZ\").placeSketch(rs).extrude(width)\n        # vertical under support\n        rs = (\n            cq.Sketch()\n            .segment((0, 0), (0, -h2 + 2.5))\n            .segment((-l1, 0.5))\n            .segment((-l1, h1))\n            .segment((0, h2))\n            .close()\n            .assemble()\n        )\n        rw = cq.Workplane(\"YZ\").placeSketch(rs).extrude(2.5)\n        rw = inverse_fillet(\n            rw, \">Y\", 5, (StringSyntaxSelector(\"<Z\") & EdgeLengthSelector(GR_RBOX_WALL))\n        )\n        rh = []\n        bs = VerticalEdgeSelector() & (HasYCoordinateSelector(\"<0\"))\n        for coord in [[0, 2.5], [0], [2.5]]:\n            es = bs & HasXCoordinateSelector(coord, min_points=2)\n            rh.append(rw.edges(es - HasZCoordinateSelector(\">4\")).chamfer(0.5))\n        r = r.faces(\"<Z\").shell(-2.5)\n        bs = (\n            HasZCoordinateSelector(0, min_points=2)\n            - EdgeLengthSelector(\"<%.1f\" % (width - 2.5))\n            - HasYCoordinateSelector(\">=0\")\n        )\n        r = r.edges(bs).chamfer(0.5).translate((-hw, 0, -2))\n        r = r.union(rh[2].translate((-hw, 0, -2)))\n        if width > GR_LID_HANDLE_W / 2:\n            r = r.union(rh[0].translate((-l2, 0, -2)))\n        r = r.union(rh[1].translate((hw - GR_RBOX_WALL, 0, -2)))\n        vs = VerticalEdgeSelector([h1 - 0.5]) & HasXCoordinateSelector([-hw, hw])\n        r = r.edges(vs).fillet(2)\n        vs = VerticalEdgeSelector(2.9) & HasYCoordinateSelector(-l1 + GR_RBOX_WALL)\n        r = r.edges(vs).fillet(1)\n        rc = cq.Workplane(\"XY\").rect(4 * hw, 4 * hw).extrude(self.lid_height + 2 * h2)\n        r = r.intersect(rc.translate((0, 0, -2 * h2)))\n        return r\n\n    def label_slot(self):\n        \"\"\"Renders the front label holder.\"\"\"\n        rs = rounded_rect_sketch(*self.label_size(), GR_RAD)\n        r = self.extrude_profile(rs, [(GR_LABEL_SLOT_TH * SQRT2, 45)], workplane=\"XZ\")\n        rc = (\n            cq.Workplane(\"XZ\")\n            .rect(*self.label_size(as_aperture=True))\n            .extrude(GR_LABEL_SLOT_TH)\n        )\n        r = r.cut(rc.edges(EdgeLengthSelector(GR_LABEL_SLOT_TH)).chamfer(2.5))\n        xl, yl = self.label_size(as_insert=True)\n        xl -= 8\n        rc = cq.Workplane(\"XZ\").rect(xl, yl).extrude(GR_LABEL_SLOT_TH)\n        r = r.cut(rc.translate((0, 0, 5)))\n        rc = (\n            cq.Workplane(\"XZ\")\n            .rect(*self.label_size(as_insert=True))\n            .extrude(GR_LABEL_SLOT_TH / 2)\n        )\n        rc = rc.edges(\"|Y and <Z\").fillet(GR_LABEL_SLOT_TH / 2)\n        r = r.cut(rc.translate((0, 0, GR_LABEL_SLOT_TH)))\n\n        # simple restraining ramps to prevent the label slipping out\n        rc = (\n            cq.Workplane(\"XZ\")\n            .rect(10, 2.5)\n            .extrude(1.25)\n            .edges(\"<Y\")\n            .chamfer(1.25 - EPS)\n        )\n        pts = [(-xl / 4, 0, yl / 2 - 2.0), (xl / 4, 0, yl / 2 - 2.0)]\n        if self.length_u < 5:\n            pts = [(0, 0, yl / 2 - 2.0)]\n        for pt in pts:\n            r = r.union(rc.translate(pt))\n        return r\n\n    def render_label(self):\n        \"\"\"Renders a label panel insert\"\"\"\n        rs = rounded_rect_sketch(*self.label_size(tol=3), GR_RAD)\n        r = cq.Workplane(\"XZ\").placeSketch(rs).extrude(self.label_th)\n        self._obj_label = \"label\"\n        self._cq_obj = r\n        return self._cq_obj\n\n    def clasp_cut(self, as_lid=False):\n        \"\"\"Renders the vertical channel where the clasps / latch are installed.\"\"\"\n        height = GR_CLASP_SLIDE_D + 6 if as_lid else self.box_height\n        w = GR_RBOX_CHAN_W + GR_CLASP_SLIDE_W\n        rs = cq.Sketch().slot(GR_CLASP_SLIDE_D, GR_CLASP_SLIDE_W, angle=90)\n        rs = cq.Workplane(\"XZ\").placeSketch(rs).extrude(w).translate((0, w / 2, 0))\n        rc = cq.Workplane(\"XY\").rect(GR_RBOX_CHAN_D, GR_RBOX_CHAN_W).extrude(height)\n        zo = -GR_CLASP_SLIDE_D / 2 + GR_CLASP_SLIDE_W / 2\n        # ensure clasp channel is deep enough for box heights <6U\n        height = max(height, GR_CLASP_SLIDE_D + 5.2)\n        pts = [(0, 0, height + zo), (0, 0, zo)]\n        return rc.union(composite_from_pts(rs, pts))\n\n    def clasp_rib(self, chamfered=False):\n        \"\"\"Renders a single clasp rib feature.\"\"\"\n        r = cq.Workplane(\"XY\").rect(GR_RIB_L, GR_RIB_W).extrude(GR_RIB_H)\n        r = r.faces(\">Z\").edges(\"<X or >X\").chamfer(1.0)\n        if chamfered:\n            rc = (\n                cq.Workplane(\"XZ\")\n                .moveTo(0, 0)\n                .lineTo(0, GR_RIB_H)\n                .lineTo(GR_RIB_L / 6, GR_RIB_H)\n                .close()\n                .extrude(GR_RIB_W)\n            )\n            r = r.cut(rc.translate((-GR_RIB_L / 1.85, GR_RIB_W / 2, 0)))\n            rc = cq.Workplane(\"XY\").rect(GR_RIB_L / 2, GR_RIB_W).extrude(GR_RIB_H / 3)\n            rc = rc.faces(\">Z\").edges(\"<X or >X\").chamfer(GR_RIB_H / 3 - EPS)\n            r = r.union(rc.translate((-GR_RIB_L / 2.33, 0, 0)))\n        return r\n\n    def clasp_ribs(self, side=\"left\", as_lid=False):\n        \"\"\"Renders a group of clasp ribs for any side for both the box and lid.\"\"\"\n        y1 = GR_RIB_SEP / 2 + GR_RIB_W / 2\n        y2 = y1 + GR_RIB_W + GR_RIB_GAP\n        zo = -GR_RBOX_CHAN_D / 2\n        pts = [(0, -y2, zo), (0, -y1, zo), (0, y1, zo), (0, y2, zo)]\n        rh = composite_from_pts(self.clasp_rib(), pts)\n        rc = composite_from_pts(self.clasp_rib(chamfered=True), pts)\n        if self.stackable or as_lid:\n            r = rh.translate((self.clasp_heights[0], 0, 0))\n        if not as_lid:\n            rc = composite_from_pts(rc, [(h, 0, 0) for h in self.clasp_heights[1:]])\n            if not self.stackable:\n                r = rc\n            else:\n                r = r.union(rc)\n        r = rotate_y(r, -90)\n        if side == \"front\":\n            r = rotate_z(r, 90)\n        elif side == \"right\":\n            r = rotate_z(r, 180)\n        return r\n\n    def handle_mount(self, side=\"left\"):\n        \"\"\"Mounting features for front handle\"\"\"\n\n        def _bracket(small_hole=False, side=\"left\"):\n            l1 = GR_HANDLE_L1 / 2\n            l2 = min(GR_HANDLE_L2 / 2, (self.box_height - 6) / 2)\n            d2 = M3_DIAM / 2 if small_hole else M3_CLR_DIAM / 2\n            rs = (\n                cq.Sketch()\n                .segment((0, 0), (-l2, 0))\n                .segment((-l1, GR_HANDLE_H))\n                .segment((l1, GR_HANDLE_H))\n                .segment((l2, 0))\n                .close()\n                .assemble()\n                .vertices(\">Y\")\n                .vertices(\"<X or >X\")\n                .fillet(GR_RAD)\n                .reset()\n                .push([(0, GR_HANDLE_H / 2)])\n                .circle(d2, mode=\"s\")\n            )\n            r = cq.Workplane(\"YZ\").placeSketch(rs).extrude(GR_HANDLE_W)\n            if not small_hole:\n                face = \">X\" if side == \"left\" else \"<X\"\n                r = (\n                    r.faces(face)\n                    .workplane()\n                    .pushPoints([(0, GR_HANDLE_H / 2)])\n                    .hole(M3_CB_DIAM, M3_CB_DEPTH)\n                )\n\n            r = inverse_fillet(r, \"<Z\", GR_RAD, EdgeLengthSelector(GR_HANDLE_W))\n            r = r.faces(\">Z\").chamfer(0.75)\n            return rotate_x(r, 90)\n\n        h1 = _bracket(small_hole=True, side=side)\n        h2 = _bracket(small_hole=False, side=side)\n        xo = GR_HANDLE_SEP if side == \"left\" else -GR_HANDLE_SEP\n        r = recentre(h1.union(h2.translate((xo, 0, 0))), \"xz\")\n        return r\n\n    def render_handle(self):\n        \"\"\"Renders the front handle\"\"\"\n        self.check_dimensions()\n        x2 = self.right_handle_centre[0]\n        if not self.long_enough_for_handle:\n            print(\"Rugged box length dimension too small to include a handle\")\n            return None\n        wt, h, rh = GR_HANDLE_TH, GR_HANDLE_SZ, GR_HANDLE_RAD\n        lt, ht = (2 * x2) - 2 * rh, h - rh - wt / 2\n        path = {\n            \"start\": \"(%f,%f) dir:-90 width:%f\" % (x2, h, wt),\n            \"path\": \"L:%f A:%f,90 L:%f A:%f,90 L:%f\" % (ht, rh, lt, rh, ht),\n        }\n        cw = Ribbon(\"XZ\", path)\n        cw.direction = -90\n        r = cw.render().extrude(wt).faces(\">Z\").edges(\"|X\").fillet(wt / 2 - EPS)\n        r = recentre(r.edges().chamfer(1), \"XY\")\n        rc = cq.Workplane(\"YZ\").circle(M3_CLR_DIAM / 2).extrude(8 * lt)\n        r = r.cut(rc.translate((-4 * lt, 0, h - M3_CLR_DIAM)))\n        self._obj_label = \"handle\"\n        self._cq_obj = r\n        return self._cq_obj\n\n    def render_back_foot(self):\n        \"\"\"Renders a corresponding rear foot the same depth as the hinge for standing\n        the box vertically.\"\"\"\n        rs = cq.Sketch().slot(2 * GR_HINGE_OFFS, 2 * GR_HINGE_RAD, 0)\n        rc = cq.Workplane(\"YZ\").placeSketch(rs).extrude(self.hinge_width - 0.4)\n        return recentre(rc).edges().chamfer(1).translate((0, 0, GR_HINGE_RAD))\n\n    def hinge_mount(self):\n        \"\"\"Mounting cutout for hinge\"\"\"\n        l1, l2, l3 = self.hinge_width + 2, self.hinge_width, (self.hinge_width - 2) / 2\n        r = cq.Workplane(\"XY\").rect(l1, GR_HINGE_W1).extrude(GR_HINGE_H1)\n        r = r.translate((0, -GR_HINGE_W1 / 2, -GR_HINGE_H1))\n        r2 = cq.Workplane(\"XY\").rect(l2, GR_HINGE_W2).extrude(GR_HINGE_H2)\n        r2 = r2.translate((0, -GR_HINGE_D - GR_HINGE_W2 / 2, -GR_HINGE_H2))\n        bs = HasZCoordinateSelector(-GR_HINGE_H1) & EdgeLengthSelector(\n            [l2, GR_HINGE_W2]\n        )\n        r = r.union(r2).edges(bs).edges(\">Y or <X or >X\").chamfer(0.75)\n        rs = rounded_rect_sketch(l3, GR_HINGE_W3, 0.5)\n        r3 = cq.Workplane(\"XY\").placeSketch(rs).extrude(GR_HINGE_H2)\n        xo, yo = GR_HINGE_SEP / 2 + l3 / 2, -GR_HINGE_W1 - 1.2 - GR_HINGE_W3 / 2\n        rh = self.hex_cut().translate(\n            (0, 0, GR_HINGE_H2 - GR_HINGE_H1 - GR_HEX_H / 2 + GR_HINGE_SKEW)\n        )\n        for pt in [(-xo, yo, -GR_HINGE_H2), (xo, yo, -GR_HINGE_H2)]:\n            r = r.union(r3.translate(pt))\n            r = r.union(rh.translate(pt))\n        return r\n\n    def hex_cut(self, depth=None):\n        \"\"\"Hexagonal shaped latch for hinge attachment\"\"\"\n        l1 = 2 if depth is None else 1.7\n        l2 = 3.5 if depth is None else 3.0\n        d = depth if depth is not None else 4.0\n        h = GR_HEX_H if depth is None else GR_HEX_H - 0.4\n        rs = (\n            cq.Sketch()\n            .segment((0, 0), (-l1, 0))\n            .segment((-l2, h / 2))\n            .segment((-l1, h))\n            .segment((l1, h))\n            .segment((l2, h / 2))\n            .segment((l1, 0))\n            .close()\n            .assemble()\n        )\n        r = cq.Workplane(\"XZ\").placeSketch(rs).extrude(d).translate((0, d, -h / 2))\n        if depth is not None:\n            r = r.edges(\"<Z and >Y\").chamfer(depth - EPS)\n        return r\n\n    def render_latch(self):\n        \"\"\"Renders the latch element used to secure the box and the lid.\"\"\"\n        l2, w2, h2 = GR_LATCH_L / 2, GR_LATCH_W / 2, GR_LATCH_H / 2\n        c2, th = GR_RIB_CTR / 2, 2.5\n        hf = GR_LATCH_H - th\n        yc = (-1.575, 1.575)\n        r = cq.Workplane(\"XY\").rect(GR_LATCH_L, GR_LATCH_W).extrude(GR_LATCH_H)\n        r = r.edges(\"|Y\").edges(\">X\").chamfer(1.0)\n        rs = cq.Sketch().slot(10, GR_LATCH_H, 0)\n        rc = cq.Workplane(\"XZ\").placeSketch(rs).extrude(GR_LATCH_W)\n        r = r.union(rc.translate((-l2 + 4.5, w2, h2)))\n        rc = cq.Workplane(\"XY\").rect(16, 15.6).extrude(10).edges(\"|Z\").fillet(4.0)\n        r = r.cut(rc.translate((-l2 - 8, 0, 0)))\n\n        rc = cq.Workplane(\"XY\").rect(5, GR_LATCH_W - 2.4).extrude(10)\n        rc = rc.faces(\"<Z\").edges(\"|X\").fillet(1.5).edges(\"|Z\").fillet(1.0)\n        r = r.cut(rc.translate((l2, 0, 2.0))).edges().chamfer(0.25)\n\n        rc = cq.Workplane(\"XY\").rect(GR_LATCH_IL, GR_LATCH_IW).extrude(hf)\n        for x in (-GR_RIB_CTR, 0, GR_RIB_CTR):\n            r = r.cut(rc.translate((x - 1.25, 0, th)))\n        r = r.faces(\">Z\").edges(EdgeLengthSelector(GR_LATCH_IW)).chamfer(1.5)\n        r = r.faces(\">Z\").edges(EdgeLengthSelector(GR_LATCH_IL)).chamfer(0.25)\n\n        rc = cq.Workplane(\"XY\").rect(20, 2.4).extrude(hf)\n        r = r.cut(rc.translate((0, 0, th)))\n        r = r.faces(\">Z\").edges(EdgeLengthSelector(1.8)).edges(\"|X\").chamfer(0.25)\n\n        rc = cq.Workplane(\"XY\").rect(8.5, 0.75).extrude(4.5)\n        rc = rc.faces(\">Z\").edges(\"|Y\").chamfer(1.5)\n        bs = EdgeLengthSelector(\">0.8\") - HasZCoordinateSelector(0, min_points=2)\n        rc = rc.edges(bs).chamfer(0.2)\n        (_, _, _), (xm, _, _) = bounds_3d(r)\n        for pt in [(x - 1.25, y, th) for x in (-c2, c2) for y in yc]:\n            r = r.union(rc.translate(pt))\n\n        rd = cq.Workplane(\"XY\").rect(3.5, 1).extrude(7)\n        for x, xo in [(-xm, 2.25), (13.75, -2.25)]:\n            rx = rc.intersect(rd.translate((xo, 0, 0)))\n            for pt in [(x, y, th) for y in yc]:\n                r = r.union(rx.translate(pt))\n\n        rc = cq.Workplane(\"XZ\").rect(2, 3.2).extrude(0.6).edges(\"<Y\").chamfer(0.6 - EPS)\n        xo = xm - self.lid_height\n        for angle, y in [(0, -w2), (180, w2)]:\n            r = r.union(rotate_z(rc, angle).translate((xo, y, h2)))\n\n        rc = cq.Workplane(\"XZ\").rect(6.0, 0.4).extrude(-1.6)\n        for pt in [(xo, y, h2 + z) for y in (-w2, w2 - 1.6) for z in (-2.1, 2.1)]:\n            r = r.cut(rc.translate(pt))\n        r = (\n            r.edges(HasYCoordinateSelector([-w2, w2], min_points=2))\n            .edges(EdgeLengthSelector([6.0, 0.4]))\n            .chamfer(0.3 - EPS)\n        )\n\n        rc = cq.Workplane(\"XZ\").circle(3.8 / 2).extrude(2).faces(\"<Y\").chamfer(0.5)\n        re = cq.Workplane(\"XY\").rect(50, 50).extrude(20).translate((0, 0, -1.7))\n        rc = rc.intersect(rotate_x(re, -10))\n        for angle, y in [(0, -w2), (180, w2)]:\n            r = r.union(rotate_z(rc, angle).translate((-17.45, y, h2)))\n        self._cq_obj = rotate_z(recentre(r, \"xy\"), -90)\n        self._obj_label = \"latch\"\n        return self._cq_obj\n\n    def render_hinge(self, as_closed=False, section=None):\n        \"\"\"Renders the rear hinge.\"\"\"\n        tol = 0.125\n        cl = 2 * (GR_HINGE_OFFS + GR_HINGE_D + GR_HINGE_W2 / 2)\n        wh, dh = GR_HINGE_W2 - GR_HINGE_TOL, GR_HINGE_H2 - 1\n        ls, ws = cl / 2, GR_HINGE_H1 - GR_HINGE_TOL\n        h = self.hinge_width - GR_HINGE_TOL\n        h3 = h / 3\n        ha, hb, hc, hd = h3 - tol, h3 + tol, 2 * h3 - tol, 2 * h3 + tol\n        cro, cri, crb, crs = GR_HINGE_RAD + GR_HINGE_TOL, GR_HINGE_RAD, 4.5 / 2, 4.0 / 2\n        ctr = (cl / 2 + wh / 2, -GR_HINGE_SKEW)\n\n        def _bracket(side=\"left\"):\n            xo = wh / 2 if side == \"left\" else cl + wh / 2\n            r = cq.Workplane(\"XY\").rect(wh, dh).extrude(h).translate((xo, dh / 2, 0))\n            xo = ls / 2 if side == \"left\" else cl + wh - ls / 2\n            rc = cq.Workplane(\"XY\").rect(ls, ws).extrude(h).translate((xo, ws / 2, 0))\n            r = r.union(rc)\n            bs = VerticalEdgeSelector() & HasYCoordinateSelector(ws)\n            if side == \"left\":\n                r = r.edges(VerticalEdgeSelector()).edges(\"<XY\").chamfer(1.0)\n                bs = bs & HasXCoordinateSelector(wh)\n            else:\n                r = r.edges(VerticalEdgeSelector()).edges(\">X and <Y\").chamfer(1.0)\n                bs = bs & HasXCoordinateSelector(cl)\n            r = r.edges(bs).chamfer(1.1)\n            r = r.faces(\">Y\").edges(EdgeLengthSelector(wh)).chamfer(1.5)\n            return r\n\n        rl = _bracket(side=\"left\")\n        for pt in [0, hc]:\n            rl = rl.cut(chamf_cyl(cro, hb, 0).translate((*ctr, pt)))\n        rr = _bracket(side=\"right\")\n        rr = rr.cut(chamf_cyl(cro, hd - ha, 0).translate((*ctr, ha)))\n        bs = EdgeLengthSelector(\">0.2\") - EdgeLengthSelector([wh, h], tolerance=0.02)\n        bs = bs - HasYCoordinateSelector(dh - 1.5, min_points=2)\n        bs = bs - (RadiusSelector(cro) & HasZCoordinateSelector([ha, hb, hc, hd]))\n        rl = rl.edges(bs).chamfer(0.5)\n        rr = rr.edges(bs).chamfer(0.5)\n        rl = rl.union(chamf_cyl(cri, hc - hb).translate((*ctr, hb)))\n        if not self.hinge_bolted:\n            rl = rl.cut(chamf_cyl(crb, hc - hb, 0).translate((*ctr, hb)))\n\n        for pt in [0, hd]:\n            rr = rr.union(chamf_cyl(cri, ha).translate((*ctr, pt)))\n        if not self.hinge_bolted:\n            rr = rr.union(chamf_cyl(crs, h, 0).translate((*ctr, 0)))\n        else:\n            rr = rr.cut(chamf_cyl(M3_DIAM / 2, h, 0).translate((*ctr, 0)))\n            rl = rl.cut(chamf_cyl(M3_CLR_DIAM / 2, h, 0).translate((*ctr, 0)))\n            rr = rr.cut(chamf_cyl(M3_CLR_DIAM / 2, ha, 0).translate((*ctr, h - ha)))\n            rr = rr.cut(\n                chamf_cyl(M3_CB_DIAM / 2, M3_CB_DEPTH, 0).translate(\n                    (*ctr, h - M3_CB_DEPTH)\n                )\n            )\n        rx = recentre(self.hex_cut(depth=GR_HEX_D))\n        rh = rotate_x(rotate_z(rx, 90), 90)\n        xo = cl + wh + GR_HEX_D / 2\n        yo = GR_HINGE_H1 + GR_HEX_H / 2 - 2 * GR_HINGE_SKEW\n        zo = GR_HINGE_SEP / 2 + (self.hinge_width - 2) / 4\n        for pt in [(-GR_HEX_D / 2, yo, h / 2 - z) for z in (-zo, zo)]:\n            rl = rl.union(rh.translate(pt))\n        rh = rotate_x(rotate_z(rx, -90), 90)\n        for pt in [(xo, yo, h / 2 - z) for z in (-zo, zo)]:\n            rr = rr.union(rh.translate(pt))\n        if as_closed:\n            rl = rotate_z(rl.translate((-ctr[0], -ctr[1], 0)), 90)\n            rr = rotate_z(rr.translate((-ctr[0], -ctr[1], 0)), -90)\n        if section is not None:\n            r = rr if section == \"outer\" else rl\n        else:\n            r = rl.union(rr)\n        self._cq_obj = r\n        self._obj_label = \"hinge\"\n        return self._cq_obj\n\n    def render(self):\n        \"\"\"Renders the rugged box body shell.\"\"\"\n        self.check_dimensions()\n        r = self.body_shell(as_lid=False)\n\n        # hollow out\n        rc = (\n            cq.Workplane(\"XY\")\n            .placeSketch(rounded_rect_sketch(self.length, self.width, GR_RAD))\n            .extrude(self.box_height - GR_RBOX_FLOOR)\n        )\n        r = r.cut(rc.translate((0, 0, GR_RBOX_FLOOR)))\n\n        # add registration features\n        pts, rots = self.align_centres\n        for pt, rot in zip(pts, rots):\n            rc = chamf_rect(\n                GR_REG_L,\n                GR_REG_W,\n                GR_REG_H,\n                angle=rot,\n                z_offset=self.box_height,\n                tol=0.75,\n            )\n            r = r.union(rc.translate(pt))\n\n        rq = quarter_circle(GR_REG_R0, GR_REG_R1, GR_REG_H, \"bl\")\n        r = r.union(rq.translate(self.left_qtr_centre))\n        rq = quarter_circle(GR_REG_R0, GR_REG_R1, GR_REG_H, \"br\")\n        r = r.union(rq.translate(self.right_qtr_centre))\n\n        # add handle mounts\n        if self.front_handle and self.long_enough_for_handle:\n            r = r.union(\n                self.handle_mount(side=\"left\").translate(self.left_handle_centre)\n            )\n            r = r.union(\n                self.handle_mount(side=\"right\").translate(self.right_handle_centre)\n            )\n\n        # add hinge mounts\n        rc = self.hinge_mount()\n        for pt in self.hinge_centres:\n            r = r.cut(rc.translate(pt))\n\n        # add side handles\n        if self.side_handles:\n            w = min(GR_SIDE_HANDLE_W, self.box_width - 2 * GR_RBOX_CORNER_W)\n            rh = self.side_handle(width=w)\n            rl = rotate_z(rh, -90)\n            rr = rotate_z(rh, 90)\n            zo = self.box_height - self.lid_height\n            r = r.union(rl.translate((-self.box_length / 2, 0, zo)))\n            r = r.union(rr.translate((self.box_length / 2, 0, zo)))\n            hw, l2 = w / 2, self.box_length / 2\n            vs = HasXCoordinateSelector([-l2, l2]) & HasYCoordinateSelector([-hw, hw])\n            r = r.edges(\"|Z\").edges(vs).fillet(2.5)\n\n        # add front label slot\n        if self.front_label:\n            r = r.union(self.label_slot().translate(self.label_centre))\n\n        # back feet\n        if self.back_feet:\n            rc = self.render_back_foot()\n            for pt in self.hinge_centres:\n                r = r.union(rc.translate((pt[0], pt[1], 0)))\n\n        # add baseplate\n        if self.inside_baseplate:\n            rb = GridfinityBaseplate(self.length_u, self.width_u, ext_depth=1.6)\n            r = r.union(rb.render().translate((0, 0, GR_RBOX_FLOOR)))\n            r = r.edges(FlatEdgeSelector(GR_RBOX_FLOOR)).chamfer(0.8)\n        else:\n            rb = self.extrude_profile(\n                rounded_rect_sketch(self.length, self.width, GR_RAD), [GR_RBOX_WALL]\n            )\n            r = r.union(rb)\n        self._cq_obj = r\n        self._obj_label = \"body\"\n        return self._cq_obj\n\n    def render_lid(self):\n        \"\"\"Renders the rugged box lid.\"\"\"\n        self.check_dimensions()\n        r = self.body_shell(as_lid=True)\n\n        if self.lid_baseplate:\n            # hollow out top half\n            rs = rounded_rect_sketch(self.length - GR_TOL, self.width - GR_TOL, GR_RAD)\n            rc = self.extrude_profile(rs, [self.lid_height - 0.5, (1.0, -45)])\n            r = r.cut(rc)\n            # add topside baseplate\n            rb = GridfinityBaseplate(\n                self.length_u, self.width_u, ext_depth=0.4, straight_bottom=True\n            )\n            rb = rb.render()\n            r = r.union(rb.translate((0, 0, 4.7 - 0.4)))\n        elif self.lid_window:\n            # hollow out completely\n            rs = rounded_rect_sketch(self.length, self.width, GR_RAD)\n            rc = self.extrude_profile(rs, [5])\n            r = r.cut(rc)\n\n        # hollow out bottom\n        rs = rounded_rect_sketch(self.length, self.width, GR_RAD)\n        r = r.cut(cq.Workplane(\"XY\").placeSketch(rs).extrude(4.6))\n\n        # add modified bottom extrusion with a looser fit\n        if self.lid_baseplate:\n            rs = self.extrude_profile(\n                rounded_rect_sketch(35, 35, 0.8), [(2.82, -22.1), (5, -45)]\n            )\n            rs = rs.faces(\">Z\").shell(-1.2)\n        else:\n            rs = self.extrude_profile(\n                rounded_rect_sketch(35, 35, 0.8),\n                [(2.82, -22.1), (4.1, -45), (9, -85), 2],\n            )\n        ra = composite_from_pts(rs, self.grid_centres)\n        ra = ra.translate((-self.half_l, -self.half_w, 0))\n        rs = rounded_rect_sketch(self.length, self.width, GR_RAD)\n        ra = ra.intersect(cq.Workplane(\"XY\").placeSketch(rs).extrude(GR_LID_WINDOW_H))\n\n        r = r.union(ra)\n        r = r.edges(\n            EdgeLengthSelector(33.4) & HasZCoordinateSelector(0, min_points=2)\n        ).chamfer(0.75)\n\n        # add optional stackable features\n        if self.stackable:\n            for k, v in self.qtr_centres(tol=0.125, at_height=self.lid_height).items():\n                rq = quarter_circle(GR_REG_R0, GR_REG_R1, GR_REG_H, k)\n                r = r.union(rq.translate(v))\n\n        if self.lid_window:\n            # hollow the grid apertures\n            ht, tp = GR_LID_WINDOW_H, 34\n            he = GR_LID_WINDOW_H / math.cos(math.radians(tp))\n            rs = (\n                cq.Workplane(\"XY\")\n                .placeSketch(rounded_rect_sketch(30, 30, 1))\n                .extrude(he, taper=-tp)\n            )\n            ra = composite_from_pts(rs, self.grid_centres)\n            ra = ra.translate((-self.half_l, -self.half_w, 0))\n            r = r.cut(ra)\n\n            # window slot\n            ext = 20\n            l, w = self.lid_window_size(width_ext=-2 + ext, tol=0)\n            rs = rounded_rect_sketch(l, w, 0.5)\n            hlw = self.lid_height - GR_LID_WINDOW_H\n            ht = hlw - self.window_th - 0.5\n            rc = (\n                cq.Workplane(\"XY\")\n                .rect(l, w)\n                .workplane(offset=self.window_th)\n                .rect(l, w)\n                .workplane(offset=ht)\n                .rect(l - 6, w - 6)\n                .workplane(offset=self.lid_height)\n                .rect(l - 6, w - 6)\n                .loft(ruled=True)\n            )\n            rc = rc.edges(VerticalEdgeSelector()).fillet(0.5)\n            # rc = self.extrude_profile(rs, [self.window_th, (ht, 60), hlw], angle=True)\n            r = r.cut(rc.translate((0, ext / 2, GR_LID_WINDOW_H)))\n            rs = rounded_rect_sketch(self.length - 5, self.width - 5, GR_RAD)\n            rc = self.extrude_profile(rs, [self.lid_height])\n            r = r.cut(rc.translate((0, 0, self.lid_height - ht)))\n\n        # add hinge mounts\n        rc = rotate_y(self.hinge_mount(), 180)\n        for pt in self.hinge_centres:\n            r = r.cut(rc.translate((pt[0], pt[1], 0)))\n\n        # add window retaining screw holes\n        if self.lid_window:\n            rc = (\n                cq.Workplane(\"XY\")\n                .circle(M2_DIAM / 2)\n                .extrude(5)\n                .faces(\">Z\")\n                .wires()\n                .toPending()\n                .extrude(0.8, taper=-45)\n                .faces(\"<Z\")\n                .chamfer(0.5)\n            )\n            for pt in self.lid_window_hole_pos(z=1):\n                r = r.cut(rc.translate(pt))\n        self._cq_obj = r\n        self._obj_label = \"lid\"\n        return self._cq_obj\n\n    def render_lid_window(self):\n        rs = rounded_rect_sketch(*self.lid_window_size(), 0.5)\n        r = cq.Workplane(\"XY\").placeSketch(rs).extrude(self.window_th)\n        r = r.translate((0, 3, 0))\n        rc = cq.Workplane(\"XY\").circle(M2_CLR_DIAM / 2).extrude(self.window_th)\n        for pt in self.lid_window_hole_pos(z=0):\n            r = r.cut(rc.translate(pt))\n        self._cq_obj = r\n        self._obj_label = \"lid_window\"\n        return self._cq_obj\n\n    def render_accessories(self):\n        \"\"\"Render functional accessories which are installed to main box body.\"\"\"\n        margin = 8\n        latch_count = 2\n        if self.side_clasps:\n            latch_count += 4\n        rl = self.render_latch()\n        sx, sy = size_2d(rl)\n        pts = [(x * (sx + margin) + sx / 2, sy / 2, 0) for x in range(latch_count)]\n        r = composite_from_pts(rl, pts)\n        oy = sy + margin\n\n        if self.front_handle:\n            rh = recentre(rotate_x(self.render_handle(), -90))\n            hsx, hsy, hsz = size_3d(rh)\n            r = r.union(rh.translate((hsx / 2, oy + hsy / 2, hsz / 2)))\n            oy += hsy + margin\n\n        rh = self.render_hinge()\n        hsx, hsy = size_2d(rh)\n        r = r.union(rh.translate((margin, oy, 0)))\n        r = r.union(rh.translate((1.5 * hsx + margin, oy + hsy / 2, 0)))\n        r = r.union(rh.translate((3 * hsx + margin, oy, 0)))\n        r = r.union(rh.translate((4.5 * hsx + margin, oy + hsy / 2, 0)))\n\n        rl = self.render_label()\n        rl = rotate_x(rl, 90)\n        r = r.union(rl.translate((40, -20, 0.5)))\n\n        self._cq_obj = r\n        self._obj_label = \"acc\"\n        return self._cq_obj\n\n    def render_assembly(self):\n        \"\"\"Renders a CadQuery Assembly object representing the entire box with accessories\"\"\"\n        self.check_dimensions()\n        r = self.render()\n        a = cq.Assembly(obj=r, name=\"Gridfinity Rugged Box\", color=self.box_color)\n\n        r = self.render_lid()\n        r = r.translate((0, 0, self.box_height))\n        a.add(r, color=self.lid_color, name=\"Lid\")\n\n        if self.lid_window:\n            r = self.render_lid_window()\n            r = r.translate((0, 0, self.box_height + GR_LID_WINDOW_H))\n            a.add(r, color=self.window_color, name=\"Lid Window\")\n\n        if self.front_handle and self.long_enough_for_handle:\n            r = self.render_handle()\n            zo = self.right_handle_centre[2] - (GR_HANDLE_SZ - M3_CB_DEPTH)\n            r = r.translate((0, -self.box_width / 2 - GR_HANDLE_H / 2, zo))\n            a.add(r, color=self.handle_color, name=\"Handle\")\n\n        rf = rotate_x(self.render_latch(), -90)\n        idx = 1\n        yo = GR_LATCH_H / 2\n        zo = self.box_height - GR_RIB_CTR + yo / 2\n        for pt in self.front_clasp_centres:\n            name = \"Latch %d\" % (idx)\n            pt = (pt[0], pt[1] - yo, zo)\n            a.add(rf.translate(pt), color=self.latch_color, name=name)\n            idx += 1\n        if self.side_clasps:\n            rl = rotate_z(rotate_x(self.render_latch(), -90), -90)\n            rr = rotate_z(rl, 180)\n            for pt in self.side_clasp_centres:\n                name = \"Latch %d\" % (idx)\n                y = -yo if pt[0] < 0 else yo\n                pt = (pt[0] + y, pt[1], zo)\n                if pt[0] < 0:\n                    a.add(rl.translate(pt), color=self.latch_color, name=name)\n                else:\n                    a.add(rr.translate(pt), color=self.latch_color, name=name)\n                idx += 1\n\n        for i, section in [(a, b) for a in (0, 1) for b in (\"inner\", \"outer\")]:\n            r = recentre(self.render_hinge(as_closed=True, section=section), \"yz\")\n            r = rotate_y(r, 90)\n            name = \"Right \" if i else \"Left \"\n            name = name + \"Hinge %s\" % (section)\n            a.add(\n                r.translate(self.hinge_centres[i]),\n                color=self.hinge_color,\n                name=name,\n            )\n\n        if self.front_label:\n            r = self.render_label()\n            a.add(r.translate(self.label_centre), color=self.label_color, name=\"Label\")\n        self._obj_label = \"assembly\"\n        self._cq_obj = a\n        return self._cq_obj\n"
  },
  {
    "path": "cqgridfinity/scripts/__init__.py",
    "content": ""
  },
  {
    "path": "cqgridfinity/scripts/gridfinitybase.py",
    "content": "#! /usr/bin/env python3\n\"\"\"\ncommand line script to make a Gridfinity baseplate\n\"\"\"\nimport argparse\n\nimport cqgridfinity\nfrom cqgridfinity import *\n\ntitle = \"\"\"\n  _____      _     _  __ _       _ _           ____\n / ____|    (_)   | |/ _(_)     (_) |         |  _ \\\\\n| |  __ _ __ _  __| | |_ _ _ __  _| |_ _   _  | |_) | __ _ ___  ___\n| | |_ | '__| |/ _` |  _| | '_ \\\\| | __| | | | |  _ < / _` / __|/ _ \\\\\n| |__| | |  | | (_| | | | | | | | | |_| |_| | | |_) | (_| \\\\__ \\\\  __/\n \\\\_____|_|  |_|\\\\__,_|_| |_|_| |_|_|\\\\__|\\\\__, | |____/ \\\\__,_|___/\\\\___|\n                                        __/ |\n                                       |___/\n\"\"\"\n\nDESC = \"\"\"\nMake a customized/parameterized Gridfinity compatible simple baseplate.\n\"\"\"\n\nEPILOG = \"\"\"\nexample usage:\n\n  6 x 3 baseplate to default STL file:\n  $ gridfinitybase 6 3 -f stl\n\"\"\"\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=DESC,\n        epilog=EPILOG,\n        prefix_chars=\"-+\",\n        formatter_class=argparse.RawTextHelpFormatter,\n    )\n\n    parser.add_argument(\n        \"length\", metavar=\"length\", type=str, help=\"Box length in U (1U = 42 mm)\"\n    )\n    parser.add_argument(\n        \"width\", metavar=\"width\", type=str, help=\"Box width in U (1U = 42 mm)\"\n    )\n    parser.add_argument(\n        \"-f\",\n        \"--format\",\n        default=\"step\",\n        help=\"Output file format (STEP, STL, SVG) default=STEP\",\n    )\n    parser.add_argument(\n        \"-s\",\n        \"--screws\",\n        default=False,\n        action=\"store_true\",\n        help=\"Add screw mounting tabs to the corners (adds +5 mm to depth)\",\n    )\n    parser.add_argument(\n        \"-d\",\n        \"--depth\",\n        default=None,\n        action=\"store\",\n        help=\"Extrude extended depth under baseplate by this amount\",\n    )\n    parser.add_argument(\n        \"-hd\",\n        \"--holediam\",\n        default=None,\n        action=\"store\",\n        help=\"Corner mounting screw hole diameter (default=5)\",\n    )\n    parser.add_argument(\n        \"-hc\",\n        \"--cskdiam\",\n        default=None,\n        action=\"store\",\n        help=\"Corner mounting screw countersink diameter (default=10)\",\n    )\n    parser.add_argument(\n        \"-ca\",\n        \"--cskangle\",\n        default=None,\n        action=\"store\",\n        help=\"Corner mounting screw countersink angle (deg) (default=82)\",\n    )\n    parser.add_argument(\n        \"-o\",\n        \"--output\",\n        default=None,\n        help=\"Output filename (inferred output file format with extension)\",\n    )\n    args = parser.parse_args()\n    argsd = vars(args)\n    print(title)\n    print(\"Version: %s\" % (cqgridfinity.__version__))\n\n    for k in [\"depth\", \"holediam\", \"cskdiam\", \"cskangle\"]:\n        if argsd[k] is not None:\n            argsd[k] = float(argsd[k])\n    base = GridfinityBaseplate(\n        length_u=int(argsd[\"length\"]),\n        width_u=int(argsd[\"width\"]),\n        ext_depth=argsd[\"depth\"],\n        corner_screws=argsd[\"screws\"],\n        csk_hole=argsd[\"holediam\"],\n        csk_diam=argsd[\"cskdiam\"],\n        csk_angle=argsd[\"cskangle\"],\n    )\n    print(\n        \"Gridfinity baseplate: %dU x %dU (%.1f mm x %.1f mm)\"\n        % (\n            base.length_u,\n            base.width_u,\n            base.length,\n            base.width,\n        )\n    )\n    if argsd[\"output\"] is not None:\n        fn = argsd[\"output\"]\n    else:\n        fn = base.filename()\n    s = [\"\\nBaseplate generated and saved as\"]\n    if argsd[\"format\"].lower() == \"stl\" or fn.lower().endswith(\".stl\"):\n        if not fn.endswith(\".stl\"):\n            fn = fn + \".stl\"\n        base.save_stl_file(filename=argsd[\"output\"])\n        s.append(\"%s in STL format\" % (fn))\n    elif argsd[\"format\"].lower() == \"svg\" or fn.lower().endswith(\".svg\"):\n        if not fn.endswith(\".svg\"):\n            fn = fn + \".svg\"\n        base.save_svg_file(filename=argsd[\"output\"])\n        s.append(\"%s in SVG format\" % (fn))\n    else:\n        if not fn.endswith(\".step\"):\n            fn = fn + \".step\"\n        base.save_step_file(filename=argsd[\"output\"])\n        s.append(\"%s in STEP format\" % (fn))\n    print(\" \".join(s))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "cqgridfinity/scripts/gridfinitybox.py",
    "content": "#! /usr/bin/env python3\n\"\"\"\ncommand line script to make a Gridfinity box\n\"\"\"\nimport argparse\n\nimport cqgridfinity\nfrom cqgridfinity import *\n\ntitle = \"\"\"\n  _____      _     _  __ _       _ _           ____\n / ____|    (_)   | |/ _(_)     (_) |         |  _ \\\\\n| |  __ _ __ _  __| | |_ _ _ __  _| |_ _   _  | |_) | _____  __\n| | |_ | '__| |/ _` |  _| | '_ \\\\| | __| | | | |  _ < / _ \\\\ \\\\/ /\n| |__| | |  | | (_| | | | | | | | | |_| |_| | | |_) | (_) >  <\n \\\\_____|_|  |_|\\\\__,_|_| |_|_| |_|_|\\\\__|\\\\__, | |____/ \\\\___/_/\\\\_\\\\\n                                        __/ |\n                                       |___/\n\"\"\"\n\nDESC = \"\"\"\nMake a customized/parameterized Gridfinity compatible box with many optional features.\n\"\"\"\n\nEPILOG = \"\"\"\nexample usages:\n\n  2x3x5 box with magnet holes saved to STL file with default filename:\n  $ gridfinitybox 2 3 5 -m -f stl\n\n  1x3x4 box with scoops, label strip, 3 internal partitions and specified name:\n  $ gridfinitybox 1 3 4 -s -l -ld 3 -o MyBox.step\n\n  Solid 3x3x3 box with 50% fill, unsupported magnet holes and no top lip:\n  $ gridfinitybox 3 3 3 -d -r 0.5 -u -n\n \n  Lite style box 3x2x3 with label strip, partitions, output to default SVG file:\n  $ gridfinitybox 3 2 3 -e -l -ld 2 -f svg\n\"\"\"\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=DESC,\n        epilog=EPILOG,\n        prefix_chars=\"-+\",\n        formatter_class=argparse.RawTextHelpFormatter,\n    )\n\n    parser.add_argument(\n        \"length\", metavar=\"length\", type=str, help=\"Box length in U (1U = 42 mm)\"\n    )\n    parser.add_argument(\n        \"width\", metavar=\"width\", type=str, help=\"Box width in U (1U = 42 mm)\"\n    )\n    parser.add_argument(\n        \"height\", metavar=\"height\", type=str, help=\"Box height in U (1U = 7 mm)\"\n    )\n    parser.add_argument(\n        \"-m\",\n        \"--magnetholes\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add bottom magnet/mounting holes\",\n    )\n    parser.add_argument(\n        \"-u\",\n        \"--unsupported\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add bottom magnet holes with 3D printer friendly strips without support\",\n    )\n    parser.add_argument(\n        \"-n\",\n        \"--nolip\",\n        action=\"store_true\",\n        default=False,\n        help=\"Do not add mating lip to the top perimeter\",\n    )\n    parser.add_argument(\n        \"-s\",\n        \"--scoops\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add finger scoops against each length-wise back wall\",\n    )\n    parser.add_argument(\n        \"-l\",\n        \"--labels\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add label strips against each length-wise front wall\",\n    )\n    parser.add_argument(\n        \"-e\",\n        \"--ecolite\",\n        action=\"store_true\",\n        default=False,\n        help=\"Make economy / lite style box with no elevated floor\",\n    )\n    parser.add_argument(\n        \"-d\",\n        \"--solid\",\n        action=\"store_true\",\n        default=False,\n        help=\"Make solid (filled) box for customized storage\",\n    )\n    parser.add_argument(\n        \"-r\",\n        \"--ratio\",\n        action=\"store\",\n        default=1.0,\n        help=\"Solid box fill ratio 0.0 = minimum, 1.0 = full height\",\n    )\n    parser.add_argument(\n        \"-ld\",\n        \"--lengthdiv\",\n        action=\"store\",\n        default=0,\n        help=\"Split box length-wise with specified number of divider walls\",\n    )\n    parser.add_argument(\n        \"-wd\",\n        \"--widthdiv\",\n        action=\"store\",\n        default=0,\n        help=\"Split box width-wise with specified number of divider walls\",\n    )\n    parser.add_argument(\n        \"-wt\",\n        \"--wall\",\n        action=\"store\",\n        default=1.0,\n        help=\"Wall thickness (default=1 mm)\",\n    )\n    parser.add_argument(\n        \"-f\",\n        \"--format\",\n        default=\"step\",\n        help=\"Output file format (STEP, STL, SVG) default=STEP\",\n    )\n    parser.add_argument(\n        \"-o\",\n        \"--output\",\n        default=None,\n        help=\"Output filename (inferred output file format with extension)\",\n    )\n    args = parser.parse_args()\n    argsd = vars(args)\n    solid_ratio = float(argsd[\"ratio\"])\n    length_div = int(argsd[\"lengthdiv\"])\n    width_div = int(argsd[\"widthdiv\"])\n    wall = float(argsd[\"wall\"])\n    box = GridfinityBox(\n        length_u=int(argsd[\"length\"]),\n        width_u=int(argsd[\"width\"]),\n        height_u=int(argsd[\"height\"]),\n        holes=argsd[\"magnetholes\"] or argsd[\"unsupported\"],\n        unsupported_holes=argsd[\"unsupported\"],\n        no_lip=argsd[\"nolip\"],\n        scoops=argsd[\"scoops\"],\n        labels=argsd[\"labels\"],\n        lite_style=argsd[\"ecolite\"],\n        solid=argsd[\"solid\"],\n        solid_ratio=solid_ratio,\n        length_div=length_div,\n        width_div=width_div,\n        wall_th=wall,\n    )\n    if argsd[\"ecolite\"]:\n        bs = \"lite \"\n    elif argsd[\"solid\"]:\n        bs = \"solid \"\n    else:\n        bs = \"\"\n    print(title)\n    print(\"Version: %s\" % (cqgridfinity.__version__))\n\n    print(\n        \"Gridfinity %sbox: %dU x %dU x %dU (%.1f mm x %.1f mm x %.1f mm), %.2f mm walls\"\n        % (\n            bs,\n            box.length_u,\n            box.width_u,\n            box.height_u,\n            box.length,\n            box.width,\n            box.height,\n            box.wall_th,\n        )\n    )\n    if argsd[\"solid\"]:\n        print(\n            \"  solid height ratio: %.2f  top height: %.2f mm / %.2f mm\"\n            % (solid_ratio, box.top_ref_height, box.max_height + GR_BOT_H)\n        )\n    s = []\n    if argsd[\"unsupported\"]:\n        s.append(\"holes with no support\")\n    elif argsd[\"magnetholes\"]:\n        s.append(\"holes\")\n    if argsd[\"nolip\"]:\n        s.append(\"no lip\")\n    if argsd[\"scoops\"]:\n        s.append(\"scoops\")\n    if argsd[\"labels\"]:\n        s.append(\"label strips\")\n    if length_div:\n        s.append(\"%d length-wise walls\" % (length_div))\n    if width_div:\n        s.append(\"%d width-wise walls\" % (width_div))\n    if len(s):\n        print(\"  with options: %s\" % (\", \".join(s)))\n    if argsd[\"output\"] is not None:\n        fn = argsd[\"output\"]\n    else:\n        fn = box.filename()\n    s = [\"\\nBox generated and saved as\"]\n    if argsd[\"format\"].lower() == \"stl\" or fn.lower().endswith(\".stl\"):\n        if not fn.endswith(\".stl\"):\n            fn = fn + \".stl\"\n        box.save_stl_file(filename=argsd[\"output\"])\n        s.append(\"%s in STL format\" % (fn))\n    elif argsd[\"format\"].lower() == \"svg\" or fn.lower().endswith(\".svg\"):\n        if not fn.endswith(\".svg\"):\n            fn = fn + \".svg\"\n        box.save_svg_file(filename=argsd[\"output\"])\n        s.append(\"%s in SVG format\" % (fn))\n    else:\n        if not fn.endswith(\".step\"):\n            fn = fn + \".step\"\n        box.save_step_file(filename=argsd[\"output\"])\n        s.append(\"%s in STEP format\" % (fn))\n    print(\" \".join(s))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "cqgridfinity/scripts/ruggedbox.py",
    "content": "#! /usr/bin/env python3\n\"\"\"\ncommand line script to make a rugged Gridfinity box\n\"\"\"\nimport argparse\n\nimport cqgridfinity\nfrom cqgridfinity import *\n\ntitle = \"\"\"\n ____                             _ ____\n|  _ \\ _   _  __ _  __ _  ___  __| | __ )  _____  __\n| |_) | | | |/ _` |/ _` |/ _ \\\\/ _` |  _ \\\\ / _ \\\\ \\\\/ /\n|  _ <| |_| | (_| | (_| |  __/ (_| | |_) | (_) >  <\n|_| \\\\_\\\\\\\\__,_|\\\\__, |\\\\__, |\\\\___|\\\\__,_|____/ \\\\___/_/\\\\_\\\\\n             |___/ |___/\n\"\"\"\n\nDESC = \"\"\"\nMake a customized/parameterized Gridfinity compatible rugged box enclosure.\nThe minimum box size is 3U x 3U x 4U.\n\"\"\"\n\nEPILOG = \"\"\"\nexample usage:\n\n  5 x 4 x 6 rugged box shell and lid saved to STL files:\n  $ ruggedbox 5 4 6 --box --lid -f stl\n\"\"\"\n\n\ndef save_asset(box, argsd, prefix=None):\n    if argsd[\"output\"] is not None:\n        fn = argsd[\"output\"]\n        if box._obj_label is not None:\n            for ext in (\".stl\", \".step\", \".svg\"):\n                if fn.lower().endswith(ext):\n                    fn = fn.replace(ext, \"_%s%s\" % (box._obj_label, ext))\n                    break\n    else:\n        fn = box.filename(prefix=prefix)\n    s = [\"Component generated and saved as\"]\n    if argsd[\"format\"].lower() == \"stl\" or fn.lower().endswith(\".stl\"):\n        if not fn.endswith(\".stl\"):\n            fn = fn + \".stl\"\n        box.save_stl_file(filename=argsd[\"output\"], prefix=prefix)\n        s.append(\"%s in STL format\" % (fn))\n    elif argsd[\"format\"].lower() == \"svg\" or fn.lower().endswith(\".svg\"):\n        if not fn.endswith(\".svg\"):\n            fn = fn + \".svg\"\n        box.save_svg_file(filename=argsd[\"output\"], prefix=prefix)\n        s.append(\"%s in SVG format\" % (fn))\n    else:\n        if not fn.endswith(\".step\"):\n            fn = fn + \".step\"\n        box.save_step_file(filename=argsd[\"output\"], prefix=prefix)\n        s.append(\"%s in STEP format\" % (fn))\n    print(\" \".join(s))\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=DESC,\n        epilog=EPILOG,\n        prefix_chars=\"-+\",\n        formatter_class=argparse.RawTextHelpFormatter,\n    )\n\n    parser.add_argument(\n        \"length\", metavar=\"length\", type=str, help=\"Box length in U (1U = 42 mm)\"\n    )\n    parser.add_argument(\n        \"width\", metavar=\"width\", type=str, help=\"Box width in U (1U = 42 mm)\"\n    )\n    parser.add_argument(\n        \"height\", metavar=\"height\", type=str, help=\"Box height in U (1U = 7 mm)\"\n    )\n    parser.add_argument(\n        \"+l\",\n        \"--label\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add label window across the front wall\",\n    )\n    parser.add_argument(\n        \"-l\",\n        \"--nolabel\",\n        action=\"store_true\",\n        default=False,\n        help=\"Remove label window across the front wall\",\n    )\n    parser.add_argument(\n        \"+p\",\n        \"--lidbaseplate\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add baseplate to top of the lid\",\n    )\n    parser.add_argument(\n        \"-p\",\n        \"--nolidbaseplate\",\n        action=\"store_true\",\n        default=False,\n        help=\"Smooth/plain lid\",\n    )\n    parser.add_argument(\n        \"+w\",\n        \"--lidwindow\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add window slot to the lid\",\n    )\n    parser.add_argument(\n        \"-w\",\n        \"--nolidwindow\",\n        action=\"store_true\",\n        default=False,\n        help=\"Do not add window slot to the lid\",\n    )\n    parser.add_argument(\n        \"-wt\",\n        \"--windowthickness\",\n        action=\"store\",\n        default=None,\n        help=\"Thickness of lid windows (mm)\",\n    )\n    parser.add_argument(\n        \"+a\",\n        \"--handle\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add front handle\",\n    )\n    parser.add_argument(\n        \"-a\",\n        \"--nohandle\",\n        action=\"store_true\",\n        default=False,\n        help=\"No front handle\",\n    )\n    parser.add_argument(\n        \"+c\",\n        \"--clasps\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add clasps to the left and right side walls\",\n    )\n    parser.add_argument(\n        \"-c\",\n        \"--noclasps\",\n        action=\"store_true\",\n        default=False,\n        help=\"No clasps on the left and right side walls\",\n    )\n    parser.add_argument(\n        \"+s\",\n        \"--stackable\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add stackable mating features to top and bottom\",\n    )\n    parser.add_argument(\n        \"-s\",\n        \"--notstackable\",\n        action=\"store_true\",\n        default=False,\n        help=\"Non-stackable box\",\n    )\n    parser.add_argument(\n        \"+v\",\n        \"--veegroove\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add v-cut grooves to side walls\",\n    )\n    parser.add_argument(\n        \"-v\",\n        \"--noveegroove\",\n        action=\"store_true\",\n        default=False,\n        help=\"No v-cut grooves (plain) side walls\",\n    )\n    parser.add_argument(\n        \"+e\",\n        \"--sidehandle\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add handles to side walls\",\n    )\n    parser.add_argument(\n        \"-e\",\n        \"--nosidehandle\",\n        action=\"store_true\",\n        default=False,\n        help=\"No handles on side walls\",\n    )\n    parser.add_argument(\n        \"+b\",\n        \"--backfeet\",\n        action=\"store_true\",\n        default=False,\n        help=\"Add standing feet to back wall\",\n    )\n    parser.add_argument(\n        \"-b\",\n        \"--nobackfeet\",\n        action=\"store_true\",\n        default=False,\n        help=\"No standing feet added to back wall\",\n    )\n    parser.add_argument(\n        \"-r\",\n        \"--normalstyle\",\n        action=\"store_true\",\n        default=False,\n        help=\"Make normal style box\",\n    )\n    parser.add_argument(\n        \"+r\",\n        \"--ribstyle\",\n        action=\"store_true\",\n        default=False,\n        help=\"Make rib style box with exposed vertical ribs\",\n    )\n    parser.add_argument(\n        \"-f\",\n        \"--format\",\n        default=\"step\",\n        help=\"Output file format (STEP, STL, SVG) default=STEP\",\n    )\n    parser.add_argument(\n        \"-o\",\n        \"--output\",\n        default=None,\n        help=\"Output filename (inferred output file format with extension)\",\n    )\n    parser.add_argument(\n        \"-gb\",\n        \"--box\",\n        action=\"store_true\",\n        default=False,\n        help=\"Generate box\",\n    )\n    parser.add_argument(\n        \"-gl\",\n        \"--lid\",\n        action=\"store_true\",\n        default=False,\n        help=\"Generate lid\",\n    )\n    parser.add_argument(\n        \"-ga\",\n        \"--acc\",\n        action=\"store_true\",\n        default=False,\n        help=\"Generate accessory components\",\n    )\n    parser.add_argument(\n        \"-gh\",\n        \"--hinge\",\n        action=\"store_true\",\n        default=False,\n        help=\"Generate hinge element\",\n    )\n    parser.add_argument(\n        \"-ge\",\n        \"--genlabel\",\n        action=\"store_true\",\n        default=False,\n        help=\"Generate label panel insert\",\n    )\n    parser.add_argument(\n        \"-gn\",\n        \"--genhandle\",\n        action=\"store_true\",\n        default=False,\n        help=\"Generate front handle\",\n    )\n    parser.add_argument(\n        \"-gt\",\n        \"--genlatch\",\n        action=\"store_true\",\n        default=False,\n        help=\"Generate latch component\",\n    )\n    parser.add_argument(\n        \"-gw\",\n        \"--genwindow\",\n        action=\"store_true\",\n        default=False,\n        help=\"Generate lid window component\",\n    )\n\n    args = parser.parse_args()\n    argsd = vars(args)\n    box = GridfinityRuggedBox(\n        length_u=int(argsd[\"length\"]),\n        width_u=int(argsd[\"width\"]),\n        height_u=int(argsd[\"height\"]),\n    )\n    if argsd[\"lidbaseplate\"]:\n        box.lid_baseplate = True\n    if argsd[\"nolidbaseplate\"]:\n        box.lid_baseplate = False\n    if argsd[\"lidwindow\"]:\n        box.lid_window = True\n    if argsd[\"nolidwindow\"]:\n        box.lid_window = False\n    if argsd[\"handle\"]:\n        box.front_handle = True\n    if argsd[\"nohandle\"]:\n        box.front_handle = False\n    if argsd[\"label\"]:\n        box.front_label = True\n    if argsd[\"nolabel\"]:\n        box.front_label = False\n    if argsd[\"clasps\"]:\n        box.side_clasps = True\n    if argsd[\"noclasps\"]:\n        box.side_clasps = False\n    if argsd[\"stackable\"]:\n        box.stackable = True\n    if argsd[\"notstackable\"]:\n        box.stackable = False\n    if argsd[\"veegroove\"]:\n        box.wall_vgrooves = True\n    if argsd[\"noveegroove\"]:\n        box.wall_vgrooves = False\n    if argsd[\"sidehandle\"]:\n        box.side_handles = True\n    if argsd[\"nosidehandle\"]:\n        box.side_handles = False\n    if argsd[\"backfeet\"]:\n        box.back_feet = True\n    if argsd[\"nobackfeet\"]:\n        box.back_feet = False\n    if argsd[\"ribstyle\"]:\n        box.rib_style = True\n    if argsd[\"normalstyle\"]:\n        box.rib_style = False\n    if argsd[\"windowthickness\"] is not None:\n        box.window_th = float(argsd[\"windowthickness\"])\n\n    print(title)\n    print(\"Version: %s\" % (cqgridfinity.__version__))\n    print(\n        \"Gridfinity rugged box: %dU x %dU x %dU\"\n        % (\n            box.length_u,\n            box.width_u,\n            box.height_u,\n        )\n    )\n    print(\n        \"  Exterior dim: %.1f mm x %.1f mm x %.1f mm\"\n        % (\n            box.box_length + 2 * (GR_RBOX_CWALL - GR_RBOX_WALL),\n            box.box_width + 2 * (GR_RBOX_CWALL - GR_RBOX_WALL),\n            box.box_height + box.lid_height,\n        )\n    )\n    print(\n        \"  Interior dim: %.1f mm x %.1f mm x %.1f mm\"\n        % (\n            box.length,\n            box.width,\n            box.height,\n        )\n    )\n    print(\"  Internal volume: %.3f L\" % (box.length * box.width * box.height / 1e6))\n    if box.lid_window:\n        print(\n            \"  Lid window dimensions: %.2f x %.2f mm, %.2f mm thickness\"\n            % (*box.lid_window_size(), box.window_th)\n        )\n\n    s = []\n    opts = [\n        \"wall_vgrooves\",\n        \"front_handle\",\n        \"stackable\",\n        \"side_clasps\",\n        \"lid_baseplate\",\n        \"inside_baseplate\",\n        \"side_handles\",\n        \"front_label\",\n        \"back_feet\",\n        \"rib_style\",\n    ]\n    for opt in opts:\n        opt_name = opt.replace(\"_\", \" \").title()\n        val = \"Y\" if box.__dict__[opt] else \"N\"\n        print(\"  %-19s: %s\" % (opt_name, val))\n    print(\"  %-19s: %s\" % (\"Lid Window\", \"Y\" if box.lid_window else \"N\"))\n\n    if argsd[\"output\"] is not None:\n        fn = argsd[\"output\"]\n    else:\n        fn = box.filename()\n    g = False\n    if argsd[\"box\"]:\n        print(\"Rendering box...\")\n        box.render()\n        save_asset(box, argsd)\n        g = True\n    if argsd[\"lid\"]:\n        print(\"Rendering lid...\")\n        box.render_lid()\n        save_asset(box, argsd)\n        g = True\n    if argsd[\"acc\"]:\n        print(\"Rendering accessory components...\")\n        r = box.render_accessories()\n        save_asset(box, argsd)\n        g = True\n    if argsd[\"hinge\"]:\n        print(\"Rendering hinge components...\")\n        r = box.render_hinge()\n        save_asset(box, argsd)\n        g = True\n    if argsd[\"genlabel\"]:\n        print(\"Rendering label panel...\")\n        r = box.render_label()\n        save_asset(box, argsd)\n        g = True\n    if argsd[\"genhandle\"]:\n        print(\"Rendering front handle...\")\n        r = box.render_handle()\n        save_asset(box, argsd)\n        g = True\n    if argsd[\"genlatch\"]:\n        print(\"Rendering latch component...\")\n        r = box.render_latch()\n        save_asset(box, argsd)\n        g = True\n    if argsd[\"genwindow\"]:\n        print(\n            \"Rendering lid window (%.2f x %.2f mm, %.2f mm thickness)...\"\n            % (*box.lid_window_size(), box.window_th)\n        )\n        r = box.render_lid_window()\n        save_asset(box, argsd)\n        g = True\n    if not g:\n        print(\"Rendering full assembly...\")\n        a = box.render_assembly()\n        if argsd[\"output\"] is not None:\n            fn = argsd[\"output\"]\n        else:\n            fn = box.filename()\n        if not fn.endswith(\".step\"):\n            fn = fn + \".step\"\n        a.save(fn)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "cqgridfinity/shims/README.md",
    "content": "# /pub/storage/workspace/gridfinity\n\nCreated by Zach Freedman as a versatile system of modular organization and storage modules.\n\nThis package defines the basic building blocks of the Gridfinity system.\nMake use of the parameters to customize the parts to your needs.\n\n\n## Parts\n\n### baseplate\n<table><tr>\n<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>\n<td valign=top>Parameters:<br/><ul>\n<li>length_u: 2</li>\n<li>width_u: 2</li>\n<li>ext_depth: 0.0</li>\n<li>straight_bottom: False</li>\n<li>corner_screws: False</li>\n<li>corner_tab_size: 21.0</li>\n<li>csk_hole: 5.0</li>\n<li>csk_diam: 10.0</li>\n<li>csk_angle: 82.0</li>\n</ul>\n</td>\n</tr></table>\n\n### box\n<table><tr>\n<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>\n<td valign=top>Parameters:<br/><ul>\n<li>length_u: 2</li>\n<li>width_u: 2</li>\n<li>height_u: 2</li>\n<li>length_div: 0.0</li>\n<li>width_div: 0.0</li>\n<li>scoops: False</li>\n<li>labels: False</li>\n<li>solid: False</li>\n<li>holes: False</li>\n<li>no_lip: False</li>\n<li>solid_ratio: 1.0</li>\n<li>lite_style: False</li>\n<li>unsupported_holes: False</li>\n<li>label_width: 12.0</li>\n<li>label_height: 10.0</li>\n<li>label_lip_height: 0.8</li>\n<li>scoop_rad: 12.0</li>\n<li>fillet_interior: True</li>\n<li>wall_th: 1.0</li>\n</ul>\n</td>\n</tr></table>\n\n### drawerspacer\n<table><tr>\n<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>\n<td valign=top>Parameters:<br/><ul>\n<li>length_u: 2</li>\n<li>width_u: 2</li>\n<li>length_th: 10.0</li>\n<li>width_th: 10.0</li>\n<li>thickness: 5.0</li>\n<li>chamf_rad: 1.0</li>\n<li>show_arrows: True</li>\n<li>arrow_h: 0.8</li>\n<li>length_fill: 0.0</li>\n<li>width_fill: 0.0</li>\n<li>align_features: True</li>\n<li>align_l: 16.0</li>\n<li>align_tol: 0.15</li>\n<li>align_min: 8.0</li>\n<li>min_margin: 4.0</li>\n<li>tolerance: 0.5</li>\n</ul>\n</td>\n</tr></table>\n\n### ruggedbox\n<table><tr>\n<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>\n<td valign=top>Parameters:<br/><ul>\n<li>length_u: 4</li>\n<li>width_u: 4</li>\n<li>height_u: 4</li>\n<li>lid_height: 10.0</li>\n<li>wall_vgrooves: True</li>\n<li>front_handle: True</li>\n<li>stackable: True</li>\n<li>side_clasps: True</li>\n<li>lid_baseplate: True</li>\n<li>inside_baseplate: True</li>\n<li>side_handles: True</li>\n<li>front_label: True</li>\n<li>label_length: 0.0</li>\n<li>label_height: 0.0</li>\n<li>label_th: 0.5</li>\n<li>back_feet: True</li>\n<li>hinge_width: 48.0</li>\n<li>hinge_bolted: False</li>\n</ul>\n</td>\n</tr></table>\n\n<br/><br/>\n\n*Generated by [PartCAD](https://partcad.org/)*\n"
  },
  {
    "path": "cqgridfinity/shims/cqgi_gf_baseplate.py",
    "content": "import sys\nsys.path.append(\".\") # Relative to `partcad.yaml`\n\nfrom cqgridfinity.gf_baseplate import GridfinityBaseplate\n\nlength_u = 2\nwidth_u = 2\next_depth = 0.0\nstraight_bottom = False\ncorner_screws = False\ncorner_tab_size = 21\ncsk_hole = 5.0\ncsk_diam = 10.0\ncsk_angle = 82\n\nresult = GridfinityBaseplate(\n    length_u=int(length_u),\n    width_u=int(width_u),\n    ext_depth=ext_depth,\n    straight_bottom=straight_bottom,\n    corner_screws=corner_screws,\n    corner_tab_size=corner_tab_size,\n    csk_hole=csk_hole,\n    csk_diam=csk_diam,\n    csk_angle=csk_angle,\n).render().val()\n\nshow_object(result)"
  },
  {
    "path": "cqgridfinity/shims/cqgi_gf_box.py",
    "content": "import sys\nsys.path.append(\".\") # Relative to `partcad.yaml`\n\nfrom cqgridfinity.gf_box import GridfinityBox\n\nlength_u = 2\nwidth_u = 2\nheight_u = 2\nlength_div = 0.0\nwidth_div = 0.0\nscoops = False\nlabels = False\nsolid = False\nholes = False\nno_lip = False\nsolid_ratio = 1.0\nlite_style = False\nunsupported_holes = False\nlabel_width = 12.0  # width of the label strip\nlabel_height = 10.0  # thickness of label overhang\nlabel_lip_height = 0.8  # thickness of label vertical lip\nscoop_rad = 12.0  # radius of optional interior scoops\nfillet_interior = True\nwall_th = 1.0\n\nresult = GridfinityBox(\n    length_u=int(length_u),\n    width_u=int(width_u),\n    height_u=int(height_u),\n    length_div=length_div,\n    width_div=width_div,\n    scoops=scoops,\n    labels=labels,\n    solid=solid,\n    holes=holes,\n    no_lip=no_lip,\n    solid_ratio=solid_ratio,\n    lite_style=lite_style,\n    unsupported_holes=unsupported_holes,\n    label_width=label_width,\n    label_height=label_height,\n    label_lip_height=label_lip_height,\n    scoop_rad=scoop_rad,\n    fillet_interior=fillet_interior,\n    wall_th=wall_th,\n).render().val()\n\nshow_object(result)"
  },
  {
    "path": "cqgridfinity/shims/cqgi_gf_drawerspacer.py",
    "content": "import sys\nsys.path.append(\".\") # Relative to `partcad.yaml`\n\nfrom cqgridfinity.gf_drawer import GridfinityDrawerSpacer\n\nlength_u = 2\nwidth_u = 2\nlength_th = 10.0\nwidth_th = 10.0\nthickness = 5.0\nchamf_rad = 1.0\nshow_arrows = True\narrow_h = 0.8\nlength_fill = 0.0\nwidth_fill = 0.0\nalign_features = True\nalign_l = 16.0\nalign_tol = 0.15\nalign_min = 8.0\nmin_margin = 4.0\ntolerance = 0.5\n\nresult = GridfinityDrawerSpacer(\n    length_u=int(length_u),\n    width_u=int(width_u),\n    length_th=length_th,\n    width_th=width_th,\n    thickness=thickness,\n    chamf_rad=chamf_rad,\n    show_arrows=show_arrows,\n    arrow_h=arrow_h,\n    length_fill=length_fill,\n    width_fill=width_fill,\n    align_features=align_features,\n    align_l=align_l,\n    align_tol=align_tol,\n    align_min=align_min,\n    min_margin=min_margin,\n    tolerance=tolerance,\n).render().val()\n\nshow_object(result)"
  },
  {
    "path": "cqgridfinity/shims/cqgi_gf_ruggedbox.py",
    "content": "import sys\nsys.path.append(\".\") # Relative to `partcad.yaml`\n\nfrom cqgridfinity.gf_ruggedbox import GridfinityRuggedBox\n\nlength_u = 4\nwidth_u = 4\nheight_u = 4\nlid_height = 10.0\nwall_vgrooves = True\nfront_handle = True\nstackable = True\nside_clasps = True\nlid_baseplate = True\ninside_baseplate = True\nside_handles = True\nfront_label = True\n# TODO(clairbee): uncomment the below when annotations are supported by CQGI\n# label_length: float = None\n# label_height: float = None\nlabel_length = 0.0\nlabel_height = 0.0\nif label_length == 0.0:\n    label_length = None\nif label_height == 0.0:\n    label_height = None\n\nlabel_th = 0.8\nback_feet = True\nhinge_width = 48.0\nhinge_bolted = False\nrib_style = False\n\nresult = GridfinityRuggedBox(\n    length_u=int(length_u),\n    width_u=int(width_u),\n    height_u=int(height_u),\n    lid_height=lid_height,\n    wall_vgrooves=wall_vgrooves,\n    front_handle=front_handle,\n    stackable=stackable,\n    side_clasps=side_clasps,\n    lid_baseplate=lid_baseplate,\n    inside_baseplate=inside_baseplate,\n    side_handles=side_handles,\n    front_label=front_label,\n    label_length=label_length,\n    label_height=label_height,\n    label_th=label_th,\n    back_feet=back_feet,\n    hinge_width=hinge_width,\n    hinge_bolted=hinge_bolted,\n    rib_style=rib_style,\n).render().val()\n\nshow_object(result)"
  },
  {
    "path": "examples/demo1.assy",
    "content": "# This is a demo of multiple gridfinity parts put together using PartCAD.\n# Use `pc show -a examples/demo1` to view it in OCP CAD Viewer or\n# use `pc render -t png -a examples/demo1` to render a PNG image of this assembly.\nlinks:\n  - part: baseplate\n    name: baseplate\n  - part: box\n    name: box\n    location: [[100, 0, 0], [0, 0, 1], 0]\n  - part: drawerspacer\n    name: drawerspacerbox\n    location: [[-200, 0, 0], [0, 0, 1], 0]\n"
  },
  {
    "path": "partcad.yaml",
    "content": "partcad: \">=0.7.16\"\n\nname: /pub/storage/workspace/gridfinity\ndesc: Created by Zach Freedman as a versatile system of modular organization and storage modules.\ncover:\n  part: ruggedbox\n\ndocs:\n  intro: |\n    This package defines the basic building blocks of the Gridfinity system.\n    Make use of the parameters to customize the parts to your needs.\n  footer: |\n    ## Implementation notes\n\n    This packages has a folder with PartCAD shims.\n    This folder contains wrappers for cqgridfinity main Python files\n    to make them compatible with CadQuery's CQGI interface that is used by PartCAD.\n    This is a non-intrusive alternative to refactoring cqgridfinity main Python files.\n    If cqgridfinity adopts CQGI, then these shims can be dropped.\n\nparts:\n  baseplate:\n    type: cadquery\n    path: cqgridfinity/shims/cqgi_gf_baseplate.py\n    parameters:\n      length_u:\n        type: int\n        default: 2\n      width_u:\n        type: int\n        default: 2\n      ext_depth:\n        type: float\n        default: 0.0\n      straight_bottom:\n        type: bool\n        default: False\n      corner_screws:\n        type: bool\n        default: False\n      corner_tab_size:\n        type: float\n        default: 21.0\n      csk_hole:\n        type: float\n        default: 5.0\n      csk_diam:\n        type: float\n        default: 10.0\n      csk_angle:\n        type: float\n        default: 82.0\n  ruggedbox:\n    type: cadquery\n    path: cqgridfinity/shims/cqgi_gf_ruggedbox.py\n    parameters:\n      length_u:\n        type: int\n        default: 4\n      width_u:\n        type: int\n        default: 4\n      height_u:\n        type: int\n        default: 4\n      lid_height:\n        type: float\n        default: 10.0\n      wall_vgrooves:\n        type: bool\n        default: True\n      front_handle:\n        type: bool\n        default: True\n      stackable:\n        type: bool\n        default: True\n      side_clasps:\n        type: bool\n        default: True\n      lid_baseplate:\n        type: bool\n        default: True\n      inside_baseplate:\n        type: bool\n        default: True\n      side_handles:\n        type: bool\n        default: True\n      front_label:\n        type: bool\n        default: True\n      label_length:\n        type: float\n        default: 0.0\n      label_height:\n        type: float\n        default: 0.0\n      label_th:\n        type: float\n        default: 0.8\n      back_feet:\n        type: bool\n        default: True\n      hinge_width:\n        type: float\n        default: 48.0\n      hinge_bolted:\n        type: bool\n        default: False\n      rib_style:\n        type: bool\n        default: False\n  box:\n    type: cadquery\n    path: cqgridfinity/shims/cqgi_gf_box.py\n    parameters:\n      length_u:\n        type: int\n        default: 2\n      width_u:\n        type: int\n        default: 2\n      height_u:\n        type: int\n        default: 2\n      length_div:\n        type: float\n        default: 0.0\n      width_div:\n        type: float\n        default: 0.0\n      scoops:\n        type: bool\n        default: False\n      labels:\n        type: bool\n        default: False\n      solid:\n        type: bool\n        default: False\n      holes:\n        type: bool\n        default: False\n      no_lip:\n        type: bool\n        default: False\n      solid_ratio:\n        type: float\n        default: 1.0\n      lite_style:\n        type: bool\n        default: False\n      unsupported_holes:\n        type: bool\n        default: False\n      label_width:\n        type: float\n        default: 12.0 # width of the label strip\n      label_height:\n        type: float\n        default: 10.0 # thickness of label overhang\n      label_lip_height:\n        type: float\n        default: 0.8 # thickness of label vertical lip\n      scoop_rad:\n        type: float\n        default: 12.0 # radius of optional interior scoops\n      fillet_interior:\n        type: bool\n        default: True\n      wall_th:\n        type: float\n        default: 1.0\n  drawerspacer:\n    type: cadquery\n    path: cqgridfinity/shims/cqgi_gf_drawerspacer.py\n    parameters:\n      length_u:\n        type: int\n        default: 2\n      width_u:\n        type: int\n        default: 2\n      length_th:\n        type: float\n        default: 10.0\n      width_th:\n        type: float\n        default: 10.0\n      thickness:\n        type: float\n        default: 5.0\n      chamf_rad:\n        type: float\n        default: 1.0\n      show_arrows:\n        type: bool\n        default: True\n      arrow_h:\n        type: float\n        default: 0.8\n      length_fill:\n        type: float\n        default: 0.0\n      width_fill:\n        type: float\n        default: 0.0\n      align_features:\n        type: bool\n        default: True\n      align_l:\n        type: float\n        default: 16.0\n      align_tol:\n        type: float\n        default: 0.15\n      align_min:\n        type: float\n        default: 8.0\n      min_margin:\n        type: float\n        default: 4.0\n      tolerance:\n        type: float\n        default: 0.5\n\nassemblies:\n  examples/demo1:\n    type: assy\n\nrender:\n  svg:\n    prefix: cqgridfinity/shims\n    exclude:\n      - assemblies\n  readme:\n    path: cqgridfinity/shims/README.md\n    exclude:\n      - assemblies\n"
  },
  {
    "path": "requirements.in",
    "content": "cadquery\ncqkit>=0.5.6\n"
  },
  {
    "path": "requirements.txt",
    "content": "#\n# This file is autogenerated by pip-compile with Python 3.12\n# by the following command:\n#\n#    pip-compile requirements.in\n#\ncadquery==2.4.0\n    # via -r requirements.in\ncadquery-ocp==7.7.2\n    # via cadquery\ncasadi==3.6.7\n    # via cadquery\ncqkit==0.5.8\n    # via -r requirements.in\nezdxf==1.3.4\n    # via cadquery\nfonttools==4.55.2\n    # via ezdxf\nmultimethod==1.9.1\n    # via cadquery\nnlopt==2.9.0\n    # via cadquery\nnptyping==2.0.1\n    # via cadquery\nnumpy==2.1.3\n    # via\n    #   casadi\n    #   ezdxf\n    #   nlopt\n    #   nptyping\npath==17.0.0\n    # via cadquery\npyparsing==3.2.0\n    # via ezdxf\ntyping-extensions==4.12.2\n    # via ezdxf\ntypish==1.9.3\n    # via cadquery\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nimport os\nimport os.path\nfrom pathlib import Path\nimport sys\nimport setuptools\n\nPACKAGE_NAME = \"cqgridfinity\"\n\n\nrequired = [\"cadquery\", \"cqkit>=0.5.6\"]\ndependency_links = []\n\n\ndef read_package_variable(key, filename=\"__init__.py\"):\n    \"\"\"Read the value of a variable from the package without importing.\"\"\"\n    module_path = os.path.join(PACKAGE_NAME, filename)\n    with open(module_path) as module:\n        for line in module:\n            parts = line.strip().split(\" \", 2)\n            if parts[:-1] == [key, \"=\"]:\n                return parts[-1].strip(\"'\")\n    sys.exit(\"'{0}' not found in '{1}'\".format(key, module_path))\n\nthis_directory = Path(__file__).parent\nlong_description = (this_directory / \"README.md\").read_text()\n\nsetuptools.setup(\n    name=read_package_variable(\"__project__\"),\n    version=read_package_variable(\"__version__\"),\n    description=\"A python library to make Gridfinity compatible objects with CadQuery.\",\n    url=\"https://github.com/michaelgale/cq-gridfinity\",\n    author=\"Michael Gale\",\n    author_email=\"michael@fxbricks.com\",\n    python_requires=\">=3.9\",\n    packages=setuptools.find_packages(),\n    long_description=long_description,\n    long_description_content_type=\"text/markdown\",\n    license=\"MIT\",\n    classifiers=[\n        \"Development Status :: 4 - Beta\",\n        \"Natural Language :: English\",\n        \"Operating System :: OS Independent\",\n        \"Programming Language :: Python :: 3.9\",\n        \"Intended Audience :: Developers\",\n        \"License :: OSI Approved :: MIT License\",\n    ],\n    install_requires=required,\n    dependency_links=dependency_links,\n    entry_points={\n            \"console_scripts\": [\n                \"gridfinitybox=cqgridfinity.scripts.gridfinitybox:main\",\n                \"gridfinitybase=cqgridfinity.scripts.gridfinitybase:main\",\n                \"ruggedbox=cqgridfinity.scripts.ruggedbox:main\",\n            ],\n        },    \n)\n"
  },
  {
    "path": "tests/common_test.py",
    "content": "import os\n\nEXPORT_STEP_FILE_PATH = \"./tests/testfiles\"\n\nenv = dict(os.environ)\nSKIP_TEST_BOX = \"SKIP_TEST_BOX\" in env\nSKIP_TEST_RBOX = \"SKIP_TEST_RBOX\" in env\nSKIP_TEST_SPACER = \"SKIP_TEST_SPACER\" in env\nSKIP_TEST_BASEPLATE = \"SKIP_TEST_BASEPLATE\" in env\n\n\ndef INCHES(x):\n    return x * 25.4\n\n\ndef _faces_match(obj, face, n):\n    nf = len(obj.faces(face).vals())\n    return nf == n\n\n\ndef _edges_match(obj, face, n):\n    nf = len(obj.faces(face).edges().vals())\n    return abs(nf - n) < 3\n\n\ndef _almost_same(x, y, tol=1e-3):\n    if isinstance(x, (list, tuple)):\n        return all((abs(xe - ye) < tol for xe, ye in zip(x, y)))\n    return abs(x - y) < tol\n\n\ndef _export_files(spec=\"all\"):\n    if \"EXPORT_STEP_FILES\" in env:\n        exp_var = env[\"EXPORT_STEP_FILES\"].lower()\n        if exp_var == \"all\":\n            return True\n        elif exp_var == spec.lower():\n            return True\n        return False\n    return False\n"
  },
  {
    "path": "tests/test_baseplate.py",
    "content": "# Gridfinity tests\nimport pytest\n\n# my modules\nfrom cqgridfinity import *\nfrom cqkit import FlatEdgeSelector\nfrom cqkit.cq_helpers import size_3d\nfrom common_test import (\n    EXPORT_STEP_FILE_PATH,\n    _almost_same,\n    _faces_match,\n    _export_files,\n    SKIP_TEST_BASEPLATE,\n)\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_BASEPLATE,\n    reason=\"Skipped intentionally by test scope environment variable\",\n)\ndef test_make_baseplate():\n    bp = GridfinityBaseplate(4, 3)\n    r = bp.render()\n    if _export_files(\"baseplate\"):\n        bp.save_step_file(path=EXPORT_STEP_FILE_PATH)\n    assert bp.filename() == \"gf_baseplate_4x3\"\n    assert _almost_same(size_3d(r), (168, 126, 4.75))\n    assert _faces_match(r, \">Z\", 16)\n    assert _faces_match(r, \"<Z\", 1)\n    edge_diff = abs(len(r.edges(FlatEdgeSelector(0)).vals()) - 104)\n    assert edge_diff < 3\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_BASEPLATE,\n    reason=\"Skipped intentionally by test scope environment variable\",\n)\ndef test_make_ext_baseplate():\n    bp = GridfinityBaseplate(5, 4, ext_depth=5, corner_screws=True)\n    r = bp.render()\n    assert _almost_same(size_3d(r), (210, 168, 9.75))\n    edge_diff = abs(len(r.edges(FlatEdgeSelector(0)).vals()) - 188)\n    assert edge_diff < 3\n"
  },
  {
    "path": "tests/test_box.py",
    "content": "# Gridfinity tests\nimport pytest\n\n# my modules\nfrom cqgridfinity import *\n\nfrom cqkit.cq_helpers import *\nfrom cqkit import *\n\nfrom common_test import (\n    EXPORT_STEP_FILE_PATH,\n    _almost_same,\n    _edges_match,\n    _faces_match,\n    _export_files,\n    SKIP_TEST_BOX,\n)\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_BOX, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_basic_box():\n    b1 = GridfinityBox(2, 3, 5, no_lip=True)\n    r = b1.render()\n    assert _almost_same(size_3d(r), (83.5, 125.5, 38.8))\n    assert _faces_match(r, \">Z\", 1)\n    assert _faces_match(r, \"<Z\", 6)\n    assert _edges_match(r, \">Z\", 16)\n    assert _edges_match(r, \"<Z\", 48)\n    assert b1.filename() == \"gf_box_2x3x5_basic\"\n    if _export_files(\"box\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n    b1 = GridfinityBox(2, 3, 5, no_lip=True)\n    if _export_files(\"box\"):\n        b1.wall_th = 1.5\n        r = b1.render()\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_BOX, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_invalid_box():\n    with pytest.raises(ValueError):\n        b1 = GridfinityBox(2, 3, 5, lite_style=True, solid=True)\n        b1.render()\n    with pytest.raises(ValueError):\n        b1 = GridfinityBox(2, 3, 5, lite_style=True, holes=True)\n        b1.render()\n    with pytest.raises(ValueError):\n        b1 = GridfinityBox(2, 3, 5, lite_style=True, wall_th=2.0)\n        b1.render()\n    with pytest.raises(ValueError):\n        b1 = GridfinityBox(2, 3, 5, wall_th=0.4)\n        b1.render()\n    with pytest.raises(ValueError):\n        b1 = GridfinityBox(2, 3, 5, wall_th=3.0)\n        b1.render()\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_BOX, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_lite_box():\n    b1 = GridfinityBox(2, 3, 5, lite_style=True)\n    r = b1.render()\n    if _export_files(\"box\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n    assert _almost_same(size_3d(r), (83.5, 125.5, 38.8))\n    assert _faces_match(r, \">Z\", 1)\n    assert _faces_match(r, \"<Z\", 6)\n    assert _edges_match(r, \">Z\", 16)\n    assert _edges_match(r, \"<Z\", 48)\n    assert b1.filename() == \"gf_box_lite_2x3x5\"\n    if _export_files(\"box\"):\n        b1 = GridfinityBox(2, 3, 5, lite_style=True)\n        b1.wall_th = 1.2\n        r = b1.render()\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n    b1 = GridfinityBox(1, 1, 1, lite_style=True)\n    r = b1.render()\n    if _export_files(\"box\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n    assert _almost_same(size_3d(r), (41.5, 41.5, 10.8))\n\n    b1 = GridfinityBox(1, 1, 2, lite_style=True)\n    r = b1.render()\n    if _export_files(\"box\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n    assert _almost_same(size_3d(r), (41.5, 41.5, 17.8))\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_BOX, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_empty_box():\n    b1 = GridfinityBox(2, 3, 5, holes=True)\n    r = b1.render()\n    if _export_files(\"box\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n    assert _almost_same(size_3d(r), (83.5, 125.5, 38.8))\n    assert _faces_match(r, \">Z\", 1)\n    assert _faces_match(r, \"<Z\", 6)\n    assert _edges_match(r, \">Z\", 16)\n    assert _edges_match(r, \"<Z\", 72)\n    assert b1.filename() == \"gf_box_2x3x5_holes\"\n    assert _almost_same(b1.top_ref_height, 7)\n    if _export_files(\"box\"):\n        b1 = GridfinityBox(2, 3, 5, holes=True)\n        b1.wall_th = 1.5\n        r = b1.render()\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n    b1 = GridfinityBox(1, 1, 1)\n    r = b1.render()\n    if _export_files(\"box\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n    assert _almost_same(size_3d(r), (41.5, 41.5, 10.8))\n\n    b1 = GridfinityBox(1, 1, 2)\n    r = b1.render()\n    if _export_files(\"box\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n    assert _almost_same(size_3d(r), (41.5, 41.5, 17.8))\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_BOX, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_solid_box():\n    b1 = GridfinitySolidBox(4, 2, 3)\n    r = b1.render()\n    if _export_files(\"box\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n    assert _almost_same(size_3d(r), (167.5, 83.5, 24.8))\n    assert _faces_match(r, \">Z\", 1)\n    assert _faces_match(r, \"<Z\", 8)\n    assert _edges_match(r, \">Z\", 16)\n    assert _edges_match(r, \"<Z\", 64)\n    assert len(r.faces(FlatFaceSelector(21)).vals()) == 1\n    assert len(r.edges(FlatEdgeSelector(21)).vals()) == 8\n    assert b1.filename() == \"gf_box_4x2x3_solid\"\n    assert _almost_same(b1.top_ref_height, 21)\n    b1.solid_ratio = 0.5\n    assert _almost_same(b1.top_ref_height, 14)\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_BOX, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_divided_box():\n    b1 = GridfinityBox(3, 3, 3, holes=True, length_div=2, width_div=1)\n    r = b1.render()\n    if _export_files(\"box\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n    assert _almost_same(size_3d(r), (125.5, 125.5, 24.8))\n    assert _faces_match(r, \">Z\", 1)\n    assert _faces_match(r, \"<Z\", 9)\n    assert _edges_match(r, \">Z\", 16)\n    assert _edges_match(r, \"<Z\", 108)\n    assert len(r.faces(FlatFaceSelector(21)).vals()) == 1\n    bs = FlatEdgeSelector(21) - EdgeLengthSelector(\"<0.1\")\n    assert len(r.edges(bs).vals()) == 54\n    assert b1.filename() == \"gf_box_3x3x3_div2x1_holes\"\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_BOX, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_all_features_box():\n    b1 = GridfinityBox(\n        4, 2, 5, holes=True, length_div=2, width_div=1, scoops=True, labels=True\n    )\n    b1.label_height = 9\n    b1.scoop_rad = 20\n    r = b1.render()\n    assert _almost_same(size_3d(r), (167.5, 83.5, 38.8))\n    s1 = str(b1)\n    assert len(s1.splitlines()) == 9\n    assert \"167.50 x 83.50 x 38.80 mm\" in s1\n    assert \"thickness: 1.00 mm\" in s1\n    assert \"20.00 mm radius\" in s1\n    assert \"label shelf 12.00 mm wide\" in s1\n    assert \"25.20\" in s1\n    assert \"54.37\" in s1\n    assert \"40.15\" in s1\n    assert \"gf_box_4x2x5_div2x1_holes_scoops_labels\" in s1\n    if _export_files(\"box\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n        b1.save_stl_file(path=EXPORT_STEP_FILE_PATH)\n    assert _faces_match(r, \">Z\", 1)\n    assert _faces_match(r, \"<Z\", 8)\n    assert _edges_match(r, \">Z\", 16)\n    assert _edges_match(r, \"<Z\", 96)\n    assert len(r.faces(FlatFaceSelector(35)).vals()) == 1\n    assert len(r.edges(FlatEdgeSelector(35)).vals()) == 51\n    assert b1.filename() == \"gf_box_4x2x5_div2x1_holes_scoops_labels\"\n    b1 = GridfinityBox(\n        2, 2, 3, holes=True, length_div=1, width_div=1, scoops=True, labels=True\n    )\n    r = b1.render()\n    assert _almost_same(size_3d(r), (83.5, 83.5, 24.8))\n    if _export_files(\"box\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n        b1 = GridfinityBox(\n            2,\n            2,\n            3,\n            holes=True,\n            length_div=1,\n            width_div=1,\n            scoops=True,\n            labels=True,\n            wall_th=1.25,\n        )\n        r = b1.render()\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n"
  },
  {
    "path": "tests/test_rbox.py",
    "content": "# Gridfinity tests\nimport pytest\n\n# my modules\nfrom cqgridfinity import *\n\nfrom cqkit.cq_helpers import *\nfrom cqkit import *\n\nfrom common_test import (\n    EXPORT_STEP_FILE_PATH,\n    _almost_same,\n    _export_files,\n    SKIP_TEST_RBOX,\n)\n\n\ndef _rugged_box():\n    b1 = GridfinityRuggedBox(5, 4, 6)\n    b1.inside_baseplate = True\n    b1.lid_baseplate = True\n    b1.front_handle = True\n    b1.front_label = True\n    b1.side_clasps = True\n    b1.stackable = True\n    b1.wall_vgrooves = True\n    b1.side_handles = True\n    b1.back_feet = True\n    b1.hinge_bolted = False\n    return b1\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_RBOX, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_rugged_box():\n    b1 = _rugged_box()\n    assert b1.filename() == \"gf_ruggedbox_5x4x6_fr-hl_sd-hc_stack_lidbp\"\n    r = b1.render()\n    assert r is not None\n    assert _almost_same(size_3d(r), (230.0, 194.15, 47.5))\n    if _export_files(\"rbox\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_RBOX, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_rugged_box_lid():\n    b1 = _rugged_box()\n    r = b1.render_lid()\n    assert r is not None\n    assert _almost_same(size_3d(r), (230.0, 188, 12.5))\n    assert b1.filename() == \"gf_ruggedbox_5x4x6_lid_fr-hl_sd-hc_stack_lidbp\"\n    if _export_files(\"rbox\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_RBOX, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_rugged_box_acc():\n    b1 = _rugged_box()\n    r = b1.render_accessories()\n    assert len(r.solids().vals()) == 16\n    assert b1.filename() == \"gf_ruggedbox_5x4x6_acc_fr-hl_sd-hc_stack_lidbp\"\n    if _export_files(\"rbox\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_RBOX, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_rugged_box_parts():\n    b1 = _rugged_box()\n    r = b1.render_handle()\n    assert r is not None\n    assert b1.filename() == \"gf_ruggedbox_5x4x6_handle_fr-hl_sd-hc_stack_lidbp\"\n    if _export_files(\"rbox\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n    r = b1.render_hinge()\n    assert r is not None\n    assert b1.filename() == \"gf_ruggedbox_5x4x6_hinge_fr-hl_sd-hc_stack_lidbp\"\n    if _export_files(\"rbox\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n    r = b1.render_label()\n    assert r is not None\n    assert b1.filename() == \"gf_ruggedbox_5x4x6_label_fr-hl_sd-hc_stack_lidbp\"\n    if _export_files(\"rbox\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n    r = b1.render_latch()\n    assert r is not None\n    assert b1.filename() == \"gf_ruggedbox_5x4x6_latch_fr-hl_sd-hc_stack_lidbp\"\n    if _export_files(\"rbox\"):\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_RBOX, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_rugged_box_assembly():\n    if _export_files(\"rbox\"):\n        b1 = _rugged_box()\n        r = b1.render_assembly()\n        assert b1.filename() == \"gf_ruggedbox_5x4x6_assembly_fr-hl_sd-hc_stack_lidbp\"\n        b1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n"
  },
  {
    "path": "tests/test_spacer.py",
    "content": "# Gridfinity tests\nimport pytest\n\n# my modules\nfrom cadquery import exporters\nfrom cqgridfinity import *\nfrom cqkit.cq_helpers import size_3d\nfrom cqkit import export_step_file\n\nfrom common_test import (\n    EXPORT_STEP_FILE_PATH,\n    _almost_same,\n    _export_files,\n    INCHES,\n    SKIP_TEST_SPACER,\n)\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_SPACER, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_spacer():\n    s0 = GridfinityDrawerSpacer(582, 481, tolerance=0.25)\n    assert s0.size_u[0] == 13\n    assert s0.size_u[1] == 11\n    assert s0.length_u == 4\n    assert s0.width_u == 3\n    assert s0.length_fill == 5 * GRU\n    assert s0.width_fill == 5 * GRU\n    assert s0.wide_enough\n    assert s0.deep_enough\n    assert _almost_same(s0.length_th, 9.25, tol=0.01)\n    assert _almost_same(s0.width_th, 17.75, tol=0.01)\n\n    s1 = GridfinityDrawerSpacer(tolerance=0.25)\n    s1.best_fit_to_dim(582, 300)\n    assert s1.size_u[0] == 13\n    assert s1.size_u[1] == 7\n    assert s1.width_u == 2\n    assert s1.width_fill == 3 * GRU\n    assert s1.wide_enough\n    assert not s1.deep_enough\n\n    s1.best_fit_to_dim(300, 582)\n    assert s1.size_u[0] == 7\n    assert s1.size_u[1] == 13\n    assert s1.length_u == 2\n    assert s1.length_fill == 3 * GRU\n    assert not s1.wide_enough\n    assert s1.deep_enough\n\n    s1.best_fit_to_dim(INCHES(11.5), INCHES(20.5))\n    assert s1.size_u[0] == 6\n    assert s1.size_u[1] == 12\n    assert s1.length_u == 2\n    assert s1.width_u == 4\n    assert s1.length_fill == 2 * GRU\n    assert s1.width_fill == 4 * GRU\n    assert s1.wide_enough\n    assert s1.deep_enough\n    assert _almost_same(s1.length_th, 8.10, tol=0.01)\n    assert _almost_same(s1.width_th, 19.80, tol=0.01)\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_SPACER, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_spacer_render():\n    s1 = GridfinityDrawerSpacer(tolerance=0.25)\n    dx, dy = INCHES(22 + 15 / 16), INCHES(16.25)\n    s1.best_fit_to_dim(dx, dy)\n    assert s1.size_u[0] == 13\n    assert s1.size_u[1] == 9\n    assert s1.length_u == 4\n    assert s1.width_u == 3\n    assert s1.length_fill == 5 * GRU\n    assert s1.width_fill == 3 * GRU\n    assert s1.wide_enough\n    assert s1.deep_enough\n    assert _almost_same(s1.length_th, 17.12, tol=0.01)\n    assert _almost_same(s1.width_th, 18.06, tol=0.01)\n    r = s1.render_full_set()\n    assert _almost_same(size_3d(r), (582.6125, 412.75, 4.75))\n    assert s1.filename() == \"gf_drawer_4x3_full_set\"\n    if _export_files(\"spacer\"):\n        s1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n    rh = s1.render_half_set()\n    assert _almost_same(size_3d(rh), (253.084, 177.0625, 4.75))\n    assert s1.filename() == \"gf_drawer_4x3_half_set\"\n    if _export_files(\"spacer\"):\n        s1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n    r = s1.render_length_filler()\n    assert s1.filename() == \"gf_drawer_4x3_length_spacer\"\n    if _export_files(\"spacer\"):\n        s1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n    r = s1.render_width_filler()\n    assert s1.filename() == \"gf_drawer_4x3_width_spacer\"\n    if _export_files(\"spacer\"):\n        s1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n    r = s1.render()\n    assert s1.filename() == \"gf_drawer_4x3_corner_spacer\"\n    if _export_files(\"spacer\"):\n        s1.save_step_file(path=EXPORT_STEP_FILE_PATH)\n\n\n@pytest.mark.skipif(\n    SKIP_TEST_SPACER, reason=\"Skipped intentionally by test scope environment variable\"\n)\ndef test_back_only_spacer():\n    s0 = GridfinityDrawerSpacer(tolerance=0.25, front_and_back=False)\n    dx, dy = 414, 366\n    s0.best_fit_to_dim(dx, dy, verbose=False)\n    assert s0.size_u[0] == 9\n    assert s0.size_u[1] == 8\n    assert s0.length_u == 3\n    assert s0.width_u == 2\n    assert s0.wide_enough\n    assert s0.deep_enough\n    assert _almost_same(s0.fb_length_th, 29.5, tol=0.01)\n    assert _almost_same(s0.width_th, 17.75, tol=0.01)\n    r = s0.render_full_set()\n    assert _almost_same(size_3d(r), (414, 366, 4.75))\n\n    s0 = GridfinityDrawerSpacer(tolerance=0.25, front_and_back=True)\n    dx, dy = 414, 366\n    s0.best_fit_to_dim(dx, dy, verbose=False)\n    assert s0.size_u[0] == 9\n    assert s0.size_u[1] == 8\n    assert s0.length_u == 3\n    assert s0.width_u == 2\n    assert s0.wide_enough\n    assert s0.deep_enough\n    assert _almost_same(s0.fb_length_th, 14.75, tol=0.01)\n    assert _almost_same(s0.width_th, 17.75, tol=0.01)\n    r = s0.render_full_set()\n    assert _almost_same(size_3d(r), (414, 366, 4.75))\n"
  },
  {
    "path": "tests/testfiles/.gitkeep",
    "content": ""
  }
]