mirror of
https://github.com/Proteus-Typer/model-a.git
synced 2024-09-20 23:41:22 +00:00
Initial import
This commit is contained in:
commit
ee43cc02de
176
.gitignore
vendored
Normal file
176
.gitignore
vendored
Normal file
@ -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
|
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"python.formatting.provider": "black",
|
||||
"emeraldwalk.runonsave": {
|
||||
"commands": [
|
||||
{
|
||||
"match": ".py",
|
||||
"isAsync": true,
|
||||
"cmd": "make"
|
||||
}
|
||||
}
|
||||
}
|
13
Makefile
Normal file
13
Makefile
Normal file
@ -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
|
||||
|
49
README.md
Normal file
49
README.md
Normal file
@ -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.
|
209
base.py
Normal file
209
base.py
Normal file
@ -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="<Z",
|
||||
back_face=">Y",
|
||||
offset_x=dim.usb_offset_x,
|
||||
offset_y=0,
|
||||
shell_t=dim.shell_t,
|
||||
)
|
||||
|
||||
# Hole for audio in right side
|
||||
model = audio_plug.add(
|
||||
model=model,
|
||||
width=dim.width,
|
||||
height=dim.height,
|
||||
thickness=dim.base_thickness,
|
||||
offset_x=dim.width - audio_plug.item_w,
|
||||
offset_y=19,
|
||||
bottom_face="<Z",
|
||||
back_face=">X",
|
||||
shell_t=dim.shell_t,
|
||||
)
|
||||
|
||||
# Hole for HDMI out in the back
|
||||
model = hdmi_out.add(
|
||||
model=model,
|
||||
width=dim.width,
|
||||
height=dim.height,
|
||||
thickness=dim.base_thickness,
|
||||
offset_x=dim.hdmi_out_offset_x,
|
||||
offset_y=0,
|
||||
bottom_face=None,
|
||||
back_face=">Y",
|
||||
shell_t=dim.shell_t,
|
||||
)
|
||||
|
||||
model = cpu_holder.add(
|
||||
model=model,
|
||||
width=dim.width,
|
||||
height=dim.height,
|
||||
thickness=dim.base_thickness,
|
||||
offset_x=dim.cpu_offset_x,
|
||||
offset_y=dim.cpu_offset_y,
|
||||
bottom_face="<Z",
|
||||
back_face=None, # Not exposing the holes
|
||||
shell_t=dim.shell_t,
|
||||
)
|
||||
|
||||
# This adds all the holes and extrusions for the battery system
|
||||
model = battery_holder.add(
|
||||
model=model,
|
||||
width=dim.width,
|
||||
height=dim.height,
|
||||
thickness=dim.base_thickness,
|
||||
offset_x=dim.battery_offset_x,
|
||||
offset_y=dim.battery_offset_y,
|
||||
bottom_face="<Z",
|
||||
back_face=">Y",
|
||||
shell_t=dim.shell_t,
|
||||
)
|
||||
|
||||
model = screen_pillars.add(
|
||||
model=model,
|
||||
width=dim.width,
|
||||
height=dim.height,
|
||||
thickness=dim.base_thickness,
|
||||
offset_x=0,
|
||||
offset_y=0,
|
||||
bottom_face="<Z",
|
||||
back_face=None,
|
||||
shell_t=dim.shell_t,
|
||||
)
|
||||
|
||||
model = keyboard.add(
|
||||
model=model,
|
||||
width=dim.width,
|
||||
height=dim.height,
|
||||
thickness=dim.base_thickness,
|
||||
bottom_face="<Z",
|
||||
back_face=None,
|
||||
offset_x=dim.shell_t,
|
||||
offset_y=keyboard.kbd_height + dim.shell_t,
|
||||
shell_t=dim.shell_t,
|
||||
)
|
||||
|
||||
return model
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
model = model()
|
||||
|
||||
left_cutout = cq.Sketch().polygon(
|
||||
[
|
||||
(0, 0),
|
||||
(dim.width / 2, 0),
|
||||
(dim.width / 2, -dim.height),
|
||||
(0, -dim.height),
|
||||
(0, 0),
|
||||
],
|
||||
mode="a",
|
||||
)
|
||||
|
||||
right_side = (
|
||||
model.faces("<Z")
|
||||
.workplaneFromTagged("mid_height")
|
||||
.transformed(offset=cq.Vector(0, 0, -dim.base_thickness / 2))
|
||||
.center(-dim.width / 2, dim.height / 2)
|
||||
.placeSketch(left_cutout)
|
||||
.cutBlind(100)
|
||||
)
|
||||
|
||||
export(right_side, "base_right.stl")
|
||||
|
||||
right_cutout = cq.Sketch().polygon(
|
||||
[
|
||||
(dim.width / 2, 0),
|
||||
(dim.width, 0),
|
||||
(dim.width, -dim.height),
|
||||
(dim.width / 2, -dim.height),
|
||||
(dim.width / 2, 0),
|
||||
],
|
||||
mode="a",
|
||||
)
|
||||
|
||||
left_side = (
|
||||
model.faces("<Z")
|
||||
.workplaneFromTagged("mid_height")
|
||||
.transformed(offset=cq.Vector(0, 0, -dim.base_thickness / 2))
|
||||
.center(-dim.width / 2, dim.height / 2)
|
||||
.placeSketch(right_cutout)
|
||||
.cutBlind(100)
|
||||
)
|
||||
export(left_side, "base_left.stl")
|
||||
|
||||
# draft = Draft(decimal_precision=1)
|
||||
# dimensions = []
|
||||
# dimensions.append(
|
||||
# draft.extension_line(
|
||||
# object_edge=[
|
||||
# cq.Vertex.makeVertex(-width / 2, -height / 2, 0),
|
||||
# cq.Vertex.makeVertex(width / 2, -height / 2, 0),
|
||||
# ],
|
||||
# offset=10.0,
|
||||
# )
|
||||
# )
|
||||
# dimensions.append(
|
||||
# draft.extension_line(
|
||||
# object_edge=[
|
||||
# cq.Vertex.makeVertex(width / 2, -height / 2, 0),
|
||||
# cq.Vertex.makeVertex(width / 2, height / 2, 0),
|
||||
# ],
|
||||
# offset=10.0,
|
||||
# )
|
||||
# )
|
||||
|
||||
export(model, "base.stl")
|
||||
|
||||
# for d in dimensions[1:]:
|
||||
# dimensions[0].add(d.toCompound())
|
||||
# dimensions[0].add(model)
|
||||
|
||||
export(
|
||||
# model[0].toCompound(),
|
||||
model,
|
||||
"base.svg",
|
||||
opt={
|
||||
"projectionDir": (0, 0, 1),
|
||||
"strokeWidth": 0.3,
|
||||
},
|
||||
)
|
3147
base.svg
Normal file
3147
base.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 786 KiB |
BIN
base_left.stl
Normal file
BIN
base_left.stl
Normal file
Binary file not shown.
BIN
base_right.stl
Normal file
BIN
base_right.stl
Normal file
Binary file not shown.
0
components/__init__.py
Normal file
0
components/__init__.py
Normal file
86
components/audio_plug.py
Normal file
86
components/audio_plug.py
Normal file
@ -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
|
150
components/battery_holder.py
Normal file
150
components/battery_holder.py
Normal file
@ -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
|
46
components/hdmi_out.py
Normal file
46
components/hdmi_out.py
Normal file
@ -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
|
101
components/keyboard.py
Normal file
101
components/keyboard.py
Normal file
@ -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
|
84
components/screen_pillars.py
Normal file
84
components/screen_pillars.py
Normal file
@ -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
|
107
components/simple_lid.py
Normal file
107
components/simple_lid.py
Normal file
@ -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",
|
||||
)
|
101
components/usb_hub.py
Normal file
101
components/usb_hub.py
Normal file
@ -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
|
110
components/zero_holder.py
Normal file
110
components/zero_holder.py
Normal file
@ -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
|
144
dimensions.py
Normal file
144
dimensions.py
Normal file
@ -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)
|
416
hinged_lid.py
Normal file
416
hinged_lid.py
Normal file
@ -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 <Y")
|
||||
.fillet(10)
|
||||
.edges("|X and >Z and >Y")
|
||||
.fillet(5)
|
||||
.edges("|Z")
|
||||
.fillet(2)
|
||||
.faces("<Z")
|
||||
.shell(-dim.shell_t)
|
||||
.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 |