Laderas con OpenSCAD

Para suavizar la transición entre dos planos paralelos a distintas alturas podemos hacer un chaflán con una esquina y un rincón, o bien hacer uso de dos superficies cilíndricas tangentes entre sí y a los planos. 

[Quizá te interese: Tangente a una elipse con OpenSCAD o Tangentes a un círculo con OpenSCAD: trabajar con sucedáneos de curvas]

El siguiente esquema nos ayudará a plantear el problema: tenemos un rectángulo de ancho por alto, en el que queremos inscribir una ladera que empiece y acabe horizontal, y tenga el punto de inflexión en (xi, yi) formando un ángulo v con la vertical.

ladera dibujada con elipses


Para solucionar el problema necesitamos la fórmula de la elipse, pero haciendo que su extremo superior sea tangente al eje de las X (la fórmula tradicional centra la elipse en el origen de coordenadas):

 

x2/a2 + (y+b)2/b2 = 1


Despejando y:

 

y1 = b · (+raiz(1 - x2/a2) - 1)
y2 = b · (-raiz(1 - x2/a2) - 1)    (esta no nos interesa)

[1]

La derivada de y es:

 

y' = - (b·x) / ( a · raiz(a2 - x2)


Así, en el punto de inflexión tendremos:

 

tan(v) = - (b·x) / ( a · raiz(a2 - x2)

[2]

El sistema de ecuaciones formado por  [1] y [2] nos permite despejar a y b que son nuestras incógnitas:
 

a = raiz( ( x·tan(v) - y )2  / ( tan2(v) - 2·y·tan(v)/x ) )


  b = y / ( raiz(1-x2/a2) - 1)

Trasladar estas ecuaciones a OpenSCAD no es inmediato por dos razones:
  • el ángulo v tiene limitaciones importantes, y debe estar entre 90º y un valor que depende de la geometría y hay que calcularlo.
  • OpenSCAD no facilita los cálculos como lo puede hacer un lenguaje de programación convencional, ni permite hacer uso de estructuras de control en las funciones, así que estéticamente dejará mucho que desear.


Para facilitar el uso de esta técnica he preparado un módulo que construye una ladera a partir de 6 parámetros:
  • tarugo: vector con las dimensiones que necesitamos como [x, y, z].
  • inflexión_x: proporción de X donde ponemos el punto de inflexión (de 0 a 1, por defecto 0,5).
  • inflexión_z: proporción de Z donde ponemos el punto de inflexión (de 0 a 1, por defecto 0,5).
  • suavidad: suavidad de la inflexión: 0 supone un ángulo v de 90º, y 1 el ángulo mínimo posible (por defecto 1).
  • sobrado: para un uso sustractivo de la ladera (por defecto no, ver ejemplo más adelante)
  • center: indica si centramos la pieza o no (por defecto no).
Estos son algunos ejemplos de uso: 

ejemplos de ladera
  • en la primera fila se juega con el parámetro suavidad: tope, medio y mínimo.
  • en la segunda se juega con la posición del punto de inflexión
  • y en la tercera son los casos extremos de desplazamiento de la inflexión


Para terminar, el código y un ejemplo práctico en el que veremos cómo hacer un tarugo con forma de chicane. La curva de un lado se hace añadiendo una ladera a un hueco que queda entre dos cubos. La curva del otro lado se hace restando una ladera a un escalón saliente.

chicane con dos tarugos y dos laderas


Para evitar efectos indeseables cuando se restan piezas del mismo ancho, se usa el parámetro sobrado=true, que hace el tarugo de la ladera algo mayor, pero respetando las cualidades deseables de las curvas.


$fs=.1 ;
$fa=.1 ;


largo = 100 ;
ancho = 30 ;
alto = 20 ;

transicion = 40 ;
desviacion = 10 ;

// uso aditivo de la ladera, volteándola para darle la orientación requerida
translate([-(largo+transicion)/4, -desviacion/2]) 
   cube([(largo-transicion)/2, ancho, alto], center=true);
translate([0,-ancho/2,0])   
   rotate([90,0,0])
      ladera([transicion, alto, desviacion], center=true);

// uso sustractivo de la ladera: el resultado mejora con "sobrado=true"
difference() {
   translate([(largo-transicion)/4, desviacion/2 , 0]) 
      cube([(largo + transicion) /2, ancho, alto], center=true);
   translate([0,ancho/2,0])   
      rotate([90,0,0])
         ladera([transicion, alto, desviacion], center=true, sobrado=true);
}



module ladera(tarugo, inf_x=.5, inf_z=.5, suavidad=1, sobrado=false, center=false) {
/* planteamiento:
   Se trata de devolver un cubo de dimensiones [ tarugo[0], tarugo[1], tarugo[2] ]
   recortado formando una vertiente que va de derecha a izquierda, idealmente un 
   par de curvas cóncava-convexa.
   
   El punto de inflexión se encontrará a inf_x * tarugo[0] del lado izquierdo
   y a inf_z * tarugo[2] del borde superior.
   
   La inflexión tendrá un ángulo v con la vertical que estará entre un valor máximo
   de 90º (con suavidad==0) y un valor mínimo calculado por tanteo con la función
   amin() (con suavidad==1)
   
   Como caso particular, si el punto de inflexión es superior-izquierda tendremos
   una curva cóncava con inicio brusco y final horizontal, y si es inferior-derecha
   tendremos una curva convexa con inicio horizontal y final abrupto.
   
   El parametro "sobrado" se pone a true cuando vamos a usar la ladera de forma 
   sustractiva: mantiene las curvas calculadas para el tarugo, que se hace algo
   sobredimensionado para evitar efectos indeseables
*/   
   function acota(v, m, M)    =  v<m ? m : (v>M ? M : v);
   function Ad2(tanu)  =  pow(tanu*Xd - Zd, 2) / (pow(tanu,2) - 2*Zd*tanu/Xd);   
   function Ai2(tanu)  =  pow(tanu*Xi - Zi, 2) / (pow(tanu,2) - 2*Zi*tanu/Xi);   
   function amin(t=45, m=0, M=90)     // búsqueda recursiva del angulo minimo
      =  ( (Zd==0 || Ad2(tan(t))>0) && (Zi==0 || Ai2(tan(t))>0) ) 
      ? ( (abs(t-m)<umbral) ? t : amin((m+t)/2, m, t) ) 
      : amin((t+M)/2, t, M);

   umbral = 1 ; // determina lo exhaustivo de la búsqueda del ángulo mínimo
   interseccion=.1 ;
   exceso = [1,1,1] * (sobrado ? interseccion : 0);

   Xd = acota(inf_x * tarugo[0], 0, tarugo[0]);
   Zd = acota(inf_z * tarugo[2], 0, tarugo[2]);

   Xi = tarugo[0] - Xd;
   Zi = tarugo[2] - Zd ;

   if ( Xd>0 && Zd>0 && Xi>0 && Zi>0 || Xd+Zd==0 || Xi+Zi==0 )
      assign( T = tan(90 - acota(suavidad, 0, 1) * (90-amin())) ) 
         assign(Ad = sqrt(Ad2(T)), Ai = sqrt(Ai2(T)) )
            assign(Bd = Zd / (1-sqrt(1-pow((Xd>Ad?Ad:Xd)/Ad,2)))
            ,      Bi = Zi / (1-sqrt(1-pow((Xi>Ai?Ai:Xi)/Ai,2))) ) 
               translate( center ? -tarugo/2 : [0,0,0] )
                  union() {   
                     intersection() {
                        translate(-exceso/2)
                           cube([Xd+exceso[0]/2, tarugo[1]+exceso[1], tarugo[2]+exceso[2]]);
                        translate([0,-(interseccion+exceso[1])/2,tarugo[2]-Bd]) 
                           rotate([-90,0,0]) 
                              scale([1,Bd/Ad,1]) 
                                 cylinder(r=Ad, h=tarugo[1]+exceso[1]+interseccion);
                     }
                     difference() {
                        translate(-exceso/2)
                           cube([tarugo[0], tarugo[1], Zi]+exceso);
                        translate([tarugo[0],-(interseccion+exceso[1])/2,Bi]) 
                           rotate([-90,0,0]) 
                              scale([1,Bi/Ai,1]) 
                                 cylinder(r=Ai, h=tarugo[1]+exceso[1]+interseccion);
                     }
                  }
   else
      echo("ladera incongruente!");
}