/** * A name tag that can easily be clipped to the neck of your bottle. * Copyright (C) 2013 Roland Hieber * * This file was modified by obelix for the * OHM2013-Logo. If you wish to use other logos please use the original * file. All other parameters were not modified. * * This file was modified again by djerun to use the catars printed * by c3cat: https://www.printables.com/model/35076-cat-ears * as the logo and optionally allow use the ears from * https://www.thingiverse.com/thing:5029374 * printed at scale 0.2 as glue-ins for additional ears. * * * The contents of this file are licenced under CC-BY-SA 3.0 Unported. * See https://creativecommons.org/licenses/by-sa/3.0/deed for the * licensing terms. */ include use // The name that is printed on your name tag. NAME = "c3cat"; // The logo that is printed on your name tag. Has to be a DXF file. You can use the thing-logo repository for inspiration. LOGO_FILE = ""; // empty string is catear model // The font to be used for the name tag. See Write.scad for details. FONT = ""; // empty string is for Orbitron font // Format of the bottle clip you want to create. FORMAT = 3; // [0=Club Mate 50cL, 1=Long Neck as 25cL Club Mate or Fritz-kola, 2=Euroform 2, 3=Steinie bottles] // Style of the ears to add... or not add. This parameter can add a LOT of calculation time. EARS_STYLE = 0; // [0=No ears, 1=Just arcs, 2=Filled ears with the same thickness, 3=Filled ears with the a thinner thickness inside] /* [Render] */ // Whether to render the clip, the body. RENDER_COLOR_CLIP = true; // Whether to render the text part. RENDER_COLOR_TEXT = true; // Whether to render the logo part. RENDER_COLOR_LOGO = true; // Whether to render the cat ears part. RENDER_COLOR_EARS = true; // Set the number of facets for circles. $fn = 360; /** * Creates one instance of a bottle clip name tag. The default values are * suitable for 0.5l Club Mate bottles (and similar bottles). By default, logo * and text are placed on the name tag so they both share half the height. In this * version the logo is fixed. * * Parameters: * ru: the radius on the upper side of the clip * rl: the radius on the lower side of the clip * ht: the height of the clip * width: the thickness of the wall. Values near 2.5 usually result in a good * clippiness for PLA prints. * name: the name that is printed on your name tag. For the default ru/rt/ht * values, this string should not exceed 18 characters to fit on the name tag. * font: the path to a font for Write.scad. */ scale([0.2, 0.2, 0.2]) { render_bottle_clip( name=NAME, font=FONT, logo=LOGO_FILE, format=FORMAT, ears_style=EARS_STYLE); } module render_bottle_clip(name="", font="", logo="", format=0, ears_style=1) { name = name == "" ? "c3cat" : name ; font = font == "" ? "write/orbitron.dxf" : font ; if (format < 0 || format > 3) { assert(false, str("Unknown format ", format, ".")); } // Format == 0: Club-Mate 0.5L bottle // Format == 1: Long Neck bottle (like fritz-kola) 0.25L // Format == 2: Euroform 2 bottle // Format == 3: Steinie bottle ru = 13; rl = format == 1 ? 15 : format == 2 ? 22.5 : 17.5 ; ht = format == 3 ? 13 : 26 ; width = 2.5; scale([5, 5, 5]) { difference() { bottle_clip( name=NAME, font=FONT, logo=LOGO_FILE, ru=ru, rl=rl, ht=ht, width=width); // Render ears if requested if (ears_style > 0) { ears(ht=ht, ru=ru, rl=rl, clip_width=width, ears_style=ears_style); } } // Render ears if requested if (ears_style > 0 && RENDER_COLOR_EARS) { ears(ht=ht, ru=ru, rl=rl, clip_width=width, ears_style=ears_style); } } } module name(name, font, rl, ht, ru) { writecylinder( text=name, where=[0, 0, 0], radius=rl+0.5, height=ht/13*7, h=ht/13*4, t=max(rl,ru), font=font); } module logo(logo, rl, ht, ru, width) { echo(logo=logo); if(logo == "") { // No logo file specified? Let's print a catear headband instead! ear_size=ht; echo(ht=ht); echo(ru=ru); echo(rl=rl); echo(width=width); translate([0, -max(ru,rl), ht*3/4+.5]) rotate([90, 0, 0]) scale([1, 1, 1]) scale([ht/100, ht/100, 1]) translate([0, -ht/2, 0]) rotate(-90, [0, 0, 1]) catear_headband( size=ear_size, height=max(ru, rl), thickness=width, stretch_len=0, tip_len=0, details=false, with_rake=false ); } else { // The logo has been split in 3 parts. // well was... TODO /* rotate([0, 0, -48]) translate([0, 0, ht*3/4-0.1]) rotate([90, 0, 0]) scale([0.9, 0.9, 1]) scale([ht/100, ht/100, 1]) translate([-25, -29, 0.5]) linear_extrude(height=max(ru,rl)*2) import("logo_1.dxf"); */ translate([0, 0, ht*3/4-0.1]) rotate([90, 0, 0]) scale([0.8, 0.8, 1]) scale([ht/100, ht/100, 1]) translate([-18, -22, 0.5]) linear_extrude(height=max(ru, rl)*2) import(logo); /* rotate([0, 0, 44]) translate([0, 0, ht*3/4-0.1]) rotate([90, 0, 0]) scale([0.7, 0.7, 1]) scale([ht/100, ht/100, 1]) translate([-25, -26, 0.5]) linear_extrude(height=max(ru,rl)*2) import("logo_3.dxf"); */ } } module bottle_clip(ru=13, rl=17.5, ht=26, width=2.5, name="c3cat", font="write/orbitron.dxf", logo="") { e = 100; // should be big enough, used for the outer boundary of the text/logo // main cylinder if (RENDER_COLOR_CLIP) { color("black") difference() { cylinder(r1=rl+width, r2=ru+width, h=ht); difference() { union() { name(name=name, font=font, rl=rl, ht=ht, ru=ru); logo(logo=logo, rl=rl, ht=ht, ru=ru, width=width); } cylinder(r1=rl+width/2, r2=ru+width/2, h=ht); } clear_anti_aliasing = 0.01; // The margin to avoid empty surfaces in the preview. translate([0, 0, -clear_anti_aliasing/2]) cylinder(r1=rl, r2=ru, h=ht+clear_anti_aliasing); // finally, subtract a cube as a gap so we can clip it to the bottle rotate([0, 0, 45]) translate([0, 0, -1]) cube([50, 50, 50]); } } // text if (RENDER_COLOR_TEXT) { color("orange") difference() { name(name=name, font=font, rl=rl, ht=ht, ru=ru); cylinder(r1=rl+width/2, r2=ru+width/2, h=ht); outer_cutoff(rl, e, ru, ht, width); } } // logo if (RENDER_COLOR_LOGO) { color("yellow") difference() { logo(logo=logo, rl=rl, ht=ht, ru=ru, width=2*width); cylinder(r1=rl+width/2, r2=ru+width/2, h=ht); outer_cutoff(rl, e, ru, ht, width); } } } module outer_cutoff(rl, e, ru, ht, width) { // outer cylinder which is subtracted, so the text and the logo end // somewhere on the outside ;-) difference () { cylinder(r1=rl+e, r2=ru+e, h=ht); translate([0, 0, -1]) // Note: bottom edges of characters are hard to print when character // depth is > 0.7 cylinder(r1=rl+width+0.7, r2=ru+width+0.7, h=ht+2); } } /** * Will create a pair of ears to the bottle clip. By default, they are cat ears. * * Parameters: * * Positioning: * ht: the height of the bottle clip, used to scale the ears * ru: the radius on the upper side of the clip, used to position the ears * rl: the radius on the lower side of the clip, used to position the ears * clip_width: the thickness of the bottle clip * * Ears: * space_between_ears: Space between both ears. * ear_tilt: The X orientation of the ears, to give a more organic look. Angle in degree * * ear_depth: the depth of the ear * ear_thickness: the thickness of the ear arcs * ear_side_len: the length of one side of the ear base triangle * ear_bend_factor: how much the ear is bent. 1 = half circle, 0.00001 = almost straight * ear_stretch_factor: how much the ear is stretched, useful for fox ears or this kind of shapes * * ear_chamfer: size of the chamfer to apply to the edges * ear_chamfer_shape: The shape of the chamfer: "cone", "curve", "curve-in", "pyramid", maybe some others * ear_details: whether to chamfer also the partial arcs * */ module ears(ht, ru, rl, clip_width, space_between_ears = 15, ear_tilt = 15, ear_depth=2, ear_thickness=.6, ear_side_len=6, ear_bend_factor=0.5, ear_stretch_factor=1.2, ear_chamfer=1, ear_chamfer_shape="curve", ear_details=true, ears_style=1) { // This is the radius on which the ears has to be to be centered on the clip. radius = ru+(clip_width/2); // Math time! // The ears are not that bad without this calculation, but I want to improve the base and the join between ears and the clip. // Actually, by tilting the ear, a side of the end will go deeper withing the clip, but the other side may be separated from the clip. // So we need to translate the ears more deeper within the clip to avoid any separation // // So! Trigonometry! // // Viewed from the right of the ear, an ear is like that: // // A // |\ // | \ // h | \ s // | \ // +----\ D // C l \ // \ // \ // \ // B // // A is the side of the ear which is going upper // B is the side of the ear which is going deeper // C is the clip level // D is the midpoint between A and B // s is the depth of the ear (from A to B!) // h is the heigh we are searching for. We will lower the ear of this size. // l is the half of length of the fingerprint of the ear in the clip. // ADC is the tilt angle // ACD is a 90 degree angle // // So, to find the h length, we can use s and the angle ADC. // // CAH => We don't have the adjacent length. So, we can't use this formula // SOH => sin(ADC) = h / (s/2) // TOA => We don't have the adjacent length. So, we can't use this formula // // sin(ADC) = h / (s/2) // sin(ADC) * (s/2) = h // // BUT! Since we will raise the Clip level C to the end of the ear A, the print of the ear within the clip will be shifted of `l`! // So, let's calculate it! // // // CAH => cos(ADC) = l / (s/2) // SOH => Is not targeting what we want. // TOA => The opposed side is less precise than the hypotenuse, so, we will not use this formula // // cos(ADC) = l / (s/2) // cos(ADC) * (s/2) = l tilt_compensation = sin(ear_tilt) * (ear_depth/2); tilt_shift = cos(ear_tilt) * (ear_depth/2); // Pythagoras! // Viewed from the top, an ear is like that: // // A s // +----- E // | / // x | / r // | / // |/ // c // // c is the center of the circle forming the top surface of the clip // E is the perfect position of the ear // r is the radius, calculated previously // s is the half of the space between ears // x is the position of the ears on the Y axis. It's what we want to calculate // A is the projection of the ears on the Y axis // A is a 90 degrees angle // // x**2 + s**2 = r**2 // x**2 = r**2 - s**2 // x = sqrt( r**2 - s**2 ) s = space_between_ears / 2; pos_ears_yaxis = sqrt(pow(radius, 2) - pow(s, 2)); // Trigonometry! // Now, lets, rotate the ears to be at tangent to the clip // // From the top, an ear is like this: // // A s // +------- E // | / // | / // x | / r // | / // | / // | / // |/ // C // // E is the position of the ears // C is the center of the clip // A is the projection of the ears on the Y axis // r is the radius // s is the half of the space between the ears. // x is the same from the last figure. // A is a 90 degrees angle // // The angle of the ear is the same as the angle ACE. // So, we need to calculate this angle // // Since, we already know every lengths s, r and x, we can choose the formula we want! // IDK which one is faster, but that could be a nice optimization. // // CAH => cos(ACE) = x/r // SOH => sin(ACE) = s/r // TOA => tan(ACE) = x/s // // Let's choose the sine, since it's linked to the our best raw non-rounded values. It will give us values with the best accuracy. ears_angle = asin(s/radius); // Like that, it's good enough. And it could be committed like that. Actually, my first model (never published it) was like that. // BUT we can do better and smarter! (and we will probably need to refine it) // If we stop our math here, we centered the middle of the ear on the clip. But the end of the ears are NOT centered. // // So, we will use (again) Pythagoras! // We want to position the ear (on its center) to put the end of the ear in the radius. // // Form the top, an ear looks like that: // e // /| // / | // / | // r / | h // / | // / | // / | // C /-------+ E // p // // C is the center of the clip // e is the end of the ear // E is the middle of the ear. // r is the Radius // h is the half of the width of the ear // E is a 90 degrees angles // p is wanted length to position correctly the center of the ear. // // h**2 + p**2 = r**2 // p**2 = r**2 - h**2 // p = sqrt(r**2 - h**2) center_radius = sqrt( pow(radius, 2) - pow((ear_side_len+(tilt_shift))/2, 2) ); // So, we need to update the projection of the ears on the Y axis, based on center_radius, instead of radius. pos_ears_yaxis_end_ear = sqrt(pow(center_radius, 2) - pow(s, 2)); translate([0, -pos_ears_yaxis_end_ear, ht-tilt_compensation-(ear_thickness/2)]) rotate(90, [0, 0, 1]) rotate(90, [0, 1, 0]) union() { scale([1, 1 ,1]) translate([0, space_between_ears/2, 0]) rotate(ears_angle, [1, 0, 0]) rotate(-ear_tilt, [0, 1, 0]) ear_with_style( style=ears_style, depth=ear_depth, thickness=ear_thickness, side_len=ear_side_len, bend_factor=ear_bend_factor, stretch_factor=ear_stretch_factor, chamfer=ear_chamfer, chamfer_shape=ear_chamfer_shape, details=ear_details ); scale([1, -1, 1]) translate([0, space_between_ears/2, 0]) rotate(ears_angle, [1, 0, 0]) rotate(-ear_tilt, [0, 1, 0]) ear_with_style( style=ears_style, depth=ear_depth, thickness=ear_thickness, side_len=ear_side_len, bend_factor=ear_bend_factor, stretch_factor=ear_stretch_factor, chamfer=ear_chamfer, chamfer_shape=ear_chamfer_shape, details=ear_details ); } } module ear_with_style(style = 1, depth, thickness, side_len=30, bend_factor=0.5, stretch_factor=1.2, chamfer=1, chamfer_shape="curve", details=true) { if (style == 1 || style == 2 || style == 3) { // Style 1: Just arcs // Style 2: Filled ears with the same thickness // Style 3: Filled ears with the a thinner thickness inside ear( depth=depth, thickness=thickness, side_len=side_len, bend_factor=bend_factor, stretch_factor=stretch_factor, chamfer=chamfer, chamfer_shape=chamfer_shape, details=details ); } if (style == 2 || style == 3) { // Style 2: Filled ears with the same thickness // Style 3: Filled ears with the a thinner thickness inside thickness_steps = thickness*0.5; depth_steps = (depth-thickness)/((side_len / thickness_steps)); for (round_side_len = [side_len-thickness_steps:-thickness_steps:0]) { // round_side_len = side_len - (thickness_steps * x) // (thickness_steps * x) + round_side_len = side_len // (thickness_steps * x) = side_len - round_side_len // x = (side_len - round_side_len) / thickness_steps x = (side_len - round_side_len) / thickness_steps; round_shift = depth_steps * x; current_depth = depth - (style == 3 ? round_shift : 0); translation = style == 2 ? 0 : round_shift / 2; translate([0, 0, translation]) color("green") ear( depth=current_depth, thickness=thickness, side_len=round_side_len, bend_factor=bend_factor, stretch_factor=stretch_factor, chamfer=chamfer, chamfer_shape=chamfer_shape, details=true ); } } } /** * Module that creates an ear shape. * * Parameters: * * depth: the depth of the ear * thickness: the thickness of the ear arcs * side_len: the length of one side of the ear base triangle * bend_factor: how much the ear is bent. 1 = half circle, 0.00001 = almost straight * stretch_factor: how much the ear is stretched, useful for fox ears or this kind of shapes * * chamfer: size of the chamfer to apply to the edges * chamfer_shape: The shape of the chamfer: "cone", "curve", "curve-in", "pyramid", maybe some others * details: whether to chamfer also the partial arcs * * By default, this module creates an ear. * * I don't remember the original author of this module. I refactored it and improved it a bit. Documentation is from me. */ module ear(depth, thickness, side_len=30, bend_factor=0.5, stretch_factor=1.2, chamfer=1, chamfer_shape="curve", details=true) { depth = depth == undef ? 20 : depth ; thickness = thickness == undef ? 3 : thickness ; // $A and $B are the end of the ear // $C is the higher end point. $A=[0, side_len/2]; $B=[0,-side_len/2]; // From the front, an ear looks like: // // $C // /|\ // / | \ // / | \ // /---+---\ // $B c $A // // c is the origin of the ear. // $Bc$C and $Ac$C are 90deg angles. // // The length [$B $A] is `side_len` // Thus, [$B c] == [c $A] == side_len/2 // // Since SOH, sin(angle) = Opposed / Hypotenuse // sin(angle) = Opposed / Hypotenuse // sin(angle) * Hypotenuse = Opposed // Hypotenuse = Opposed / sin(angle) // // Thus, the `(side_len/2/sin(120))` implies that "The opposed side of the 120 angle is `(side_len/2)` length" and is the formula to know the size of the hypotenuse. // Moreover, a 120deg angle in a right rectangle does not exists: the sum of all the angles of a triangle needs to be 180deg. I think the author tried, instead, to use the 60 as an angle, since the sinus is the same. // // The correct formula should be `((side_len/2)*tan(60))`: since $C$B$A should a 60deg angle to have an equilateral `$A$B$C` triangle, the product of the tangent ("TOA") of this angle and the adjacent segment [$B c] will give us the size of length of the opposed segment [$C c]. // // MOREOVER, the product of the magic number `1.5` with the divisor `sin(120)`... is actually the result of the `tan(60)`! // // The original formula was: // $C=[-(side_len/2/sin(120))*1.5*stretch_factor, 0]; $C=[-((side_len/2)*tan(60))*stretch_factor, 0]; // $a, $b and $c are the distance between the different points of the triangle. $c=sqrt(pow($A.x-$B.x, 2)+pow($A.y-$B.y, 2)); $b=sqrt(pow($A.x-$C.x, 2)+pow($A.y-$C.y, 2)); $a=sqrt(pow($C.x-$B.x, 2)+pow($C.y-$B.y, 2)); // $hc is the height of $C. $hc=-$C.x; // If I correctly understood, $alpha should be the angle of the $B$A$C. This should be 60deg... but is not. idk why. $alpha=asin($hc/$b); $beta=$alpha; $gamma=180-$alpha-$beta; $delta=180*bend_factor; $bend_radius=$a/(2*cos(90-$delta/2)); $bend_offset=$bend_radius*sin(90-$delta/2); translate([0, -$c/2, 0]) rotate($beta, [0, 0, 1]) translate([0, $a/2, 0]) translate([$bend_offset, 0, 0]) color("#00ffff") chamfer(size=(details)?chamfer:0, child_h=depth, child_bot=-depth/2, shape=chamfer_shape) partial_ring( part=$delta/360, radius=$bend_radius, thickness=thickness, height=depth ); translate([0, $c/2, 0]) rotate(-$alpha, [0, 0, 1]) translate([0, -$b/2, 0]) translate([$bend_offset, 0, 0]) color("#ff00ff") chamfer(size=(details)?chamfer:0, child_h=depth, child_bot=-depth/2, shape=chamfer_shape) partial_ring( part=$delta/360, radius=$bend_radius, thickness=thickness, height=depth ); translate($A) color("#aaaaaa") chamfer(size=chamfer, child_h=depth, child_bot=-depth/2, shape=chamfer_shape) cylinder(h=depth, d=thickness, center=true); translate($B) color("#bbbbbb") chamfer(size=chamfer, child_h=depth, child_bot=-depth/2, shape=chamfer_shape) cylinder(h=depth, d=thickness, center=true); translate($C) color("#cccccc") chamfer(size=chamfer, child_h=depth, child_bot=-depth/2, shape=chamfer_shape) cylinder(h=depth, d=thickness, center=true); } /** * This module is not mine. */ module partial_ring(part, radius, thickness, height) { rotate(180-180*part, [0, 0, 1]) rotate_extrude(angle=360*part) translate([radius, 0]) square([thickness, height], center=true); } /** * This module is not mine. */ module chamfer(size=2, child_h=5, child_bot=0, shape="curve") { chamfer_size=size; module chamfer_shape() { if (shape == "cone") { $fn=16; cylinder(chamfer_size/2,chamfer_size/2,0); } else if (shape == "curve") { $fn=4; for( y = [0:1/$fn:1]) { cylinder(chamfer_size/2*(1-y),chamfer_size/2/cos(180/$fn)*y,0); } } else if (shape == "curve-in") { $fn=16; intersection() { sphere(chamfer_size/2/cos(180/$fn)); translate([0,0,chamfer_size/2]) cube(chamfer_size, center=true); } } else if (shape == "pyramid") { $fn=4; cylinder(chamfer_size/2/cos(180/$fn),chamfer_size/2,0); } } module lower_chamfer() { minkowski() { linear_extrude(0.0001) difference() { square([1000,1000],center=true); projection()children(0); } chamfer_shape(); } } module upper_chamfer() { scale([1,1,-1])lower_chamfer()children(); } render()difference() { children(); translate([0,0,child_bot])lower_chamfer()children(); translate([0,0,child_bot+child_h])upper_chamfer()children(); } }