Compare commits

27 Commits

Author SHA1 Message Date
337b4a93bc README 2023-04-15 20:14:42 -03:00
993c82f5d8 Use pillar parameters in hinged lid 2023-04-15 19:58:12 -03:00
fbf1a79dfb Fix hinged lid mounting pillar positions 2023-04-15 19:55:41 -03:00
bae753ba0a Don't modify dim.pillar positions 2023-04-15 19:52:36 -03:00
bd215573eb Make screen pillars more parametric 2023-04-15 19:49:53 -03:00
ea06783e66 Make screen_pillars more parametric 2023-04-15 19:41:26 -03:00
e015585a8b Make keyboard more parametric 2023-04-15 19:29:46 -03:00
7217c7bc3b Moved specific keyboard sizes and positions to dimensions.py 2023-04-15 19:21:03 -03:00
158d7bfd87 Housekeeping 2023-04-15 19:08:38 -03:00
812b9c1285 Add linting 2023-04-15 18:59:08 -03:00
1a60b2327e Move components into package 2023-04-15 18:48:04 -03:00
e6b08d2fa4 Use a reasonable file naming convention 2023-04-15 18:40:37 -03:00
07b9cc47ba Atomic exporter so fstl doesn't whine 2023-04-15 18:39:03 -03:00
cc1ff47e53 Build automatically using make 2023-04-15 18:22:44 -03:00
bd4b8d4cc5 Use a reasonable file naming convention 2023-04-15 18:22:23 -03:00
6d527cbbb8 Make hdmi out position parametric 2023-04-15 17:55:26 -03:00
99e003d8f7 Refactoring dimensions into a separate module 2023-04-15 17:49:39 -03:00
c53263626c Refactoring dimensions into a separate module 2023-04-15 17:38:43 -03:00
8e0d3ad1a2 New front bezel piece for hinged lid 2023-04-15 15:53:52 -03:00
cf751ce523 New design for hinged lid screen holder, with captured nuts 2023-04-15 12:37:01 -03:00
67de0b13b2 Adjust position of right side pillars and audio board 2023-04-12 14:11:27 -03:00
517bb93d8d Make USB hub cable holder slightly wider 2023-04-12 13:37:46 -03:00
59f67b5077 Move USB power in a bit to the right 2023-04-12 13:36:17 -03:00
e7c2d1d69d Make back slightly taller to fit battery completely in 2023-04-12 13:23:58 -03:00
3a4e6a66ae Move left-front pillar a bit back to stop interfering with hinge 2023-04-12 13:20:53 -03:00
4f53615c0e Split model for hinged lid, for test print 2023-04-08 15:50:39 -03:00
1a8e93775f Call model only once 2023-04-08 14:43:29 -03:00
42 changed files with 1552 additions and 1483 deletions

2
notebook_nueva/.flake8 Normal file
View File

@ -0,0 +1,2 @@
[flake8]
extend-ignore = E501, E266

176
notebook_nueva/.gitignore vendored Normal file
View 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
notebook_nueva/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"python.formatting.provider": "black",
"emeraldwalk.runonsave": {
"commands": [
{
"match": ".py",
"isAsync": true,
"cmd": "make"
}
}
}

13
notebook_nueva/Makefile Normal file
View 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
notebook_nueva/README.md Normal file
View 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
notebook_nueva/base.py Normal file
View 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,
},
)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 952 KiB

After

Width:  |  Height:  |  Size: 786 KiB

View File

View File

@ -81,7 +81,7 @@ vents = hex_vents(size=3, width=width, height=height)
holes = [ holes = [
# Power inlet # Power inlet
{ {
"x": -17, "x": -18.5,
"y": -1 + pillar_height, "y": -1 + pillar_height,
"shape": cq.Sketch().trapezoid(12, 6.5, 90, mode="a").vertices().fillet(1), "shape": cq.Sketch().trapezoid(12, 6.5, 90, mode="a").vertices().fillet(1),
}, },

View File

@ -1,43 +1,42 @@
import cadquery as cq import cadquery as cq
import math
# Size of the kbd board # These should be set from dimensions.py
kbd_height = 95.5 elements = None
kbd_width = 305 kbd_pillar_positions = []
back_thickness = 19 kbd_height = 0
front_thickness = 12 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
# Pythagoras
actual_height = (kbd_height**2 - (back_thickness - front_thickness) ** 2) ** 0.5
kbd_angle = math.acos(actual_height / kbd_height) * 180 / math.pi
kbd_pillar_positions = [ def init():
(19, 16), global elements
(142.5, 25.5),
(kbd_width - 20, 16),
(23.5, 79.5),
(145.5, 82.5),
(kbd_width - 19, 79.5),
]
elements = [ elements = [
# Shorter pillars # Shorter pillars
{ {
"x": 0, "x": 0,
"y": 0, "y": 0,
"z": 5.5, "z": kbd_pillar_offset_1,
"shape": cq.Sketch().push(kbd_pillar_positions).circle(5, mode="a"), "shape": cq.Sketch().push(kbd_pillar_positions).circle(kbd_pillar_radius_1, mode="a"),
}, },
# Taller pillars with holes for self-tapping screws # Taller pillars with holes for self-tapping screws
{ {
"x": 0, "x": 0,
"y": 0, "y": 0,
"z": 2.5, "z": kbd_pillar_offset_2,
"shape": ( "shape": (
cq.Sketch() cq.Sketch()
.push(kbd_pillar_positions) .push(kbd_pillar_positions)
.circle(2.4, mode="a") .circle(kbd_pillar_radius_2, mode="a")
.circle(1.1, mode="s") .circle(kbd_screw_radius, mode="s")
), ),
}, },
] ]
@ -61,7 +60,7 @@ def add(
if bottom_face: if bottom_face:
model = ( model = (
model.faces(bottom_face) model.faces(bottom_face)
.workplane(centerOption="CenterOfBoundBox", offset=-front_thickness) .workplane(centerOption="CenterOfBoundBox", offset=-kbd_front_thickness)
.center( .center(
-width / 2, -width / 2,
height / 2, height / 2,
@ -88,16 +87,15 @@ def add(
.placeSketch( .placeSketch(
cq.Sketch().polygon( cq.Sketch().polygon(
[ [
[0, front_thickness], [0, kbd_front_thickness],
[shell_t, front_thickness], [shell_t, kbd_front_thickness],
[actual_height + shell_t, back_thickness], [kbd_actual_height + shell_t, kbd_back_thickness],
[actual_height + shell_t, 1000], [kbd_actual_height + shell_t, 1000],
[0, 1000], [0, 1000],
[0, front_thickness], [0, kbd_front_thickness],
] ]
) )
) )
.cutBlind(-1000) .cutBlind(-1000)
) )
return model return model

View File

@ -2,7 +2,14 @@ from utils import extrude_shape, punch_hole
import cadquery as cq import cadquery as cq
elements = None elements = None
bottom_holes = None # Not really vents FIXME 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): def init(positions, thickness):
@ -13,7 +20,9 @@ def init(positions, thickness):
{ {
"x": 0, "x": 0,
"y": 0, "y": 0,
"shape": cq.Sketch().push(positions).trapezoid(12, 12, 90, mode="a"), "shape": cq.Sketch()
.push(positions)
.trapezoid(pillar_width, pillar_height, 90, mode="a"),
"height": thickness, "height": thickness,
} }
] ]
@ -22,13 +31,13 @@ def init(positions, thickness):
{ {
"x": 0, "x": 0,
"y": 0, "y": 0,
"shape": cq.Sketch().push(positions).circle(3, mode="a"), "shape": cq.Sketch().push(positions).circle(screw_head_radius, mode="a"),
"depth": thickness - 13, # (screw thread length - threaded insert depth) "depth": screw_head_depth,
}, },
{ {
"x": 0, "x": 0,
"y": 0, "y": 0,
"shape": cq.Sketch().push(positions).circle(1.8, mode="a"), "shape": cq.Sketch().push(positions).circle(screw_radius, mode="a"),
"depth": 100, "depth": 100,
}, },
] ]

View 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",
)

View File

@ -31,13 +31,13 @@ elements = [
"height": 8, "height": 8,
}, },
{ {
"x": item_w / 2 + 5, "x": item_w / 2 + 5.5,
"y": item_h - 3, "y": item_h - 3,
"shape": (cq.Sketch().circle(2.5, mode="a")), "shape": (cq.Sketch().circle(2.5, mode="a")),
"height": 8, "height": 8,
}, },
{ {
"x": item_w / 2 - 5, "x": item_w / 2 - 5.5,
"y": item_h - 3, "y": item_h - 3,
"shape": (cq.Sketch().circle(2.5, mode="a")), "shape": (cq.Sketch().circle(2.5, mode="a")),
"height": 8, "height": 8,

View 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)

View File

@ -1,44 +1,19 @@
import cadquery as cq import cadquery as cq
from cadquery import exporters
import screen_pillars import dimensions as dim
import components.keyboard as keyboard
import components.screen_pillars as screen_pillars
from utils import export
from modelo import ( mounting_pillar_positions = [(x, -y) for x, y in dim.mounting_pillar_positions]
height,
mounting_pillar_positions,
ti_depth,
ti_radius,
width,
thickness as model_thickness,
shell_t,
)
import screen_mount
import keyboard
hinge_radius = 5.5
screw_radius = 1.5 # M3
ring_radius = 5 # M3
hinge_offset = max(p[1] for p in mounting_pillar_positions) + 6
hinge_width = 25
thickness = 43
mounting_pillar_positions = [(x, -y) for x, y in mounting_pillar_positions]
mounting_pillars = ( mounting_pillars = (
cq.Sketch() cq.Sketch()
.push(mounting_pillar_positions) .push(mounting_pillar_positions)
.trapezoid(-12, 12, 90, mode="a") .trapezoid(screen_pillars.pillar_height, screen_pillars.pillar_width, 90, mode="a")
.circle(ti_radius, mode="s") .circle(dim.ti_radius, mode="s")
.clean() .clean()
) )
# 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
hinge_slant = shell_t + 2
def model(): def model():
# Create a 2-part hinged lid # Create a 2-part hinged lid
@ -46,8 +21,8 @@ def model():
model = ( model = (
cq.Workplane("XY") cq.Workplane("XY")
# Hollow box # Hollow box
.workplane(offset=-thickness / 2) .workplane(offset=-dim.hl_full_thickness / 2)
.box(width, height, thickness) .box(dim.width, dim.height, dim.hl_full_thickness)
.tag("base") .tag("base")
.edges("|X and >Z and <Y") .edges("|X and >Z and <Y")
.fillet(10) .fillet(10)
@ -56,90 +31,110 @@ def model():
.edges("|Z") .edges("|Z")
.fillet(2) .fillet(2)
.faces("<Z") .faces("<Z")
.shell(-shell_t) .shell(-dim.shell_t)
.faces(">X") .faces(">X")
.workplane() .workplane()
.center(height / 2 - hinge_offset, thickness / 2 - hinge_radius) .center(
dim.height / 2 - dim.hl_hinge_offset,
dim.hl_full_thickness / 2 - dim.hl_hinge_radius,
)
.tag("rightSide") .tag("rightSide")
# Outer surface of the hinge # Outer surface of the hinge
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.placeSketch(cq.Sketch().circle(hinge_radius)) .placeSketch(cq.Sketch().circle(dim.hl_hinge_radius))
.extrude(-hinge_width) .extrude(-dim.hl_hinge_width)
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-width + hinge_width) .workplane(offset=-dim.width + dim.hl_hinge_width)
.placeSketch(cq.Sketch().circle(hinge_radius)) .placeSketch(cq.Sketch().circle(dim.hl_hinge_radius))
.extrude(-hinge_width) .extrude(-dim.hl_hinge_width)
# Cut middle section between the hinges # Cut middle section between the hinges
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-hinge_width) .workplane(offset=-dim.hl_hinge_width)
.placeSketch( .placeSketch(
cq.Sketch().polygon( cq.Sketch().polygon(
[ [
(-hinge_radius, -hinge_radius), (-dim.hl_hinge_radius, -dim.hl_hinge_radius),
(-hinge_radius, 0), (-dim.hl_hinge_radius, 0),
(-hinge_radius - hinge_slant, hinge_radius), (-dim.hl_hinge_radius - dim.hl_hinge_slant, dim.hl_hinge_radius),
(-hinge_slant, hinge_radius), (-dim.hl_hinge_slant, dim.hl_hinge_radius),
(-hinge_slant, hinge_radius - hinge_slant), (-dim.hl_hinge_slant, dim.hl_hinge_radius - dim.hl_hinge_slant),
(hinge_radius, hinge_radius - hinge_slant), (dim.hl_hinge_radius, dim.hl_hinge_radius - dim.hl_hinge_slant),
(hinge_radius, -hinge_radius), (dim.hl_hinge_radius, -dim.hl_hinge_radius),
(-hinge_radius, -hinge_radius), (-dim.hl_hinge_radius, -dim.hl_hinge_radius),
] ]
) )
) )
.cutBlind(-width + 2 * hinge_width - 1) .cutBlind(-dim.width + 2 * dim.hl_hinge_width - 1)
# Pillars to attach to base # Pillars to attach to base
.workplaneFromTagged("base") .workplaneFromTagged("base")
.workplane( .workplane(
centerOption="CenterOfBoundBox", offset=model_thickness - thickness / 2 centerOption="CenterOfBoundBox",
offset=dim.base_thickness - dim.hl_full_thickness / 2,
) )
.workplaneFromTagged("base") .workplaneFromTagged("base")
.workplane(offset=thickness / 2 - shell_t) .workplane(offset=dim.hl_full_thickness / 2 - dim.shell_t)
.center(-width / 2, height / 2 - shell_t) .center(-dim.width / 2, dim.height / 2 - dim.shell_t)
.placeSketch(mounting_pillars) .placeSketch(mounting_pillars)
.extrude(-10) .extrude(-10)
# Hole for screws # Hole for screws
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.placeSketch(cq.Sketch().circle(screw_radius)) .placeSketch(cq.Sketch().circle(dim.hl_screw_radius))
.cutBlind(-hinge_width) .cutBlind(-dim.hl_hinge_width)
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-width + hinge_width) .workplane(offset=-dim.width + dim.hl_hinge_width)
.placeSketch(cq.Sketch().circle(screw_radius)) .placeSketch(cq.Sketch().circle(dim.hl_screw_radius))
.cutBlind(-hinge_width) .cutBlind(-dim.hl_hinge_width)
# Holes for rings & screw heads # Holes for rings & screw heads
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.placeSketch(cq.Sketch().circle(ring_radius)) .placeSketch(cq.Sketch().circle(dim.hl_ring_radius))
.cutBlind(-5) .cutBlind(-5)
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-width + 4) .workplane(offset=-dim.width + 4)
.placeSketch(cq.Sketch().circle(ring_radius)) .placeSketch(cq.Sketch().circle(dim.hl_ring_radius))
.cutBlind(-5) .cutBlind(-5)
# Split hinge halves # Split hinge halves
.faces(">X") .faces(">X")
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-hinge_width / 2) .workplane(offset=-dim.hl_hinge_width / 2)
.placeSketch(cq.Sketch().trapezoid(hinge_radius * 2 + 1, hinge_radius * 2, 90)) .placeSketch(
cq.Sketch().trapezoid(
dim.hl_hinge_radius * 2 + 1, dim.hl_hinge_radius * 2, 90
)
)
.cutBlind(-1) .cutBlind(-1)
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-hinge_width) .workplane(offset=-dim.hl_hinge_width)
.placeSketch(cq.Sketch().trapezoid(hinge_radius * 2 + 1, hinge_radius * 2, 90)) .placeSketch(
cq.Sketch().trapezoid(
dim.hl_hinge_radius * 2 + 1, dim.hl_hinge_radius * 2, 90
)
)
.cutBlind(-1) .cutBlind(-1)
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-width + hinge_width / 2) .workplane(offset=-dim.width + dim.hl_hinge_width / 2)
.placeSketch(cq.Sketch().trapezoid(hinge_radius * 2 + 1, hinge_radius * 2, 90)) .placeSketch(
cq.Sketch().trapezoid(
dim.hl_hinge_radius * 2 + 1, dim.hl_hinge_radius * 2, 90
)
)
.cutBlind(-1) .cutBlind(-1)
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-width + hinge_width) .workplane(offset=-dim.width + dim.hl_hinge_width)
.placeSketch(cq.Sketch().trapezoid(hinge_radius * 2 + 1, hinge_radius * 2, 90)) .placeSketch(
cq.Sketch().trapezoid(
dim.hl_hinge_radius * 2 + 1, dim.hl_hinge_radius * 2, 90
)
)
.cutBlind(-1) .cutBlind(-1)
# Threaded inserts # Threaded inserts
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-hinge_width / 2) .workplane(offset=-dim.hl_hinge_width / 2)
.placeSketch(cq.Sketch().circle(ti_radius)) .placeSketch(cq.Sketch().circle(dim.ti_radius))
.cutBlind(-ti_depth) .cutBlind(-dim.ti_depth)
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-width + hinge_width / 2) .workplane(offset=-dim.width + dim.hl_hinge_width / 2)
.placeSketch(cq.Sketch().circle(ti_radius)) .placeSketch(cq.Sketch().circle(dim.ti_radius))
.cutBlind(ti_depth) .cutBlind(dim.ti_depth)
# Split two halves # Split two halves
# First cut for the right hinge # First cut for the right hinge
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
@ -148,140 +143,164 @@ def model():
.polygon( .polygon(
[ [
(0, 0), (0, 0),
(-hinge_radius - 0.2, 0), (-dim.hl_hinge_radius - 0.2, 0),
(-hinge_radius - hinge_slant, hinge_radius), (-dim.hl_hinge_radius - dim.hl_hinge_slant, dim.hl_hinge_radius),
(0, hinge_radius), (0, dim.hl_hinge_radius),
(0, 0), (0, 0),
] ]
) )
.polygon( .polygon(
[ [
(-hinge_radius - 0.2, 0), (-dim.hl_hinge_radius - 0.2, 0),
(-hinge_radius - 0.2, -1000), (-dim.hl_hinge_radius - 0.2, -1000),
(-hinge_radius, -1000), (-dim.hl_hinge_radius, -1000),
(-hinge_radius, 0), (-dim.hl_hinge_radius, 0),
(-hinge_radius - 0.2, 0), (-dim.hl_hinge_radius - 0.2, 0),
] ]
) )
.circle(hinge_radius, mode="s") .circle(dim.hl_hinge_radius, mode="s")
) )
.cutBlind(-hinge_width / 2 - 1) .cutBlind(-dim.hl_hinge_width / 2 - 1)
# Second cut for the right hinge # Second cut for the right hinge
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-hinge_width / 2) .workplane(offset=-dim.hl_hinge_width / 2)
.placeSketch( .placeSketch(
cq.Sketch() cq.Sketch()
.polygon( .polygon(
[ [
(0, 0), (0, 0),
(hinge_radius + 0.2, 0), (dim.hl_hinge_radius + 0.2, 0),
(hinge_radius + 0.2 + hinge_slant, hinge_radius), (
(0, hinge_radius), dim.hl_hinge_radius + 0.2 + dim.hl_hinge_slant,
dim.hl_hinge_radius,
),
(0, dim.hl_hinge_radius),
(0, 0), (0, 0),
] ]
) )
.circle(hinge_radius, mode="s") .circle(dim.hl_hinge_radius, mode="s")
) )
.cutBlind(-hinge_width / 2 - 1) .cutBlind(-dim.hl_hinge_width / 2 - 1)
# First cut for the left hinge # First cut for the left hinge
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-width + hinge_width) .workplane(offset=-dim.width + dim.hl_hinge_width)
.placeSketch( .placeSketch(
cq.Sketch() cq.Sketch()
.polygon( .polygon(
[ [
(0, 0), (0, 0),
(hinge_radius + 0.2, 0), (dim.hl_hinge_radius + 0.2, 0),
(hinge_radius + 0.2 + hinge_slant, hinge_radius), (
(0, hinge_radius), dim.hl_hinge_radius + 0.2 + dim.hl_hinge_slant,
dim.hl_hinge_radius,
),
(0, dim.hl_hinge_radius),
(0, 0), (0, 0),
] ]
) )
.circle(hinge_radius, mode="s") .circle(dim.hl_hinge_radius, mode="s")
) )
.cutBlind(-hinge_width / 2 - 1) .cutBlind(-dim.hl_hinge_width / 2 - 1)
# Second cut for the left hinge # Second cut for the left hinge
.workplaneFromTagged("rightSide") .workplaneFromTagged("rightSide")
.workplane(offset=-width + hinge_width / 2) .workplane(offset=-dim.width + dim.hl_hinge_width / 2)
.placeSketch( .placeSketch(
cq.Sketch() cq.Sketch()
.polygon( .polygon(
[ [
(0, 0), (0, 0),
(-hinge_radius - 0.2, 0), (-dim.hl_hinge_radius - 0.2, 0),
(-hinge_radius - hinge_slant, hinge_radius), (-dim.hl_hinge_radius - dim.hl_hinge_slant, dim.hl_hinge_radius),
(0, hinge_radius), (0, dim.hl_hinge_radius),
(0, 0), (0, 0),
] ]
) )
.polygon( .polygon(
[ [
(-hinge_radius - 0.2, 0), (-dim.hl_hinge_radius - 0.2, 0),
(-hinge_radius - 0.2, -1000), (-dim.hl_hinge_radius - 0.2, -1000),
(-hinge_radius, -1000), (-dim.hl_hinge_radius, -1000),
(-hinge_radius, 0), (-dim.hl_hinge_radius, 0),
(-hinge_radius - 0.2, 0), (-dim.hl_hinge_radius - 0.2, 0),
] ]
) )
.circle(hinge_radius, mode="s") .circle(dim.hl_hinge_radius, mode="s")
) )
.cutBlind(-hinge_width / 2 - 1) .cutBlind(-dim.hl_hinge_width / 2 - 1)
) )
# Screen mount # Screen mount
model = ( model = (
# 1st layer
model.workplaneFromTagged("base") model.workplaneFromTagged("base")
.center(0, -32) .center(0, -32)
.workplane(offset=dim.hl_full_thickness / 2 - dim.shell_t)
.tag("screen_plane") .tag("screen_plane")
.workplane(offset=thickness / 2 - shell_t)
.placeSketch( .placeSketch(
cq.Sketch() cq.Sketch()
.trapezoid( .trapezoid(
screen_mount.scr_w + 2 * shell_t, screen_mount.scr_h + 2 * shell_t, 90 dim.scr_w + 2 * dim.hl_bezel_width,
dim.scr_h + 2 * dim.hl_bezel_height,
90,
) )
.vertices() .vertices()
.fillet(2) .fillet(2)
) )
.extrude(-9) .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") .workplaneFromTagged("screen_plane")
.workplane(offset=thickness / 2 - shell_t)
.placeSketch( .placeSketch(
cq.Sketch() cq.Sketch().trapezoid(
.trapezoid(screen_mount.vis_w, screen_mount.vis_h, 90) dim.scr_w - 40,
.vertices() dim.scr_h + 2 * dim.hl_bezel_height,
.fillet(2) 90,
) )
.cutBlind(-9)
.workplaneFromTagged("screen_plane")
.workplane(offset=thickness / 2 - shell_t)
.placeSketch(
cq.Sketch()
.trapezoid(screen_mount.scr_w - 40, screen_mount.scr_h + 2 * shell_t, 90)
.vertices()
.fillet(2)
) )
.cutBlind(-9) .cutBlind(-100)
# Hole to place screen
.workplaneFromTagged("screen_plane") .workplaneFromTagged("screen_plane")
.workplane(offset=thickness / 2 - shell_t - screen_mount.scr_thickness - 2) .workplane(offset=-dim.scr_thickness - 2)
.placeSketch(cq.Sketch().trapezoid(screen_mount.scr_w, screen_mount.scr_h, 90)) .placeSketch(cq.Sketch().trapezoid(dim.scr_w, dim.scr_h, 90))
.cutBlind(screen_mount.scr_thickness) .cutBlind(dim.scr_thickness)
) )
# Cut off shape of the base # Cut off shape of the base
model = ( model = (
model.workplaneFromTagged("rightSide") model.workplaneFromTagged("rightSide")
.center(-height + hinge_offset, -thickness + hinge_radius) .center(
-dim.height + dim.hl_hinge_offset,
-dim.hl_full_thickness + dim.hl_hinge_radius,
)
.placeSketch( .placeSketch(
cq.Sketch().polygon( cq.Sketch().polygon(
[ [
(0, 0), (0, 0),
(0, keyboard.front_thickness), (0, keyboard.kbd_front_thickness),
(shell_t, keyboard.front_thickness), (dim.shell_t, keyboard.kbd_front_thickness),
(keyboard.actual_height + shell_t, keyboard.back_thickness), (keyboard.kbd_actual_height + dim.shell_t, keyboard.kbd_back_thickness),
(keyboard.actual_height + shell_t, model_thickness), (keyboard.kbd_actual_height + dim.shell_t, dim.base_thickness),
(height, model_thickness), (dim.height, dim.base_thickness),
(height, 0), (dim.height, 0),
(0, 0), (0, 0),
] ]
) )
@ -292,16 +311,106 @@ def model():
return model 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__": if __name__ == "__main__":
model = model() model = model()
exporters.export(model, "hinged_lid.stl") export(model, "hinged_lid.stl")
exporters.export( export(front_bezel(), "hinged_lid_bezel.stl")
export(
model, model,
"hinged_lid.svg", "hinged_lid.svg",
opt={ opt={
"projectionDir": (0, 0, 1), "projectionDir": (0, 0, -1),
"strokeWidth": 0.3, "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")

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,109 +0,0 @@
import cadquery as cq
from cadquery import exporters
from modelo import mounting_pillar_positions, shell_t, width
from utils import hex_vents, punch_hole, extrude_shape2
# Dimensions for countersunk M4 screws
m4_top = 9
m4_bottom = 4
lip_thickness = 1.5
# Position of pillar + shell_t + pillar "radius" + lip
height = max([y for _, y in mounting_pillar_positions]) + 6 + shell_t + lip_thickness
thickness = shell_t
front_lip = 8
def model():
# Create the basic shape of the case lid
model = (
cq.Workplane("XY")
# Hollow box
.box(width, height, thickness)
.edges("|Z and >Y")
.fillet(2)
)
# Make many holes
vent = hex_vents(size=6, width=width * 0.9, height=height * 0.9)[0]
model = punch_hole(
model=model,
face=">Z",
w=width,
h=height,
x_offset=0.05 * width,
y_offset=0.05 * height,
hole=vent,
depth=thickness,
)
# Add screw holes
for position in mounting_pillar_positions:
model = (
model.faces(">Z")
.workplane(centerOption="CenterOfBoundBox")
.center(-width / 2 + position[0], height / 2 - position[1] - shell_t)
.placeSketch(cq.Sketch().circle(m4_top / 2 + 1.5))
.extrude(-thickness)
.faces(">Z")
.workplane(centerOption="CenterOfBoundBox")
.center(-width / 2 + position[0], height / 2 - position[1] - shell_t)
.cskHole(m4_bottom, m4_top, 82, depth=None)
)
# Add front lip
model = (
model.faces(">Z")
.workplane(centerOption="CenterOfBoundBox")
.center(0, -height / 2 + lip_thickness / 2)
.placeSketch(cq.Sketch().trapezoid(width - 2 * shell_t, lip_thickness, 90))
.extrude(-front_lip - thickness)
)
return model
def decorative_cover():
# A decorative thingie to cover the ugly seam in the middle
model = cq.Workplane("XY").box(10, height, 1).edges("|Z").fillet(1)
vent = hex_vents(size=6, width=width * 0.9, height=height * 0.9, density=0.775)[0]
model = extrude_shape2(
model=model,
face=">Z",
w=width,
h=height,
x_offset=0.05 * width,
y_offset=0.05 * height,
hole=vent,
depth=3,
)
return model
if __name__ == "__main__":
model = model()
exporters.export(model, "lid.stl")
cover = decorative_cover()
exporters.export(cover, "lid_cover.stl")
exporters.export(
model,
"lid.svg",
opt={
"projectionDir": (0, 0, 1),
},
)
exporters.export(
model.faces(">X").workplane(offset=-width / 2).split(keepTop=True),
"right_side_lid.stl",
)
exporters.export(
model.faces(">X").workplane(offset=-width / 2).split(keepBottom=True),
"left_side_lid.stl",
)

View File

@ -1,242 +0,0 @@
import cadquery as cq
from cadquery import exporters
from cq_warehouse.drafting import Draft
import audio_plug
import battery_holder
import hdmi_out
import keyboard
import screen_pillars
import usb_hub
import zero_holder as cpu_holder
# Base for the notebook. Basically a kbd base that extends back
# as much as possible
# Thickness of the outer material
shell_t = 3
# Size of the kbd board
kbd_height = 95.5
kbd_width = 305
# Size of the whole object
width = kbd_width + 2 * shell_t
height = 159
thickness = 27 + shell_t # 27 inside
# Insert Positions
ti_radius = 2.35
ti_depth = 6.25
# These are placed where convenient, and are used to join the top and bottom
# parts of the case.
# Measured from top-left corner OUTSIDE
mounting_pillar_positions = [
(6, 6),
(6, 48),
(120, 6),
(170, 6),
(width - 6, 6),
(width - 6, 30),
(120, 48),
(170, 48),
]
screen_pillars.init(mounting_pillar_positions, thickness - shell_t)
# 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
def model():
# Create the basic shape of the case bottom.
model = (
cq.Workplane("XY")
.workplane(offset=thickness / 2)
.tag("mid_height")
# Hollow box
.box(width, height, thickness)
.edges("|Z")
.fillet(2)
.faces(">Z")
.shell(-shell_t)
)
# Now the basic box shape is in place, start adding things
# and cutting holes.
model = usb_hub.add(
model=model,
width=width,
height=height,
thickness=thickness,
bottom_face="<Z",
back_face=">Y",
offset_x=usb_offset_x,
offset_y=0,
shell_t=shell_t,
)
# Hole for audio in right side
model = audio_plug.add(
model=model,
width=width,
height=height,
thickness=thickness,
offset_x=width - audio_plug.item_w,
offset_y=40,
bottom_face="<Z",
back_face=">X",
shell_t=shell_t,
)
# Hole for HDMI out in the back
model = hdmi_out.add(
model=model,
width=width,
height=height,
thickness=thickness,
offset_x=138,
offset_y=0,
bottom_face=None,
back_face=">Y",
shell_t=shell_t,
)
model = cpu_holder.add(
model=model,
width=width,
height=height,
thickness=thickness,
offset_x=cpu_offset_x,
offset_y=cpu_offset_y,
bottom_face="<Z",
back_face=None, # Not exposing the holes
shell_t=shell_t,
)
# This adds all the holes and extrusions for the battery system
model = battery_holder.add(
model=model,
width=width,
height=height,
thickness=thickness,
offset_x=battery_offset_x,
offset_y=battery_offset_y,
bottom_face="<Z",
back_face=">Y",
shell_t=shell_t,
)
model = screen_pillars.add(
model=model,
width=width,
height=height,
thickness=thickness,
offset_x=0,
offset_y=0,
bottom_face="<Z",
back_face=None,
shell_t=shell_t,
)
model = keyboard.add(
model=model,
width=width,
height=height,
thickness=thickness,
bottom_face="<Z",
back_face=None,
offset_x=shell_t,
offset_y=kbd_height + shell_t,
shell_t=shell_t,
)
return model
if __name__ == "__main__":
model = model()
left_cutout = cq.Sketch().polygon(
[(0, 0), (width / 2, 0), (width / 2, -height), (0, -height), (0, 0)],
mode="a",
)
right_side = (
model.faces("<Z")
.workplaneFromTagged("mid_height")
.transformed(offset=cq.Vector(0, 0, -thickness / 2))
.center(-width / 2, height / 2)
.placeSketch(left_cutout)
.cutBlind(100)
)
exporters.export(right_side, "right_side.stl")
right_cutout = cq.Sketch().polygon(
[
(width / 2, 0),
(width, 0),
(width, -height),
(width / 2, -height),
(width / 2, 0),
],
mode="a",
)
left_side = (
model.faces("<Z")
.workplaneFromTagged("mid_height")
.transformed(offset=cq.Vector(0, 0, -thickness / 2))
.center(-width / 2, height / 2)
.placeSketch(right_cutout)
.cutBlind(100)
)
exporters.export(left_side, "left_side.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,
)
)
exporters.export(model, "model.stl")
for d in dimensions[1:]:
dimensions[0].add(d.toCompound())
dimensions[0].add(model)
exporters.export(
dimensions[0].toCompound(),
"model.svg",
opt={
"projectionDir": (0, 0, 1),
"strokeWidth": 0.3,
},
)

View File

@ -1,2 +1,3 @@
cadquery cadquery
git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
flake8

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 227 KiB

View File

@ -1,55 +1,34 @@
import cadquery as cq import cadquery as cq
from cadquery import exporters
from modelo import ( import dimensions as dim
kbd_height, from utils import export
kbd_width, from components import screen_pillars
mounting_pillar_positions,
shell_t, viewport_cutout = (
ti_depth, cq.Sketch().trapezoid(dim.vis_w, dim.vis_h, 90, mode="a").vertices().fillet(2)
ti_radius,
) )
screen_cutout = cq.Sketch().trapezoid(dim.scr_w, dim.scr_h, 90, mode="a")
# Size of the whole object
width = kbd_width + 2 * shell_t
height = 66
height_bottom = 59
thickness = 48 # Will be shorter after construction
# Visible screen size
vis_w = 219
vis_h = 55
viewport_cutout = cq.Sketch().trapezoid(vis_w, vis_h, 90, mode="a").vertices().fillet(2)
# Whole screen size
scr_w = 231
scr_h = 65
scr_thickness = 5.5
screen_cutout = cq.Sketch().trapezoid(scr_w, scr_h, 90, mode="a")
# Screen angle
scr_angle = 20
# Circuit board and cable hole. # Circuit board and cable hole.
# This is in the back of the screen, and is a bit shorter in height than the # 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. # screen. It's wider so it removes enough material to make the shape simpler.
board_cutout = cq.Sketch().trapezoid( board_cutout = cq.Sketch().trapezoid(
scr_w + 5, dim.scr_w + 5,
scr_h - 10, dim.scr_h - 10,
90, 90,
mode="a", mode="a",
) )
kbd_cable_hole = cq.Sketch().trapezoid(20, 9, 90, mode="a").vertices().fillet(1) 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, adn there's 2 pillars we don't use # 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 mounting_pillar_positions[:-2]] mounting_pillar_positions = [(x, -y) for x, y in dim.mounting_pillar_positions[:-2]]
mounting_pillars = ( mounting_pillars = (
cq.Sketch() cq.Sketch()
.push(mounting_pillar_positions) .push(dim.mounting_pillar_positions)
.trapezoid(-12, 12, 90, mode="a") .trapezoid(screen_pillars.pillar_width, screen_pillars.pillar_height, 90, mode="a")
.circle(ti_radius, mode="s") .circle(dim.ti_radius, mode="s")
.clean() .clean()
) )
@ -59,10 +38,10 @@ def model():
cq.Workplane("XY") cq.Workplane("XY")
.workplane() .workplane()
.tag("mid_height") .tag("mid_height")
.box(width, height, thickness) .box(dim.width, dim.tl_height, dim.tl_full_thickness)
# The screen goes rotated # The screen goes rotated
.faces(">Z") .faces(">Z")
.transformed(rotate=(scr_angle, 0, 0)) .transformed(rotate=(dim.tl_scr_angle, 0, 0))
# Move the screen "lower" so it doesn't interfere # Move the screen "lower" so it doesn't interfere
# so much with the back # so much with the back
.center(0, -2) .center(0, -2)
@ -79,18 +58,18 @@ def model():
# Make bottom smaller to fit with base # Make bottom smaller to fit with base
.faces(">X") .faces(">X")
.workplane(centerOption="CenterOfBoundBox") .workplane(centerOption="CenterOfBoundBox")
.center(-height / 2, -thickness / 2) .center(-dim.tl_height / 2, -dim.tl_full_thickness / 2)
.placeSketch( .placeSketch(
cq.Sketch() cq.Sketch()
.polygon( .polygon(
[ [
(height_bottom, 0), (dim.tl_height_bottom, 0),
(height_bottom, thickness / 3), (dim.tl_height_bottom, dim.tl_full_thickness / 3),
(height, thickness - 21), (dim.tl_height, dim.tl_full_thickness - 21),
(height, thickness), (dim.tl_height, dim.tl_full_thickness),
(height + 5, thickness + 5), (dim.tl_height + 5, dim.tl_full_thickness + 5),
(height + 5, 0), (dim.tl_height + 5, 0),
(height_bottom, 0), (dim.tl_height_bottom, 0),
] ]
) )
.vertices() .vertices()
@ -103,26 +82,26 @@ def model():
# Cut off viewport hole so we can see the screen # Cut off viewport hole so we can see the screen
.workplaneFromTagged("slanted") .workplaneFromTagged("slanted")
.placeSketch(viewport_cutout) .placeSketch(viewport_cutout)
.cutBlind(-shell_t) .cutBlind(-dim.shell_t)
# Make hole for screen assembly so the whole screen fits # Make hole for screen assembly so the whole screen fits
.workplaneFromTagged("slanted") .workplaneFromTagged("slanted")
.workplane(offset=-shell_t, centerOption="CenterOfBoundBox") .workplane(offset=-dim.shell_t, centerOption="CenterOfBoundBox")
# Left bezel is wider than right one, so this hole is displaced to the left # Left bezel is wider than right one, so this hole is displaced to the left
.center(-3, 0) .center(-3, 0)
.placeSketch(screen_cutout) .placeSketch(screen_cutout)
.cutBlind(-scr_thickness) .cutBlind(-dim.scr_thickness)
# Make it hollow # Make it hollow
.faces("<Z") .faces("<Z")
# Can't be exactly shell_t because cq fails # Can't be exactly shell_t because cq fails
.shell(-shell_t + 0.01) .shell(-dim.shell_t + 0.01)
# Cut hole for the screen board and cables # Cut hole for the screen board and cables
.workplaneFromTagged("slanted") .workplaneFromTagged("slanted")
.workplane(offset=-scr_thickness, centerOption="CenterOfBoundBox") .workplane(offset=-dim.scr_thickness, centerOption="CenterOfBoundBox")
.placeSketch(board_cutout) .placeSketch(board_cutout)
.cutBlind(-6) .cutBlind(-6)
.workplaneFromTagged("mid_height") .workplaneFromTagged("mid_height")
.workplane(offset=-thickness / 2, centerOption="CenterOfBoundBox") .workplane(offset=-dim.tl_full_thickness / 2, centerOption="CenterOfBoundBox")
.center(-width / 2, height_bottom - height / 2 - shell_t) .center(-dim.width / 2, dim.tl_height_bottom - dim.tl_height / 2 - dim.shell_t)
.placeSketch(mounting_pillars) .placeSketch(mounting_pillars)
.extrude(10) .extrude(10)
# Fillet the front edge of the screen case so it looks softer # Fillet the front edge of the screen case so it looks softer
@ -132,25 +111,23 @@ def model():
if __name__ == "__main__": if __name__ == "__main__":
print("Exporting") model = model()
exporters.export(model(), "screen_mount.stl") export(model, "tandy_lid.stl")
offset_width = -width / 2 offset_width = -dim.width / 2
right_side = ( right_side = (
model() model.faces(">X")
.faces(">X")
.workplane(centerOption="CenterOfBoundBox", offset=offset_width) .workplane(centerOption="CenterOfBoundBox", offset=offset_width)
.split(keepTop=True) .split(keepTop=True)
) )
exporters.export(right_side, "right_screen_mount.stl") export(right_side, "tandy_lid_right.stl")
left_side = ( left_side = (
model() model.faces(">X")
.faces(">X")
.workplane(centerOption="CenterOfBoundBox", offset=offset_width) .workplane(centerOption="CenterOfBoundBox", offset=offset_width)
.split(keepBottom=True) .split(keepBottom=True)
) )
exporters.export(left_side, "left_screen_mount.stl") export(left_side, "tandy_lid_left.stl")

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,10 @@
import cadquery as cq import shutil
import tempfile
from math import floor 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): def extrude_shape(*, model, face, w, h, x_offset, y_offset, element, height):
return ( return (
@ -71,3 +75,9 @@ def hex_vents(*, size, width, height, density=0.85):
] ]
return vents return vents
def export(model, fname, **kwarg):
tmpfile = tempfile.mktemp(suffix="." + fname.split(".")[-1])
exporters.export(model, tmpfile, **kwarg)
shutil.move(tmpfile, fname)