851 lines
31 KiB
OpenSCAD
851 lines
31 KiB
OpenSCAD
/*
|
|
////////////////////////////////////////////////////
|
|
See my other designs:
|
|
https://www.thingiverse.com/steeveeet/designs
|
|
////////////////////////////////////////////////////
|
|
|
|
This file is designed to be entirely customisable.
|
|
|
|
The easiest way to use this is to copy the top sections (Basic and Advanced Customisation) into a new file
|
|
and add `include <U_Box.scad>;` at the top of that file. That will bring all the functionality of this file into a new file
|
|
so that you can define the box specific value for it.
|
|
|
|
There are 3 main sections to this file:
|
|
|
|
*** Basic Customisation ***
|
|
Top section defines the variables for the box -
|
|
these are all exposed through the Customizer UI.
|
|
The last section (display settings) is used to control what is displayed
|
|
(and thus exported to STL when the time comes)
|
|
|
|
|
|
*** Advanced Customisation ***
|
|
The configuratino fo the end panels are here.
|
|
These modules define the shapes to use to cut out holes from the front and back panels,
|
|
and to add (text) decoration to the front and back panels
|
|
|
|
In the PCB mount locations you'll find an array of point constructed
|
|
from the rectangle defined by the PCB Width and Height defined in the UI.
|
|
If you wish to ovverride this (if you need more than 4 mounting points,
|
|
or they are not in a rectangle) simply construct that array in directly and the model will update
|
|
|
|
|
|
*** Implementation ***
|
|
This section contains the modules and functions that are used to actually construict the parts of the box
|
|
It's unlikely you will need to change this, though I'd be very interested if you find that you do!
|
|
|
|
////////////////////////////////////////////////////
|
|
*/
|
|
|
|
/********************************************************************
|
|
*** Basic Customisation ***
|
|
*********************************************************************/
|
|
|
|
/* [Case Settings] */
|
|
Length = 80;
|
|
Width = 60;
|
|
Height = 30;
|
|
WallThickness = 2; // [1:5]
|
|
|
|
FilletRadius = 2; // [0.1:12]
|
|
// Decorations or ventilation holes
|
|
VentType = "VentHoles"; // [None, Decorations, VentHoles:Ventilation Holes]
|
|
// Width of vent/decoration holes
|
|
VentWidth = 1.5;
|
|
|
|
/* [Fastening Options] */
|
|
// Methods of securing the closure of the box
|
|
FasteningType = "Push"; // [Screw:Screw Fit, Push:Push Fit]
|
|
// Number for fastenings along thge edge
|
|
NumberOfFastenings = 2; // [0:10]
|
|
// Thickness of the fastening tabs
|
|
FasteningTabThickness = 2; // [1:5]
|
|
// Diameter of fastener hole for screw fitting
|
|
FasteningHoleDiameter = 2; // [0.1:5]
|
|
|
|
/* [End Panels] */
|
|
BackPanelType = "IntegratedWithBase"; // [None, IntegratedWithTop:Integrated With Top, IntegratedWithBase:Integrated With Base, Discrete]
|
|
FrontPanelType = "IntegratedWithBase"; // [None, IntegratedWithTop:Integrated With Top, IntegratedWithBase:Integrated With Base, Discrete]
|
|
// Panel Text Type
|
|
EmbossPanelDecorations = 1; // [0:Demboss, 1:Emboss]
|
|
// End Panel Tolerance
|
|
PanelTolerance = 0.9;
|
|
// Use example panel decorations
|
|
examplePanels = 1; // [0:No, 1:Yes]
|
|
|
|
|
|
/* [PCB Mounts] */
|
|
// Show PCB Ghost
|
|
ShowPCB = 1; // [0:No, 1:Yes]
|
|
// Add PCB Mounts
|
|
PCBMounts = 1; // [0:No, 1:Yes]
|
|
// Back left corner X position
|
|
PCBPosX = 10;
|
|
// Back left corner Y position
|
|
PCBPosY = 10;
|
|
// PCB Length
|
|
PCBLength = 40;
|
|
// PCB Width
|
|
PCBWidth = 30;
|
|
// Mount height
|
|
MountHeight = 10;
|
|
// Mount diameter
|
|
MountDia = 8;
|
|
// Hole diameter
|
|
MountHoleDia = 2.5;
|
|
|
|
/* [Display Settings] */
|
|
// Print Layout
|
|
PrintLayout = 0; // [0:No, 1:Yes]
|
|
// Top Case
|
|
ShowTCase = 0; // [0:No, 1:Yes]
|
|
// Bottom Case
|
|
ShowBCase = 1; // [0:No, 1:Yes]
|
|
// Front panel
|
|
ShowFPanel = 1; // [0:No, 1:Yes]
|
|
// Back panel
|
|
ShowBPanel = 1; // [0:No, 1:Yes]
|
|
|
|
/* [Hidden] */
|
|
// Case color
|
|
CaseColour = "Teal";
|
|
// End Panel color
|
|
PanelColour = "Aqua";
|
|
// Fillet Smootheness
|
|
Resolution = 50; // [4:100]
|
|
|
|
|
|
/************************************************************
|
|
*** Advanced Customisation ***
|
|
************************************************************/
|
|
{
|
|
//////////////////////////////////////////
|
|
/////////// Panel Configuration //////////
|
|
//////////////////////////////////////////
|
|
|
|
module FrontPanelCutouts(){
|
|
if (examplePanels)
|
|
{
|
|
SquareHole ([10,5],[10,10],1);
|
|
CylinderHole([40,10],5);
|
|
}
|
|
}
|
|
|
|
module FrontPanelDecoration(){
|
|
if (examplePanels)
|
|
{
|
|
LText([7.5,17.5], "On/Off");
|
|
CText([40,10], 5, "12345");
|
|
}
|
|
}
|
|
|
|
module BackPanelCutouts(){
|
|
if (examplePanels)
|
|
{
|
|
SquareHole ([5,0],[10,10],1);
|
|
CylinderHole([10,10],10);
|
|
}
|
|
}
|
|
|
|
module BackPanelDecoration(){
|
|
if (examplePanels)
|
|
{
|
|
LText([17.5,5], "Power");
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////
|
|
/////////// PCB Mount locations //////////
|
|
//////////////////////////////////////////
|
|
|
|
pcbMountLocations = [[0,0], [PCBWidth,0], [0,PCBLength], [PCBWidth,PCBLength] ];
|
|
|
|
}
|
|
|
|
/************************************************************
|
|
*** Implementation ***
|
|
************************************************************/
|
|
{
|
|
//////////////////////////////
|
|
/////////// Helpers //////////
|
|
//////////////////////////////
|
|
|
|
$fn = Resolution;
|
|
|
|
////////// CONSTANT: Thickness of end panels //////////
|
|
function PanelThickness() = WallThickness;
|
|
|
|
////////// CONSTANT: Helper to define how much space is needed for end panel ribs, depending on type //////////
|
|
function SpaceForEndPanel(type) =
|
|
type == "IntegratedWithTop" ? PanelThickness() :
|
|
type == "IntegratedWithBase" ? PanelThickness() :
|
|
type == "Discrete" ? 2*WallThickness + PanelThickness() + PanelTolerance :
|
|
0; // None
|
|
|
|
////////// CONSTANT: How much of the length of the case will be taken up by support ribs for the end panels //////////
|
|
function SpaceForEndPanels() = 2*max(SpaceForEndPanel(FrontPanelType), SpaceForEndPanel(BackPanelType));
|
|
|
|
////////// CONSTANT: Vector describing exterior dimensions of the case //////////
|
|
function CaseDims() = [Length, Width, Height];
|
|
|
|
////////// CONSTANT: The clear space before fittings for end panels //////////
|
|
function InteriorDims() = [Length - SpaceForEndPanels(), Width - 2*WallThickness, Height - 2*WallThickness];
|
|
|
|
////////// Calculated size of tabs. Default to 8mm, but reduce if a small length or height //////////
|
|
function TabDiameter() =
|
|
min(Height/2, // Don't collide with Vent Holes in reduced height boxes
|
|
min(InteriorDims().x/2, // Don't collide with other tabs in reduced length boxes
|
|
20)); // Sensible max height
|
|
|
|
|
|
//////////////////////////////////////////////////
|
|
/////////// Maths functions & Utilities //////////
|
|
//////////////////////////////////////////////////
|
|
|
|
|
|
////////// Utility to work out how many items you might fit ion a given length //////////
|
|
function numItemsAlongLength(length, itemSpacing) = ceil(length / itemSpacing);
|
|
|
|
// Helper functions to calculate the bounding box of an array of vec2s
|
|
function bbox(v) = [minimum(v), maximum(v)];
|
|
{
|
|
// Return the maximum of all the "elem"th elemtns in a vector of vectors
|
|
function minimumElem(v, elem, i = 0) =
|
|
(i < len(v) - 1) ?
|
|
min(v[i][elem], minimumElem(v, elem, i+1)) :
|
|
v[i][elem];
|
|
|
|
// Return the maximum of all the "elem"th elemtns in a vector of vectors
|
|
function maximumElem(v, elem, i = 0) =
|
|
(i < len(v) - 1) ?
|
|
max(v[i][elem], maximumElem(v, elem, i+1)) :
|
|
v[i][elem];
|
|
|
|
function minimum(v) =
|
|
[minimumElem(v, 0), minimumElem(v, 1)];
|
|
function maximum(v) =
|
|
[maximumElem(v, 0), maximumElem(v, 1)];
|
|
}
|
|
|
|
///// Operator: distribute copies of the children evenly along a vector //////////
|
|
module distribute(start, end, numItems){
|
|
vec = end-start;
|
|
length = norm(vec);
|
|
normVec = vec/length;
|
|
|
|
itemSpacing = length / (2*numItems);
|
|
|
|
// Even number of tabs
|
|
for (i=[0: 1: numItems-1])
|
|
{
|
|
tabLoc = start + (itemSpacing * (1 + 2*i)) * normVec;
|
|
|
|
translate([tabLoc.x, tabLoc.y, tabLoc.z])
|
|
{
|
|
children();
|
|
}
|
|
}
|
|
}
|
|
|
|
/////////// Generic rounded box - Aligned along the X axis //////////
|
|
module RoundBox(dims, fillet = 0, center = false){
|
|
if (fillet > 0){
|
|
// Calculate the correct offset depending on whether the cube should be offset
|
|
|
|
// Split the total height between cubeDims & cylHeight
|
|
cylHeight = dims.x/2;
|
|
// The fillet radius must (at most) be half either Y, or Z, but we cannot have cubeDims Y or Z equal to zero
|
|
cappedFillet = min(min(fillet, dims.y/2.001), dims.z/2.001);
|
|
|
|
cubeDims = [
|
|
dims.x/2,
|
|
dims.y - 2*cappedFillet,
|
|
dims.z - 2*cappedFillet,
|
|
];
|
|
|
|
offset = center ? [0,0,0] : dims/2;
|
|
translate(offset){
|
|
minkowski()
|
|
{
|
|
rotate([0, 90, 0]){
|
|
cylinder(r = cappedFillet, h = cylHeight, center = true);
|
|
}
|
|
cube(cubeDims, center = true);
|
|
}
|
|
}
|
|
} else {
|
|
cube(dims, center = center);
|
|
}
|
|
}// End of RoundBox Module
|
|
|
|
/////////// Generic hollow rounded box - Aligned along the X axis //////////
|
|
module HollowRoundBox(dims, thickness, fillet = 0, center = false){
|
|
offset = center ? [0,0,0] : dims/2;
|
|
translate(offset){
|
|
difference() {
|
|
RoundBox(dims, fillet, true);
|
|
RoundBox([dims.x * 1.1, dims.y - 2*thickness, dims.z - 2*thickness], fillet, true);
|
|
}
|
|
}
|
|
}// End of RoundBox Module
|
|
|
|
////////////////////////////////////////////////////////
|
|
////////////////////// Main Case ///////////////////////
|
|
////////////////////////////////////////////////////////
|
|
|
|
module PanelRibs(panelType, xMultiplier = 1)
|
|
{
|
|
// For discrete panels, define the ribs to hold them in place
|
|
discreteRibDims = [WallThickness, CaseDims().y - 2*WallThickness, CaseDims().z - 2*WallThickness];
|
|
// Effective thickness of the discrete panel, including tolerance
|
|
discretePanelThickness = PanelTolerance + WallThickness;
|
|
// Location for slot for discrete panels
|
|
slotLocation = CaseDims().x/2 - (discreteRibDims.x + discretePanelThickness/2);
|
|
// Offset (from slotLocation) for retaining ribs
|
|
slotOffset = (discreteRibDims.x + discretePanelThickness)/2;
|
|
|
|
// For integrated panels, the rib can be smaller
|
|
integratedRibDims = [WallThickness/2, CaseDims().y - 2*WallThickness, CaseDims().z - 2*WallThickness];
|
|
// For integrated panels, the rib can be smaller
|
|
integratedRibOffset = (WallThickness + PanelTolerance/2) + integratedRibDims.x/2;
|
|
|
|
// Front Panel
|
|
if(panelType == "Discrete")
|
|
{
|
|
// Add ribs for supporting discrete panel
|
|
translate([xMultiplier * (slotLocation + slotOffset), 0, 0]){
|
|
HollowRoundBox(discreteRibDims, WallThickness, FilletRadius, true);
|
|
}
|
|
translate([xMultiplier * (slotLocation - slotOffset), 0, 0]){
|
|
HollowRoundBox(discreteRibDims, WallThickness, FilletRadius, true);
|
|
}
|
|
}
|
|
else if(panelType == "IntegratedWithThis"){
|
|
// Add a supporting rib for the panel integrated with this half
|
|
translate([xMultiplier * (CaseDims().x/2 - WallThickness/2), 0, 0]){
|
|
HollowRoundBox([WallThickness, CaseDims().y - 2*WallThickness, CaseDims().z - 2*WallThickness], PanelTolerance/2, FilletRadius, true);
|
|
}
|
|
}
|
|
else if(panelType == "IntegratedWithOther"){
|
|
// Add a supporting rib for the panel integrated with the other half
|
|
translate([xMultiplier * (CaseDims().x/2 - integratedRibOffset), 0, 0]){
|
|
HollowRoundBox(integratedRibDims, WallThickness/2, FilletRadius, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////// Case //////////////////////////////////
|
|
module Case(dims, thickness, fillet = 0, fPanelType = "None", bPanelType = "None"){
|
|
// Hull with integrated panels (where appropriate)
|
|
union(){
|
|
// Decorated hull
|
|
difference()
|
|
{
|
|
// Main hull
|
|
union() {
|
|
|
|
// Main Shell
|
|
HollowRoundBox(dims, thickness, fillet, true);
|
|
|
|
// Add ribs for supporting the end panels
|
|
PanelRibs(fPanelType, 1);
|
|
PanelRibs(bPanelType, -1);
|
|
}
|
|
// Remove the top
|
|
translate([0, 0, Height / 2]){
|
|
cube([dims.x*1.1, dims.y*1.1, dims.z], true);
|
|
}
|
|
|
|
// Add side decoration / vents
|
|
if (VentType != "None")
|
|
{
|
|
union()
|
|
{
|
|
overlap = VentWidth; //make sure that we cleanly overlap the top/bottom/side of the case
|
|
decDims = [
|
|
VentType == "VentHoles" ?
|
|
(thickness + fillet) + overlap :
|
|
thickness/2 + overlap,
|
|
VentWidth,
|
|
(dims.z / 4) + overlap
|
|
];
|
|
padding = dims.x/20; // (half) the gap between sets of vents (and between vents and end)
|
|
ventFillet = VentWidth/3;
|
|
|
|
// Coords for vents
|
|
xStart = padding;
|
|
xEnd = InteriorDims().x/2 - padding;
|
|
yPos = VentType == "VentHoles" ?
|
|
InteriorDims().y/2 + decDims.x/2 - fillet : // Calculated from the inside wall
|
|
CaseDims().y/2 - decDims.x/2 + overlap; // Calculated from the ouside wall
|
|
zPos = -0.5*(dims.z - decDims.z) - overlap;
|
|
|
|
numVents = numItemsAlongLength(xEnd-xStart, VentWidth+thickness);
|
|
|
|
distribute([xStart, yPos, zPos], [xEnd, yPos, zPos], numVents)
|
|
{
|
|
rotate([0, 0, 90])
|
|
RoundBox(decDims, ventFillet, true);
|
|
}
|
|
distribute([-xStart, yPos, zPos], [-xEnd, yPos, zPos], numVents)
|
|
{
|
|
rotate([0, 0, 90])
|
|
RoundBox(decDims, ventFillet, true);
|
|
}
|
|
distribute([xStart, -yPos, zPos], [xEnd, -yPos, zPos], numVents)
|
|
{
|
|
rotate([0, 0, 90])
|
|
RoundBox(decDims, ventFillet, true);
|
|
}
|
|
distribute([-xStart, -yPos, zPos], [-xEnd, -yPos, zPos], numVents)
|
|
{
|
|
rotate([0, 0, 90])
|
|
RoundBox(decDims, ventFillet, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////// Fixing tabs //////////////////////////////////
|
|
module Tab(radius, thickness){
|
|
difference(){
|
|
union(){
|
|
rotate([90, 0, 0])
|
|
{
|
|
cylinder(r = radius, thickness, $fn = 6);
|
|
}
|
|
}
|
|
// Chamfer
|
|
translate([0, -radius/2, -2*radius/3]){
|
|
rotate([45, 0, 0]){
|
|
cube([2*radius, radius, radius], center=true);
|
|
}
|
|
}
|
|
|
|
// Tolerance cut
|
|
tolerance = 0.1;
|
|
tolBox = [2*radius, 2*thickness, radius];
|
|
translate([0, (thickness-tolerance), tolBox.z/2]){
|
|
cube(tolBox, center=true);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////// Case with tabs //////////////////////////////////
|
|
module CaseWithTabs(dims, thickness, fillet = 0, fPanelType = "None", bPanelType = "None"){
|
|
cylRad = TabDiameter()/2;
|
|
holeDia = FasteningHoleDiameter;
|
|
|
|
// Calculate the number of tabs and their location based on the length of the box.
|
|
// Assume a gap of TabDiameter on each side of each tab before spawning a new tab
|
|
numItems = NumberOfFastenings;
|
|
|
|
// For push fitting, offset the registration sphere so it's not a complete hemisphere. Makes it easier to close.
|
|
// Setting this to zero will result in a complete hemisphere
|
|
sphereOffset = 0.125*holeDia;
|
|
|
|
difference(){
|
|
union(){
|
|
Case(dims, thickness, fillet, fPanelType, bPanelType);
|
|
|
|
// Tabs
|
|
distribute(
|
|
[InteriorDims().x/2, InteriorDims().y/2, 0],
|
|
[-InteriorDims().x/2, InteriorDims().y/2, 0],
|
|
numItems)
|
|
{
|
|
Tab(cylRad, FasteningTabThickness);
|
|
}
|
|
|
|
if (FasteningType == "Push"){
|
|
// Add registration spheres
|
|
distribute(
|
|
[InteriorDims().x/2, -InteriorDims().y/2 - sphereOffset, -cylRad/3],
|
|
[-InteriorDims().x/2, -InteriorDims().y/2 - sphereOffset, -cylRad/3],
|
|
numItems)
|
|
{
|
|
difference(){
|
|
sphere(d = holeDia);
|
|
// Ensure the sphere doesn't stick out of the other side of the wall!
|
|
translate([0,-(holeDia - 2.*sphereOffset)/2,0])
|
|
{
|
|
cube(holeDia, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (FasteningType == "Push"){
|
|
// Add cutouts for registration spheres
|
|
distribute(
|
|
[InteriorDims().x/2, InteriorDims().y/2 + sphereOffset, cylRad/3],
|
|
[-InteriorDims().x/2, InteriorDims().y/2 + sphereOffset, cylRad/3],
|
|
numItems
|
|
)
|
|
{
|
|
sphere(d = holeDia);
|
|
}
|
|
}
|
|
else if (FasteningType == "Screw"){
|
|
// Screw fitting so add the case holes
|
|
distribute(
|
|
[InteriorDims().x/2, InteriorDims().y/2, cylRad/3],
|
|
[-InteriorDims().x/2, InteriorDims().y/2, cylRad/3],
|
|
numItems
|
|
)
|
|
{
|
|
rotate([90, 0, 0]){
|
|
cylinder(d = holeDia, 3.0*thickness, center=true);
|
|
}
|
|
}
|
|
distribute(
|
|
[InteriorDims().x/2, -InteriorDims().y/2, -cylRad/3],
|
|
[-InteriorDims().x/2, -InteriorDims().y/2, -cylRad/3],
|
|
numItems)
|
|
{
|
|
rotate([90, 0, 0]){
|
|
cylinder(d = holeDia, 3.0*thickness, center=true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////
|
|
////////////////////// PCB Mounts ///////////////////////
|
|
/////////////////////////////////////////////////////////
|
|
|
|
/////////////////////// PCB Mount /////////////////////////////
|
|
module PCBMount(){
|
|
FilletRadius = 2;
|
|
color(CaseColour)
|
|
union(){
|
|
difference(){
|
|
// Main cylinder
|
|
cylinder(d = MountDia, MountHeight);
|
|
// central hole
|
|
cylinder(d = MountHoleDia, MountHeight*1.01);
|
|
|
|
// Chamfer to help centre screws
|
|
chamferRad = (MountDia-MountHoleDia)/4;
|
|
rotate_extrude(){
|
|
translate([MountHoleDia / 2, (MountHeight-chamferRad)*1.01, 0]){
|
|
difference(){
|
|
square(chamferRad);
|
|
translate([chamferRad,0,0])
|
|
circle(chamferRad, $fn=4);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fillet
|
|
rotate_extrude(){
|
|
translate([MountDia / 2, 0, 0]){
|
|
difference(){
|
|
square(FilletRadius);
|
|
translate([FilletRadius,FilletRadius,0])
|
|
circle(FilletRadius);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
module PCBMounts(){
|
|
if (PCBMounts)
|
|
{
|
|
translate(-InteriorDims()/2 + [PCBPosX, PCBPosY, 0]){
|
|
if (ShowPCB)
|
|
{
|
|
// PCB Ghost
|
|
pcbGhostPadding = 5;
|
|
bbox = bbox(pcbMountLocations);
|
|
w = bbox[1][0] - bbox[0][0];
|
|
l = bbox[1][1] - bbox[0][1];
|
|
translate(-[pcbGhostPadding, pcbGhostPadding, -(MountHeight+1)]){
|
|
% square([l + 2*pcbGhostPadding, w + 2*pcbGhostPadding]);
|
|
}
|
|
}
|
|
|
|
for(footLocation = pcbMountLocations)
|
|
{
|
|
translate([footLocation.y, footLocation.x, 0])
|
|
{
|
|
PCBMount();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////
|
|
////////////////////// End Panels ///////////////////////
|
|
/////////////////////////////////////////////////////////
|
|
|
|
/////////////// Circular hole helper //////////////////////
|
|
// loc = location of centre
|
|
// d = diameter of holes
|
|
module CylinderHole(loc, d){
|
|
translate([loc.x, loc.y, -PanelThickness()*0.1]){
|
|
cylinder(d = d, PanelThickness() * 1.2, $fn = Resolution);
|
|
}
|
|
}
|
|
|
|
/////////////// Rectangular hole helper /////////////////////
|
|
// loc = location of bottom left corner
|
|
// sz = dimensions of box
|
|
// f = radius of fillet
|
|
module SquareHole(loc, sz, f){
|
|
translate([loc.x, loc.y, -PanelThickness()*0.1]){
|
|
rotate([-90, -90, 0])
|
|
{
|
|
RoundBox([PanelThickness() * 1.2, sz.x, sz.y], f);
|
|
}
|
|
}
|
|
}
|
|
|
|
/////////////// Text helper /////////////////////
|
|
// loc = location of bottom left corner
|
|
// content = text to print
|
|
// sz = size of text
|
|
// ft = font to use
|
|
module LText(loc, content, sz = min(Width,Height)/10, ft = "Arial Black"){
|
|
translate([loc.x, loc.y, PanelThickness()]){
|
|
linear_extrude(height = 1, center = true){
|
|
text(content, size = sz, font = ft);
|
|
}
|
|
}
|
|
}
|
|
|
|
/////////////// Arc Text helper /////////////////////
|
|
// loc = location of the centre of the arc
|
|
// content = text to print
|
|
// rad = radius of arc
|
|
// a0 = Start angle (default 0 - west)
|
|
// a1 = End angle (default 180 - east)
|
|
// sz = size of text
|
|
// ft = font to use
|
|
/////////////////////////////////////////////////////
|
|
module CText(loc, r, content, a0=0, a1=180, sz = min(Width,Height)/10, ft = "Arial Black"){
|
|
angleStep = (a1-a0) / (len(content)-1);
|
|
translate([loc.x, loc.y, PanelThickness()])
|
|
for (i = [0: len(content) - 1] ) {
|
|
rotate([0, 0, 90 - (i * angleStep)])
|
|
translate([0, r, 0]) {
|
|
linear_extrude(height = 1, center = true){
|
|
text(content[i], font = ft, size = sz, valign = "baseline", halign = "center");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////// End Panel //////////////////////
|
|
module Panel(){
|
|
panelDims = [PanelThickness(), InteriorDims().y - PanelTolerance, InteriorDims().z - PanelTolerance];
|
|
cutout = true;
|
|
difference(){
|
|
color(PanelColour)
|
|
RoundBox(panelDims, FilletRadius, true);
|
|
|
|
// Translate so that bottom left corner of panel is "origin" for controls
|
|
translate(-panelDims/2){
|
|
rotate([90, 0, 90]){
|
|
children(0);
|
|
}
|
|
}
|
|
if (!EmbossPanelDecorations){
|
|
color(CaseColour){
|
|
translate(-panelDims/2){
|
|
rotate([90, 0, 90]){
|
|
children(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (EmbossPanelDecorations){
|
|
color(CaseColour){
|
|
translate(-panelDims/2){
|
|
rotate([90, 0, 90]){
|
|
children(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////
|
|
////////////////////// Main Scene Build /////////////////////////////////
|
|
/////////////////////////////////////////////////////////////////////////
|
|
|
|
printLayoutPadding = 10;
|
|
|
|
// Utility function to establish whether the panel is integated with This half or the Other
|
|
function panelType(type, this, other) =
|
|
type == this ? "IntegratedWithThis" :
|
|
type == other ? "IntegratedWithOther" :
|
|
type;
|
|
|
|
// Utility to work out the offset from the edge of the case depending on panel type.
|
|
function panelOffset(type) =
|
|
type == "Discrete" ?
|
|
(WallThickness + PanelThickness()/2 + PanelTolerance/2) : // Discrete
|
|
PanelThickness()/2; // Integrated (or None, in which case the translate doesn't matter)
|
|
|
|
///////////////////////// Calculate the transforms for each part, depening on whether we're visualising or printing /////////////////////////
|
|
BaseTranslate = !PrintLayout ?
|
|
CaseDims() / 2:
|
|
CaseDims() / 2;
|
|
BaseRotate = !PrintLayout ?
|
|
[0,0,0]:
|
|
[0,0,0];
|
|
|
|
|
|
TopTranslate = !PrintLayout ?
|
|
[CaseDims().x/2, CaseDims().y/2, CaseDims().z/1.95] :
|
|
[CaseDims().x/2, CaseDims().y*1.5 + printLayoutPadding, CaseDims().z/2];
|
|
TopRotate = !PrintLayout ?
|
|
[180, 0, 0] :
|
|
[0,0,0];
|
|
|
|
|
|
FPanelTranslate = (!PrintLayout || FrontPanelType != "Discrete") ?
|
|
FrontPanelType == "IntegratedWithTop" ?
|
|
TopTranslate + [CaseDims().x/2 - panelOffset(FrontPanelType) - 0.01, 0, 0] :
|
|
BaseTranslate + [CaseDims().x/2 - panelOffset(FrontPanelType) - 0.01, 0, 0] :
|
|
[CaseDims().x + CaseDims().z/2 + printLayoutPadding, CaseDims().y/2, PanelThickness()/2];
|
|
FPanelRotate = (!PrintLayout || FrontPanelType != "Discrete") ?
|
|
FrontPanelType == "IntegratedWithTop" ?
|
|
TopRotate + [180,0,0]:
|
|
BaseRotate + [0,0,0]:
|
|
[0,-90,0];
|
|
|
|
|
|
BPanelTranslate = (!PrintLayout || BackPanelType != "Discrete") ?
|
|
BackPanelType == "IntegratedWithTop" ?
|
|
TopTranslate + [-(CaseDims().x/2 - panelOffset(BackPanelType) - 0.01), 0, 0] :
|
|
BaseTranslate + [-(CaseDims().x/2 - panelOffset(BackPanelType) - 0.01), 0, 0] :
|
|
[CaseDims().x + CaseDims().z/2 + printLayoutPadding, CaseDims().y*1.5 + printLayoutPadding, PanelThickness()/2];
|
|
BPanelRotate = (!PrintLayout || BackPanelType != "Discrete") ?
|
|
BackPanelType == "IntegratedWithTop" ?
|
|
TopRotate + [180,0,180]:
|
|
BaseRotate + [0,0,180]:
|
|
[0,-90,0];
|
|
|
|
|
|
PCBTranslate = !PrintLayout ? CaseDims() / 2 : [0,0,0];
|
|
PCBRotate = !PrintLayout ? [0,0,0]: [0,0,0];
|
|
|
|
if (ShowTCase)
|
|
{
|
|
union()
|
|
{
|
|
translate(TopTranslate){
|
|
rotate(TopRotate){
|
|
color(CaseColour){
|
|
CaseWithTabs(CaseDims(), WallThickness, FilletRadius,
|
|
fPanelType = panelType(FrontPanelType, "IntegratedWithTop", "IntegratedWithBase"),
|
|
bPanelType = panelType(BackPanelType, "IntegratedWithTop", "IntegratedWithBase"));
|
|
}
|
|
}
|
|
}
|
|
// TODO: translated (but don't rotate) the panel with the case - also works for print mode
|
|
// means we have to have the panel translation in local coords (at least not for discrete mode)
|
|
if (FrontPanelType == "IntegratedWithTop")
|
|
{
|
|
translate(FPanelTranslate) {
|
|
rotate(FPanelRotate){
|
|
Panel(){
|
|
FrontPanelCutouts();
|
|
FrontPanelDecoration();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (BackPanelType == "IntegratedWithTop")
|
|
{
|
|
translate(BPanelTranslate) {
|
|
rotate(BPanelRotate){
|
|
Panel(){
|
|
BackPanelCutouts();
|
|
BackPanelDecoration();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ShowBCase) {
|
|
union()
|
|
{
|
|
translate(BaseTranslate){
|
|
rotate(BaseRotate){
|
|
color(CaseColour){
|
|
CaseWithTabs(CaseDims(), WallThickness, FilletRadius,
|
|
fPanelType = panelType(FrontPanelType, "IntegratedWithBase", "IntegratedWithTop"),
|
|
bPanelType = panelType(BackPanelType, "IntegratedWithBase", "IntegratedWithTop"));
|
|
}
|
|
PCBMounts();
|
|
|
|
}
|
|
}
|
|
if (FrontPanelType == "IntegratedWithBase")
|
|
{
|
|
translate(FPanelTranslate) {
|
|
rotate(FPanelRotate){
|
|
Panel(){
|
|
FrontPanelCutouts();
|
|
FrontPanelDecoration();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (BackPanelType == "IntegratedWithBase")
|
|
{
|
|
translate(BPanelTranslate) {
|
|
rotate(BPanelRotate){
|
|
Panel(){
|
|
BackPanelCutouts();
|
|
BackPanelDecoration();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (FrontPanelType == "Discrete" && ShowFPanel)
|
|
{
|
|
translate(FPanelTranslate) {
|
|
rotate(FPanelRotate){
|
|
Panel(){
|
|
FrontPanelCutouts();
|
|
FrontPanelDecoration();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (BackPanelType == "Discrete" && ShowBPanel)
|
|
{
|
|
translate(BPanelTranslate) {
|
|
rotate(BPanelRotate){
|
|
Panel(){
|
|
BackPanelCutouts();
|
|
BackPanelDecoration();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|