├── render ├── m.blend └── render.py ├── .gitignore ├── img ├── z-butt-1u-al.jpg ├── z-butt-1u-lp.jpg ├── z-butt-1u-mx.jpg ├── z-butt-1u-tp.jpg ├── z-butt-2u-mx.jpg ├── z-butt-7u-mx.jpg ├── z-butt-1u-container.jpg └── z-butt-iso-enter-mx.jpg ├── README.md ├── docs └── container.md ├── Makefile └── scad └── z-butt.scad /render/m.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yatara-cc/z-butt-openscad/HEAD/render/m.blend -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /stl/ 2 | /scad/z-butt-*.scad 3 | /render/environment.jpg 4 | /release 5 | *.zip 6 | -------------------------------------------------------------------------------- /img/z-butt-1u-al.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yatara-cc/z-butt-openscad/HEAD/img/z-butt-1u-al.jpg -------------------------------------------------------------------------------- /img/z-butt-1u-lp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yatara-cc/z-butt-openscad/HEAD/img/z-butt-1u-lp.jpg -------------------------------------------------------------------------------- /img/z-butt-1u-mx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yatara-cc/z-butt-openscad/HEAD/img/z-butt-1u-mx.jpg -------------------------------------------------------------------------------- /img/z-butt-1u-tp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yatara-cc/z-butt-openscad/HEAD/img/z-butt-1u-tp.jpg -------------------------------------------------------------------------------- /img/z-butt-2u-mx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yatara-cc/z-butt-openscad/HEAD/img/z-butt-2u-mx.jpg -------------------------------------------------------------------------------- /img/z-butt-7u-mx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yatara-cc/z-butt-openscad/HEAD/img/z-butt-7u-mx.jpg -------------------------------------------------------------------------------- /img/z-butt-1u-container.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yatara-cc/z-butt-openscad/HEAD/img/z-butt-1u-container.jpg -------------------------------------------------------------------------------- /img/z-butt-iso-enter-mx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yatara-cc/z-butt-openscad/HEAD/img/z-butt-iso-enter-mx.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Z-Butt OpenSCAD 2 | 3 | A port of Zappycobra's [Z-Butt](https://github.com/imyownyear/Z-Butt) system to OpenSCAD. 4 | 5 | STL models are available for download in the [releases](https://github.com/yatara-cc/z-butt-openscad/releases) section. 6 | 7 | 8 | ## Images 9 | 10 | ![Z-Butt OpenSCAD 1u MX](img/z-butt-1u-mx.jpg) 11 | 12 | ![Z-Butt OpenSCAD 2u MX](img/z-butt-2u-mx.jpg) 13 | 14 | ![Z-Butt OpenSCAD 1u Alps](img/z-butt-1u-al.jpg) 15 | 16 | ![Z-Butt OpenSCAD 1u Topre](img/z-butt-1u-tp.jpg) 17 | 18 | ![Z-Butt OpenSCAD 1u Kailh Low-Profile](img/z-butt-1u-lp.jpg) 19 | 20 | ![Z-Butt OpenSCAD ISO Enter](img/z-butt-iso-enter-mx.jpg) 21 | 22 | ![Z-Butt OpenSCAD 7u MX](img/z-butt-7u-mx.jpg) 23 | 24 | ![Z-Butt OpenSCAD 1u Container](img/z-butt-1u-container.jpg) 25 | 26 | 27 | ## Compatibility 28 | 29 | - MX, Alps, Topre or Kailh Low-Profile stems for switches and stabilizers 30 | - Key sizes of 1u, 1.25u, 1.5u, 1.75u, 2u, 2.25u, 2.75u, 3u, 4u, 6u, 6.25u, and 7u 31 | - ISO enter and big-ass enter 32 | 33 | 34 | ## Usage 35 | 36 | `scad/z-butt.scad` is a library. It won't generate any geometry by itself. Instead it should be included in other OpenSCAD files where it's functions can be called. 37 | 38 | 39 | ### Modules 40 | 41 | 42 | - `(mx/al/tp/lp)_master_base` Base for casting a silicone mould of an existing keycap. 43 | - `(mx/al/tp/lp)_sculpt_base` Base for sculpting a custom-shaped keycap 44 | - `(mx/al/tp/lp)_stem_cavity` Stem cavity base (may need to be inverted for printing). 45 | - `(mx/al/tp/lp)_sprues_only` Sprues with a frame for placing inverted on top of an existing blank key when casting a cavity mold. 46 | - `container` A container for casting silicone molds 47 | 48 | 49 | #### Prefixes and Arguments 50 | 51 | - `mx` module prefix is for Cherry MX stems 52 | - `al` module prefix is for Alps stems 53 | - `tp` module prefix is for Topre stems 54 | - `lp` module prefix is for Kailh Low-Profile stems 55 | - `xu`: number of key units in X (1 unit is 19.05mm or 0.75 inches). 56 | - `yu`: Number of key units in Y 57 | - `name`: Name of specially-shaped key, eg. `iso-enter`, `big-ass-enter` 58 | - `yn`: Number of compartments in a container 59 | 60 | 61 | ### Examples 62 | 63 | 64 | An Alps-stem master base, 2u in the X-axis: 65 | 66 | ``` 67 | include 68 | 69 | al_master_base(xu=2); 70 | ``` 71 | 72 | 73 | An MX-stem ISO enter cavity base: 74 | 75 | ``` 76 | include 77 | 78 | mx_stem_cavity(name="iso-enter"); 79 | ``` 80 | 81 | 82 | ### Parameters 83 | 84 | - Measurements can be altered by changing values in the “User Parameters” section of the `z-butt.scad` library. Of particular note: 85 | - `sprue_max_distance`: make sprue placement denser or sparser 86 | - `container_inset` tune the gap to the internal walls of the container 87 | - MX stems for stabilizers have been included on larger spacebars, though these are tentative and it is advised to check them before printing. Stem placement can be edited in the functions `switches_xu` and `stabilizers_xu`. 88 | 89 | 90 | ### Building 91 | 92 | For building on Linux, OpenSCAD and GNU Make should be installed. 93 | 94 | From the root of the repository, run 95 | 96 | ``` 97 | make 98 | ``` 99 | 100 | Or to use, for example, four cores in parallel: 101 | 102 | ``` 103 | make -j 4 104 | ``` 105 | 106 | 107 | -------------------------------------------------------------------------------- /docs/container.md: -------------------------------------------------------------------------------- 1 | # Containers 2 | 3 | All Z-Butt models have sizes compatible with Lego bricks, so that they can sit in a rectangular tower of bricks to make silicone molds. However, there are also container models available which may provide a more efficient workflow for making silicone molds. 4 | 5 | Z-Butts, like keycaps, are measured in units, abbreviated `u` which correspond to the key spacing on standard keyboards. 1u is 0.75 inches or 19.05mm. These units are used for width and depth measurements (depth here refers to the dimension parallel to the keyboard, as opposed to height which refers to the dimensions perpendicular to the keyboard). In the OpenSCAD source code width is the X dimension, depth is the Y dimension and height is the Z dimension. 6 | 7 | Most keys have a depth of 1u. Only the ISO Enter key and “Big Ass Enter” key have a depth of 2u and require a 2u deep container. 1u deep containers have two compartments by default so two molds can be made at the same time; 2u containers have a single compartment. 8 | 9 | Containers are built from two end pieces and an optional number of center sections. The following table can be used to determine which container pieces are required to house Z-Butt pieces for specific keys. 10 | 11 | ``` 12 | +-------+-------+------------+--------------------+---------------------------+ 13 | | Width | Depth | Example | Z-Butt size | Containers required | 14 | +-------+-------+------------+--------------------+---------------------------+ 15 | | 1u | 1u | Alpha | 32x32mm, 4x4 studs | 2 1u-0s | 16 | | 1.25u | 1u | Shift | 40x32mm, 5x4 studs | 2 1u-0s, 1 1u-1s | 17 | | 1.5u | 1u | Tab | 40x32mm, 5x4 studs | 2 1u-0s, 1 1u-1s | 18 | | 1.75u | 1u | Caps Lock | 48x32mm, 6x4 studs | 2 1u-0s, 1 1u-2s | 19 | | 2u | 1u | Backspace | 48x32mm, 6x4 studs | 2 1u-0s, 1 1u-2s | 20 | | 2.25u | 1u | ANSI Enter | 56x32mm, 7x4 studs | 2 1u-0s, 1 1u-2s, 1 1u-1s | 21 | | 2.5u | 1u | | 64x32mm, 8x4 studs | 2 1u-0s, 2 1u-2s | 22 | | 2.75u | 1u | Shift | 64x32mm, 8x4 studs | 2 1u-0s, 2 1u-2s | 23 | +-------+-------+------------+--------------------+---------------------------+ 24 | | 1.5u | 2u | ISO Enter | 40x48mm, 5x6 studs | 2 2u-0s, 1 2u-1s | 25 | | 2.25u | 2u | BAE | 56x48mm, 7x6 studs | 2 2u-0s, 1 2u-1s, 1 2u-1s | 26 | +-------+-------+------------+--------------------+---------------------------+ 27 | ``` 28 | 29 | ``` 30 | +-------+-------+------------+--------------------+---------------------------+ 31 | | Width | Depth | Example | Z-Butt size | Containers required | 32 | +-------+-------+------------+--------------------+---------------------------+ 33 | | 1u | 1u | Alpha | 32x32mm, 4x4 studs | 2 1u-0s | 34 | | 1.25u | 1u | Shift | 40x32mm, 5x4 studs | 2 1u-0s, 1 1u-1s | 35 | | 1.5u | 1u | Tab | 40x32mm, 5x4 studs | 2 1u-0s, 1 1u-1s | 36 | | 1.75u | 1u | Caps Lock | 48x32mm, 6x4 studs | 2 1u-0s, 1 1u-2s | 37 | | 2u | 1u | Backspace | 48x32mm, 6x4 studs | 2 1u-0s, 1 1u-2s | 38 | | 2.25u | 1u | ANSI Enter | 56x32mm, 7x4 studs | 2 1u-0s, 1 1u-2s, 1 1u-1s | 39 | | 2.5u | 1u | | 64x32mm, 8x4 studs | 2 1u-0s, 2 1u-2s | 40 | | 2.75u | 1u | Shift | 64x32mm, 8x4 studs | 2 1u-0s, 2 1u-2s | 41 | +-------+-------+------------+--------------------+---------------------------+ 42 | | 1.5u | 2u | ISO Enter | 40x48mm, 5x6 studs | 2 2u-0s, 1 2u-1s | 43 | | 2.25u | 2u | BAE | 56x48mm, 7x6 studs | 2 2u-0s, 1 2u-1s, 1 2u-1s | 44 | +-------+-------+------------+--------------------+---------------------------+ 45 | ``` 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | 4 | XUs := 1 1.25 1.5 1.75 2 2.25 2.75 3 4 6 6.25 7 5 | YUs := 1 2 6 | XSs := 0 1 2 4 8 7 | NAMEs := iso-enter big-ass-enter 8 | BASEs := mx al tp lp 9 | STLs := 10 | JPGs := 11 | ZIPs := 12 | 13 | RENDER_SAMPLES := 9 14 | RENDER_PERCENTAGE := 100 15 | 16 | 17 | .PHONY : stl render release 18 | .SECONDARY : 19 | 20 | 21 | all : # Redefined later 22 | 23 | 24 | 25 | define KEY 26 | scad/z-butt-$(2)-$(1)-master-base.scad : 27 | echo -e "include \n\n\n$(1)_master_base($(3));\n" > $$@ 28 | 29 | scad/z-butt-$(2)-$(1)-sculpt-base.scad : 30 | echo -e "include \n\n\n$(1)_sculpt_base($(3));\n" > $$@ 31 | 32 | scad/z-butt-$(2)-$(1)-stem-cavity-sla.scad : 33 | echo -e "include \n\n\nrotate([0, 180, 0]){$(1)_stem_cavity($(3));}\n" > $$@ 34 | 35 | scad/z-butt-$(2)-$(1)-stem-cavity-fdm.scad : 36 | echo -e "include \n\n\nrotate([0, 180, 0]){$(1)_stem_cavity($(3), fdm=true);}\n" > $$@ 37 | 38 | scad/z-butt-$(2)-$(1)-sprues-only.scad : 39 | echo -e "include \n\n\n$(1)_sprues_only($(3));\n" > $$@ 40 | 41 | 42 | STLs := $(STLs) \ 43 | stl/z-butt-$(2)-$(1)-master-base.stl \ 44 | stl/z-butt-$(2)-$(1)-sculpt-base.stl \ 45 | stl/z-butt-$(2)-$(1)-stem-cavity-fdm.stl \ 46 | stl/z-butt-$(2)-$(1)-stem-cavity-sla.stl \ 47 | stl/z-butt-$(2)-$(1)-sprues-only.stl 48 | endef 49 | 50 | 51 | define CONTAINER 52 | scad/z-butt-$(1)u-$(2)s-container.scad : 53 | echo -e "include \n\n\ncontainer(yu=$(1), xs=$(2));\n" > $$@ 54 | 55 | STLs := $(STLs) \ 56 | stl/z-butt-$(1)u-$(2)s-container.stl 57 | endef 58 | 59 | 60 | $(foreach base,$(BASEs), \ 61 | $(foreach xu,$(XUs),$(eval $(call KEY,$(base),$(xu)u,xu=$(xu)))) \ 62 | $(foreach name,$(NAMEs),$(eval $(call KEY,$(base),$(name),name=\"$(name)\"))) \ 63 | ) 64 | $(foreach yu,$(YUs),$(foreach xs,$(XSs),$(eval $(call CONTAINER,$(yu),$(xs))))) 65 | 66 | 67 | define RENDER_KEY 68 | img/z-butt-$(2)-$(1).jpg : render/render.py \ 69 | stl/z-butt-$(2)-$(1)-master-base.stl \ 70 | stl/z-butt-$(2)-$(1)-sculpt-base.stl \ 71 | stl/z-butt-$(2)-$(1)-stem-cavity-sla.stl \ 72 | stl/z-butt-$(2)-$(1)-sprues-only.stl 73 | 74 | @mkdir -p img 75 | blender -b -P render/render.py -- --name=$(2)-$(1) --output=$$@ \ 76 | --samples=$(RENDER_SAMPLES) --percentage=$(RENDER_PERCENTAGE) \ 77 | --distance=$(3) --pan=$(4) --tilt=$(5) --aim-z=$(6) 78 | 79 | JPGs := $(JPGs) img/z-butt-$(2)-$(1).jpg 80 | endef 81 | 82 | define RENDER_CONTAINER 83 | img/z-butt-$(1)-container.jpg : render/render.py \ 84 | stl/z-butt-$(1)-0s-container.stl \ 85 | stl/z-butt-$(1)-1s-container.stl 86 | 87 | @mkdir -p img 88 | blender -b -P render/render.py -- --name=$(1) --output=$$@ \ 89 | --samples=$(RENDER_SAMPLES) --percentage=$(RENDER_PERCENTAGE) \ 90 | --distance=$(2) --pan=$(3) --tilt=$(4) --aim-z=$(5) 91 | 92 | JPGs := $(JPGs) img/z-butt-$(1)-container.jpg 93 | endef 94 | 95 | define ZIP 96 | release/z-butt-openscad-$(1).zip : $(STLs) 97 | @mkdir -p release 98 | zip -r $$@ stl/*-$(1)-*.stl stl/*-$(1).stl docs/$(1).md 99 | 100 | ZIPs := $(ZIPs) release/z-butt-openscad-$(1).zip 101 | endef 102 | 103 | 104 | $(eval $(call RENDER_KEY,mx,1u,160,-20,-60,-15)) 105 | $(eval $(call RENDER_KEY,al,1u,160,22,-60,-15)) 106 | $(eval $(call RENDER_KEY,tp,1u,160,8,-60,-15)) 107 | $(eval $(call RENDER_KEY,lp,1u,160,-30,-65,-15)) 108 | $(eval $(call RENDER_KEY,mx,2u,160,0,-60,-15)) 109 | $(eval $(call RENDER_KEY,mx,7u,290,15,-60,-25)) 110 | $(eval $(call RENDER_KEY,mx,iso-enter,210,-18,-70,-15)) 111 | $(eval $(call RENDER_CONTAINER,1u,140,25,-32,5)) 112 | 113 | $(eval $(call ZIP,mx)) 114 | $(eval $(call ZIP,al)) 115 | $(eval $(call ZIP,tp)) 116 | $(eval $(call ZIP,lp)) 117 | $(eval $(call ZIP,container)) 118 | 119 | 120 | all : $(JPGs) $(STLs) 121 | 122 | clean : 123 | rm -rf \ 124 | stl img release \ 125 | scad/z-butt-*.scad 126 | 127 | 128 | stl : $(STLs) 129 | 130 | jpg : $(JPGs) 131 | 132 | zip : $(ZIPs) 133 | 134 | 135 | release : 136 | git tag -f stable; 137 | git push -f 138 | git push -f --tags 139 | 140 | rebuild : 141 | $(MAKE) clean 142 | $(MAKE) jpg 143 | $(MAKE) stl -j 8 144 | 145 | 146 | stl/%.stl : scad/%.scad scad/z-butt.scad 147 | @mkdir -p stl 148 | openscad -o /tmp/$*.stl $< 2> >(tee /tmp/$*.stl.stderr.log >&2) 149 | [[ ! $$(grep "WARNING" /tmp/$*.stl.stderr.log) ]] 150 | sed -i 's/OpenSCAD_Model/$*/g' /tmp/$*.stl 151 | mv /tmp/$*.stl $@ 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /render/render.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import math 4 | import argparse 5 | 6 | import bpy 7 | 8 | SPACING = 10 9 | LEGO_STUD = 8 10 | 11 | 12 | 13 | def render(path, x=1024, y=512, percentage=100): 14 | scene = bpy.data.scenes["Scene"] 15 | 16 | scene.render.resolution_x = x 17 | scene.render.resolution_y = y 18 | scene.render.resolution_percentage = percentage 19 | 20 | bpy.context.scene.render.image_settings.file_format = "JPEG" 21 | bpy.context.scene.render.filepath = path 22 | 23 | bpy.ops.render.render(write_still=True) 24 | 25 | 26 | 27 | def create_material_plastic(color, roughness, name): 28 | 29 | material = bpy.data.materials.new(name=name) 30 | material.use_nodes = True 31 | [material.node_tree.nodes.remove(v) for v in material.node_tree.nodes] 32 | 33 | noise = material.node_tree.nodes.new('ShaderNodeTexNoise') 34 | noise.inputs["Scale"].default_value = 75 35 | noise.inputs["Detail"].default_value = 2 36 | 37 | bump = material.node_tree.nodes.new('ShaderNodeBump') 38 | bump.inputs["Distance"].default_value = 0.015 39 | material.node_tree.links.new( 40 | noise.outputs["Color"], bump.inputs["Height"]) 41 | 42 | principled = material.node_tree.nodes.new('ShaderNodeBsdfPrincipled') 43 | principled.inputs["Base Color"].default_value = tuple(list(color) + [1.0]) 44 | principled.inputs["Roughness"].default_value = roughness 45 | material.node_tree.links.new( 46 | bump.outputs["Normal"], principled.inputs["Normal"]) 47 | 48 | output = material.node_tree.nodes.new('ShaderNodeOutputMaterial') 49 | material.node_tree.links.new( 50 | principled.outputs["BSDF"], output.inputs["Surface"]) 51 | 52 | return material 53 | 54 | 55 | 56 | def create_material_glass(): 57 | material = bpy.data.materials.new(name="Glass") 58 | material.use_nodes = True 59 | [material.node_tree.nodes.remove(v) for v in material.node_tree.nodes] 60 | 61 | glossy = material.node_tree.nodes.new('ShaderNodeBsdfGlossy') 62 | glossy.inputs["Roughness"].default_value = 0.01 63 | 64 | output = material.node_tree.nodes.new('ShaderNodeOutputMaterial') 65 | material.node_tree.links.new(glossy.outputs["BSDF"], output.inputs["Surface"]) 66 | 67 | return material 68 | 69 | 70 | 71 | def create_area_lamp(name="Area", location=(0, 0, 0), 72 | size=1, strength=100, color=(1, 1, 1, 1)): 73 | 74 | distance = math.sqrt( 75 | pow(location[0], 2) + 76 | pow(location[1], 2) + 77 | pow(location[2], 2) 78 | ) 79 | 80 | bpy.ops.object.lamp_add(type='AREA') 81 | area = bpy.data.objects["Area"] 82 | area.name = name 83 | area.location = location 84 | area.data.size = size 85 | emission = area.data.node_tree.nodes["Emission"] 86 | emission.inputs["Strength"].default_value = strength * distance 87 | emission.inputs["Color"].default_value = color 88 | return area 89 | 90 | 91 | 92 | def load_obj(path, name, color): 93 | bpy.ops.import_mesh.stl( 94 | filepath=path, 95 | axis_forward='Y', 96 | axis_up='Z', 97 | filter_glob="*.stl" 98 | ) 99 | obj = bpy.context.active_object 100 | obj.name = name 101 | 102 | # Put base of object on XY-plane: 103 | obj.location.z = -obj.bound_box[0][2] 104 | 105 | obj.data.materials.append(create_material_plastic( 106 | color, roughness=0.1, name=f"plastic-{name}")) 107 | return obj 108 | 109 | 110 | 111 | def arrange_objects(objects): 112 | max_width = max([v.dimensions.x for v in objects]) 113 | max_depth = max([v.dimensions.y for v in objects]) 114 | 115 | assert len(objects) == 4 116 | 117 | if max_width > max_depth * 2: 118 | objects[0].location.x = -objects[0].dimensions.x 119 | total_depth = sum([v.dimensions.y for v in objects]) + SPACING * len(objects) 120 | 121 | y = total_depth / 2 122 | for obj in objects: 123 | depth = obj.dimensions.y + SPACING 124 | y -= depth / 2 125 | obj.location.x = -obj.bound_box[0][0] - obj.dimensions.x / 2 126 | obj.location.y = y 127 | y -= depth / 2 128 | else: 129 | objects[0].location.x = -objects[0].bound_box[0][0] + SPACING / 2 130 | objects[1].location.x = -objects[1].bound_box[0][0] + SPACING / 2 131 | objects[2].location.x = -objects[2].bound_box[6][0] - SPACING / 2 132 | objects[3].location.x = -objects[3].bound_box[6][0] - SPACING / 2 133 | objects[0].location.y = -objects[0].bound_box[0][1] + SPACING / 2 134 | objects[2].location.y = -objects[2].bound_box[0][1] + SPACING / 2 135 | objects[1].location.y = -objects[1].bound_box[6][1] - SPACING / 2 136 | objects[3].location.y = -objects[3].bound_box[6][1] - SPACING / 2 137 | 138 | 139 | 140 | def load_objects(name): 141 | if name[-3:] in ("-mx", "-al", "-tp", "-lp"): 142 | objects = [ 143 | load_obj( 144 | f"stl/z-butt-{name}-sculpt-base.stl", 145 | name="Sculpt Base", 146 | color=(1.0, 0.5, 1.0) 147 | ), 148 | load_obj( 149 | f"stl/z-butt-{name}-master-base.stl", 150 | name="Master Base", 151 | color=(1.0, 0.75, 0.5) 152 | ), 153 | load_obj( 154 | f"stl/z-butt-{name}-sprues-only.stl", 155 | name="Sprues Only", 156 | color=(0.5, 0.75, 1.0) 157 | ), 158 | load_obj( 159 | f"stl/z-butt-{name}-stem-cavity-sla.stl", 160 | name="Stem Cavity", 161 | color=(0.75, 1.0, 0.5) 162 | ), 163 | ] 164 | 165 | arrange_objects(objects) 166 | else: 167 | back = load_obj( 168 | f"stl/z-butt-{name}-0s-container.stl", 169 | name="Sculpt Base", 170 | color=(0.75, 0.125, 0.125) 171 | ) 172 | back.location.x -= (SPACING + LEGO_STUD) 173 | 174 | mid = load_obj( 175 | f"stl/z-butt-{name}-1s-container.stl", 176 | name="Sculpt Base", 177 | color=(0.75, 0.125, 0.125) 178 | ) 179 | 180 | front = load_obj( 181 | f"stl/z-butt-{name}-0s-container.stl", 182 | name="Sculpt Base", 183 | color=(0.75, 0.125, 0.125) 184 | ) 185 | front.rotation_euler.z = math.radians(180) 186 | front.location.x += (SPACING + LEGO_STUD) 187 | 188 | 189 | def main(): 190 | while sys.argv: 191 | v = sys.argv.pop(0) 192 | if v == "--": 193 | sys.argv.insert(0, __file__) 194 | break 195 | 196 | parser = argparse.ArgumentParser( 197 | description="Render objects as JPG.") 198 | 199 | parser.add_argument( 200 | "--samples", "-s", 201 | type=int, default=6, 202 | help="Number of render samples.") 203 | parser.add_argument( 204 | "--percentage", "-p", 205 | type=float, default=100, 206 | help="Render size percentage.") 207 | parser.add_argument( 208 | "--pan", "-P", 209 | type=float, default=12.5, 210 | help="Camera pan.") 211 | parser.add_argument( 212 | "--tilt", "-T", 213 | type=float, default=-60, 214 | help="Camera tilt.") 215 | parser.add_argument( 216 | "--distance", "-D", 217 | type=float, default=100, 218 | help="Camera distance.") 219 | parser.add_argument( 220 | "--aim-z", "-z", 221 | type=float, default=0, 222 | help="Camera aim Z.") 223 | 224 | parser.add_argument( 225 | "--name", "-n", 226 | help="STL model name.") 227 | parser.add_argument( 228 | "--output", "-o", 229 | help="Path to JPG output file.") 230 | 231 | args = parser.parse_args() 232 | 233 | 234 | scene = bpy.data.scenes["Scene"] 235 | scene.render.engine = 'CYCLES' 236 | bpy.context.scene.cycles.use_square_samples = True 237 | bpy.context.scene.cycles.samples = args.samples 238 | bpy.context.scene.render.layers[0].cycles.use_denoising = True 239 | 240 | 241 | world = bpy.data.worlds["World"] 242 | world.use_nodes = True 243 | if os.path.exists("render/environment.jpg"): 244 | environment = world.node_tree.nodes.new('ShaderNodeTexEnvironment') 245 | image = bpy.data.images.load(filepath="render/environment.jpg") 246 | environment.image = image 247 | world.node_tree.links.new( 248 | environment.outputs["Color"], 249 | world.node_tree.nodes["Background"].inputs["Color"] 250 | ) 251 | else: 252 | background_luminosity = 0.3 253 | world.node_tree.nodes["Background"].inputs["Color"].default_value = \ 254 | tuple([background_luminosity] * 3 + [1.0]) 255 | 256 | # Delete default objects: 257 | bpy.ops.object.select_all(action="SELECT") 258 | bpy.data.objects['Camera'].select = False 259 | bpy.ops.object.delete() 260 | 261 | load_objects(args.name); 262 | 263 | create_area_lamp( 264 | name="Area Key", location=(250, -100, 300), 265 | size=100, strength=3000, color=(1, 1, 1.2, 1)) 266 | create_area_lamp( 267 | name="Area Fill", location=(-200, 0, 80), 268 | size=200, strength=750, color=(1, 1, 1.1, 1)) 269 | create_area_lamp( 270 | name="Area Rim", location=(-300, 300, 80), 271 | size=10, strength=2000, color=(1.1, 1.1, 1, 1)) 272 | 273 | bpy.ops.mesh.primitive_plane_add( 274 | radius=1000, 275 | location=(0, 0, 0), 276 | rotation=(0, 0, 0) 277 | ) 278 | plane = bpy.data.objects["Plane"] 279 | 280 | glass = create_material_plastic( 281 | (1, 1, 1), roughness=0.0, name="plastic-plane") 282 | glass.node_tree.nodes["Principled BSDF"] \ 283 | .inputs["Specular"].default_value = 0.0 284 | plane.data.materials.append(glass) 285 | 286 | bpy.context.scene.camera.data.clip_end = 500 287 | 288 | field_of_view = 50.0 289 | 290 | tz = args.aim_z - math.sin(math.radians(args.tilt)) * args.distance 291 | out = math.cos(math.radians(args.tilt)) * args.distance 292 | 293 | tx = math.sin(math.radians(args.pan)) * out 294 | ty = -math.cos(math.radians(args.pan)) * out 295 | 296 | scene.camera.data.angle = math.radians(field_of_view) 297 | scene.camera.rotation_mode = 'XYZ' 298 | scene.camera.rotation_euler = ( 299 | math.radians(90 + args.tilt), 300 | 0, 301 | math.radians(args.pan) 302 | ) 303 | scene.camera.location = (tx, ty, tz) 304 | 305 | 306 | render(args.output, x=870, y=620, percentage=args.percentage) 307 | if os.path.exists("/tmp"): 308 | bpy.ops.wm.save_as_mainfile(filepath=f"/tmp/z-butt-{args.name}.blend") 309 | 310 | 311 | 312 | main() 313 | -------------------------------------------------------------------------------- /scad/z-butt.scad: -------------------------------------------------------------------------------- 1 | // Constants 2 | 3 | unit_u = 19.05; 4 | unit_lego = 1.6; 5 | unit_lego_stud = 5 * unit_lego; 6 | 7 | 8 | // User Parameteters 9 | 10 | 11 | plate_size = 32; 12 | plate_height = 3; 13 | plate_chamfer = 1; 14 | plate_inset = 0; // Distance to shrink the main plate size. 15 | 16 | regst_inset = 4; // Distance between plate and registration block. 17 | regst_height = 2; 18 | regst_radius = 4; 19 | regst_offset = 0.1; // Gap between positive and negative models. 20 | 21 | indent_depth = 1; 22 | 23 | sprue_diameter_base = 3.1; 24 | sprue_diameter_tip = 2.0; 25 | sprue_diameter_stem = 1.4; 26 | sprue_height = 12; 27 | sprue_height_tip = 2; 28 | sprue_max_distance = 8; 29 | sprue_plate_height = 1; 30 | sprue_plate_width = 1.5; 31 | 32 | stem_clearance_z = 0.5; 33 | 34 | mx_tp_diameter_pos = 5.6; 35 | mx_tp_diameter_neg = 5.8; 36 | 37 | mx_crux_pos = [ 38 | "horiz_length", 4.0, 39 | "horiz_thick", 1.3, 40 | "vert_length", 3.9, 41 | "vert_thick", 1.1 42 | ]; 43 | mx_crux_neg = [ 44 | "horiz_length", 4.38, 45 | "horiz_thick", 1.26, 46 | "vert_length", 4.38, 47 | "vert_thick", 1.15 48 | ]; 49 | mx_bevel = 0.25; 50 | mx_stem_bevel = false; 51 | mx_height_pos = 3; 52 | mx_height_neg = 5; 53 | 54 | al_w1 = 4.95; 55 | al_l1 = 5.95; 56 | al_w2 = 0.95; 57 | al_l2 = 7.85; 58 | al_w0 = 2.25; 59 | al_l0 = 4.5; 60 | al_wd = 2.5; 61 | al_height = 3.9; 62 | al_stem_l1 = 4.45; 63 | al_stem_w1 = 2.2; 64 | al_stem_l0 = 2.55; 65 | al_stem_w0 = 0.9; 66 | al_stem_ch = 0.5; 67 | 68 | tp_keycap_stem_ext = 1; 69 | tp_keycap_stem_slot_x_min = 1.15; 70 | tp_keycap_stem_inner_diameter = 3.75; 71 | tp_switch_stem_height = 4; 72 | 73 | lp_depth = 1.6; 74 | lp_stem = [ 75 | "distance_x", 5.75, 76 | "extension_z", 1.8, 77 | "size_x", 1.27, 78 | "size_y", 2.95, 79 | "bevel", 0.25, 80 | ]; 81 | 82 | mx_al_tp_key = [ 83 | "base_sx", 18.5, 84 | "base_sy", 18.5, 85 | "cavity_sx", 14.925, 86 | "cavity_sy", 14.925, 87 | "cavity_sz", 5, 88 | "cavity_ch_xy", 2, 89 | "indent_inset", 3 90 | ]; 91 | lp_key = [ 92 | "base_sx", 17.65, 93 | "base_sy", 16.5, 94 | "cavity_sx", 16.1, 95 | "cavity_sy", 14.9, 96 | "cavity_sz", 1.6, 97 | "cavity_ch_xy", 1.6, 98 | "indent_inset", 1.5 99 | ]; 100 | cavity_bevel = 0.25; 101 | 102 | key_sculpt_size = 14.925; 103 | key_sculpt_inset_z = 0.5; 104 | key_sculpt_bevel = 0.93; 105 | 106 | key_wall = 1.5; // Thickness of the key wall. 107 | base_inset = 0.25; // Distance that key overhangs the base. 108 | 109 | container_wall = 3; 110 | container_base = 2; 111 | container_overlap = unit_lego_stud / 2; 112 | container_height = 28; // Internal height 113 | container_inset = 0.1; // XY Gap between contents and internal container walls. 114 | container_gap_edge = 0.1; // XY Gap between connecting container edges. 115 | container_clips = true; 116 | container_clip_inset = 0.15; // Gap between touching clip faces. 117 | 118 | 119 | // Internal Parameters 120 | 121 | 122 | overlap = 0.1; // Offset to avoid coplanar faces in Boolean operations. 123 | container_clip_width = unit_lego_stud / 2; 124 | container_clip_extension = unit_lego_stud - 2 * container_wall; 125 | 126 | 127 | // Functions 128 | 129 | 130 | function attr(p, k) = p[search([k], [for(i = [0 : 2: len(p) - 2]) p[i]])[0] * 2 + 1]; 131 | 132 | 133 | function calc_xu (xu=1, name="") = 134 | (name == "iso-enter") ? 1.5 : 135 | (name == "big-ass-enter") ? 2.25 : 136 | xu; 137 | 138 | function calc_yu (yu=1, name="") = 139 | (name == "iso-enter") ? 2 : 140 | (name == "big-ass-enter") ? 2 : 141 | yu; 142 | 143 | 144 | function calc_plate_size (u=1) = unit_lego_stud * round((plate_size + unit_u * (u - 1)) / unit_lego_stud); 145 | 146 | 147 | function calc_base_size (size, u=1) = size + unit_u * (u - 1) - base_inset * 2; 148 | 149 | 150 | function calc_cavity_size (size, u=1) = size + unit_u * (u - 1); 151 | 152 | 153 | function calc_key_sculpt_size (u=1) = key_sculpt_size + unit_u * (u - 1); 154 | 155 | 156 | // See `https://deskthority.net/wiki/Space_by_keyboard`. 157 | function stabilizers_xy (xu=1, yu=1, name="") = 158 | (name == "iso-enter") ? [[0.125, -0.625], [0.125, 0.625]] : 159 | (name == "big-ass-enter") ? [[0.5, -0.625], [0.5, 0.625]] : 160 | (xu == 7) ? [[-3, 0], [3, 0]] : 161 | (xu == 6.25) ? [[-xu / 2 + 0.5, 0], [-xu / 2 + 5.75, 0]] : // (Cherry) 162 | (xu == 6) ? [[-2.5, 0], [2.5, 0]] : // (Cherry) 163 | (xu == 4) ? [[-1.5, 0], [1.5, 0]] : 164 | (xu == 3) ? [[-1, 0], [1, 0]] : 165 | (xu >= 2) ? [[-0.625, 0], [0.625, 0]] : 166 | []; 167 | 168 | 169 | // See `https://deskthority.net/wiki/Space_by_keyboard`. 170 | function switches_xy (xu=1, yu=1, name="") = 171 | (name == "iso-enter") ? [[0.125, 0]] : 172 | (name == "big-ass-enter") ? [[0.5, 0], [-0.625, -0.5]] : 173 | (xu == 7) ? [[0, 0]] : 174 | (xu == 6.25) ? [[-xu / 2 + 3.75, 0]] : // (Cherry) 175 | (xu == 6) ? [[0.5, 0]] : // (Cherry) 176 | [[0, 0]]; 177 | 178 | 179 | // Generic Geometry Modules 180 | 181 | 182 | module rotate_z_copy (angle) { 183 | children(); 184 | rotate([0, 0, angle]) { 185 | children(); 186 | } 187 | } 188 | 189 | 190 | module mirror_copy (v) { 191 | children(); 192 | mirror(v) { 193 | children(); 194 | } 195 | } 196 | 197 | 198 | module translate_copy (v, n=2) { 199 | for (i = [0 : n - 1]) { 200 | translate([v[0] * i, v[1] * i, v[2] * i]) { 201 | children(); 202 | } 203 | } 204 | } 205 | 206 | 207 | module centered_cube (size) { 208 | // A cube with equal sides in XY and centered in XY. 209 | translate([-size[0] / 2, -size[1] / 2, 0]) { 210 | cube(size); 211 | } 212 | } 213 | 214 | 215 | module chamfered_cube (sx, sy, sz, ch_xy, ch_z) { 216 | // An XY-centered cube with chamfered top edges. 217 | // 218 | // `ch_xy` = XY chamfer indent 219 | // `ch_z` = Z chamfer indent 220 | 221 | hull() { 222 | centered_cube([sx, sy, sz - ch_z]); 223 | centered_cube([sx - 2 * ch_xy, sy - 2 * ch_xy, sz]); 224 | } 225 | } 226 | 227 | 228 | 229 | module fdm_corners (sx, sy, sz, fz) { 230 | // Corner pillars for a bottom-chamfered cube 231 | // with overhangs not exceeding 45 degrees. 232 | // 233 | // `fz` height of non-chamfered edge in Z. 234 | 235 | xy = fz; 236 | dz = sz - fz; 237 | dxy = dz / 2; 238 | 239 | mirror_copy([1, 0, 0]) { 240 | mirror_copy([0, 1, 0]) { 241 | hull() { 242 | translate([-sx / 2, -sy / 2, 0]) { 243 | scale([xy, xy, fz]) { 244 | cube(1); 245 | } 246 | } 247 | translate([-sx / 2 + dz, -sy / 2 + dz, sz - fz]) { 248 | scale([xy, xy, fz]) { 249 | cube(1); 250 | } 251 | } 252 | translate([-sx / 2 + dxy, -sy / 2 + dxy, sz - fz]) { 253 | scale([xy, xy, fz]) { 254 | cube(1); 255 | } 256 | } 257 | } 258 | } 259 | } 260 | } 261 | 262 | 263 | 264 | module bevel_corner_square (sx, sy, r, n) { 265 | // An XY-centered square with a bevel on a single corner. 266 | // 267 | // `r` = corner radius. 268 | // `n` = corner segments. 269 | 270 | dx = sx / 2; 271 | dy = sy / 2; 272 | cx = dx - r; 273 | cy = dy - r; 274 | a = 90 / n; 275 | 276 | points = concat( 277 | [[-dx, dy]], 278 | [for (t = [0 : n]) [cx + r * sin(a * t), cy + r * cos(a * t)]], 279 | [[dx, -dy], [-dx, -dy]] 280 | ); 281 | 282 | polygon(points=points); 283 | } 284 | 285 | 286 | // Z-Butt Components 287 | 288 | 289 | module bevelled_key (sx, sy, sz, ch_xy, bevel) { 290 | translate([0, 0, -overlap]) { 291 | minkowski() { 292 | chamfered_cube(sx, sy, sz + overlap, ch_xy, sz); 293 | scale([1, 1, 0]) { 294 | sphere(bevel); 295 | } 296 | } 297 | } 298 | } 299 | 300 | 301 | module stem_copy (xu=1, yu=1, name="", switches=true, stabilizers=true) { 302 | if (switches) { 303 | for (xy = switches_xy(xu=xu, yu=yu, name=name)) { 304 | translate([unit_u * xy[0], unit_u * xy[1], 0]) { 305 | children(); 306 | } 307 | } 308 | } 309 | if (stabilizers) { 310 | for (xy = stabilizers_xy(xu=xu, yu=yu, name=name)) { 311 | translate([unit_u * xy[0], unit_u * xy[1], 0]) { 312 | children(); 313 | } 314 | } 315 | } 316 | } 317 | 318 | 319 | module top_plate (key, xu=1, yu=1, fdm=false) { 320 | sx = calc_plate_size(xu) - plate_inset; 321 | sy = calc_plate_size(yu) - plate_inset; 322 | fz = plate_height - plate_chamfer; 323 | if (fdm) { 324 | chamfered_cube( 325 | sx, 326 | sy, 327 | attr(key, "cavity_sz") + fz, 328 | attr(key, "cavity_sz"), attr(key, "cavity_sz")); 329 | if (attr(key, "cavity_sz") > plate_height) { 330 | fdm_corners(sx, sy, attr(key, "cavity_sz") + fz, fz); 331 | } 332 | } else { 333 | chamfered_cube( 334 | sx, sy, 335 | plate_height, 336 | plate_chamfer, plate_chamfer); 337 | translate([0, 0, fz]) { 338 | bevelled_key( 339 | calc_base_size(attr(key, "base_sx"), xu), 340 | calc_base_size(attr(key, "base_sy"), yu), 341 | attr(key, "cavity_sz"), 342 | attr(key, "cavity_ch_xy"), key_sculpt_bevel, $fn=16); 343 | } 344 | } 345 | } 346 | 347 | 348 | module bottom_plate (xu=1, yu=1) { 349 | rotate([180, 0, 0]) { 350 | chamfered_cube ( 351 | calc_plate_size(xu) - plate_inset, 352 | calc_plate_size(yu) - plate_inset, 353 | plate_height, 354 | plate_chamfer, plate_chamfer); 355 | } 356 | } 357 | 358 | 359 | module registration_cube (xu=1, yu=1, offset=0) { 360 | size_x = calc_plate_size(xu) - regst_inset; 361 | size_y = calc_plate_size(yu) - regst_inset; 362 | 363 | translate([0, 0, -regst_height]) { 364 | linear_extrude(regst_height + overlap) { 365 | bevel_corner_square( 366 | size_x + offset, 367 | size_y + offset, 368 | regst_radius + offset, 25); 369 | } 370 | } 371 | } 372 | 373 | 374 | module base (key, xu=1, yu=1, name="") { 375 | if (name == "iso-enter") { 376 | union() { 377 | translate([0, 0.5 * unit_u, 0]) { 378 | base(key, 1.5, 1); 379 | } 380 | translate([0.125 * unit_u, 0, 0]) { 381 | base(key, 1.25, 2); 382 | } 383 | } 384 | } else if (name == "big-ass-enter") { 385 | union() { 386 | translate([0.375 * unit_u, 0, 0]) { 387 | base(key, 1.5, 2); 388 | } 389 | translate([0, -0.5 * unit_u, 0]) { 390 | base(key, 2.25, 1); 391 | } 392 | } 393 | } else { 394 | size_x = calc_base_size(attr(key, "base_sx"), xu); 395 | size_y = calc_base_size(attr(key, "base_sy"), yu); 396 | 397 | scale([size_x, size_y, regst_height + overlap]) { 398 | translate([-.5, -.5, -1]) { 399 | cube([1, 1, 1]); 400 | } 401 | } 402 | } 403 | } 404 | 405 | 406 | module indent (key, xu, yu, name="") { 407 | if (name == "iso-enter") { 408 | union() { 409 | translate([0, 0.5 * unit_u, 0]) { 410 | indent(key, 1.5, 1); 411 | } 412 | translate([0.125 * unit_u, 0, 0]) { 413 | indent(key, 1.25, 2); 414 | } 415 | } 416 | } else if (name == "big-ass-enter") { 417 | union() { 418 | translate([0.375 * unit_u, 0, 0]) { 419 | indent(key, 1.5, 2); 420 | } 421 | translate([0, -0.5 * unit_u, 0]) { 422 | indent(key, 2.25, 1); 423 | } 424 | } 425 | } else { 426 | sx = calc_base_size(attr(key, "base_sx"), xu) - 2 * attr(key, "indent_inset"); 427 | sy = calc_base_size(attr(key, "base_sy"), yu) - 2 * attr(key, "indent_inset"); 428 | translate([0, 0, overlap]) { 429 | rotate([180, 0, 0]) { 430 | chamfered_cube(sx, sy, indent_depth + overlap, indent_depth, indent_depth); 431 | } 432 | } 433 | } 434 | } 435 | 436 | 437 | module key_sculpt (key, xu=1, yu=1, name="") { 438 | if (name == "iso-enter") { 439 | union() { 440 | translate([0, 0.5 * unit_u, 0]) { 441 | key_sculpt(key, 1.5, 1); 442 | } 443 | translate([0.125 * unit_u, 0, 0]) { 444 | key_sculpt(key, 1.25, 2); 445 | } 446 | } 447 | } else if (name == "big-ass-enter") { 448 | union() { 449 | translate([0.375 * unit_u, 0, 0]) { 450 | key_sculpt(key, 1.5, 2); 451 | } 452 | translate([0, -0.5 * unit_u, 0]) { 453 | key_sculpt(key, 2.25, 1); 454 | } 455 | } 456 | } else { 457 | inset = 2 * key_sculpt_bevel; 458 | sx = calc_key_sculpt_size(xu) - inset; 459 | sy = calc_key_sculpt_size(yu) - inset; 460 | 461 | bevelled_key( 462 | sx, sy, 463 | attr(key, "cavity_sz") - key_sculpt_inset_z, 464 | attr(key, "cavity_ch_xy"), 465 | key_sculpt_bevel, $fn=24); 466 | } 467 | } 468 | 469 | 470 | module key_cavity (key, xu=1, yu=1) { 471 | inset = 2 * cavity_bevel; 472 | sx = calc_cavity_size(attr(key, "cavity_sx"), xu) - inset; 473 | sy = calc_cavity_size(attr(key, "cavity_sy"), yu) - inset; 474 | 475 | bevelled_key(sx, sy, attr(key, "cavity_sz"), 476 | attr(key, "cavity_ch_xy"), cavity_bevel, $fn=24); 477 | } 478 | 479 | 480 | module sprue_base (height) { 481 | r = sprue_diameter_base / 2; 482 | 483 | rotate_extrude() { 484 | polygon(points=[ 485 | [0, 0], 486 | [sprue_diameter_tip / 2, 0], 487 | [r, -sprue_height_tip], 488 | [r, -height], 489 | [0, -height], 490 | ]); 491 | } 492 | } 493 | 494 | 495 | module sprue_copy (length, include_last=false) { 496 | n = floor(length / sprue_max_distance); 497 | x = length / n; 498 | end = n - (include_last ? 0 : 1); 499 | 500 | if (n > 0) { 501 | for (i = [0 : end]) { 502 | translate([x * i, 0, 0]) { 503 | children(); 504 | } 505 | } 506 | } 507 | } 508 | 509 | 510 | module sprues_base (key, height, xu=1, yu=1, name="") { 511 | $fn = 24; 512 | if (name == "iso-enter") { 513 | dxa = calc_base_size(attr(key, "base_sx"), 1.5); 514 | dxb = calc_base_size(attr(key, "base_sx"), 1.25); 515 | dya = calc_base_size(attr(key, "base_sy"), 2); 516 | dyb = calc_base_size(attr(key, "base_sy"), 1); 517 | translate([-dxa / 2, dya / 2, 0]) { 518 | sprue_copy(dxa, sprue_max_distance, include_last=false) { 519 | sprue_base(height); 520 | } 521 | } 522 | rotate([0, 0, 90]) { 523 | translate([-dya / 2, dxb - dxa / 2, 0]) { 524 | sprue_copy(dya - dyb, sprue_max_distance) { 525 | sprue_base(height); 526 | } 527 | } 528 | translate([(dya / 2) - dyb, dxa / 2, 0]) { 529 | sprue_copy(dyb, sprue_max_distance, include_last=false) { 530 | sprue_base(height); 531 | } 532 | } 533 | } 534 | rotate([0, 0, 180]) { 535 | translate([-dxa / 2, dya / 2, 0]) { 536 | sprue_copy(dxb, sprue_max_distance, include_last=false) { 537 | sprue_base(height); 538 | } 539 | } 540 | } 541 | rotate([0, 0, 270]) { 542 | translate([-dya / 2, dxa / 2, 0]) { 543 | sprue_copy(dya, sprue_max_distance, include_last=false) { 544 | sprue_base(height); 545 | } 546 | } 547 | } 548 | } else if (name == "big-ass-enter") { 549 | dxa = calc_base_size(attr(key, "base_sx"), 2.25); 550 | dxb = calc_base_size(attr(key, "base_sx"), 1.5); 551 | dya = calc_base_size(attr(key, "base_sy"), 2); 552 | dyb = calc_base_size(attr(key, "base_sy"), 1); 553 | translate([dxa / 2 - dxb, dya / 2, 0]) { 554 | sprue_copy(dxb, sprue_max_distance, include_last=false) { 555 | sprue_base(height); 556 | } 557 | } 558 | translate([-dxa / 2, dyb - (dya / 2), 0]) { 559 | sprue_copy(dxa - dxb, sprue_max_distance, include_last=false) { 560 | sprue_base(height); 561 | } 562 | } 563 | rotate([0, 0, 90]) { 564 | translate([dyb - (dya / 2), dxb - dxa / 2, 0]) { 565 | sprue_copy(dya - dyb, sprue_max_distance, include_last=false) { 566 | sprue_base(height); 567 | } 568 | } 569 | translate([-dya / 2, dxa / 2, 0]) { 570 | sprue_copy(dyb, sprue_max_distance, include_last=false) { 571 | sprue_base(height); 572 | } 573 | } 574 | } 575 | rotate([0, 0, 180]) { 576 | translate([-dxa / 2, dya / 2, 0]) { 577 | sprue_copy(dxa, sprue_max_distance, include_last=false) { 578 | sprue_base(height); 579 | } 580 | } 581 | } 582 | rotate([0, 0, 270]) { 583 | translate([-dya / 2, dxa / 2, 0]) { 584 | sprue_copy(dya, sprue_max_distance, include_last=false) { 585 | sprue_base(height); 586 | } 587 | } 588 | } 589 | } else { 590 | dx = calc_base_size(attr(key, "base_sx"), xu); 591 | dy = calc_base_size(attr(key, "base_sy"), yu); 592 | 593 | rotate_z_copy(180) { 594 | translate([-dx / 2, -dy / 2, 0]) { 595 | sprue_copy(dx, sprue_max_distance) { 596 | sprue_base(height); 597 | } 598 | } 599 | rotate([0, 0, 90]) { 600 | translate([-dy / 2, -dx / 2, 0]) { 601 | sprue_copy(dy, sprue_max_distance) { 602 | sprue_base(height); 603 | } 604 | } 605 | } 606 | } 607 | } 608 | } 609 | 610 | 611 | module mx_sprues_stem (height) { 612 | d = (( 613 | mx_stem_bevel ? 614 | mx_stem_bevel * 2 + sqrt(2) * (mx_tp_diameter_pos - 2 * mx_stem_bevel): 615 | (mx_tp_diameter_pos) 616 | ) - sprue_diameter_stem) / 2; 617 | 618 | for (i = [0 : 3]) { 619 | rotate([0, 0, 45 + 90 * i]) { 620 | translate([-d, 0, -height]) { 621 | cylinder(h=height + overlap, d=sprue_diameter_stem, $fn = 16); 622 | } 623 | } 624 | } 625 | } 626 | 627 | 628 | module al_sprues_stem (height) { 629 | d = (mx_tp_diameter_pos - sprue_diameter_stem) / 2; 630 | rotate_z_copy(180) { 631 | translate([2, 0, -height]) { 632 | cylinder(h=height + al_stem_ch, d=sprue_diameter_stem, $fn = 16); 633 | } 634 | } 635 | } 636 | 637 | 638 | module tp_sprues_stem (height, ext=0) { 639 | d = (mx_tp_diameter_pos + tp_keycap_stem_inner_diameter) / 4; 640 | 641 | rotate_z_copy(180) { 642 | translate([d, 0, -height]) { 643 | cylinder(h=height - ext + overlap, d=sprue_diameter_stem, $fn = 16); 644 | } 645 | } 646 | } 647 | 648 | 649 | module lp_sprues_stem (height) { 650 | ext_z = attr(lp_stem, "extension_z"); 651 | sx = attr(lp_stem, "size_x"); 652 | sy = attr(lp_stem, "size_y"); 653 | 654 | h = height - ext_z + overlap; 655 | lp_copy() { 656 | translate([0, 0, -height]) { 657 | cylinder(h=h, d=sprue_diameter_stem, $fn = 16); 658 | } 659 | } 660 | } 661 | 662 | 663 | module mx_stem (height) { 664 | $fn = 32; 665 | translate([0, 0, -overlap]) { 666 | linear_extrude(height + overlap * 3) { 667 | if (mx_stem_bevel) { 668 | minkowski() { 669 | scale(mx_tp_diameter_pos - 2 * mx_stem_bevel) { 670 | translate([-0.5, -0.5]) { 671 | square(1); 672 | } 673 | } 674 | circle(r=mx_stem_bevel); 675 | } 676 | } else { 677 | circle(d=mx_tp_diameter_pos); 678 | } 679 | } 680 | } 681 | } 682 | 683 | 684 | module mx_cross (dimensions, height) { 685 | inset = 2 * mx_bevel; 686 | h = height - mx_bevel; 687 | 688 | minkowski() { 689 | linear_extrude(h) { 690 | union() { 691 | scale([ 692 | attr(dimensions, "horiz_length") - inset, 693 | attr(dimensions, "horiz_thick") - inset, 694 | ]) { 695 | translate([-0.5, -0.5]) { 696 | square(1); 697 | } 698 | } 699 | scale([ 700 | attr(dimensions, "vert_thick") - inset, 701 | attr(dimensions, "vert_length") - inset, 702 | ]) { 703 | translate([-0.5, -0.5]) { 704 | square(1); 705 | } 706 | } 707 | } 708 | } 709 | sphere(mx_bevel, $fn=16); 710 | } 711 | } 712 | 713 | 714 | module al_outer_2d () { 715 | union() { 716 | square([al_l1, al_w1], center=true); 717 | square([al_l0 + al_w2 * 2, al_w0], center=true); 718 | translate([0, al_wd / 2]) { 719 | square([al_l2, al_w2], center=true); 720 | } 721 | translate([0, -al_wd / 2]) { 722 | square([al_l2, al_w2], center=true); 723 | } 724 | } 725 | } 726 | 727 | 728 | module al_inner_2d () { 729 | square([al_l0, al_w0], center=true); 730 | } 731 | 732 | 733 | module al_switch_stem (height) { 734 | linear_extrude(height) { 735 | difference() { 736 | al_outer_2d(); 737 | al_inner_2d(); 738 | } 739 | } 740 | } 741 | 742 | 743 | module al_stem (height) { 744 | h = height + overlap; 745 | 746 | rotate([180, 0, 0]) { 747 | translate([0, 0, -h]) { 748 | difference() { 749 | chamfered_cube(al_stem_l1, al_stem_w1, h, 750 | al_stem_ch, al_stem_ch); 751 | centered_cube([al_stem_l0, al_stem_w0, 100]); 752 | } 753 | } 754 | } 755 | 756 | } 757 | 758 | 759 | module tp_keycap_stem_pos (depth, ext=0) { 760 | $fn = 32; 761 | translate([0, 0, -ext]) { 762 | linear_extrude(depth + ext + overlap) { 763 | circle(d=mx_tp_diameter_pos, $fn=32); 764 | } 765 | } 766 | } 767 | 768 | 769 | 770 | module tp_keycap_stem_neg (depth, ext=0) { 771 | slot_max = 2; 772 | slot_depth = min(depth, 6.5 - ext); 773 | bevel = 0.375; 774 | 775 | sx1 = slot_max / 2 - bevel; 776 | sx2 = tp_keycap_stem_slot_x_min / 2 + bevel; 777 | 778 | rotate([90, 0, 0]) { 779 | scale([1, 1, mx_tp_diameter_pos + 2 * overlap]) { 780 | translate([0, 0, -0.5]) { 781 | linear_extrude(1) { 782 | difference() { 783 | minkowski() { 784 | circle(bevel, $fn=8); 785 | polygon(points=[ 786 | [-sx1, slot_depth - bevel], 787 | [-sx1, -ext - overlap], 788 | [sx1, -ext - overlap], 789 | [sx1, slot_depth - bevel], 790 | ]); 791 | } 792 | mirror_copy([1, 0]) { 793 | minkowski() { 794 | circle(bevel, $fn=8); 795 | polygon(points=[ 796 | [sx2, -ext + bevel], 797 | [sx2, 1.5], 798 | [sx2 * 2, 5], 799 | [sx2 * 2, -ext + bevel], 800 | ]); 801 | } 802 | } 803 | } 804 | } 805 | } 806 | } 807 | } 808 | 809 | 810 | translate([0, 0, -ext - overlap]) { 811 | cylinder(d=tp_keycap_stem_inner_diameter, h=(depth + ext + overlap), $fn=32); 812 | } 813 | 814 | } 815 | 816 | 817 | module tp_switch_stem_pos (z_base) { 818 | thickness = 1; 819 | 820 | h = tp_switch_stem_height - z_base; 821 | d = mx_tp_diameter_neg + 2 * thickness; 822 | 823 | translate([0, 0, z_base]) { 824 | cylinder(d=d, h=h, $fn=32); 825 | } 826 | } 827 | 828 | 829 | 830 | module tp_switch_stem_neg (indent_z) { 831 | depth = tp_keycap_stem_ext + stem_clearance_z; 832 | guide_depth = 1; 833 | guide_ext = 1.5; 834 | bevel = 0.5; 835 | 836 | h = tp_switch_stem_height + depth + overlap; 837 | gx = tp_keycap_stem_slot_x_min - bevel * 2; 838 | dy = mx_tp_diameter_neg / 2 - guide_ext; 839 | dz = -depth; 840 | gz = tp_switch_stem_height + depth - guide_depth; 841 | 842 | difference() { 843 | translate([0, 0, -depth]) { 844 | cylinder(d=mx_tp_diameter_neg, h=h, $fn=32); 845 | } 846 | rotate_z_copy(180) { 847 | minkowski() { 848 | sphere(r=bevel, $fn=8); 849 | translate([0, dy + bevel, dz - bevel]) { 850 | scale([gx, guide_ext, gz]) { 851 | translate([-0.5, 0, 0]) { 852 | cube(1); 853 | } 854 | } 855 | } 856 | } 857 | } 858 | } 859 | } 860 | 861 | 862 | 863 | module lp_copy () { 864 | dx = attr(lp_stem, "distance_x"); 865 | translate([-dx / 2, 0]) { 866 | children(); 867 | } 868 | translate([dx / 2, 0]) { 869 | children(); 870 | } 871 | } 872 | 873 | 874 | module lp_switch_neg_2d () { 875 | sx_neg = 1.25; 876 | sy_neg = 3; 877 | 878 | lp_copy() { 879 | scale([sx_neg, sy_neg]) { 880 | translate([-0.5, -0.5]) { 881 | square(1); 882 | } 883 | } 884 | } 885 | } 886 | 887 | module lp_switch_stem_pos (height) { 888 | sx_pos = 10.25; 889 | sy_pos = 4.45; 890 | linear_extrude(height) { 891 | scale([sx_pos, sy_pos]) { 892 | translate([-0.5, -0.5]) { 893 | square(1); 894 | } 895 | } 896 | } 897 | } 898 | 899 | 900 | module lp_switch_stem_neg () { 901 | h = attr(lp_stem, "extension_z") + 0.25; 902 | sx_pos = 10.25; 903 | sy_pos = 4.45; 904 | translate([0, 0, -h]) { 905 | linear_extrude(h + overlap) { 906 | lp_switch_neg_2d (); 907 | } 908 | } 909 | } 910 | 911 | 912 | module lp_stem (cavity_height) { 913 | $fn = 32; 914 | ext_z = attr(lp_stem, "extension_z"); 915 | sx = attr(lp_stem, "size_x"); 916 | sy = attr(lp_stem, "size_y"); 917 | bevel = attr(lp_stem, "bevel"); 918 | 919 | translate([0, 0, -ext_z]) { 920 | linear_extrude(cavity_height + ext_z + overlap) { 921 | lp_copy() { 922 | rotate_z_copy(180) { 923 | translate([0, sy / 2 - bevel]) { 924 | minkowski() { 925 | scale([sx - bevel * 2, sy * 2 / 5 - bevel * 2]) { 926 | translate([-0.5, -1]) { 927 | square(1); 928 | } 929 | } 930 | circle(r=bevel, $fn=8); 931 | } 932 | } 933 | } 934 | scale([sx - bevel, sy - bevel * 2]) { 935 | translate([-0.5, -0.5]) { 936 | square(1); 937 | } 938 | } 939 | } 940 | } 941 | } 942 | } 943 | 944 | 945 | module master_base (key, xu=1, yu=1, name="") { 946 | nxu = calc_xu(xu, name); 947 | nyu = calc_yu(yu, name); 948 | union () { 949 | difference() { 950 | bottom_plate(key, xu=nxu, yu=nyu); 951 | registration_cube(xu=nxu, yu=nyu, offset=regst_offset); 952 | } 953 | difference() { 954 | base(key, xu=nxu, yu=nyu, name=name); 955 | indent(key, xu=nxu, yu=nyu, name=name); 956 | } 957 | sprues_base(key, regst_height + overlap, xu=nxu, yu=nyu, name=name, $fn=48); 958 | } 959 | } 960 | 961 | 962 | module mx_master_base (xu=1, yu=1, name="") { 963 | color("LightSteelBlue") { 964 | union () { 965 | master_base(mx_al_tp_key, xu=xu, yu=yu, name=name); 966 | translate([0, 0, -indent_depth - overlap]) { 967 | stem_copy(xu=xu, yu=yu, name=name) { 968 | mx_cross(mx_crux_pos, mx_height_pos + indent_depth + overlap); 969 | } 970 | } 971 | } 972 | } 973 | } 974 | 975 | 976 | module al_master_base (xu=1, yu=1, name="") { 977 | color("LightSteelBlue") { 978 | union () { 979 | master_base(mx_al_tp_key, xu=xu, yu=yu, name=name); 980 | translate([0, 0, -indent_depth - overlap]) { 981 | stem_copy(xu=xu, yu=yu, name=name, stabilizers=false) { 982 | al_switch_stem(al_height + indent_depth + overlap); 983 | } 984 | stem_copy(xu=xu, yu=yu, name=name, switches=false) { 985 | mx_cross(mx_crux_pos, mx_height_pos + indent_depth + overlap); 986 | } 987 | } 988 | } 989 | } 990 | } 991 | 992 | 993 | module tp_master_base (xu=1, yu=1, name="") { 994 | color("LightSteelBlue") { 995 | difference () { 996 | union () { 997 | master_base(mx_al_tp_key, xu=xu, yu=yu, name=name); 998 | stem_copy(xu=xu, yu=yu, name=name, stabilizers=false) { 999 | tp_switch_stem_pos(-indent_depth - overlap); 1000 | } 1001 | } 1002 | stem_copy(xu=xu, yu=yu, name=name, stabilizers=false) { 1003 | tp_switch_stem_neg(); 1004 | } 1005 | } 1006 | } 1007 | } 1008 | 1009 | 1010 | module lp_master_base (xu=1, yu=1, name="") { 1011 | color("LightSteelBlue") { 1012 | difference() { 1013 | union () { 1014 | master_base(lp_key, xu=xu, yu=yu, name=name); 1015 | translate([0, 0, -indent_depth - overlap]) { 1016 | stem_copy(xu=xu, yu=yu, name=name, stabilizers=false) { 1017 | lp_switch_stem_pos(indent_depth + overlap); 1018 | } 1019 | stem_copy(xu=xu, yu=yu, name=name, switches=false) { 1020 | mx_cross(mx_crux_pos, indent_depth + overlap); 1021 | } 1022 | } 1023 | } 1024 | stem_copy(xu=xu, yu=yu, name=name, stabilizers=false) { 1025 | lp_switch_stem_neg(); 1026 | } 1027 | } 1028 | } 1029 | } 1030 | 1031 | 1032 | module sculpt_base (key, xu=1, yu=1, name="") { 1033 | nxu = calc_xu(xu, name); 1034 | nyu = calc_yu(yu, name); 1035 | union () { 1036 | difference() { 1037 | bottom_plate(xu=nxu, yu=nyu); 1038 | registration_cube(xu=nxu, yu=nyu, offset=regst_offset); 1039 | } 1040 | sprues_base(key, regst_height + overlap, xu=xu, yu=yu, name=name, $fn=48); 1041 | base(key, xu=xu, yu=yu, name=name); 1042 | key_sculpt(key, xu=xu, yu=yu, name=name); 1043 | } 1044 | } 1045 | 1046 | 1047 | module mx_sculpt_base (xu=1, yu=1, name="") { 1048 | key = mx_al_tp_key; 1049 | color("SteelBlue") { 1050 | difference() { 1051 | sculpt_base(key, xu=xu, yu=yu, name=name); 1052 | translate([0, 0, -key_sculpt_inset_z]) { 1053 | stem_copy(xu=xu, yu=yu, name=name) { 1054 | cylinder(h=attr(key, "cavity_sz") + overlap, d=mx_tp_diameter_neg, $fn=48); 1055 | } 1056 | } 1057 | } 1058 | } 1059 | } 1060 | 1061 | 1062 | module al_sculpt_base (xu=1, yu=1, name="") { 1063 | key = mx_al_tp_key; 1064 | color("SteelBlue") { 1065 | difference() { 1066 | sculpt_base(key, xu=xu, yu=yu, name=name); 1067 | translate([0, 0, -key_sculpt_inset_z]) { 1068 | stem_copy(xu=xu, yu=yu, name=name, stabilizers=false) { 1069 | linear_extrude(attr(key, "cavity_sz") + overlap) { 1070 | al_inner_2d(); 1071 | } 1072 | } 1073 | stem_copy(xu=xu, yu=yu, name=name, switches=false) { 1074 | cylinder(h=attr(key, "cavity_sz") + overlap, d=mx_tp_diameter_neg, $fn=48); 1075 | } 1076 | } 1077 | } 1078 | } 1079 | } 1080 | 1081 | 1082 | module tp_sculpt_base (xu=1, yu=1, name="") { 1083 | key = mx_al_tp_key; 1084 | ext = tp_keycap_stem_ext + stem_clearance_z; 1085 | h = attr(key, "cavity_sz") + ext; 1086 | color("SteelBlue") { 1087 | difference() { 1088 | sculpt_base(key, xu=xu, yu=yu, name=name); 1089 | translate([0, 0, -ext]) { 1090 | stem_copy(xu=xu, yu=yu, name=name) { 1091 | cylinder(h=h + overlap, d=mx_tp_diameter_neg, $fn=48); 1092 | } 1093 | } 1094 | } 1095 | } 1096 | } 1097 | 1098 | 1099 | module lp_sculpt_base (xu=1, yu=1, name="") { 1100 | key = lp_key; 1101 | color("SteelBlue") { 1102 | difference() { 1103 | sculpt_base(key, xu=xu, yu=yu, name=name); 1104 | translate([0, 0, -key_sculpt_inset_z]) { 1105 | stem_copy(xu=xu, yu=yu, name=name, stabilizers=false) { 1106 | linear_extrude(attr(key, "cavity_sz") + overlap) { 1107 | lp_switch_neg_2d(); 1108 | } 1109 | } 1110 | stem_copy(xu=xu, yu=yu, name=name, switches=false) { 1111 | cylinder(h=attr(key, "cavity_sz") + overlap, d=mx_tp_diameter_neg, $fn=48); 1112 | } 1113 | } 1114 | } 1115 | } 1116 | } 1117 | 1118 | 1119 | module stem_cavity_positive (key, xu=1, yu=1, fdm=false) { 1120 | union() { 1121 | top_plate(key, xu=xu, yu=yu, fdm=fdm); 1122 | registration_cube(xu=xu, yu=yu); 1123 | } 1124 | } 1125 | 1126 | 1127 | module stem_cavity_negative (key, xu=1, yu=1) { 1128 | union() { 1129 | base(key, xu=xu, yu=yu); 1130 | key_cavity(key, xu=xu, yu=yu); 1131 | } 1132 | } 1133 | 1134 | 1135 | module stem_cavity (key, xu=1, yu=1, name="", fdm=false) { 1136 | if (name == "iso-enter") { 1137 | difference() { 1138 | stem_cavity_positive(key, xu=1.5, yu=2, fdm=fdm); 1139 | union() { 1140 | translate([0, 0.5 * unit_u, 0]) { 1141 | stem_cavity_negative(key, xu=1.5); 1142 | } 1143 | translate([0.125 * unit_u, 0, 0]) { 1144 | stem_cavity_negative(key, xu=1.25, yu=2); 1145 | } 1146 | } 1147 | } 1148 | } else if (name == "big-ass-enter") { 1149 | difference() { 1150 | stem_cavity_positive(key, xu=2.25, yu=2, fdm=fdm); 1151 | union() { 1152 | translate([0.375 * unit_u, 0, 0]) { 1153 | stem_cavity_negative(key, xu=1.5, yu=2); 1154 | } 1155 | translate([0, -0.5 * unit_u, 0]) { 1156 | stem_cavity_negative(key, xu=2.25, yu=1); 1157 | } 1158 | } 1159 | } 1160 | } else { 1161 | difference() { 1162 | stem_cavity_positive(key, xu=xu, yu=yu, fdm=fdm); 1163 | stem_cavity_negative(key, xu=xu, yu=yu); 1164 | } 1165 | } 1166 | } 1167 | 1168 | 1169 | module mx_stem_cavity (xu=1, yu=1, name="", fdm=false) { 1170 | key = mx_al_tp_key; 1171 | union () { 1172 | color("CornflowerBlue") { 1173 | union() { 1174 | difference() { 1175 | union() { 1176 | stem_cavity(key, xu=xu, yu=yu, name=name, fdm=fdm); 1177 | sprues_base(key, sprue_height, xu=xu, yu=yu, name=name, $fn=48); 1178 | stem_copy(xu=xu, name=name, yu=yu) { 1179 | mx_stem(attr(key, "cavity_sz")); 1180 | } 1181 | } 1182 | stem_copy(xu=xu, yu=yu, name=name) { 1183 | mx_cross(mx_crux_neg, attr(key, "cavity_sz")); 1184 | } 1185 | } 1186 | stem_copy(xu=xu, yu=yu, name=name) { 1187 | mx_sprues_stem(sprue_height); 1188 | } 1189 | } 1190 | } 1191 | } 1192 | } 1193 | 1194 | 1195 | module al_stem_cavity (xu=1, yu=1, name="", fdm=false) { 1196 | key = mx_al_tp_key; 1197 | color("CornflowerBlue") { 1198 | difference() { 1199 | union() { 1200 | stem_cavity(key, xu=xu, yu=yu, name=name, fdm=fdm); 1201 | sprues_base(key, sprue_height, xu=xu, yu=yu, name=name, $fn=48); 1202 | stem_copy(xu=xu, yu=yu, name=name, stabilizers=false) { 1203 | al_sprues_stem(sprue_height); 1204 | al_stem(attr(key, "cavity_sz")); 1205 | } 1206 | stem_copy(xu=xu, yu=yu, name=name, switches=false) { 1207 | mx_sprues_stem(sprue_height); 1208 | mx_stem(attr(key, "cavity_sz")); 1209 | } 1210 | } 1211 | stem_copy(xu=xu, yu=yu, name=name, switches=false) { 1212 | mx_cross(mx_crux_neg, attr(key, "cavity_sz")); 1213 | } 1214 | } 1215 | } 1216 | } 1217 | 1218 | 1219 | module tp_stem_cavity (xu=1, yu=1, name="", fdm=false) { 1220 | key = mx_al_tp_key; 1221 | union () { 1222 | color("CornflowerBlue") { 1223 | union() { 1224 | difference() { 1225 | union() { 1226 | stem_cavity(key, xu=xu, yu=yu, name=name, fdm=fdm); 1227 | sprues_base(key, sprue_height, xu=xu, yu=yu, name=name, $fn=48); 1228 | stem_copy(xu=xu, name=name, yu=yu, stabilizers=false) { 1229 | tp_keycap_stem_pos( 1230 | attr(key, "cavity_sz"), ext=tp_keycap_stem_ext); 1231 | } 1232 | } 1233 | stem_copy(xu=xu, yu=yu, name=name, stabilizers=false) { 1234 | tp_keycap_stem_neg( 1235 | attr(key, "cavity_sz"), ext=tp_keycap_stem_ext); 1236 | } 1237 | } 1238 | stem_copy(xu=xu, yu=yu, name=name, stabilizers=false) { 1239 | tp_sprues_stem(sprue_height, ext=tp_keycap_stem_ext); 1240 | } 1241 | } 1242 | } 1243 | } 1244 | } 1245 | 1246 | 1247 | module lp_stem_cavity (xu=1, yu=1, name="", fdm=false) { 1248 | key = lp_key; 1249 | color("CornflowerBlue") { 1250 | difference() { 1251 | union() { 1252 | stem_cavity(key, xu=xu, yu=yu, name=name, fdm=fdm); 1253 | sprues_base(key, sprue_height, xu=xu, yu=yu, name=name, $fn=48); 1254 | stem_copy(xu=xu, name=name, yu=yu, stabilizers=false) { 1255 | lp_stem(attr(key, "cavity_sz")); 1256 | lp_sprues_stem(sprue_height); 1257 | } 1258 | stem_copy(xu=xu, yu=yu, name=name, switches=false) { 1259 | mx_sprues_stem(sprue_height); 1260 | mx_stem(attr(key, "cavity_sz")); 1261 | } 1262 | } 1263 | stem_copy(xu=xu, yu=yu, name=name, switches=false) { 1264 | mx_cross(mx_crux_neg, attr(key, "cavity_sz")); 1265 | } 1266 | } 1267 | } 1268 | } 1269 | 1270 | 1271 | module sprues_only_base (key, xu=1, yu=1, name="") { 1272 | plate_x = calc_plate_size(xu); 1273 | plate_y = calc_plate_size(yu); 1274 | base_x = calc_base_size(attr(key, "base_sx"), xu); 1275 | base_y = calc_base_size(attr(key, "base_sy"), yu); 1276 | 1277 | translate([0, 0, -sprue_height]) { 1278 | linear_extrude(sprue_plate_height) { 1279 | union() { 1280 | difference() { 1281 | bevel_corner_square( 1282 | plate_x, plate_y, 1283 | regst_radius, 25); 1284 | bevel_corner_square( 1285 | plate_x - sprue_plate_width * 2, 1286 | plate_y - sprue_plate_width * 2, 1287 | regst_radius - sprue_plate_width, 25); 1288 | } 1289 | } 1290 | stem_copy(xu=xu, yu=yu, name=name) { 1291 | circle(d=mx_tp_diameter_pos, $fn=48); 1292 | } 1293 | if (name != "" || yu > xu) { 1294 | rotate([0, 0, 90]) { 1295 | translate([-base_y / 2, 0, 0]) { 1296 | sprue_copy(base_y, include_last=true) { 1297 | square([sprue_plate_width, plate_x], center=true); 1298 | } 1299 | } 1300 | } 1301 | } else { 1302 | translate([-base_x / 2, 0, 0]) { 1303 | sprue_copy(base_x, include_last=true) { 1304 | square([sprue_plate_width, plate_y], center=true); 1305 | } 1306 | } 1307 | } 1308 | } 1309 | } 1310 | } 1311 | 1312 | 1313 | module mx_sprues_only (xu=1, yu=1, name="") { 1314 | key = mx_al_tp_key; 1315 | nxu = calc_xu(xu, name); 1316 | nyu = calc_yu(yu, name); 1317 | 1318 | color("SkyBlue") { 1319 | union() { 1320 | sprues_base(key, sprue_height, xu=nxu, yu=nyu, name=name, $fn=48); 1321 | stem_copy(xu=xu, yu=yu, name=name) { 1322 | mx_sprues_stem(sprue_height); 1323 | } 1324 | sprues_only_base(key, xu=nxu, yu=nyu, name=name); 1325 | } 1326 | } 1327 | } 1328 | 1329 | 1330 | module al_sprues_only (xu=1, yu=1, name="") { 1331 | key = mx_al_tp_key; 1332 | nxu = calc_xu(xu, name); 1333 | nyu = calc_yu(yu, name); 1334 | 1335 | color("SkyBlue") { 1336 | union() { 1337 | sprues_base(key, sprue_height, xu=nxu, yu=nyu, name=name, $fn=48); 1338 | stem_copy(xu=xu, yu=yu, name=name) { 1339 | al_sprues_stem(sprue_height, $fn=48); 1340 | } 1341 | sprues_only_base(key, xu=nxu, yu=nyu, name=name); 1342 | } 1343 | } 1344 | } 1345 | 1346 | 1347 | module tp_sprues_only (xu=1, yu=1, name="") { 1348 | key = mx_al_tp_key; 1349 | nxu = calc_xu(xu, name); 1350 | nyu = calc_yu(yu, name); 1351 | 1352 | color("SkyBlue") { 1353 | union() { 1354 | sprues_base(key, sprue_height, xu=nxu, yu=nyu, name=name, $fn=48); 1355 | stem_copy(xu=xu, yu=yu, name=name) { 1356 | tp_sprues_stem(sprue_height, ext=tp_keycap_stem_ext); 1357 | } 1358 | sprues_only_base(key, xu=nxu, yu=nyu, name=name); 1359 | } 1360 | } 1361 | } 1362 | 1363 | 1364 | module lp_sprues_only (xu=1, yu=1, name="") { 1365 | key = lp_key; 1366 | nxu = calc_xu(xu, name); 1367 | nyu = calc_yu(yu, name); 1368 | 1369 | color("SkyBlue") { 1370 | union() { 1371 | sprues_base(key, sprue_height, xu=nxu, yu=nyu, name=name, $fn=48); 1372 | stem_copy(xu=xu, yu=yu, name=name) { 1373 | lp_sprues_stem(sprue_height, $fn=48); 1374 | } 1375 | sprues_only_base(key, xu=nxu, yu=nyu, name=name); 1376 | } 1377 | } 1378 | } 1379 | 1380 | 1381 | module container (yu=1, name="", yn=2, xs=0) { 1382 | plate_x = calc_plate_size(1); 1383 | plate_y = calc_plate_size(calc_yu(yu=yu, name=name)); 1384 | 1385 | wxy = container_wall; 1386 | wz = container_base; 1387 | gxy = container_gap_edge; 1388 | 1389 | cxy = 0.5; // XY Chamfer 1390 | cz = 1; // Z Chamfer 1391 | 1392 | dy = plate_y + wxy; 1393 | ox = plate_x + wxy * 2; 1394 | oy = dy * yn + wxy; 1395 | oz = container_height + wz; 1396 | 1397 | module chamfer () { 1398 | polyhedron( 1399 | points=[ 1400 | [0, 0, -cz], 1401 | [-cxy, 0, 0], 1402 | [0, cxy, 0], 1403 | [cxy, 0, 0], 1404 | [0, -cxy, 0] 1405 | ], 1406 | faces=[ 1407 | [1, 2, 3, 4], 1408 | [0, 1, 4], 1409 | [0, 2, 1], 1410 | [0, 3, 2], 1411 | [0, 4, 3] 1412 | ] 1413 | ); 1414 | } 1415 | 1416 | module outer () { 1417 | x = (xs ? unit_lego_stud * (xs + 2) : ox); 1418 | minkowski() { 1419 | translate([0, 0, cz]) { 1420 | scale([x - 2 * cxy, oy - 2 * cxy, oz - cz]) { 1421 | translate([-0.5, -0.5, 0]) { 1422 | cube(1); 1423 | } 1424 | } 1425 | } 1426 | chamfer(); 1427 | } 1428 | } 1429 | 1430 | module internal () { 1431 | x = (xs ? unit_lego_stud * (xs + 2) : plate_x + container_inset); 1432 | for (i = [0 : yn - 1]) { 1433 | translate([0, dy * (i - (yn - 1) / 2), wz]) { 1434 | centered_cube([ 1435 | x, 1436 | plate_y + container_inset, 1437 | oz 1438 | ]); 1439 | } 1440 | } 1441 | } 1442 | 1443 | module connection () { 1444 | centered_cube([container_overlap + gxy, dy / 2 + gxy, oz + overlap * 2]); 1445 | hull () { 1446 | centered_cube([container_overlap + gxy, dy / 2 + gxy, overlap + cz + gxy / 2]); 1447 | centered_cube([container_overlap + 2 * cxy + gxy, dy / 2 + 2 * cxy + gxy, overlap]); 1448 | } 1449 | } 1450 | 1451 | 1452 | module connections () { 1453 | translate([container_overlap / 2 - gxy / 2, 0, -overlap]) { 1454 | scale([ox, oy + overlap * 2, oz + overlap * 2]) { 1455 | translate([0, -0.5, 0]) { 1456 | cube(1); 1457 | } 1458 | } 1459 | } 1460 | for (i = [0 : yn]) { 1461 | translate([0, dy * (i - (yn - 0.5) / 2), -overlap]) { 1462 | connection(); 1463 | translate([container_overlap, -dy / 2, 0]) { 1464 | connection(); 1465 | } 1466 | } 1467 | } 1468 | } 1469 | 1470 | module clip() { 1471 | bevel = 0.25; 1472 | touching_height = 2; 1473 | thin = 0.001; 1474 | 1475 | width = container_clip_width; 1476 | ey = container_clip_extension; 1477 | eyr = ey - (container_clip_inset + bevel); 1478 | a = 30; 1479 | hi = (container_clip_inset / 2 + bevel) / cos(a); 1480 | 1481 | yi = -0.5 * ey; 1482 | yo = yi + eyr; 1483 | xo = width + 2 * (yo * sin(a) - hi); 1484 | xi = width + 2 * (yi * sin(a) - hi); 1485 | 1486 | xm = 1; 1487 | sy = sqrt(pow(ey, 2) + pow((xo - xm) / 2, 2)); 1488 | 1489 | minkowski() { 1490 | sphere(r=bevel, $fn=12); 1491 | hull() { 1492 | translate([0, yo, 0]) { 1493 | scale([xo, thin, touching_height]) { 1494 | translate([-0.5, -1, -0.5]) { 1495 | cube([1, 1, 1]); 1496 | } 1497 | } 1498 | } 1499 | translate([0, yi, 0]) { 1500 | scale([xm, thin, touching_height / 2 + sy]) { 1501 | translate([-0.5, 0, -1]) { 1502 | cube([1, 1, 1]); 1503 | } 1504 | } 1505 | } 1506 | translate([0, yi, 0]) { 1507 | scale([xi, thin, touching_height]) { 1508 | translate([-0.5, 0, -0.5]) { 1509 | cube([1, 1, 1]); 1510 | } 1511 | } 1512 | } 1513 | } 1514 | } 1515 | } 1516 | 1517 | module copy_clip_heights() { 1518 | for (z = [8, 28]) { 1519 | translate([0, 0, z]) { 1520 | children(); 1521 | } 1522 | } 1523 | } 1524 | 1525 | module clips() { 1526 | width = container_clip_width; 1527 | dx = ox / 2 + container_clip_extension / 2; 1528 | dy = oy / 2 + container_clip_extension / 2; 1529 | 1530 | copy_clip_heights () { 1531 | if (xs) { 1532 | for (x = [0 : xs - 1]) { 1533 | rotate_z_copy(180) { 1534 | translate([width * (x * 2 - xs + .5), dy, 0]) { 1535 | clip(); 1536 | } 1537 | } 1538 | } 1539 | } else { 1540 | qx = ox / (2 * width); 1541 | qy = oy / (2 * width); 1542 | nx = floor(qx / 2); 1543 | ny = 2 * floor(qy / 2); 1544 | for (x = [0 : nx - 1]) { 1545 | rotate([0, 0, 180]) { 1546 | translate([width * (x * 2 + 0.5), dy, 0]) { 1547 | clip(); 1548 | } 1549 | } 1550 | translate([width * (x * 2 + 0.5 - 2 * nx), dy, 0]) { 1551 | clip(); 1552 | } 1553 | } 1554 | for (y = [0 : ny - 1]) { 1555 | translate([-dx, width * (y * 2 - ny + 0.5), 0]) { 1556 | rotate([0, 0, 90]) { 1557 | clip(); 1558 | } 1559 | } 1560 | } 1561 | } 1562 | } 1563 | } 1564 | 1565 | color("khaki") { 1566 | difference() { 1567 | outer(); 1568 | union () { 1569 | internal(); 1570 | if (xs) { 1571 | rotate_z_copy(180) { 1572 | translate([unit_lego_stud * xs / 2, 0, 0]) { 1573 | connections(); 1574 | } 1575 | } 1576 | } else { 1577 | connections(); 1578 | } 1579 | } 1580 | } 1581 | if (container_clips) { 1582 | clips(); 1583 | } 1584 | } 1585 | } 1586 | 1587 | 1588 | 1589 | module container_tesselate (xu=1, yu=1, xn=2, ext=0) { 1590 | xp = calc_plate_size(xu); 1591 | yp = calc_plate_size(yu); 1592 | ey = unit_lego_stud * ext / 2; 1593 | 1594 | rotate_z_copy(180) { 1595 | translate([0, ey, 0]) { 1596 | container(xu=xu, yu=yu, xn=xn); 1597 | } 1598 | 1599 | translate([0, ey + yp + container_wall * 2 + container_clip_extension, 0]) { 1600 | rotate([0, 0, 180]) { 1601 | container(xu=xu, yu=yu, xn=xn); 1602 | } 1603 | } 1604 | 1605 | translate([xp + yp / 2 + container_wall * 2.5 + container_clip_extension, ey, 0]) { 1606 | rotate([0, 0, 90]) { 1607 | container(xu=xu, yu=yu, xn=xn); 1608 | } 1609 | } 1610 | 1611 | } 1612 | } 1613 | --------------------------------------------------------------------------------