commit ee43cc02deb7488d8a61814335255833e4080d5d Author: Roberto Alsina Date: Sat Apr 15 20:35:47 2023 -0300 Initial import diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..91b8e6c --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +extend-ignore = E501, E266 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2228173 --- /dev/null +++ b/.gitignore @@ -0,0 +1,176 @@ + + +### Created by https://www.gitignore.io +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +.lint diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d59a47a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.formatting.provider": "black", + "emeraldwalk.runonsave": { + "commands": [ + { + "match": ".py", + "isAsync": true, + "cmd": "make" + } + } + } \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2ccbfc6 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +STL_FILES = base.stl hinged_lid.stl simple_lid.stl tandy_lid.stl + +all: $(STL_FILES) lint + +%.stl: %.py dimensions.py utils.py components/*py + python $< + +lint: .lint + +.lint: **.py + flake8 + touch .lint + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a61226b --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Homemade Computer Project To Be Named Later + +Here are the files used to build the case for my homemade +computer. You can see a lot about it in [this article](http://ralsina.me/weblog/posts/so-i-built-a-laptop.html) (or at least about it as it was in early march 2023) + +The main gist is to use [CadQuery](https://cadquery.readthedocs.io/en/latest/) +and Python to build flexible, deeply parametric cases for computers based on +Single Board Computers (think Raspberry Pi and similar things). + +## How flexible? + +Suppose you buy a mechanic keyboard and use it as the base to build something like a classic C64-style wedge using a cheap 3d printer: + +![Image of a wedge-style computer case](https://pbs.twimg.com/media/FtsB6wiX0AEqrHF?format=jpg&name=large) + +Yes, you can do this using pretty much any mechanical keyboard you are willing to butcher. + +And then you remove a few screws, replace a few components and turn it into a +Tandy Model-100 style laptop: + +![Image of a Tandy style laptop case](http://ralsina.me/galleries/laptop/IMG20230302142042.thumbnail.jpg) + +Or into a "normal" notebook: + +![Image of something somewhat notebook-like](https://pbs.twimg.com/media/FtTFtMoXoAEblTb?format=jpg&name=large) + +And what's inside? + +* A SBC +* Maybe batteries +* Maybe a USB hub +* Perhaps a soundcard? +* Storage? + +How would I know, you are going to be the one that builds it! + +## How is it going + +The basic concepts work, as proven by me building the damned things. But still: + +* The software needs a lot of work +* It has to be made much more user friendly +* All the "lids" are pretty custom one-offs (they need to be made more parametric) +* The component library is very limited (just the things I am using in my build) + +None of those things is an insurmountable problem, and I am working on them, +and I have plans to fix it all. Eventually. Some day. + +In the meantime, if you want to use any of this and need a hand, just contact me at roberto.alsina@gmail.com and I'll try to help. \ No newline at end of file diff --git a/base.py b/base.py new file mode 100644 index 0000000..b5936b2 --- /dev/null +++ b/base.py @@ -0,0 +1,209 @@ +import cadquery as cq +# from cq_warehouse.drafting import Draft + +import components.audio_plug as audio_plug +import components.battery_holder as battery_holder +import components.hdmi_out as hdmi_out +import components.keyboard as keyboard +import components.screen_pillars as screen_pillars +import components.usb_hub as usb_hub +import components.zero_holder as cpu_holder +import dimensions as dim + +from utils import export + +# Base for the notebook. Basically a kbd base that extends back +# as much as possible + +screen_pillars.init(dim.mounting_pillar_positions, dim.base_thickness - dim.shell_t) + + +def model(): + # Create the basic shape of the case bottom. + model = ( + cq.Workplane("XY") + .workplane(offset=dim.base_thickness / 2) + .tag("mid_height") + # Hollow box + .box(dim.width, dim.height, dim.base_thickness) + .edges("|Z") + .fillet(2) + .faces(">Z") + .shell(-dim.shell_t) + ) + + # Now the basic box shape is in place, start adding things + # and cutting holes. + + model = usb_hub.add( + model=model, + width=dim.width, + height=dim.height, + thickness=dim.base_thickness, + bottom_face=" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/base_left.stl b/base_left.stl new file mode 100644 index 0000000..cf49c38 Binary files /dev/null and b/base_left.stl differ diff --git a/base_right.stl b/base_right.stl new file mode 100644 index 0000000..4d5141d Binary files /dev/null and b/base_right.stl differ diff --git a/components/__init__.py b/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/audio_plug.py b/components/audio_plug.py new file mode 100644 index 0000000..baabceb --- /dev/null +++ b/components/audio_plug.py @@ -0,0 +1,86 @@ +# Hole to expose a USB audio card (YMMV) + +import cadquery as cq + +from utils import extrude_shape, punch_hole + +# The hole is for a random USB sound card. +# Consumers should set proper offsets for the hole + +item_w = 49 +item_h = 20.5 + +hole_w = 17 +hole_h = 5 + +holes = [ + # 2-jack plug + { + "x": -item_h / 2, + "y": 4, + "shape": cq.Sketch() + .trapezoid(hole_w, hole_h, 90, mode="a") + .vertices() + .fillet(2), + }, +] + +elements = [ + # Outline + { + "x": item_w / 2, + "y": item_h / 2, + "shape": ( + cq.Sketch() + .trapezoid(item_w, item_h, 90, mode="a") + .trapezoid(item_w - 2, item_h - 2, 90, mode="s") + ), + "height": 0.2, + }, +] + + +def add( + *, + model, + width, + height, + thickness, + offset_x, + offset_y, + bottom_face, + back_face, + shell_t +): + # Extrusions + if bottom_face: + for element in elements: + model = extrude_shape( + model=model, + face=bottom_face, + w=width, + h=height, + x_offset=offset_x, + y_offset=offset_y, + element=element, + height=-(element["height"] + shell_t), + ) + + # Holes + if back_face: + for hole in holes: + model = punch_hole( + model=model, + face=back_face, + # FIXME: This is weird because it's the RIGHT side, + # So it's height instead of w, offset_y instead of x + # need to work on making these coherent + w=height, + h=thickness, + x_offset=height - offset_y, + y_offset=shell_t, + hole=hole, + depth=shell_t, + ) + + return model diff --git a/components/battery_holder.py b/components/battery_holder.py new file mode 100644 index 0000000..5dbb36c --- /dev/null +++ b/components/battery_holder.py @@ -0,0 +1,150 @@ +import cadquery as cq + +from utils import extrude_shape, punch_hole, hex_vents + +stand_positions = [(3.5, 3.5), (61.5, 3.5), (61.5, 52.5), (3.5, 52.5)] +stands = ( + cq.Sketch().push(stand_positions).circle(3, mode="a").circle(2.65 / 2, mode="s") +) +pillar_height = 7 +width = 85 +height = 56 + +# This is a holder for DuPont cables so they connect to this +# things' pogo pins which are used to power the CPU +pin_positions = [(3.5, 0), (4 * 2.54 + 3.5, 0)] +pin_holder_width = 25 +pin_holder_height = 15 +pin_holder = ( + cq.Sketch() + .polygon( + [ + (0.5, 0), + (pin_holder_width, 0), + (pin_holder_width, pin_holder_height), + (0, pin_holder_height), + (0.5, 0), + ], + mode="a", + ) + .push(pin_positions) + .polygon( + [(0, 0), (2.6, 0), (2.6, pin_holder_height), (0, pin_holder_height), (0, 0)], + mode="s", + ) +) + +elements = [ + # Battery holder stands + { + "x": 0, + "y": 0, + "shape": stands, + "height": pillar_height, + }, + { + "x": 0, + "y": 0, + "shape": cq.Sketch().push(stand_positions).circle(5), + "height": 0, + }, + # Pogo pin connector channels + { + "x": 3.5, + "y": 43.5, + "shape": pin_holder, + "height": 3, + }, + # Perimeter + { + "x": width / 2, + "y": height / 2, + "shape": ( + cq.Sketch() + .trapezoid(width, height, 90, mode="a") + .trapezoid(width - 2, height - 2, 90, mode="s") + .vertices() + .fillet(3) + ), + "height": 0.2, + }, +] + + +vents = hex_vents(size=3, width=width, height=height) + + +# Hole distances are relative to the rightmost pillar +# seen from the back of the case, that's why they are negative +# Heights are relative to base of pillars +# All distances are measured to the CENTER of the hole +holes = [ + # Power inlet + { + "x": -18.5, + "y": -1 + pillar_height, + "shape": cq.Sketch().trapezoid(12, 6.5, 90, mode="a").vertices().fillet(1), + }, + # Power button + { + "x": -70, + "y": 5.5 + pillar_height, + "shape": cq.Sketch().trapezoid(7, 7, 90, mode="a").vertices().fillet(1), + }, +] + + +def add( + *, + model, + width, + height, + thickness, + offset_x, + offset_y, + bottom_face, + back_face, + shell_t +): + if bottom_face: + # Vents + for vent in vents: + model = punch_hole( + model=model, + face=bottom_face, + w=width, + h=height, + x_offset=offset_x + vent["x"], + y_offset=shell_t + offset_y + vent["y"], + hole=vent, + depth=shell_t, + ) + + # Battery holder stands and pogo pin holder + for element in elements: + model = extrude_shape( + model=model, + face=bottom_face, + w=width, + h=height, + x_offset=offset_x, + y_offset=shell_t + offset_y, + element=element, + height=-(element["height"] + shell_t), + ) + + if back_face: + # Holes + for hole in holes: + model = punch_hole( + model=model, + face=back_face, + w=width, + h=thickness, + x_offset=width - offset_x, + y_offset=shell_t, + hole=hole, + depth=shell_t, + ) + + return model diff --git a/components/hdmi_out.py b/components/hdmi_out.py new file mode 100644 index 0000000..07ad6e3 --- /dev/null +++ b/components/hdmi_out.py @@ -0,0 +1,46 @@ +# Hole to expose a USB audio card (YMMV) + +import cadquery as cq + +from utils import punch_hole + +# The hole is for a random USB sound card. +# Consumers should set proper offsets for the hole + +holes = [ + # Hole for HDMI female adapter + { + "x": 0, + "y": 7, + "shape": cq.Sketch().trapezoid(22, 12.5, 90, mode="a").vertices().fillet(2), + }, +] + + +def add( + *, + model, + width, + height, + thickness, + offset_x, + offset_y, + bottom_face, + back_face, + shell_t +): + # Holes + if back_face: + for hole in holes: + model = punch_hole( + model=model, + face=back_face, + w=width, + h=thickness, + x_offset=width - offset_x, + y_offset=shell_t, + hole=hole, + depth=shell_t, + ) + + return model diff --git a/components/keyboard.py b/components/keyboard.py new file mode 100644 index 0000000..9e7dcfa --- /dev/null +++ b/components/keyboard.py @@ -0,0 +1,101 @@ +import cadquery as cq + +# These should be set from dimensions.py +elements = None +kbd_pillar_positions = [] +kbd_height = 0 +kbd_width = 0 +kbd_back_thickness = 0 +kbd_front_thickness = 0 +kbd_actual_height = 0 +kbd_angle = 0 +kbd_pillar_offset_1 = 0 +kbd_pillar_radius_1 = 0 +kbd_pillar_offset_2 = 0 +kbd_pillar_radius_2 = 0 +kbd_screw_radius = 0 + + +def init(): + global elements + + elements = [ + # Shorter pillars + { + "x": 0, + "y": 0, + "z": kbd_pillar_offset_1, + "shape": cq.Sketch().push(kbd_pillar_positions).circle(kbd_pillar_radius_1, mode="a"), + }, + # Taller pillars with holes for self-tapping screws + { + "x": 0, + "y": 0, + "z": kbd_pillar_offset_2, + "shape": ( + cq.Sketch() + .push(kbd_pillar_positions) + .circle(kbd_pillar_radius_2, mode="a") + .circle(kbd_screw_radius, mode="s") + ), + }, + ] + + +def add( + *, + model, + width, + height, + thickness, + offset_x, + offset_y, + bottom_face, + back_face, + shell_t +): + # This one is special, it creates angled things and cuts off the + # case, so ... it's going to do weird stuff + + if bottom_face: + model = ( + model.faces(bottom_face) + .workplane(centerOption="CenterOfBoundBox", offset=-kbd_front_thickness) + .center( + -width / 2, + height / 2, + ) + .transformed(rotate=cq.Vector(kbd_angle, 0, 0)) + .tag("kbd_sloped") + ) + for element in elements: + model = ( + model.workplaneFromTagged("kbd_sloped") + .center(offset_x + element["x"], -offset_y - element["y"]) + .workplane(offset=element["z"]) + .placeSketch(element["shape"]) + .extrude(100) + ) + + model = ( + model.workplaneFromTagged("mid_height") + .transformed(offset=cq.Vector(0, 0, -thickness / 2)) + .split(keepTop=True) + .faces(">X") + .workplane(centerOption="CenterOfBoundBox") + .center(-height / 2, -thickness / 2) + .placeSketch( + cq.Sketch().polygon( + [ + [0, kbd_front_thickness], + [shell_t, kbd_front_thickness], + [kbd_actual_height + shell_t, kbd_back_thickness], + [kbd_actual_height + shell_t, 1000], + [0, 1000], + [0, kbd_front_thickness], + ] + ) + ) + .cutBlind(-1000) + ) + return model diff --git a/components/screen_pillars.py b/components/screen_pillars.py new file mode 100644 index 0000000..4cea0d4 --- /dev/null +++ b/components/screen_pillars.py @@ -0,0 +1,84 @@ +from utils import extrude_shape, punch_hole +import cadquery as cq + +elements = None +bottom_holes = None + +# These are set from dimensions.py +pillar_width = 0 +pillar_height = 0 +screw_head_radius = 0 +screw_head_depth = 0 +screw_radius = 0 + + +def init(positions, thickness): + """Because these need to match in multiple models, we create the + elemments dynamically""" + global elements, bottom_holes + elements = [ + { + "x": 0, + "y": 0, + "shape": cq.Sketch() + .push(positions) + .trapezoid(pillar_width, pillar_height, 90, mode="a"), + "height": thickness, + } + ] + + bottom_holes = [ + { + "x": 0, + "y": 0, + "shape": cq.Sketch().push(positions).circle(screw_head_radius, mode="a"), + "depth": screw_head_depth, + }, + { + "x": 0, + "y": 0, + "shape": cq.Sketch().push(positions).circle(screw_radius, mode="a"), + "depth": 100, + }, + ] + + +def add( + *, + model, + width, + height, + thickness, + offset_x, + offset_y, + bottom_face, + back_face, + shell_t +): + if bottom_face: + # Mounting pillars + for element in elements: + model = extrude_shape( + model=model, + face=bottom_face, + w=width, + h=height, + x_offset=offset_x, + y_offset=shell_t + offset_y, + element=element, + height=-(element["height"] + shell_t), + ) + # Screw holes + for hole in bottom_holes: + model = punch_hole( + model=model, + face=bottom_face, + w=width, + h=height, + x_offset=offset_x, + y_offset=shell_t + offset_y, + hole=hole, + depth=hole["depth"], + ) + + return model diff --git a/components/simple_lid.py b/components/simple_lid.py new file mode 100644 index 0000000..9ddf3f0 --- /dev/null +++ b/components/simple_lid.py @@ -0,0 +1,107 @@ +import cadquery as cq + +import dimensions as dim +from utils import extrude_shape2, hex_vents, punch_hole, export + + +def model(): + # Create the basic shape of the case lid + model = ( + cq.Workplane("XY") + # Hollow box + .box(dim.width, dim.sl_height, dim.sl_thickness) + .edges("|Z and >Y") + .fillet(2) + ) + + # Make many holes + vent = hex_vents(size=6, width=dim.width * 0.9, height=dim.sl_height * 0.9)[0] + model = punch_hole( + model=model, + face=">Z", + w=dim.width, + h=dim.sl_height, + x_offset=0.05 * dim.width, + y_offset=0.05 * dim.sl_height, + hole=vent, + depth=dim.sl_thickness, + ) + + # Add screw holes + for position in dim.mounting_pillar_positions: + model = ( + model.faces(">Z") + .workplane(centerOption="CenterOfBoundBox") + .center( + -dim.width / 2 + position[0], + dim.sl_height / 2 - position[1] - dim.shell_t, + ) + .placeSketch(cq.Sketch().circle(dim.m4_top / 2 + 1.5)) + .extrude(-dim.sl_thickness) + .faces(">Z") + .workplane(centerOption="CenterOfBoundBox") + .center( + -dim.width / 2 + position[0], + dim.sl_height / 2 - position[1] - dim.shell_t, + ) + .cskHole(dim.m4_bottom, dim.m4_top, 82, depth=None) + ) + + # Add front lip + + model = ( + model.faces(">Z") + .workplane(centerOption="CenterOfBoundBox") + .center(0, -dim.sl_height / 2 + dim.sl_lip_thickness / 2) + .placeSketch( + cq.Sketch().trapezoid(dim.width - 2 * dim.shell_t, dim.sl_lip_thickness, 90) + ) + .extrude(-dim.sl_front_lip - dim.sl_thickness) + ) + + return model + + +def decorative_cover(): + # A decorative thingie to cover the ugly seam in the middle + model = cq.Workplane("XY").box(10, dim.sl_height, 1).edges("|Z").fillet(1) + vent = hex_vents( + size=6, width=dim.width * 0.9, height=dim.sl_height * 0.9, density=0.775 + )[0] + + model = extrude_shape2( + model=model, + face=">Z", + w=dim.width, + h=dim.sl_height, + x_offset=0.05 * dim.width, + y_offset=0.05 * dim.sl_height, + hole=vent, + depth=3, + ) + return model + + +if __name__ == "__main__": + model = model() + export(model, "simple_lid.stl") + + cover = decorative_cover() + export(cover, "simple_lid_cover.stl") + + export( + model, + "simple_lid.svg", + opt={ + "projectionDir": (0, 0, 1), + }, + ) + + export( + model.faces(">X").workplane(offset=-dim.width / 2).split(keepTop=True), + "simple_lid_right.stl", + ) + export( + model.faces(">X").workplane(offset=-dim.width / 2).split(keepBottom=True), + "simple_lid_left.stl", + ) diff --git a/components/usb_hub.py b/components/usb_hub.py new file mode 100644 index 0000000..205da34 --- /dev/null +++ b/components/usb_hub.py @@ -0,0 +1,101 @@ +import cadquery as cq + +from utils import punch_hole, extrude_shape + +# Measurements for my USB hub, YMMV + +# The hole is for a USB-A plug, y is measured in the hub +# (from the bottom face to middle of the hole) +# Consumers should set proper offsets for the hole + +item_w = 17 +item_h = 93 + +holes = [ + # USB-A port + { + "x": -item_w / 2, + "y": 4, + "shape": cq.Sketch().trapezoid(13, 5, 90, mode="a").vertices().fillet(1), + }, +] + +elements = [ + # Thing to grab the hub + { + "x": item_w / 2, + "y": 5, + "shape": ( + cq.Sketch().trapezoid(22, 10, 90, mode="a").trapezoid(17, 10, 90, mode="s") + ), + "height": 8, + }, + { + "x": item_w / 2 + 5.5, + "y": item_h - 3, + "shape": (cq.Sketch().circle(2.5, mode="a")), + "height": 8, + }, + { + "x": item_w / 2 - 5.5, + "y": item_h - 3, + "shape": (cq.Sketch().circle(2.5, mode="a")), + "height": 8, + }, + # Outline + { + "x": item_w / 2, + "y": item_h / 2, + "shape": ( + cq.Sketch() + .trapezoid(item_w, item_h, 90, mode="a") + .trapezoid(item_w - 2, item_h - 2, 90, mode="s") + .vertices() + .fillet(3) + ), + "height": 0.2, + }, +] + + +def add( + *, + model, + width, + height, + thickness, + offset_x, + offset_y, + bottom_face, + back_face, + shell_t +): + # USB Hub extrusions + if bottom_face: + for element in elements: + model = extrude_shape( + model=model, + face=bottom_face, + w=width, + h=height, + x_offset=offset_x, + y_offset=shell_t + offset_y, + element=element, + height=-(element["height"] + shell_t), + ) + + # Holes + if back_face: + for hole in holes: + model = punch_hole( + model=model, + face=back_face, + w=width, + h=thickness, + x_offset=width - offset_x, + y_offset=shell_t, + hole=hole, + depth=shell_t, + ) + + return model diff --git a/components/zero_holder.py b/components/zero_holder.py new file mode 100644 index 0000000..42ff080 --- /dev/null +++ b/components/zero_holder.py @@ -0,0 +1,110 @@ +import cadquery as cq + +from utils import extrude_shape, punch_hole, hex_vents + +width = 65 +height = 30 +pillar_height = 7 + +stand_positions = [(3.5, 3.5), (3.5, 26.5), (61.5, 26.5), (61.5, 3.5)] + +stands = ( + cq.Sketch().push(stand_positions).circle(3, mode="a").circle(2.65 / 2, mode="s") +) + + +elements = [ + # CPU holder stands + { + "x": 0, + "y": 0, + "shape": stands, + "height": pillar_height, + }, + { + "x": 0, + "y": 0, + "shape": cq.Sketch().push(stand_positions).circle(5), + "height": 0, + }, + # Perimeter + { + "x": width / 2, + "y": height / 2, + "shape": ( + cq.Sketch() + .trapezoid(width, height, 90, mode="a") + .trapezoid(width - 2, height - 2, 90, mode="s") + .vertices() + .fillet(3) + ), + "height": 0.2, + }, +] + +vents = hex_vents(size=3, width=width, height=height) + +holes = [ + # One hole for everything TODO: improve + { + "x": -width / 2, + "y": 1 + pillar_height, + "shape": cq.Sketch().trapezoid(50, 6, 90, mode="a").vertices().fillet(1), + } +] + + +def add( + *, + model, + width, + height, + thickness, + offset_x, + offset_y, + bottom_face, + back_face, + shell_t +): + if bottom_face: + # Vents + for vent in vents: + model = punch_hole( + model=model, + face=bottom_face, + w=width, + h=height, + x_offset=offset_x + vent["x"], + y_offset=shell_t + offset_y + vent["y"], + hole=vent, + depth=shell_t, + ) + + # CPU holder extrusions + for element in elements: + model = extrude_shape( + model=model, + face=bottom_face, + w=width, + h=height, + x_offset=offset_x, + y_offset=shell_t + offset_y, + element=element, + height=-(element["height"] + shell_t), + ) + + # Holes + if back_face: + for hole in holes: + model = punch_hole( + model=model, + face=back_face, + w=width, + h=thickness, + x_offset=width - offset_x, + y_offset=shell_t, + hole=hole, + depth=shell_t, + ) + + return model diff --git a/dimensions.py b/dimensions.py new file mode 100644 index 0000000..6093942 --- /dev/null +++ b/dimensions.py @@ -0,0 +1,144 @@ +import math + +import components.audio_plug as audio_plug +import components.usb_hub as usb_hub +import components.keyboard as keyboard +import components.screen_pillars as screen_pillars + +## Standard things (TODO move to separate file) + +# M3 threaded insert sizes +ti_radius = 2.35 +ti_depth = 6.25 + +# M3 hex nut dimensions +m3_hn_diam = 5.5 +m3_hn_hole = 3 +m3_hn_thickness = 2.5 + +# Dimensions for countersunk M4 screws +m4_top = 9 +m4_bottom = 4 + +## Keyboard dimensions +keyboard.kbd_height = 95.5 +keyboard.kbd_width = 305 +keyboard.kbd_back_thickness = 19 +keyboard.kbd_front_thickness = 12 +# Pythagoras +keyboard.kbd_actual_height = ( + keyboard.kbd_height**2 + - (keyboard.kbd_back_thickness - keyboard.kbd_front_thickness) ** 2 +) ** 0.5 +keyboard.kbd_angle = ( + math.acos(keyboard.kbd_actual_height / keyboard.kbd_height) * 180 / math.pi +) +keyboard.kbd_pillar_positions = [ + (19, 16), + (142.5, 25.5), + (keyboard.kbd_width - 20, 16), + (23.5, 79.5), + (145.5, 82.5), + (keyboard.kbd_width - 19, 79.5), +] +keyboard.kbd_pillar_offset_1 = 5.5 +keyboard.kbd_pillar_radius_1 = 5 +keyboard.kbd_pillar_offset_2 = 2.5 +keyboard.kbd_pillar_radius_2 = 2.4 +keyboard.kbd_screw_radius = 1.1 +keyboard.init() + +## Screen dimensions +# Whole screen size +scr_w = 231 +scr_h = 65 +scr_thickness = 5.5 +# Visible screen size +vis_w = 219 +vis_h = 55 + + +## Dimensions for the base of the computer + +# Thickness of the outer material +shell_t = 3 + +# Size of the base +width = keyboard.kbd_width + 2 * shell_t +height = 159 +base_thickness = 30 + shell_t # 30 inside + + +# These are placed where convenient, and are used to join the top and bottom +# parts of the case. +# Measured from back-left corner OUTSIDE +mounting_pillar_positions = [ + (6, 6), + (6, 43), + (120, 6), + (170, 6), + (width - 6, 6), + (width - 6, 43), + (120, 48), + (170, 48), +] + +# Offset for the USB port from back-left corner +# of the case to left side of the hub +usb_offset_x = width - audio_plug.item_w - usb_hub.item_w + +# CPU holder position from back-left corner of the case +cpu_offset_x = 177 +cpu_offset_y = 2 + +# Battery holder position from back-left corner of the case +battery_offset_x = 15 +battery_offset_y = 3 + +# HDMI out hole from back-left corner of the case +hdmi_out_offset_x = 138 + +## Dimensions for the Tandy lid + +# Size of the whole object +tl_height = 66 +tl_height_bottom = 59 +tl_full_thickness = 48 # Will be shorter after construction +# Screen angle +tl_scr_angle = 20 + + +## Dimensions for the hinged lid + +# This is a constant used to control how far back the hinges go +# when open. It's arbitrary and can be adjusted experimentally +# printing small samples +hl_hinge_slant = shell_t + 2 + +hl_bezel_width = m3_hn_diam + 2 +hl_bezel_height = 1 +hl_bezel_thickness = 2 + +hl_hinge_radius = 5.5 +hl_screw_radius = 1.5 # M3 +hl_ring_radius = 5 # M3 +hl_hinge_offset = max(p[1] for p in mounting_pillar_positions) + 6 +hl_hinge_width = 25 +# Base + this lid +hl_full_thickness = 43 + + +## Dimensions for the simple lid +sl_lip_thickness = 1.5 +sl_height = ( + max([y for _, y in mounting_pillar_positions]) + 6 + shell_t + sl_lip_thickness +) +sl_thickness = shell_t +sl_front_lip = 8 + +## Dimensions for pillars that connect base and lids +screen_pillars.pillar_width = 12 +screen_pillars.pillar_height = 12 +screen_pillars.screw_head_radius = 3 +screen_pillars.screw_radius = 1.8 +screen_pillars.screw_head_depth = base_thickness - 13 # (screw thread length - threaded insert depth) diff --git a/hinged_lid.py b/hinged_lid.py new file mode 100644 index 0000000..951d92e --- /dev/null +++ b/hinged_lid.py @@ -0,0 +1,416 @@ +import cadquery as cq + +import dimensions as dim +import components.keyboard as keyboard +import components.screen_pillars as screen_pillars +from utils import export + +mounting_pillar_positions = [(x, -y) for x, y in dim.mounting_pillar_positions] +mounting_pillars = ( + cq.Sketch() + .push(mounting_pillar_positions) + .trapezoid(screen_pillars.pillar_height, screen_pillars.pillar_width, 90, mode="a") + .circle(dim.ti_radius, mode="s") + .clean() +) + + +def model(): + # Create a 2-part hinged lid + + model = ( + cq.Workplane("XY") + # Hollow box + .workplane(offset=-dim.hl_full_thickness / 2) + .box(dim.width, dim.height, dim.hl_full_thickness) + .tag("base") + .edges("|X and >Z and Z and >Y") + .fillet(5) + .edges("|Z") + .fillet(2) + .faces("X") + .workplane() + .center( + dim.height / 2 - dim.hl_hinge_offset, + dim.hl_full_thickness / 2 - dim.hl_hinge_radius, + ) + .tag("rightSide") + # Outer surface of the hinge + .workplaneFromTagged("rightSide") + .placeSketch(cq.Sketch().circle(dim.hl_hinge_radius)) + .extrude(-dim.hl_hinge_width) + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.width + dim.hl_hinge_width) + .placeSketch(cq.Sketch().circle(dim.hl_hinge_radius)) + .extrude(-dim.hl_hinge_width) + # Cut middle section between the hinges + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.hl_hinge_width) + .placeSketch( + cq.Sketch().polygon( + [ + (-dim.hl_hinge_radius, -dim.hl_hinge_radius), + (-dim.hl_hinge_radius, 0), + (-dim.hl_hinge_radius - dim.hl_hinge_slant, dim.hl_hinge_radius), + (-dim.hl_hinge_slant, dim.hl_hinge_radius), + (-dim.hl_hinge_slant, dim.hl_hinge_radius - dim.hl_hinge_slant), + (dim.hl_hinge_radius, dim.hl_hinge_radius - dim.hl_hinge_slant), + (dim.hl_hinge_radius, -dim.hl_hinge_radius), + (-dim.hl_hinge_radius, -dim.hl_hinge_radius), + ] + ) + ) + .cutBlind(-dim.width + 2 * dim.hl_hinge_width - 1) + # Pillars to attach to base + .workplaneFromTagged("base") + .workplane( + centerOption="CenterOfBoundBox", + offset=dim.base_thickness - dim.hl_full_thickness / 2, + ) + .workplaneFromTagged("base") + .workplane(offset=dim.hl_full_thickness / 2 - dim.shell_t) + .center(-dim.width / 2, dim.height / 2 - dim.shell_t) + .placeSketch(mounting_pillars) + .extrude(-10) + # Hole for screws + .workplaneFromTagged("rightSide") + .placeSketch(cq.Sketch().circle(dim.hl_screw_radius)) + .cutBlind(-dim.hl_hinge_width) + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.width + dim.hl_hinge_width) + .placeSketch(cq.Sketch().circle(dim.hl_screw_radius)) + .cutBlind(-dim.hl_hinge_width) + # Holes for rings & screw heads + .workplaneFromTagged("rightSide") + .placeSketch(cq.Sketch().circle(dim.hl_ring_radius)) + .cutBlind(-5) + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.width + 4) + .placeSketch(cq.Sketch().circle(dim.hl_ring_radius)) + .cutBlind(-5) + # Split hinge halves + .faces(">X") + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.hl_hinge_width / 2) + .placeSketch( + cq.Sketch().trapezoid( + dim.hl_hinge_radius * 2 + 1, dim.hl_hinge_radius * 2, 90 + ) + ) + .cutBlind(-1) + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.hl_hinge_width) + .placeSketch( + cq.Sketch().trapezoid( + dim.hl_hinge_radius * 2 + 1, dim.hl_hinge_radius * 2, 90 + ) + ) + .cutBlind(-1) + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.width + dim.hl_hinge_width / 2) + .placeSketch( + cq.Sketch().trapezoid( + dim.hl_hinge_radius * 2 + 1, dim.hl_hinge_radius * 2, 90 + ) + ) + .cutBlind(-1) + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.width + dim.hl_hinge_width) + .placeSketch( + cq.Sketch().trapezoid( + dim.hl_hinge_radius * 2 + 1, dim.hl_hinge_radius * 2, 90 + ) + ) + .cutBlind(-1) + # Threaded inserts + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.hl_hinge_width / 2) + .placeSketch(cq.Sketch().circle(dim.ti_radius)) + .cutBlind(-dim.ti_depth) + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.width + dim.hl_hinge_width / 2) + .placeSketch(cq.Sketch().circle(dim.ti_radius)) + .cutBlind(dim.ti_depth) + # Split two halves + # First cut for the right hinge + .workplaneFromTagged("rightSide") + .placeSketch( + cq.Sketch() + .polygon( + [ + (0, 0), + (-dim.hl_hinge_radius - 0.2, 0), + (-dim.hl_hinge_radius - dim.hl_hinge_slant, dim.hl_hinge_radius), + (0, dim.hl_hinge_radius), + (0, 0), + ] + ) + .polygon( + [ + (-dim.hl_hinge_radius - 0.2, 0), + (-dim.hl_hinge_radius - 0.2, -1000), + (-dim.hl_hinge_radius, -1000), + (-dim.hl_hinge_radius, 0), + (-dim.hl_hinge_radius - 0.2, 0), + ] + ) + .circle(dim.hl_hinge_radius, mode="s") + ) + .cutBlind(-dim.hl_hinge_width / 2 - 1) + # Second cut for the right hinge + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.hl_hinge_width / 2) + .placeSketch( + cq.Sketch() + .polygon( + [ + (0, 0), + (dim.hl_hinge_radius + 0.2, 0), + ( + dim.hl_hinge_radius + 0.2 + dim.hl_hinge_slant, + dim.hl_hinge_radius, + ), + (0, dim.hl_hinge_radius), + (0, 0), + ] + ) + .circle(dim.hl_hinge_radius, mode="s") + ) + .cutBlind(-dim.hl_hinge_width / 2 - 1) + # First cut for the left hinge + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.width + dim.hl_hinge_width) + .placeSketch( + cq.Sketch() + .polygon( + [ + (0, 0), + (dim.hl_hinge_radius + 0.2, 0), + ( + dim.hl_hinge_radius + 0.2 + dim.hl_hinge_slant, + dim.hl_hinge_radius, + ), + (0, dim.hl_hinge_radius), + (0, 0), + ] + ) + .circle(dim.hl_hinge_radius, mode="s") + ) + .cutBlind(-dim.hl_hinge_width / 2 - 1) + # Second cut for the left hinge + .workplaneFromTagged("rightSide") + .workplane(offset=-dim.width + dim.hl_hinge_width / 2) + .placeSketch( + cq.Sketch() + .polygon( + [ + (0, 0), + (-dim.hl_hinge_radius - 0.2, 0), + (-dim.hl_hinge_radius - dim.hl_hinge_slant, dim.hl_hinge_radius), + (0, dim.hl_hinge_radius), + (0, 0), + ] + ) + .polygon( + [ + (-dim.hl_hinge_radius - 0.2, 0), + (-dim.hl_hinge_radius - 0.2, -1000), + (-dim.hl_hinge_radius, -1000), + (-dim.hl_hinge_radius, 0), + (-dim.hl_hinge_radius - 0.2, 0), + ] + ) + .circle(dim.hl_hinge_radius, mode="s") + ) + .cutBlind(-dim.hl_hinge_width / 2 - 1) + ) + + # Screen mount + model = ( + # 1st layer + model.workplaneFromTagged("base") + .center(0, -32) + .workplane(offset=dim.hl_full_thickness / 2 - dim.shell_t) + .tag("screen_plane") + .placeSketch( + cq.Sketch() + .trapezoid( + dim.scr_w + 2 * dim.hl_bezel_width, + dim.scr_h + 2 * dim.hl_bezel_height, + 90, + ) + .vertices() + .fillet(2) + ) + .extrude(-2 - dim.scr_thickness) + # Hole for screws + .workplaneFromTagged("screen_plane") + .workplane(offset=1) + .rect( + dim.scr_w + 2 * dim.hl_bezel_width - dim.m3_hn_diam - 1, + dim.scr_h + 2 * dim.hl_bezel_height - dim.m3_hn_diam - 1, + forConstruction=True, + ) + .vertices() + .hole(dim.m3_hn_hole, depth=10) + # Holes for captured nuts + .workplaneFromTagged("screen_plane") + .workplane(offset=1) + .rect( + dim.scr_w + 2 * dim.hl_bezel_width - dim.m3_hn_diam - 1, + dim.scr_h + 2 * dim.hl_bezel_height - dim.m3_hn_diam - 1, + forConstruction=True, + ) + .vertices() + .hole(dim.m3_hn_diam, depth=dim.m3_hn_thickness + 0.5) + # Remove middle of the screen holder + .workplaneFromTagged("screen_plane") + .placeSketch( + cq.Sketch().trapezoid( + dim.scr_w - 40, + dim.scr_h + 2 * dim.hl_bezel_height, + 90, + ) + ) + .cutBlind(-100) + # Hole to place screen + .workplaneFromTagged("screen_plane") + .workplane(offset=-dim.scr_thickness - 2) + .placeSketch(cq.Sketch().trapezoid(dim.scr_w, dim.scr_h, 90)) + .cutBlind(dim.scr_thickness) + ) + + # Cut off shape of the base + model = ( + model.workplaneFromTagged("rightSide") + .center( + -dim.height + dim.hl_hinge_offset, + -dim.hl_full_thickness + dim.hl_hinge_radius, + ) + .placeSketch( + cq.Sketch().polygon( + [ + (0, 0), + (0, keyboard.kbd_front_thickness), + (dim.shell_t, keyboard.kbd_front_thickness), + (keyboard.kbd_actual_height + dim.shell_t, keyboard.kbd_back_thickness), + (keyboard.kbd_actual_height + dim.shell_t, dim.base_thickness), + (dim.height, dim.base_thickness), + (dim.height, 0), + (0, 0), + ] + ) + ) + .cutBlind(-1000) + ) + + return model + + +def front_bezel(): + model = ( + cq.Workplane("XY") + # Hollow box + .tag("base") + .placeSketch( + cq.Sketch() + .trapezoid( + dim.scr_w + 2 * dim.hl_bezel_width + 2 * dim.hl_bezel_thickness, + dim.scr_h + 2 * dim.hl_bezel_height + 2 * dim.hl_bezel_thickness, + 90, + ) + .vertices() + .fillet(2) + ) + .extrude(-2 - dim.scr_thickness - dim.hl_bezel_thickness) + .workplaneFromTagged("base") + .workplane(offset=-dim.hl_bezel_thickness) + .placeSketch( + cq.Sketch() + .trapezoid( + dim.scr_w + 2 * dim.hl_bezel_width, + dim.scr_h + 2 * dim.hl_bezel_height, + 90, + ) + .vertices() + .fillet(2) + ) + .cutBlind(-100) + # Holes for screws + .workplaneFromTagged("base") + .rect( + dim.scr_w + 2 * dim.hl_bezel_width - dim.m3_hn_diam - 1, + dim.scr_h + 2 * dim.hl_bezel_height - dim.m3_hn_diam - 1, + forConstruction=True, + ) + .vertices() + .hole(dim.m3_hn_hole, depth=10) + # Viewport hole + .workplaneFromTagged("base") + .placeSketch( + cq.Sketch() + .trapezoid( + dim.vis_w, + dim.vis_h, + 90, + ) + .vertices() + .fillet(2) + ) + .cutBlind("last") + # Cable gap + .workplaneFromTagged("base") + .workplane(offset=-dim.scr_thickness - dim.hl_bezel_thickness) + .center(0, 10) + .placeSketch( + cq.Sketch() + .trapezoid( + dim.vis_w, + dim.vis_h, + 90, + ) + .vertices() + .fillet(2) + ) + .cutBlind(-10) + ) + return model + + +if __name__ == "__main__": + model = model() + + export(model, "hinged_lid.stl") + + export(front_bezel(), "hinged_lid_bezel.stl") + + export( + model, + "hinged_lid.svg", + opt={ + "projectionDir": (0, 0, -1), + "strokeWidth": 0.3, + }, + ) + + offset_width = -dim.width / 2 + + right_side = ( + model.faces(">X") + .workplane(centerOption="CenterOfBoundBox", offset=offset_width) + .split(keepTop=True) + ) + + export(right_side, "hinged_lid_right.stl") + + left_side = ( + model.faces(">X") + .workplane(centerOption="CenterOfBoundBox", offset=offset_width) + .split(keepBottom=True) + ) + + export(left_side, "hinged_lid_left.stl") diff --git a/hinged_lid.stl b/hinged_lid.stl new file mode 100644 index 0000000..aaca320 Binary files /dev/null and b/hinged_lid.stl differ diff --git a/hinged_lid.svg b/hinged_lid.svg new file mode 100644 index 0000000..36c3048 --- /dev/null +++ b/hinged_lid.svg @@ -0,0 +1,402 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hinged_lid_bezel.stl b/hinged_lid_bezel.stl new file mode 100644 index 0000000..81fead3 Binary files /dev/null and b/hinged_lid_bezel.stl differ diff --git a/hinged_lid_left.stl b/hinged_lid_left.stl new file mode 100644 index 0000000..3b974b4 Binary files /dev/null and b/hinged_lid_left.stl differ diff --git a/hinged_lid_right.stl b/hinged_lid_right.stl new file mode 100644 index 0000000..8831d8a Binary files /dev/null and b/hinged_lid_right.stl differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..430e0cb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +cadquery +git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse +flake8 diff --git a/simple_lid.stl b/simple_lid.stl new file mode 100644 index 0000000..a4ef5d1 Binary files /dev/null and b/simple_lid.stl differ diff --git a/simple_lid.svg b/simple_lid.svg new file mode 100644 index 0000000..19f9445 --- /dev/null +++ b/simple_lid.svg @@ -0,0 +1,1269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/simple_lid_cover.stl b/simple_lid_cover.stl new file mode 100644 index 0000000..939be39 Binary files /dev/null and b/simple_lid_cover.stl differ diff --git a/simple_lid_left.stl b/simple_lid_left.stl new file mode 100644 index 0000000..cb0aedb Binary files /dev/null and b/simple_lid_left.stl differ diff --git a/simple_lid_right.stl b/simple_lid_right.stl new file mode 100644 index 0000000..23a9aff Binary files /dev/null and b/simple_lid_right.stl differ diff --git a/tandy_lid.py b/tandy_lid.py new file mode 100644 index 0000000..64e40d3 --- /dev/null +++ b/tandy_lid.py @@ -0,0 +1,133 @@ +import cadquery as cq + +import dimensions as dim +from utils import export +from components import screen_pillars + +viewport_cutout = ( + cq.Sketch().trapezoid(dim.vis_w, dim.vis_h, 90, mode="a").vertices().fillet(2) +) +screen_cutout = cq.Sketch().trapezoid(dim.scr_w, dim.scr_h, 90, mode="a") + +# Circuit board and cable hole. +# This is in the back of the screen, and is a bit shorter in height than the +# screen. It's wider so it removes enough material to make the shape simpler. +board_cutout = cq.Sketch().trapezoid( + dim.scr_w + 5, + dim.scr_h - 10, + 90, + mode="a", +) + +kbd_cable_hole = cq.Sketch().trapezoid(20, 9, 90, mode="a").vertices().fillet(1) + +# y needs to be inverted because this is the top side, and there's 2 pillars we don't use +mounting_pillar_positions = [(x, -y) for x, y in dim.mounting_pillar_positions[:-2]] + +mounting_pillars = ( + cq.Sketch() + .push(dim.mounting_pillar_positions) + .trapezoid(screen_pillars.pillar_width, screen_pillars.pillar_height, 90, mode="a") + .circle(dim.ti_radius, mode="s") + .clean() +) + + +def model(): + return ( + cq.Workplane("XY") + .workplane() + .tag("mid_height") + .box(dim.width, dim.tl_height, dim.tl_full_thickness) + # The screen goes rotated + .faces(">Z") + .transformed(rotate=(dim.tl_scr_angle, 0, 0)) + # Move the screen "lower" so it doesn't interfere + # so much with the back + .center(0, -2) + .tag("slanted") + # Arbitrary huge trapezoid to cut off the material *in front* + # of the inclined screen + .placeSketch(cq.Sketch().trapezoid(1000, 1000, 90, mode="a")) + .cutBlind(1000) + # Trim the top + .workplaneFromTagged("mid_height") + .workplane(offset=21) + .placeSketch(cq.Sketch().trapezoid(1000, 1000, 90, mode="a")) + .cutBlind(100) + # Make bottom smaller to fit with base + .faces(">X") + .workplane(centerOption="CenterOfBoundBox") + .center(-dim.tl_height / 2, -dim.tl_full_thickness / 2) + .placeSketch( + cq.Sketch() + .polygon( + [ + (dim.tl_height_bottom, 0), + (dim.tl_height_bottom, dim.tl_full_thickness / 3), + (dim.tl_height, dim.tl_full_thickness - 21), + (dim.tl_height, dim.tl_full_thickness), + (dim.tl_height + 5, dim.tl_full_thickness + 5), + (dim.tl_height + 5, 0), + (dim.tl_height_bottom, 0), + ] + ) + .vertices() + .fillet(3) + ) + .cutBlind(-1000) + # Fillet top of the object + .edges("|X and >Z") + .fillet(3) + # Cut off viewport hole so we can see the screen + .workplaneFromTagged("slanted") + .placeSketch(viewport_cutout) + .cutBlind(-dim.shell_t) + # Make hole for screen assembly so the whole screen fits + .workplaneFromTagged("slanted") + .workplane(offset=-dim.shell_t, centerOption="CenterOfBoundBox") + # Left bezel is wider than right one, so this hole is displaced to the left + .center(-3, 0) + .placeSketch(screen_cutout) + .cutBlind(-dim.scr_thickness) + # Make it hollow + .faces("(0, -10, 5)") + .fillet(2) + ) + + +if __name__ == "__main__": + model = model() + export(model, "tandy_lid.stl") + + offset_width = -dim.width / 2 + + right_side = ( + model.faces(">X") + .workplane(centerOption="CenterOfBoundBox", offset=offset_width) + .split(keepTop=True) + ) + + export(right_side, "tandy_lid_right.stl") + + left_side = ( + model.faces(">X") + .workplane(centerOption="CenterOfBoundBox", offset=offset_width) + .split(keepBottom=True) + ) + + export(left_side, "tandy_lid_left.stl") diff --git a/tandy_lid.stl b/tandy_lid.stl new file mode 100644 index 0000000..e3e3e0d Binary files /dev/null and b/tandy_lid.stl differ diff --git a/tandy_lid_left.stl b/tandy_lid_left.stl new file mode 100644 index 0000000..a9db41d Binary files /dev/null and b/tandy_lid_left.stl differ diff --git a/tandy_lid_right.stl b/tandy_lid_right.stl new file mode 100644 index 0000000..a34ab66 Binary files /dev/null and b/tandy_lid_right.stl differ diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..8837f43 --- /dev/null +++ b/utils.py @@ -0,0 +1,83 @@ +import shutil +import tempfile +from math import floor + +import cadquery as cq +from cadquery import exporters + + +def extrude_shape(*, model, face, w, h, x_offset, y_offset, element, height): + return ( + model.faces(face) + .workplane(centerOption="CenterOfBoundBox") + .center(-w / 2 + x_offset + element["x"], -h / 2 + y_offset + element["y"]) + .placeSketch(element["shape"]) + .extrude(height) + ) + + +def punch_hole(*, model, face, w, h, x_offset, y_offset, hole, depth): + return ( + model.faces(face) + .workplane(centerOption="CenterOfBoundBox") + .center(-w / 2 + x_offset + hole["x"], -h / 2 + y_offset + hole["y"]) + .placeSketch(hole["shape"]) + .cutBlind(-depth) + ) + + +def extrude_shape2(*, model, face, w, h, x_offset, y_offset, hole, depth): + return ( + model.faces(face) + .workplane(centerOption="CenterOfBoundBox") + .center(-w / 2 + x_offset + hole["x"], -h / 2 + y_offset + hole["y"]) + .placeSketch(hole["shape"]) + .extrude(-depth) + ) + + +def hex_vents(*, size, width, height, density=0.85): + # size is radius of the hexagon + # Information about how this works: + # https://www.redblobgames.com/grids/hexagons/ + + x_step = size * (3**0.5) + y_step = size * 3 / 2 + + x_count = floor(width / x_step) - 1 + + if height > 4 * size: + y_count = floor((height - 2 * size) / (1.5 * size)) + else: + y_count = 1 + + x_size = (x_count + 0.5) * x_step # Assumes at least 2 rows + y_size = 2 * size + 1.5 * size * (y_count - 1) + + x_offset = (width - x_size) / 2 + 0.5 * x_step + y_offset = (height - y_size) / 2 + size + + vent_positions = [] + for x in range(0, x_count): + for y in range(0, y_count): + vent_positions.append( + ( + (x + (y % 2) / 2) * x_step + x_offset, + y * y_step + y_offset, + ) + ) + vents = [ + { + "x": 0, + "y": 0, + "shape": cq.Sketch().push(vent_positions).regularPolygon(size * density, 6), + } + ] + + return vents + + +def export(model, fname, **kwarg): + tmpfile = tempfile.mktemp(suffix="." + fname.split(".")[-1]) + exporters.export(model, tmpfile, **kwarg) + shutil.move(tmpfile, fname)