From 8d0fae76521ca9088b1bc0ca5782ce9d3ad3908a Mon Sep 17 00:00:00 2001 From: Ajabep Date: Wed, 14 Jan 2026 17:30:33 +0100 Subject: [PATCH] Add the ear generation in this file. --- c3cat-bottle-clip/c3cat-bottle-clip.scad | 381 +++++++++++++++++++++-- 1 file changed, 362 insertions(+), 19 deletions(-) diff --git a/c3cat-bottle-clip/c3cat-bottle-clip.scad b/c3cat-bottle-clip/c3cat-bottle-clip.scad index 807a29b..74072f0 100644 --- a/c3cat-bottle-clip/c3cat-bottle-clip.scad +++ b/c3cat-bottle-clip/c3cat-bottle-clip.scad @@ -12,13 +12,6 @@ * https://www.thingiverse.com/thing:5029374 * printed at scale 0.2 as glue-ins for additional ears. * - * `Ohren_4.stl` and `catears.stl` need to be placed in `../stls/` - * for the symlinks to work otherwise `catears.stl` needs to be placed - * at `./catears.stl` and `Ohren_4.stl` at `./catear.stl`. - * - * Version of the modification: 1.0 - * - * See examples.scad for examples on how to use this module. * * 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 @@ -73,11 +66,6 @@ $fn = 360; * font: the path to a font for Write.scad. */ -/** - * currently openscad fails to render the original `Ohren_4.stl` outside of the preview mode - * according to [the wiki](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/FAQ#Why_is_my_imported_STL_file_appearing_with_F5_but_not_F6?) this is the stls fault - * using meshlab to run `Filters` -> `Cleaning and Repairing` -> `Remove T-Vertices` by `Edge-Flip` with `Ratio` of `1000000` before importing the stl works but but two errors remain. - */ scale([0.2, 0.2, 0.2]) { render_bottle_clip( name=NAME, @@ -248,11 +236,366 @@ module outer_cutoff(rl, e, ru, ht, width) { } } -module ear() { - if (USE_TINY_EARS) { - rotate(-90, [0, 0, 1]) union() { - scale([1, 1 ,1]) translate([0, -85]) import("catear.stl"); - scale([1, -1, 1]) translate([0, -85]) import("catear.stl"); - } - } +/** + * 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) { + + // 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 hypothenus, 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( + 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( + 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 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 ; + + echo("Generating ONE single 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 + ); + + $A=[0, side_len/2]; + $B=[0,-side_len/2]; + $C=[-(side_len/2/sin(120))*1.5*stretch_factor, 0]; + $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=-$C.x; + $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(); + } }