Konubinix' opinionated web of thoughts

Cascade Overflow Funnel

Fleeting

Table des matières

Objectif

Remplir tous nos bidons d’eau de pluie (récupérateur → bidons 5L) en une seule fois, sans avoir à déplacer chaque bidon quand il est plein. Les bidons sont alignés et branchés en série : le premier reçoit l’eau du récupérateur, et quand il déborde l’excédent cascade dans le suivant via un tuyau de trop-plein, et ainsi de suite.

Chaque bidon reçoit un bouchon fileté imprimé 3D qui remplace le bouchon d’origine et qui reste vissé en permanence. Ce bouchon accueille par un pas de vis à course limitée deux pièces interchangeables à la main :

  • un entonnoir (vissé pendant le remplissage) qui reçoit l’eau par le haut et porte un tuyau de trop-plein pour cascader l’excédent vers le bidon suivant ;
  • une pièce de fermeture (vissée après remplissage) qui scelle le bidon pour le stockage.

Principe : on ne dévisse plus jamais le bouchon du bidon — on bascule entonnoir ↔ pièce de fermeture à la main.

Le bouchon

Trouvons le bon filetage

Pour que le bouchon se visse proprement sur le col du bidon, il faut identifier le standard de filetage exact (Ø, pas, profil). Méthode : mesurer grossièrement, croiser avec un catalogue de standards pour lister les candidats, puis imprimer des anneaux de test de chaque candidat pour confirmer par essai au vissage.

Sources et mesures

Deux bidons candidats disponibles commercialement (eau déminéralisée, vides) :

Ces mesures sont approximatives (pied à coulisse rudimentaire). Il faut les croiser avec un catalogue de standards de filetage plastique pour trouver le bon candidat avant d’imprimer le bouchon complet.

Références : SCAT Europe (catalogue de standards), Thingiverse thing #5397832 (modèles paramétriques existants).

Les robinets vendus pour bidons 5L sont souvent étiquetés “DIN 40” ou “DIN 45”. Méthode de travail : imprimer des anneaux de test (juste le filetage, rapide) pour confirmer le standard avant d’imprimer un bouchon complet.

Standards identifiés (source : SCAT Europe, gewinde-normen.de)

Standard Norme Ø ext max Ø ext min Ø int noyau max Pas (mm)
S 40/41 (plastique) DIN 6063-1 41.00 39.50 37.00 3.50
GL 38 (verre) DIN 168-1 37.49 36.88 35.10 4.23
GL 40 (verre) DIN 168-1 40.00 39.30 37.30 4.00
DIN 45 DIN 45 44.30 39.70 40.80 4.00
S 45 (plastique) DIN 6063-1/2 45.00 44.30 41.00 4.00
GL 45 (verre) DIN 168-1 45.00 44.30 42.30 4.00
KS 36 (plastique) DIN 6063-1 36.00 - 33.00 3.00
KS 40 (plastique) DIN 6063-1 40.00 - 37.00 3.00

Filetage retenu par bidon

Les mesures sont approximatives. Candidats par ordre de vraisemblance :

  • MIEUXA (~35-40mm, pas ~3mm) :
    1. KS 40 (DIN 6063-1) : Ø 40mm, pas 3.0mm — éliminé (test 1)
    2. DIN 45 : Ø ext 39.7-44.3mm, pas 4.0mm — inutile, S 40/41 fait l’affaire (test 2)
    3. S 40/41 (DIN 6063-1) : Ø 41mm, pas 3.5mm — confirmé d=42, pas=3.5 (test 2)
  • Netto/Ardea (~33-38mm, pas ~4mm) : confirmé d=40, pas=4.0 (test 5)
    1. GL 40 (DIN 168-1) : Ø 40mm, pas 4.0mm — éliminé, d=41 trop gros (test 4)
    2. ~GL 38 (DIN 168-1) : Ø 37.5mm, pas 4.23mm — éliminé, d=38.5 trop fin (test 3)
    3. Filetage custom entre GL 38 et GL 40

Paramètres figés à partir d’ici — dimensions du col et spec du filetage — qui seront réutilisés par toutes les variantes ultérieures de bouchon (nu, avec entonnoir, pièce de fermeture) :

wallThickness=2;
innerDiameter=42;
innerDepth=15;
threadPitch=3.50;
threadDepth=1.25;
threadWidth=1.3;
threadTurns=1.5;
threads=1;
threadStart=1.3;

wallThickness=2;
innerDiameter=40;
innerDepth=15;
threadPitch=4.00;
threadDepth=1.25;
threadWidth=1.3;
threadTurns=3;
threads=1;
threadStart=1.3;

Anneaux de test pour tester les filetages

Avant d’investir dans un bouchon entier, on confirme le bon standard de filetage en imprimant juste un anneau fileté et en essayant de le visser sur le col du bidon. C’est rapide (quelques minutes en precision=1), et c’est la seule façon fiable de savoir si les mesures au pied à coulisse correspondent à un vrai standard.

L’anneau est un cylindre qui porte juste le filetage paramétré (rayon, pas), sans dessus ni stries — minimum vital pour tester le vissage.

Anneau de test — paramétrique

// --- PARAMÈTRES À CHANGER POUR CHAQUE TEST ---
// Candidats MIEUXA : KS40 (d=41, p=3.0) / S40/41 (d=42, p=3.5)
// Candidats Netto  : GL38 (d=38.5, p=4.23) / GL40 (d=41, p=4.0)
innerDiameter=42;   // Ø ext du col + 1mm jeu
threadPitch=3.5;   // pas du filetage
// --- FIN PARAMÈTRES ---

precision=1; // rough pour impression rapide
wallThickness=2;
innerDepth=8; // court, juste assez pour tester le vissage
threadDepth=1.25;
threadWidth=1.3;
threadTurns=1.5;
threads=1;
threadStart=1.3;
smoothness=(precision==1)?4:((precision==2)?6:20);
facets=(precision==1)?6:((precision==2)?20:50);
segments=facets*threadTurns;
$fn=max(20,facets);
module screwThread(stR,stZ,stPitch,stDepth,stWidth,stTurns,stN,stSmooth,stFacets)
{
  stSegs=stFacets*stTurns;
  union()
    {
      for(t=[0:stN-1])
        {
          rotate([0,0,360*t/stN])
            translate([stR,0,stZ])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
          rotate([0,0,360*t/stN+stTurns*360])
            translate([stR,0,stZ+stPitch*stN*stTurns])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
        }
      for(tw=[0:stSegs-1])
        {
          translate([0,0,stZ])
            union()
            {
              for(t=[0:stN-1])
                {
                  translate([0,0,stPitch*stN*tw/stSegs*stTurns])
                    rotate([0,0,360*(t/stN)+tw*360*stTurns/stSegs])
                    rotate([atan(stPitch*stN/(stR*2*3.1416)),0,0])
                    rotate_extrude(convexity=10,angle=360/stFacets,$fn=50)
                    translate([stR,0,0])
                    scale([stDepth,stWidth/2,0])circle(r=1,$fn=stSmooth);
                }
            }
        }
    }
}

module capThread()
{
  screwThread(innerDiameter/2,wallThickness+innerDepth-threadStart-threadTurns*threadPitch*threads-threadWidth/2,threadPitch,threadDepth,threadWidth,threadTurns,threads,smoothness,facets);
}

union()
{
  capThread();
  difference()
    {
      translate([0,0,innerDepth/2+wallThickness])
        cylinder(h=innerDepth,r=(innerDiameter)/2+wallThickness,center=true);
      cylinder(h=innerDepth*4,r=innerDiameter/2,center=true);
    }
}

Valeurs à tester :

Test innerDiameter threadPitch Standard visé
1 41 3.00 KS 40 (MIEUXA)
2 42 3.50 S 40/41 (MIEUXA) semble faire l’affaire
3 38.5 4.23 GL 38 (Netto) trop fin, le filetage bloque le passage de l’anneau
4 41 4.00 GL 40 (Netto) trop gros, le cou flotte dedans, mais toujours trop fin pour le MIEUXA
5 40 4.00 custom (Netto) semble faire l’affaire
6 43 4.00 DIN 45 (MIEUXA) inutile, S 40/41 (test 2) fait déjà l’affaire

Mise en place d’une jupe d’étanchéité

Avec le bon filetage, le bouchon se visse sur le col, mais rien n’empêche l’eau de fuir entre le col et le bouchon. La solution vient directement de l’observation des bouchons d’origine des bidons du commerce : ils portent tous, sous leur disque, une petite jupe annulaire intérieure qui vient s’emboîter dans le col et sceller contre sa paroi interne pendant le vissage. On reproduit cette jupe dans nos bouchons imprimés. On l’itère d’abord seule (debug) pour trouver les bonnes cotes (Ø, hauteur, épaisseur, jeu radial), puis on assemble le premier bouchon complet qui visse et scelle.

Paramètres retenus de la jupe pour chaque bidon (sealHeight = hauteur axiale, sealThickness = épaisseur radiale, sealGap = jeu radial par rapport au Ø intérieur du col) + ridges = nombre de stries extérieures de préhension au vissage :

ridges=25;
sealHeight=7;
sealThickness=1.2;
sealGap=3.6;

ridges=25;
sealHeight=8;
sealThickness=1.0;
sealGap=3.05;

Jupe seule (debug)

Pour valider les dimensions de la bague d’étanchéité intérieure (celle qui vient sceller contre la lèvre du col) sans imprimer tout un bouchon, on l’imprime seule sur un petit plancher via la jupe de test. Ça se monte sur le col comme un bouchon-tampon — on juge à la main si l’étanchéité est correcte.

Jupe MIEUXA (S 40/41)

wallThickness=2;
innerDiameter=42;
innerDepth=15;
threadPitch=3.50;
threadDepth=1.25;
threadWidth=1.3;
threadTurns=1.5;
threads=1;
threadStart=1.3;
ridges=25;
sealHeight=7;
sealThickness=1.2;
sealGap=3.6;
$fn=50;

union()
{
  // cylinder(h=wallThickness,r=innerDiameter/2-sealGap);
  difference()
  {
    cylinder(h=sealHeight,r=innerDiameter/2-sealGap);
    cylinder(h=sealHeight+1,r=innerDiameter/2-sealGap-sealThickness);
  }
    }
Jupe Netto/Ardea (custom d=40)

wallThickness=2;
innerDiameter=40;
innerDepth=15;
threadPitch=4.00;
threadDepth=1.25;
threadWidth=1.3;
threadTurns=3;
threads=1;
threadStart=1.3;
ridges=25;
sealHeight=8;
sealThickness=1.0;
sealGap=3.05;
  // pour debug
sealHeight=3;
wallThickness=1;
$fn=50;

union()
{
  // cylinder(h=wallThickness,r=innerDiameter/2-sealGap);
  difference()
  {
    cylinder(h=sealHeight,r=innerDiameter/2-sealGap);
    cylinder(h=sealHeight+1,r=innerDiameter/2-sealGap-sealThickness);
  }
    }

Bouchon complet (filetage + jupe)

Avec les bonnes cotes de filetage et de jupe, on assemble le premier bouchon complet fermé : filetage + corps cylindrique + disque de fermeture + bague d’étanchéité. Ces bouchons nus servent à boucher un bidon qu’on ne remplit pas (et aussi à vérifier que le standard choisi tient vraiment à la pression, à la manipulation).

Deux jeux de paramètres (params-mieuxa et params-netto) sont les sources de vérité réutilisées par toutes les variantes ultérieures (bouchons avec entonnoir, entonnoirs séparés).

Bouchon MIEUXA (S 40/41)

wallThickness=2;
innerDiameter=42;
innerDepth=15;
threadPitch=3.50;
threadDepth=1.25;
threadWidth=1.3;
threadTurns=1.5;
threads=1;
threadStart=1.3;
ridges=25;
sealHeight=7;
sealThickness=1.2;
sealGap=3.6;

precision=3;
wallThickness=2;
innerDiameter=42;
innerDepth=15;
threadPitch=3.50;
threadDepth=1.25;
threadWidth=1.3;
threadTurns=1.5;
threads=1;
threadStart=1.3;
ridges=25;
sealHeight=7;
sealThickness=1.2;
sealGap=3.6;
smoothness=(precision==1)?4:((precision==2)?6:20);
facets=(precision==1)?6:((precision==2)?20:50);
segments=facets*threadTurns;
$fn=max(20,facets);
module screwThread(stR,stZ,stPitch,stDepth,stWidth,stTurns,stN,stSmooth,stFacets)
{
  stSegs=stFacets*stTurns;
  union()
    {
      for(t=[0:stN-1])
        {
          rotate([0,0,360*t/stN])
            translate([stR,0,stZ])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
          rotate([0,0,360*t/stN+stTurns*360])
            translate([stR,0,stZ+stPitch*stN*stTurns])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
        }
      for(tw=[0:stSegs-1])
        {
          translate([0,0,stZ])
            union()
            {
              for(t=[0:stN-1])
                {
                  translate([0,0,stPitch*stN*tw/stSegs*stTurns])
                    rotate([0,0,360*(t/stN)+tw*360*stTurns/stSegs])
                    rotate([atan(stPitch*stN/(stR*2*3.1416)),0,0])
                    rotate_extrude(convexity=10,angle=360/stFacets,$fn=50)
                    translate([stR,0,0])
                    scale([stDepth,stWidth/2,0])circle(r=1,$fn=stSmooth);
                }
            }
        }
    }
}

module capThread()
{
  screwThread(innerDiameter/2,wallThickness+innerDepth-threadStart-threadTurns*threadPitch*threads-threadWidth/2,threadPitch,threadDepth,threadWidth,threadTurns,threads,smoothness,facets);
}
union()
{
  capThread();

  difference()
  {
    union()
      {
        translate([0,0,innerDepth/2+wallThickness])
          cylinder(h=innerDepth,r=(innerDiameter)/2+wallThickness,center=true);

        translate([0,0,wallThickness])
          rotate_extrude(convexity = 10)
          translate([innerDiameter/2, 0, 0])
          circle(r = wallThickness, $fn = smoothness);
      }
    cylinder(h=innerDepth*4,r=innerDiameter/2,center=true);
  }

  translate([0,0,wallThickness*1.5])
  rotate([180,0,0])
  rotate_extrude(convexity = 10)
  translate([innerDiameter/2-wallThickness/2, 0, 0])
  difference()
  {
    square(wallThickness,center=false);
    circle(r = wallThickness/2, $fn = smoothness);
  }

  for(ridge=[0:ridges-1])
    {
      hull()
        {
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,wallThickness*5/4])
            sphere(r=wallThickness/4,$fn=smoothness);
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,innerDepth+wallThickness*3/4])
            sphere(r=wallThickness/4,$fn=smoothness);
        }
    }
    translate([0,0,wallThickness/2])cylinder(h=wallThickness,r=innerDiameter/2,center=true);
  difference()
  {
    cylinder(h=sealHeight,r=innerDiameter/2-sealGap);
    cylinder(h=sealHeight+1,r=innerDiameter/2-sealGap-sealThickness);
  }
      }

Bouchon Netto/Ardea (custom d=40, pas=4.0)

wallThickness=2;
innerDiameter=40;
innerDepth=15;
threadPitch=4.00;
threadDepth=1.25;
threadWidth=1.3;
threadTurns=3;
threads=1;
threadStart=1.3;
ridges=25;
sealHeight=8;
sealThickness=1.0;
sealGap=3.05;

precision=3;
wallThickness=2;
innerDiameter=40;
innerDepth=15;
threadPitch=4.00;
threadDepth=1.25;
threadWidth=1.3;
threadTurns=3;
threads=1;
threadStart=1.3;
ridges=25;
sealHeight=8;
sealThickness=1.0;
sealGap=3.05;
smoothness=(precision==1)?4:((precision==2)?6:20);
facets=(precision==1)?6:((precision==2)?20:50);
segments=facets*threadTurns;
$fn=max(20,facets);
module screwThread(stR,stZ,stPitch,stDepth,stWidth,stTurns,stN,stSmooth,stFacets)
{
  stSegs=stFacets*stTurns;
  union()
    {
      for(t=[0:stN-1])
        {
          rotate([0,0,360*t/stN])
            translate([stR,0,stZ])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
          rotate([0,0,360*t/stN+stTurns*360])
            translate([stR,0,stZ+stPitch*stN*stTurns])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
        }
      for(tw=[0:stSegs-1])
        {
          translate([0,0,stZ])
            union()
            {
              for(t=[0:stN-1])
                {
                  translate([0,0,stPitch*stN*tw/stSegs*stTurns])
                    rotate([0,0,360*(t/stN)+tw*360*stTurns/stSegs])
                    rotate([atan(stPitch*stN/(stR*2*3.1416)),0,0])
                    rotate_extrude(convexity=10,angle=360/stFacets,$fn=50)
                    translate([stR,0,0])
                    scale([stDepth,stWidth/2,0])circle(r=1,$fn=stSmooth);
                }
            }
        }
    }
}

module capThread()
{
  screwThread(innerDiameter/2,wallThickness+innerDepth-threadStart-threadTurns*threadPitch*threads-threadWidth/2,threadPitch,threadDepth,threadWidth,threadTurns,threads,smoothness,facets);
}
union()
{
  capThread();

  difference()
  {
    union()
      {
        translate([0,0,innerDepth/2+wallThickness])
          cylinder(h=innerDepth,r=(innerDiameter)/2+wallThickness,center=true);

        translate([0,0,wallThickness])
          rotate_extrude(convexity = 10)
          translate([innerDiameter/2, 0, 0])
          circle(r = wallThickness, $fn = smoothness);
      }
    cylinder(h=innerDepth*4,r=innerDiameter/2,center=true);
  }

  translate([0,0,wallThickness*1.5])
  rotate([180,0,0])
  rotate_extrude(convexity = 10)
  translate([innerDiameter/2-wallThickness/2, 0, 0])
  difference()
  {
    square(wallThickness,center=false);
    circle(r = wallThickness/2, $fn = smoothness);
  }

  for(ridge=[0:ridges-1])
    {
      hull()
        {
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,wallThickness*5/4])
            sphere(r=wallThickness/4,$fn=smoothness);
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,innerDepth+wallThickness*3/4])
            sphere(r=wallThickness/4,$fn=smoothness);
        }
    }
    translate([0,0,wallThickness/2])cylinder(h=wallThickness,r=innerDiameter/2,center=true);
  difference()
  {
    cylinder(h=sealHeight,r=innerDiameter/2-sealGap);
    cylinder(h=sealHeight+1,r=innerDiameter/2-sealGap-sealThickness);
  }
      }

L’entonnoir

Première variante enrichie : on part du corps commun du bouchon (filetage + paroi + stries + chanfrein), on perce le dessus, et on y prolonge un cône — l’entonnoir. L’eau du récupérateur tombe dans l’entonnoir, est guidée par le cône vers le trou central, et descend dans le bidon. Pas encore de trop-plein ni de séparation bouchon/entonnoir à ce stade : c’est le monobloc le plus simple.

Impression avec l’ouverture large de l’entonnoir posée sur le plateau → ni support ni overhang problématique.

Orientation : bas = partie fine (sortie, côté trou), haut = partie large (entrée, où tombe l’eau). Le code SCAD est écrit avec Z inversé par rapport à cette orientation.

La grandeur active de l’entonnoir est sa génératrice — la distance mesurée sur le flanc du cône de la lèvre extérieure jusqu’au trou de sortie. funnelSlope fixe la pente (rapport Δr/h) et funnelGenLength la longueur de cette génératrice ; hauteur et diamètre en découlent (dérivation). À funnelSlope=0.68 et funnelGenLength=70mm, la génératrice de 70mm donne assez de course pour ramener vers le trou l’eau tombant plusieurs centimètres hors axe, absorbant les imprécisions de pose et évitant les projections hors du cône.

Le diamètre du trou à la jonction avec le collet doit avoir à peu près la même dimension, pour éviter d’avoir des parties en l’air lors de l’impression.

funnelSlope=0.68;
funnelGenLength=70;
funnelHoleDiameter=39;
funnelHeight=funnelGenLength/sqrt(funnelSlope*funnelSlope+1);
funnelTopDiameter=2*(funnelSlope*funnelHeight+funnelHoleDiameter/2);

translate([0,0,-funnelHeight])
difference()
{
  cylinder(h=funnelHeight, r1=funnelTopDiameter/2+wallThickness, r2=funnelHoleDiameter/2+wallThickness);
  translate([0,0,-0.1])
    cylinder(h=funnelHeight+0.2, r1=funnelTopDiameter/2, r2=funnelHoleDiameter/2);
}

difference()
{
  translate([0,0,wallThickness/2])
    cylinder(h=wallThickness, r=innerDiameter/2, center=true);
  cylinder(h=wallThickness*3, r=funnelHoleDiameter/2, center=true);
}

translate([0,0,-funnelHeight])
difference()
{
  cylinder(h=funnelHeight, r1=funnelTopDiameter/2+wallThickness, r2=funnelHoleDiameter/2+wallThickness);
  translate([0,0,-0.1])
    cylinder(h=funnelHeight+0.2, r1=funnelTopDiameter/2, r2=funnelHoleDiameter/2);
}

union()
{
  capThread();

  difference()
  {
    union()
      {
        translate([0,0,innerDepth/2+wallThickness])
          cylinder(h=innerDepth,r=(innerDiameter)/2+wallThickness,center=true);

        translate([0,0,wallThickness])
          rotate_extrude(convexity = 10)
          translate([innerDiameter/2, 0, 0])
          circle(r = wallThickness, $fn = smoothness);
      }
    cylinder(h=innerDepth*4,r=innerDiameter/2,center=true);
  }

  translate([0,0,wallThickness*1.5])
  rotate([180,0,0])
  rotate_extrude(convexity = 10)
  translate([innerDiameter/2-wallThickness/2, 0, 0])
  difference()
  {
    square(wallThickness,center=false);
    circle(r = wallThickness/2, $fn = smoothness);
  }

  for(ridge=[0:ridges-1])
    {
      hull()
        {
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,wallThickness*5/4])
            sphere(r=wallThickness/4,$fn=smoothness);
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,innerDepth+wallThickness*3/4])
            sphere(r=wallThickness/4,$fn=smoothness);
        }
    }
  difference()
  {
    translate([0,0,wallThickness/2])
      cylinder(h=wallThickness, r=innerDiameter/2, center=true);
    cylinder(h=wallThickness*3, r=funnelHoleDiameter/2, center=true);
  }

  translate([0,0,-funnelHeight])
  difference()
  {
    cylinder(h=funnelHeight, r1=funnelTopDiameter/2+wallThickness, r2=funnelHoleDiameter/2+wallThickness);
    translate([0,0,-0.1])
      cylinder(h=funnelHeight+0.2, r1=funnelTopDiameter/2, r2=funnelHoleDiameter/2);
  }
    }

union()
{
  $fn=50;

  union()
  {
    // cylinder(h=wallThickness,r=innerDiameter/2-sealGap);
    difference()
    {
      cylinder(h=sealHeight,r=innerDiameter/2-sealGap);
      cylinder(h=sealHeight+1,r=innerDiameter/2-sealGap-sealThickness);
    }
      }
  difference()
  {
    translate([0,0,wallThickness/2])
      cylinder(h=wallThickness, r=innerDiameter/2, center=true);
    cylinder(h=wallThickness*3, r=funnelHoleDiameter/2, center=true);
  }

  translate([0,0,-funnelHeight])
  difference()
  {
    cylinder(h=funnelHeight, r1=funnelTopDiameter/2+wallThickness, r2=funnelHoleDiameter/2+wallThickness);
    translate([0,0,-0.1])
      cylinder(h=funnelHeight+0.2, r1=funnelTopDiameter/2, r2=funnelHoleDiameter/2);
  }
    }

Bouchon entonnoir MIEUXA (S 40/41)

precision=3;
wallThickness=2;
innerDiameter=42;
innerDepth=15;
threadPitch=3.50;
threadDepth=1.25;
threadWidth=1.3;
threadTurns=1.5;
threads=1;
threadStart=1.3;
ridges=25;
sealHeight=7;
sealThickness=1.2;
sealGap=3.6;
funnelSlope=0.68;
funnelGenLength=70;
funnelHoleDiameter=39;
funnelHeight=funnelGenLength/sqrt(funnelSlope*funnelSlope+1);
funnelTopDiameter=2*(funnelSlope*funnelHeight+funnelHoleDiameter/2);
smoothness=(precision==1)?4:((precision==2)?6:20);
facets=(precision==1)?6:((precision==2)?20:50);
segments=facets*threadTurns;
$fn=max(20,facets);
module screwThread(stR,stZ,stPitch,stDepth,stWidth,stTurns,stN,stSmooth,stFacets)
{
  stSegs=stFacets*stTurns;
  union()
    {
      for(t=[0:stN-1])
        {
          rotate([0,0,360*t/stN])
            translate([stR,0,stZ])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
          rotate([0,0,360*t/stN+stTurns*360])
            translate([stR,0,stZ+stPitch*stN*stTurns])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
        }
      for(tw=[0:stSegs-1])
        {
          translate([0,0,stZ])
            union()
            {
              for(t=[0:stN-1])
                {
                  translate([0,0,stPitch*stN*tw/stSegs*stTurns])
                    rotate([0,0,360*(t/stN)+tw*360*stTurns/stSegs])
                    rotate([atan(stPitch*stN/(stR*2*3.1416)),0,0])
                    rotate_extrude(convexity=10,angle=360/stFacets,$fn=50)
                    translate([stR,0,0])
                    scale([stDepth,stWidth/2,0])circle(r=1,$fn=stSmooth);
                }
            }
        }
    }
}

module capThread()
{
  screwThread(innerDiameter/2,wallThickness+innerDepth-threadStart-threadTurns*threadPitch*threads-threadWidth/2,threadPitch,threadDepth,threadWidth,threadTurns,threads,smoothness,facets);
}
union()
{
  capThread();

  difference()
  {
    union()
      {
        translate([0,0,innerDepth/2+wallThickness])
          cylinder(h=innerDepth,r=(innerDiameter)/2+wallThickness,center=true);

        translate([0,0,wallThickness])
          rotate_extrude(convexity = 10)
          translate([innerDiameter/2, 0, 0])
          circle(r = wallThickness, $fn = smoothness);
      }
    cylinder(h=innerDepth*4,r=innerDiameter/2,center=true);
  }

  translate([0,0,wallThickness*1.5])
  rotate([180,0,0])
  rotate_extrude(convexity = 10)
  translate([innerDiameter/2-wallThickness/2, 0, 0])
  difference()
  {
    square(wallThickness,center=false);
    circle(r = wallThickness/2, $fn = smoothness);
  }

  for(ridge=[0:ridges-1])
    {
      hull()
        {
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,wallThickness*5/4])
            sphere(r=wallThickness/4,$fn=smoothness);
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,innerDepth+wallThickness*3/4])
            sphere(r=wallThickness/4,$fn=smoothness);
        }
    }
  difference()
  {
    translate([0,0,wallThickness/2])
      cylinder(h=wallThickness, r=innerDiameter/2, center=true);
    cylinder(h=wallThickness*3, r=funnelHoleDiameter/2, center=true);
  }

  translate([0,0,-funnelHeight])
  difference()
  {
    cylinder(h=funnelHeight, r1=funnelTopDiameter/2+wallThickness, r2=funnelHoleDiameter/2+wallThickness);
    translate([0,0,-0.1])
      cylinder(h=funnelHeight+0.2, r1=funnelTopDiameter/2, r2=funnelHoleDiameter/2);
  }
    }

Bouchon entonnoir Netto/Ardea (custom d=40, pas=4.0)

precision=3;
wallThickness=2;
innerDiameter=40;
innerDepth=15;
threadPitch=4.00;
threadDepth=1.25;
threadWidth=1.3;
threadTurns=3;
threads=1;
threadStart=1.3;
ridges=25;
sealHeight=8;
sealThickness=1.0;
sealGap=3.05;
funnelSlope=0.68;
funnelGenLength=70;
funnelHoleDiameter=39;
funnelHeight=funnelGenLength/sqrt(funnelSlope*funnelSlope+1);
funnelTopDiameter=2*(funnelSlope*funnelHeight+funnelHoleDiameter/2);
smoothness=(precision==1)?4:((precision==2)?6:20);
facets=(precision==1)?6:((precision==2)?20:50);
segments=facets*threadTurns;
$fn=max(20,facets);
module screwThread(stR,stZ,stPitch,stDepth,stWidth,stTurns,stN,stSmooth,stFacets)
{
  stSegs=stFacets*stTurns;
  union()
    {
      for(t=[0:stN-1])
        {
          rotate([0,0,360*t/stN])
            translate([stR,0,stZ])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
          rotate([0,0,360*t/stN+stTurns*360])
            translate([stR,0,stZ+stPitch*stN*stTurns])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
        }
      for(tw=[0:stSegs-1])
        {
          translate([0,0,stZ])
            union()
            {
              for(t=[0:stN-1])
                {
                  translate([0,0,stPitch*stN*tw/stSegs*stTurns])
                    rotate([0,0,360*(t/stN)+tw*360*stTurns/stSegs])
                    rotate([atan(stPitch*stN/(stR*2*3.1416)),0,0])
                    rotate_extrude(convexity=10,angle=360/stFacets,$fn=50)
                    translate([stR,0,0])
                    scale([stDepth,stWidth/2,0])circle(r=1,$fn=stSmooth);
                }
            }
        }
    }
}

module capThread()
{
  screwThread(innerDiameter/2,wallThickness+innerDepth-threadStart-threadTurns*threadPitch*threads-threadWidth/2,threadPitch,threadDepth,threadWidth,threadTurns,threads,smoothness,facets);
}
union()
{
  capThread();

  difference()
  {
    union()
      {
        translate([0,0,innerDepth/2+wallThickness])
          cylinder(h=innerDepth,r=(innerDiameter)/2+wallThickness,center=true);

        translate([0,0,wallThickness])
          rotate_extrude(convexity = 10)
          translate([innerDiameter/2, 0, 0])
          circle(r = wallThickness, $fn = smoothness);
      }
    cylinder(h=innerDepth*4,r=innerDiameter/2,center=true);
  }

  translate([0,0,wallThickness*1.5])
  rotate([180,0,0])
  rotate_extrude(convexity = 10)
  translate([innerDiameter/2-wallThickness/2, 0, 0])
  difference()
  {
    square(wallThickness,center=false);
    circle(r = wallThickness/2, $fn = smoothness);
  }

  for(ridge=[0:ridges-1])
    {
      hull()
        {
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,wallThickness*5/4])
            sphere(r=wallThickness/4,$fn=smoothness);
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,innerDepth+wallThickness*3/4])
            sphere(r=wallThickness/4,$fn=smoothness);
        }
    }
  difference()
  {
    translate([0,0,wallThickness/2])
      cylinder(h=wallThickness, r=innerDiameter/2, center=true);
    cylinder(h=wallThickness*3, r=funnelHoleDiameter/2, center=true);
  }

  translate([0,0,-funnelHeight])
  difference()
  {
    cylinder(h=funnelHeight, r1=funnelTopDiameter/2+wallThickness, r2=funnelHoleDiameter/2+wallThickness);
    translate([0,0,-0.1])
      cylinder(h=funnelHeight+0.2, r1=funnelTopDiameter/2, r2=funnelHoleDiameter/2);
  }
    }

Mise en place d’un pas de vis à course limitée entre entonnoir et bouchon

Pourquoi imprimer le bouchon et l’entonnoir en deux pièces séparées : on veut orienter le tuyau de trop-plein librement une fois le bouchon vissé. Avec un monobloc, l’orientation du tuyau dépend de combien de tours on a serré le filetage sur le col — incontrôlable. En séparant bouchon (filetage sur le col) et entonnoir (qui porte le tuyau), puis en les assemblant par un pas de vis multi-tours qui se termine dans une gorge annulaire, on visse d’abord le bouchon à fond sur le bidon, puis on engage l’entonnoir et on le serre jusqu’à ce que le filet femelle de la jupe quitte le filetage mâle du collet pour tomber dans la gorge — la rotation devient alors libre à 360°, ce qui permet de pointer le tuyau vers le bidon suivant sans défaire le serrage.

L’assemblage combine trois fonctions :

  • étanchéité axiale par un joint fibre plat posé sur le plancher du collet, écrasé par la pointe annulaire du manchon descendant du bouchon ;
  • tenue mécanique radiale par une jupe extérieure du bouchon qui enveloppe le collet, avec son filetage femelle piégé dans la gorge annulaire ceinturant le collet — translation axiale bloquée par le pied du filetage mâle au-dessus, qui referme la gorge ;
  • rotation libre après que le filet femelle a quitté l’engagement avec le filet mâle pour rejoindre la gorge, celle-ci étant continue sur 360°.

Le filetage compte plusieurs tours d’engagement : ça donne une course de descente longue avec une rotation totale importante, ce qui permet d’arrêter le serrage à n’importe quel angle dans la zone d’engagement avant la chute dans la gorge — utile pour ajuster l’orientation du tuyau de trop-plein si la chute dans la gorge ne tombe pas pile sur l’angle voulu.

Manchon du bouchon (plug tube)

Le manchon est un tube creux qui descend du bouchon dans le collet pour écraser le joint. Son Ø extérieur est juste sous le Ø intérieur du collet (0.3mm de jeu) pour glisser sans frotter ; la paroi (2.5mm) est assez raide pour transmettre la compression du joint sans fléchir.

jointWall=2.5;
jointPlugR=funnelHoleDiameter/2-0.3;
jointPlugInnerR=jointPlugR-jointWall;

Joint fibre plat

On choisit un joint fibre plat du commerce (plomberie, jardinerie, 30x38x2mm ici) plutôt qu’un joint imprimé ou un o-ring. Arguments : pas cher, trouvable partout, fiable en statique, insensible à la pression basse qu’on a ici, et compense les imperfections de surface de l’impression 3D (le PLA n’est pas miroir-lisse). On le pose à plat sur le plancher du collet ; la pointe du manchon vient l’écraser de 0.3mm (compression contrôlée pour ne pas déchirer la fibre).

Le plancher du collet a un petit bec de rétention qui dépasse radialement dans le trou central (=gasketRetentionLip~) : ça empêche le joint de tomber par gravité dans la cavité du cône quand on manipule l’entonnoir avant clipsage.

gasketID=30;
gasketOD=38;
gasketThickness=2.0;
gasketCompression=0.3;
gasketSeatClearance=0.2;
gasketRetentionLip=0.5;

Filetage + gorge annulaire

Le principe : la paroi externe du collet porte un filetage mâle hélicoïdal multi-tours qui descend depuis le sommet du collet jusqu’à une gorge annulaire continue sur 360° en pied du filet. La face interne de la jupe du bouchon porte le filetage femelle correspondant — empreinte du mâle, même pas, même profondeur, même sens. Pour visser : on présente le bouchon au-dessus du collet, on engage les premiers filets en haut, puis on tourne dans le sens serrage — le filet femelle descend le long du filet mâle au rythme du pas. Arrivé en bas, le filet femelle quitte le filet mâle pour glisser dans la gorge ; la translation axiale est piégée vers le haut par le pied du filet mâle qui referme le plafond de la gorge, la rotation reste libre puisque la gorge est continue sur 360°.

Calibration de la descente. Le pied du filet mâle (= plafond de la gorge, collarGrooveTopZ) est calé pour que, quand le sommet du filet femelle l’atteint en descendant, la pointe du manchon ait écrasé le joint fibre exactement de gasketCompression. Ça se gère via la hauteur du manchon (jointHeight) et l’ancrage du pied du filet femelle au bord ouvert de la jupe. La descente pendant le serrage est progressive (hélicoïdale continue) : on sent la résistance monter doucement, on peut arrêter avant la chute dans la gorge si le tuyau de trop-plein se trouve déjà bien orienté à un pas près, ou continuer jusqu’à la chute pour bénéficier de la rotation libre.

Single-start avec plusieurs tours. Un seul filet hélicoïdal qui tourne sur collarThreadTurns tours pour atteindre la gorge. Avantage : le ratio rotation/descente est élevé, donc l’angle final dans la zone d’engagement (avant la chute dans la gorge) se règle finement. Avec collarThreadTurns × 360° de rotation totale pour collarThreadPitch × collarThreadTurns mm de descente, on dispose de plusieurs positions sealed-and-correctly-oriented dans la course, espacées chacune d’un pitch (= 360° de rotation).

collarThreadPitch=2.0;        // mm/turn — pas du filetage
collarThreadTurns=2;          // tours d'engagement
collarThreadStarts=1;         // single-start
collarThreadDepth=1.0;        // profondeur radiale du filet (mm)
collarThreadWidth=1.2;        // largeur axiale du profil de filet (mm)
collarSkirtClearance=0.3;     // jeu radial entre crête mâle et flanc lisse de la jupe (mm)
collarGrooveDepthExtra=0.5;   // surcreusement de la gorge sous le mur du collet (mm)
collarGrooveAxialExtra=0.5;   // jeu axial entre filet femelle et plancher de la gorge (mm)
collarThreadAboveGasket=3.7;  // hauteur du pied du filet mâle au-dessus du joint (mm)
collarWall=2.5;               // épaisseur paroi du collet (mm)
collarDiskGap=1.0;            // jeu axial disque bouchon / haut du collet (mm)

Géométrie dérivée

Les trois blocs précédents (manchon, joint, filetage) fournissent tous les paramètres utilisateur. Tout le reste — les rayons imbriqués du collet et de la jupe, la position axiale de la gorge, la hauteur du collet et du manchon, la position du filet femelle sur la jupe — se déduit.

Deux quantités méritent un mot. jointHeight (hauteur du manchon) est calibrée pour que, une fois le bouchon vissé à fond sur le collet, la pointe du manchon appuie sur le joint fibre exactement avec la compression voulue (gasketCompression). collarGrooveTopZ — le pied du filet mâle, qui est aussi le plafond de la gorge — est placé à la hauteur axiale telle que le filet femelle l’atteint précisément quand le manchon a fini son écrasement du joint.

La hauteur du collet collarHeight = collarGrooveTopZ + collarThreadAxialExtentcollarThreadAxialExtent = collarThreadPitch × collarThreadTurns est la course axiale couverte par le filetage. Le filet mâle commence pile à collarGrooveTopZ et monte jusqu’au sommet du collet.

La gorge a une hauteur axiale collarThreadAxialExtent + collarGrooveAxialExtra : assez pour absorber la crête du filet femelle complète quand celle-ci quitte le filet mâle, plus un jeu sous la pointe (collarGrooveAxialExtra) pour que la chute soit franche.

Le filet femelle de la jupe, lui, débouche pile sur le bord ouvert du bas de la jupe — sinon il n’y a pas de point d’entrée pour engager le filet mâle au démarrage du vissage. Comme le filet femelle est ancré au bord ouvert (z=-jointHeight) et qu’il monte sur collarThreadAxialExtent, son sommet est à z=-jointHeight + collarThreadAxialExtent dans le repère du bouchon. Pour que ce sommet coïncide avec le plafond de la gorge à l’arrêt (manchon en compression), on fixe collarThreadAboveGasket = collarThreadAxialExtent - gasketCompression — la valeur exacte qui aligne simultanément (1) le pied du filet femelle au bord ouvert et (2) le sommet du filet femelle contre le plafond de la gorge quand le joint est compressé.

// Rayons
jointBoreR=gasketOD/2+gasketSeatClearance;
collarFloorHoleR=gasketID/2-gasketRetentionLip;
collarOuterR=jointBoreR+collarWall;
// Crête mâle au-dessus du mur du collet ; flanc lisse de la jupe au-delà,
// décalé d'un jeu collarSkirtClearance pour que les deux crêtes
// s'engagent sans frotter sur la paroi opposée.
threadCrestR=collarOuterR+collarThreadDepth;
skirtInnerR=threadCrestR+collarSkirtClearance;
skirtOuterR=skirtInnerR+jointWall;
femaleCrestR=skirtInnerR-collarThreadDepth;
// Gorge surcreusée par rapport au mur du collet pour absorber la
// crête du filet femelle quand celle-ci y descend en fin de course.
collarGrooveInnerR=collarOuterR-collarGrooveDepthExtra;

// Cotes axiales
collarThreadAxialExtent=collarThreadPitch*collarThreadTurns;
// Plafond de la gorge = pied du filet mâle. Au moment où le filet
// femelle atteint cette cote en descendant, le manchon a écrasé
// le joint de gasketCompression.
collarGrooveTopZ=wallThickness+gasketThickness+collarThreadAboveGasket;
collarThreadBottomZ=collarGrooveTopZ;
collarThreadTopZ=collarThreadBottomZ+collarThreadAxialExtent;
collarHeight=collarThreadTopZ;
collarGrooveBottomZ=collarGrooveTopZ-collarThreadAxialExtent-collarGrooveAxialExtra;

// Hauteur du manchon — cote du bottom de la jupe sous le disque du
// bouchon, calée sur la compression du joint à l'arrêt.
jointHeight=collarHeight+collarDiskGap-(wallThickness+gasketThickness-gasketCompression);

jointWall=2.5;
jointPlugR=funnelHoleDiameter/2-0.3;
jointPlugInnerR=jointPlugR-jointWall;
gasketID=30;
gasketOD=38;
gasketThickness=2.0;
gasketCompression=0.3;
gasketSeatClearance=0.2;
gasketRetentionLip=0.5;
collarThreadPitch=2.0;        // mm/turn — pas du filetage
collarThreadTurns=2;          // tours d'engagement
collarThreadStarts=1;         // single-start
collarThreadDepth=1.0;        // profondeur radiale du filet (mm)
collarThreadWidth=1.2;        // largeur axiale du profil de filet (mm)
collarSkirtClearance=0.3;     // jeu radial entre crête mâle et flanc lisse de la jupe (mm)
collarGrooveDepthExtra=0.5;   // surcreusement de la gorge sous le mur du collet (mm)
collarGrooveAxialExtra=0.5;   // jeu axial entre filet femelle et plancher de la gorge (mm)
collarThreadAboveGasket=3.7;  // hauteur du pied du filet mâle au-dessus du joint (mm)
collarWall=2.5;               // épaisseur paroi du collet (mm)
collarDiskGap=1.0;            // jeu axial disque bouchon / haut du collet (mm)
// Rayons
jointBoreR=gasketOD/2+gasketSeatClearance;
collarFloorHoleR=gasketID/2-gasketRetentionLip;
collarOuterR=jointBoreR+collarWall;
// Crête mâle au-dessus du mur du collet ; flanc lisse de la jupe au-delà,
// décalé d'un jeu collarSkirtClearance pour que les deux crêtes
// s'engagent sans frotter sur la paroi opposée.
threadCrestR=collarOuterR+collarThreadDepth;
skirtInnerR=threadCrestR+collarSkirtClearance;
skirtOuterR=skirtInnerR+jointWall;
femaleCrestR=skirtInnerR-collarThreadDepth;
// Gorge surcreusée par rapport au mur du collet pour absorber la
// crête du filet femelle quand celle-ci y descend en fin de course.
collarGrooveInnerR=collarOuterR-collarGrooveDepthExtra;

// Cotes axiales
collarThreadAxialExtent=collarThreadPitch*collarThreadTurns;
// Plafond de la gorge = pied du filet mâle. Au moment où le filet
// femelle atteint cette cote en descendant, le manchon a écrasé
// le joint de gasketCompression.
collarGrooveTopZ=wallThickness+gasketThickness+collarThreadAboveGasket;
collarThreadBottomZ=collarGrooveTopZ;
collarThreadTopZ=collarThreadBottomZ+collarThreadAxialExtent;
collarHeight=collarThreadTopZ;
collarGrooveBottomZ=collarGrooveTopZ-collarThreadAxialExtent-collarGrooveAxialExtra;

// Hauteur du manchon — cote du bottom de la jupe sous le disque du
// bouchon, calée sur la compression du joint à l'arrêt.
jointHeight=collarHeight+collarDiskGap-(wallThickness+gasketThickness-gasketCompression);

Collet de l’entonnoir

Le collet est la partie qui reçoit le bouchon : un cylindre creux posé au sommet (bas-réel) de l’entonnoir, avec le filetage mâle ajouté sur sa paroi externe et la gorge annulaire soustraite en pied du filet, et le plancher support du joint à l’intérieur.

Coque du collet. Anneau cylindrique plein 360°, obtenu par rotate_extrude d’un rectangle (r,z) entre jointBoreR et collarOuterR, de z=0 à z=collarHeight.

rotate_extrude(convexity=10)
polygon(points=[
                [jointBoreR, 0],
                [collarOuterR, 0],
                [collarOuterR, collarHeight],
                [jointBoreR, collarHeight]
                ]);

Filetage mâle (additif). Hélice continue plaquée sur la paroi externe du collet à r=collarOuterR. Pied à collarThreadBottomZ, sommet à collarThreadTopZ — qui est aussi le sommet du collet, donc le filet débouche pile en haut, point d’engagement naturel pour le filet femelle de la jupe. Le profil elliptique (profondeur collarThreadDepth, largeur axiale collarThreadWidth) est généré par le module générique screwThread.

screwThread(collarOuterR,
            collarThreadBottomZ,
            collarThreadPitch,
            collarThreadDepth,
            collarThreadWidth,
            collarThreadTurns,
            collarThreadStarts,
            smoothness,
            facets);

Gorge annulaire (soustractive). Recess 360° en pied du filet mâle, axialement de collarGrooveBottomZ à collarGrooveTopZ, radialement de collarGrooveInnerR jusqu’au-delà de collarOuterR (le débordement à +1mm garantit qu’on retire bien tout le mur dans cette zone). Le surcreusement collarGrooveDepthExtra par rapport au mur extérieur du collet dégage assez de jeu radial pour que les crêtes du filet femelle de la jupe y descendent sans accrocher.

rotate_extrude(convexity=10)
polygon(points=[
                [collarGrooveInnerR, collarGrooveBottomZ],
                [collarOuterR + 1,   collarGrooveBottomZ],
                [collarOuterR + 1,   collarGrooveTopZ],
                [collarGrooveInnerR, collarGrooveTopZ]
                ]);

Plancher support du joint. Disque plein percé en son centre pour laisser passer l’eau qui descend dans la cavité du cône. Le bord du trou porte le bec de rétention du joint (cf. gasketRetentionLip).

difference()
{
  cylinder(h=wallThickness,r=jointBoreR);
  cylinder(h=wallThickness*3,r=collarFloorHoleR,center=true);
}

Chanfrein sous le plancher. Sans ce chanfrein, l’anneau intérieur du plancher (de collarFloorHoleR à funnelHoleDiameter/2) est en porte-à-faux au-dessus de la cavité du cône — impression impossible sans support. Le chanfrein descend depuis le bord du trou du plancher et rejoint la paroi intérieure du cône qui s’élargit en descendant. Triangle (r,z) qui se ferme là où les deux pentes se croisent.

collarChamferAngle = angle du chanfrein par rapport à l’horizontale. Plus il est grand, plus le chanfrein est vertical (meilleur pour l’impression) mais plus il plonge profondément dans la cavité du cône avant de rejoindre la paroi (il doit rattraper la pente fixe du cône). 45° est la limite PLA avec buse fine ; en buse 0.8 on force à 50° pour éviter les ratés d’impression. Contrainte : doit rester inférieur à l’angle du cône lui-même (sinon le chanfrein parallélise la paroi et ne la rejoint jamais).

collarChamferAngle=50;
funnelInnerSlope=(funnelTopDiameter-funnelHoleDiameter)/(2*funnelHeight);
chamferDrPerDz=1/tan(collarChamferAngle);
chamferDepth=(funnelHoleDiameter/2-collarFloorHoleR)/(chamferDrPerDz-funnelInnerSlope);
rotate_extrude(convexity=10)
polygon(points=[
                [collarFloorHoleR,0],
                [funnelHoleDiameter/2,0],
                [funnelHoleDiameter/2+funnelInnerSlope*chamferDepth,-chamferDepth]
                ]);

Assemblage final. La coque + le filetage mâle + le plancher + le chanfrein sous le plancher, moins la gorge annulaire. Une seule opération difference().

difference()
{
  union()
    {
      rotate_extrude(convexity=10)
      polygon(points=[
                      [jointBoreR, 0],
                      [collarOuterR, 0],
                      [collarOuterR, collarHeight],
                      [jointBoreR, collarHeight]
                      ]);
      screwThread(collarOuterR,
                  collarThreadBottomZ,
                  collarThreadPitch,
                  collarThreadDepth,
                  collarThreadWidth,
                  collarThreadTurns,
                  collarThreadStarts,
                  smoothness,
                  facets);
      difference()
      {
        cylinder(h=wallThickness,r=jointBoreR);
        cylinder(h=wallThickness*3,r=collarFloorHoleR,center=true);
      }
      collarChamferAngle=50;
      funnelInnerSlope=(funnelTopDiameter-funnelHoleDiameter)/(2*funnelHeight);
      chamferDrPerDz=1/tan(collarChamferAngle);
      chamferDepth=(funnelHoleDiameter/2-collarFloorHoleR)/(chamferDrPerDz-funnelInnerSlope);
      rotate_extrude(convexity=10)
      polygon(points=[
                      [collarFloorHoleR,0],
                      [funnelHoleDiameter/2,0],
                      [funnelHoleDiameter/2+funnelInnerSlope*chamferDepth,-chamferDepth]
                      ]);
    }
  rotate_extrude(convexity=10)
  polygon(points=[
                  [collarGrooveInnerR, collarGrooveBottomZ],
                  [collarOuterR + 1,   collarGrooveBottomZ],
                  [collarOuterR + 1,   collarGrooveTopZ],
                  [collarGrooveInnerR, collarGrooveTopZ]
                  ]);
}

union()
{
  translate([0,0,-funnelHeight])
  difference()
  {
    cylinder(h=funnelHeight, r1=funnelTopDiameter/2+wallThickness, r2=funnelHoleDiameter/2+wallThickness);
    translate([0,0,-0.1])
      cylinder(h=funnelHeight+0.2, r1=funnelTopDiameter/2, r2=funnelHoleDiameter/2);
  }
  difference()
  {
    union()
      {
        rotate_extrude(convexity=10)
        polygon(points=[
                        [jointBoreR, 0],
                        [collarOuterR, 0],
                        [collarOuterR, collarHeight],
                        [jointBoreR, collarHeight]
                        ]);
        screwThread(collarOuterR,
                    collarThreadBottomZ,
                    collarThreadPitch,
                    collarThreadDepth,
                    collarThreadWidth,
                    collarThreadTurns,
                    collarThreadStarts,
                    smoothness,
                    facets);
        difference()
        {
          cylinder(h=wallThickness,r=jointBoreR);
          cylinder(h=wallThickness*3,r=collarFloorHoleR,center=true);
        }
        collarChamferAngle=50;
        funnelInnerSlope=(funnelTopDiameter-funnelHoleDiameter)/(2*funnelHeight);
        chamferDrPerDz=1/tan(collarChamferAngle);
        chamferDepth=(funnelHoleDiameter/2-collarFloorHoleR)/(chamferDrPerDz-funnelInnerSlope);
        rotate_extrude(convexity=10)
        polygon(points=[
                        [collarFloorHoleR,0],
                        [funnelHoleDiameter/2,0],
                        [funnelHoleDiameter/2+funnelInnerSlope*chamferDepth,-chamferDepth]
                        ]);
      }
    rotate_extrude(convexity=10)
    polygon(points=[
                    [collarGrooveInnerR, collarGrooveBottomZ],
                    [collarOuterR + 1,   collarGrooveBottomZ],
                    [collarOuterR + 1,   collarGrooveTopZ],
                    [collarGrooveInnerR, collarGrooveTopZ]
                    ]);
  }
    }

Corps du bouchon

Le bouchon s’imprime plug-down : le manchon et la jupe descendent sous le disque de fermeture, le tout reposant sur le plateau (même cote z=-jointHeight). Le dessous du disque ponte les deux (~3mm à traverser en bridge sans support), orientation naturelle pour ne pas avoir de porte-à-faux sur la géométrie délicate. L’assemblage se fait en cinq composants unifiés.

Filetage. Généré par le wrapper capThread() (qui appelle le module générique screwThread avec les paramètres du bouchon). Spirale paramétrée par params-mieuxa ou params-netto selon le col visé.

capThread();

Paroi filetée. Anneau cylindrique sur lequel s’enroule le filetage. Elle descend jusqu’à z=0 (et coexiste avec le disque sur [0, wallThickness]) au lieu de démarrer en pointe à z=wallThickness — sinon on obtient un knife edge sur le pourtour du disque qui fragilise l’impression. Supportée en bas par le disque et la jupe, donc pas besoin de chanfrein.

rotate_extrude(convexity=10)
polygon(points=[
                [innerDiameter/2, 0],
                [innerDiameter/2+wallThickness, 0],
                [innerDiameter/2+wallThickness, innerDepth+wallThickness],
                [innerDiameter/2, innerDepth+wallThickness]
                ]);

Disque de fermeture. Ferme le dessus du bouchon (sinon l’eau coulerait dans le filetage). Trapèze (r,z) : à z=0 il va de jointPlugInnerR jusqu’à skirtInnerR (pour rejoindre la jupe) ; à z=wallThickness il revient à innerDiameter/2 (pour rejoindre la paroi filetée au-dessus). Cette bascule de rayon crée l’étagement “jupe en bas, paroi au-dessus”.

rotate_extrude(convexity=10)
polygon(points=[
                [jointPlugInnerR, 0],
                [skirtInnerR, 0],
                [innerDiameter/2, wallThickness],
                [jointPlugInnerR, wallThickness]
                ]);

Manchon (tube plug). Tube creux qui descend sous le disque vers le plateau. Sa pointe annulaire (rayon jointPlugR extérieur, jointPlugInnerR intérieur) est ce qui écrase le joint fibre. Les cylindres extérieur et intérieur du difference() ont exactement la même hauteur : une dissymétrie (même 0.01mm) créerait à l’interface avec le disque (au même rayon jointPlugInnerR) des faces coplanaires dupliquées qui font tousser CGAL et produisent un STL non-manifold.

translate([0,0,-jointHeight])
difference()
{
  cylinder(h=jointHeight, r=jointPlugR);
  cylinder(h=jointHeight, r=jointPlugInnerR);
}

Jupe extérieure. Descend du disque jusqu’à z=-jointHeight (même cote que le bas du manchon). Cylindre creux 360° (plug-skirt-cylinder) auquel on unit le filetage femelle (plug-skirt-female-thread) — une crête hélicoïdale qui dépasse vers l’axe depuis la face interne de la jupe.

rotate_extrude(convexity=10)
polygon(points=[
                [skirtOuterR, -jointHeight],
                [skirtOuterR, 0],
                [skirtInnerR, 0],
                [skirtInnerR, -jointHeight]
                ]);

Filetage femelle. Crête hélicoïdale ajoutée à la face interne de la jupe, miroir du filet mâle : même pas, même profondeur, même largeur, même nombre de tours, même sens. Le screwThread est placé à r=skirtInnerR (le flanc lisse de la jupe), et son profil ellipsoïdal s’étend ±collarThreadDepth radialement — la moitié interne (r < skirtInnerR) protrude dans la cavité comme la crête visible du filet, la moitié externe (r > skirtInnerR) se fond dans la matière de la jupe sans effet visible.

Le pied du filet femelle est placé à z = -jointHeight — le bord ouvert du bas de la jupe — pour donner un point d’entrée hélicoïdal au démarrage du vissage. La sphère de bout du screwThread déborde axialement sous la jupe par collarThreadWidth/2 ; on la clippe par une intersection avec un cylindre borné à la hauteur de la jupe, pour que la pièce reste imprimable plug-down (pas de porte-à-faux sous le plateau).

intersection()
{
  translate([0, 0, -jointHeight])
    cylinder(h=jointHeight, r=skirtInnerR + collarThreadDepth + 1);
  translate([0, 0, -jointHeight])
    screwThread(skirtInnerR,
                0,
                collarThreadPitch,
                collarThreadDepth,
                collarThreadWidth,
                collarThreadTurns,
                collarThreadStarts,
                smoothness,
                facets);
}

union()
{
  rotate_extrude(convexity=10)
  polygon(points=[
                  [skirtOuterR, -jointHeight],
                  [skirtOuterR, 0],
                  [skirtInnerR, 0],
                  [skirtInnerR, -jointHeight]
                  ]);
  intersection()
  {
    translate([0, 0, -jointHeight])
      cylinder(h=jointHeight, r=skirtInnerR + collarThreadDepth + 1);
    translate([0, 0, -jointHeight])
      screwThread(skirtInnerR,
                  0,
                  collarThreadPitch,
                  collarThreadDepth,
                  collarThreadWidth,
                  collarThreadTurns,
                  collarThreadStarts,
                  smoothness,
                  facets);
  }
}

Bague d’étanchéité sur le col du bidon. Un petit anneau intérieur qui vient sceller contre la lèvre du col du bidon pendant le vissage (avant que le joint fibre plat ne prenne le relais).

Stries extérieures. Cannelures verticales espacées régulièrement sur la paroi extérieure, pour la préhension pendant le vissage.

for(ridge=[0:ridges-1])
  {
    hull()
      {
        rotate([0,0,360/ridges*ridge])
          translate([innerDiameter/2+wallThickness,0,wallThickness*5/4])
          sphere(r=wallThickness/4,$fn=smoothness);
        rotate([0,0,360/ridges*ridge])
          translate([innerDiameter/2+wallThickness,0,innerDepth+wallThickness*3/4])
          sphere(r=wallThickness/4,$fn=smoothness);
      }
  }

union()
{
  capThread();
  rotate_extrude(convexity=10)
  polygon(points=[
                  [innerDiameter/2, 0],
                  [innerDiameter/2+wallThickness, 0],
                  [innerDiameter/2+wallThickness, innerDepth+wallThickness],
                  [innerDiameter/2, innerDepth+wallThickness]
                  ]);
  rotate_extrude(convexity=10)
  polygon(points=[
                  [jointPlugInnerR, 0],
                  [skirtInnerR, 0],
                  [innerDiameter/2, wallThickness],
                  [jointPlugInnerR, wallThickness]
                  ]);
  translate([0,0,-jointHeight])
  difference()
  {
    cylinder(h=jointHeight, r=jointPlugR);
    cylinder(h=jointHeight, r=jointPlugInnerR);
  }
  union()
  {
    rotate_extrude(convexity=10)
    polygon(points=[
                    [skirtOuterR, -jointHeight],
                    [skirtOuterR, 0],
                    [skirtInnerR, 0],
                    [skirtInnerR, -jointHeight]
                    ]);
    intersection()
    {
      translate([0, 0, -jointHeight])
        cylinder(h=jointHeight, r=skirtInnerR + collarThreadDepth + 1);
      translate([0, 0, -jointHeight])
        screwThread(skirtInnerR,
                    0,
                    collarThreadPitch,
                    collarThreadDepth,
                    collarThreadWidth,
                    collarThreadTurns,
                    collarThreadStarts,
                    smoothness,
                    facets);
    }
  }
  difference()
  {
    cylinder(h=sealHeight,r=innerDiameter/2-sealGap);
    cylinder(h=sealHeight+1,r=innerDiameter/2-sealGap-sealThickness);
  }
  for(ridge=[0:ridges-1])
    {
      hull()
        {
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,wallThickness*5/4])
            sphere(r=wallThickness/4,$fn=smoothness);
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,innerDepth+wallThickness*3/4])
            sphere(r=wallThickness/4,$fn=smoothness);
        }
    }
    }

Bouchon avec manchon MIEUXA (S 40/41)

precision=3;
wallThickness=2;
innerDiameter=42;
innerDepth=15;
threadPitch=3.50;
threadDepth=1.25;
threadWidth=1.3;
threadTurns=1.5;
threads=1;
threadStart=1.3;
ridges=25;
sealHeight=7;
sealThickness=1.2;
sealGap=3.6;
funnelSlope=0.68;
funnelGenLength=70;
funnelHoleDiameter=39;
funnelHeight=funnelGenLength/sqrt(funnelSlope*funnelSlope+1);
funnelTopDiameter=2*(funnelSlope*funnelHeight+funnelHoleDiameter/2);
jointWall=2.5;
jointPlugR=funnelHoleDiameter/2-0.3;
jointPlugInnerR=jointPlugR-jointWall;
gasketID=30;
gasketOD=38;
gasketThickness=2.0;
gasketCompression=0.3;
gasketSeatClearance=0.2;
gasketRetentionLip=0.5;
collarThreadPitch=2.0;        // mm/turn — pas du filetage
collarThreadTurns=2;          // tours d'engagement
collarThreadStarts=1;         // single-start
collarThreadDepth=1.0;        // profondeur radiale du filet (mm)
collarThreadWidth=1.2;        // largeur axiale du profil de filet (mm)
collarSkirtClearance=0.3;     // jeu radial entre crête mâle et flanc lisse de la jupe (mm)
collarGrooveDepthExtra=0.5;   // surcreusement de la gorge sous le mur du collet (mm)
collarGrooveAxialExtra=0.5;   // jeu axial entre filet femelle et plancher de la gorge (mm)
collarThreadAboveGasket=3.7;  // hauteur du pied du filet mâle au-dessus du joint (mm)
collarWall=2.5;               // épaisseur paroi du collet (mm)
collarDiskGap=1.0;            // jeu axial disque bouchon / haut du collet (mm)
// Rayons
jointBoreR=gasketOD/2+gasketSeatClearance;
collarFloorHoleR=gasketID/2-gasketRetentionLip;
collarOuterR=jointBoreR+collarWall;
// Crête mâle au-dessus du mur du collet ; flanc lisse de la jupe au-delà,
// décalé d'un jeu collarSkirtClearance pour que les deux crêtes
// s'engagent sans frotter sur la paroi opposée.
threadCrestR=collarOuterR+collarThreadDepth;
skirtInnerR=threadCrestR+collarSkirtClearance;
skirtOuterR=skirtInnerR+jointWall;
femaleCrestR=skirtInnerR-collarThreadDepth;
// Gorge surcreusée par rapport au mur du collet pour absorber la
// crête du filet femelle quand celle-ci y descend en fin de course.
collarGrooveInnerR=collarOuterR-collarGrooveDepthExtra;

// Cotes axiales
collarThreadAxialExtent=collarThreadPitch*collarThreadTurns;
// Plafond de la gorge = pied du filet mâle. Au moment où le filet
// femelle atteint cette cote en descendant, le manchon a écrasé
// le joint de gasketCompression.
collarGrooveTopZ=wallThickness+gasketThickness+collarThreadAboveGasket;
collarThreadBottomZ=collarGrooveTopZ;
collarThreadTopZ=collarThreadBottomZ+collarThreadAxialExtent;
collarHeight=collarThreadTopZ;
collarGrooveBottomZ=collarGrooveTopZ-collarThreadAxialExtent-collarGrooveAxialExtra;

// Hauteur du manchon — cote du bottom de la jupe sous le disque du
// bouchon, calée sur la compression du joint à l'arrêt.
jointHeight=collarHeight+collarDiskGap-(wallThickness+gasketThickness-gasketCompression);
smoothness=(precision==1)?4:((precision==2)?6:20);
facets=(precision==1)?6:((precision==2)?20:50);
segments=facets*threadTurns;
$fn=max(20,facets);
module screwThread(stR,stZ,stPitch,stDepth,stWidth,stTurns,stN,stSmooth,stFacets)
{
  stSegs=stFacets*stTurns;
  union()
    {
      for(t=[0:stN-1])
        {
          rotate([0,0,360*t/stN])
            translate([stR,0,stZ])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
          rotate([0,0,360*t/stN+stTurns*360])
            translate([stR,0,stZ+stPitch*stN*stTurns])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
        }
      for(tw=[0:stSegs-1])
        {
          translate([0,0,stZ])
            union()
            {
              for(t=[0:stN-1])
                {
                  translate([0,0,stPitch*stN*tw/stSegs*stTurns])
                    rotate([0,0,360*(t/stN)+tw*360*stTurns/stSegs])
                    rotate([atan(stPitch*stN/(stR*2*3.1416)),0,0])
                    rotate_extrude(convexity=10,angle=360/stFacets,$fn=50)
                    translate([stR,0,0])
                    scale([stDepth,stWidth/2,0])circle(r=1,$fn=stSmooth);
                }
            }
        }
    }
}

module capThread()
{
  screwThread(innerDiameter/2,wallThickness+innerDepth-threadStart-threadTurns*threadPitch*threads-threadWidth/2,threadPitch,threadDepth,threadWidth,threadTurns,threads,smoothness,facets);
}
union()
{
  capThread();
  rotate_extrude(convexity=10)
  polygon(points=[
                  [innerDiameter/2, 0],
                  [innerDiameter/2+wallThickness, 0],
                  [innerDiameter/2+wallThickness, innerDepth+wallThickness],
                  [innerDiameter/2, innerDepth+wallThickness]
                  ]);
  rotate_extrude(convexity=10)
  polygon(points=[
                  [jointPlugInnerR, 0],
                  [skirtInnerR, 0],
                  [innerDiameter/2, wallThickness],
                  [jointPlugInnerR, wallThickness]
                  ]);
  translate([0,0,-jointHeight])
  difference()
  {
    cylinder(h=jointHeight, r=jointPlugR);
    cylinder(h=jointHeight, r=jointPlugInnerR);
  }
  union()
  {
    rotate_extrude(convexity=10)
    polygon(points=[
                    [skirtOuterR, -jointHeight],
                    [skirtOuterR, 0],
                    [skirtInnerR, 0],
                    [skirtInnerR, -jointHeight]
                    ]);
    intersection()
    {
      translate([0, 0, -jointHeight])
        cylinder(h=jointHeight, r=skirtInnerR + collarThreadDepth + 1);
      translate([0, 0, -jointHeight])
        screwThread(skirtInnerR,
                    0,
                    collarThreadPitch,
                    collarThreadDepth,
                    collarThreadWidth,
                    collarThreadTurns,
                    collarThreadStarts,
                    smoothness,
                    facets);
    }
  }
  difference()
  {
    cylinder(h=sealHeight,r=innerDiameter/2-sealGap);
    cylinder(h=sealHeight+1,r=innerDiameter/2-sealGap-sealThickness);
  }
  for(ridge=[0:ridges-1])
    {
      hull()
        {
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,wallThickness*5/4])
            sphere(r=wallThickness/4,$fn=smoothness);
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,innerDepth+wallThickness*3/4])
            sphere(r=wallThickness/4,$fn=smoothness);
        }
    }
    }

Bouchon avec manchon Netto/Ardea (custom d=40, pas=4.0)

precision=3;
wallThickness=2;
innerDiameter=40;
innerDepth=15;
threadPitch=4.00;
threadDepth=1.25;
threadWidth=1.3;
threadTurns=3;
threads=1;
threadStart=1.3;
ridges=25;
sealHeight=8;
sealThickness=1.0;
sealGap=3.05;
funnelSlope=0.68;
funnelGenLength=70;
funnelHoleDiameter=39;
funnelHeight=funnelGenLength/sqrt(funnelSlope*funnelSlope+1);
funnelTopDiameter=2*(funnelSlope*funnelHeight+funnelHoleDiameter/2);
jointWall=2.5;
jointPlugR=funnelHoleDiameter/2-0.3;
jointPlugInnerR=jointPlugR-jointWall;
gasketID=30;
gasketOD=38;
gasketThickness=2.0;
gasketCompression=0.3;
gasketSeatClearance=0.2;
gasketRetentionLip=0.5;
collarThreadPitch=2.0;        // mm/turn — pas du filetage
collarThreadTurns=2;          // tours d'engagement
collarThreadStarts=1;         // single-start
collarThreadDepth=1.0;        // profondeur radiale du filet (mm)
collarThreadWidth=1.2;        // largeur axiale du profil de filet (mm)
collarSkirtClearance=0.3;     // jeu radial entre crête mâle et flanc lisse de la jupe (mm)
collarGrooveDepthExtra=0.5;   // surcreusement de la gorge sous le mur du collet (mm)
collarGrooveAxialExtra=0.5;   // jeu axial entre filet femelle et plancher de la gorge (mm)
collarThreadAboveGasket=3.7;  // hauteur du pied du filet mâle au-dessus du joint (mm)
collarWall=2.5;               // épaisseur paroi du collet (mm)
collarDiskGap=1.0;            // jeu axial disque bouchon / haut du collet (mm)
// Rayons
jointBoreR=gasketOD/2+gasketSeatClearance;
collarFloorHoleR=gasketID/2-gasketRetentionLip;
collarOuterR=jointBoreR+collarWall;
// Crête mâle au-dessus du mur du collet ; flanc lisse de la jupe au-delà,
// décalé d'un jeu collarSkirtClearance pour que les deux crêtes
// s'engagent sans frotter sur la paroi opposée.
threadCrestR=collarOuterR+collarThreadDepth;
skirtInnerR=threadCrestR+collarSkirtClearance;
skirtOuterR=skirtInnerR+jointWall;
femaleCrestR=skirtInnerR-collarThreadDepth;
// Gorge surcreusée par rapport au mur du collet pour absorber la
// crête du filet femelle quand celle-ci y descend en fin de course.
collarGrooveInnerR=collarOuterR-collarGrooveDepthExtra;

// Cotes axiales
collarThreadAxialExtent=collarThreadPitch*collarThreadTurns;
// Plafond de la gorge = pied du filet mâle. Au moment où le filet
// femelle atteint cette cote en descendant, le manchon a écrasé
// le joint de gasketCompression.
collarGrooveTopZ=wallThickness+gasketThickness+collarThreadAboveGasket;
collarThreadBottomZ=collarGrooveTopZ;
collarThreadTopZ=collarThreadBottomZ+collarThreadAxialExtent;
collarHeight=collarThreadTopZ;
collarGrooveBottomZ=collarGrooveTopZ-collarThreadAxialExtent-collarGrooveAxialExtra;

// Hauteur du manchon — cote du bottom de la jupe sous le disque du
// bouchon, calée sur la compression du joint à l'arrêt.
jointHeight=collarHeight+collarDiskGap-(wallThickness+gasketThickness-gasketCompression);
smoothness=(precision==1)?4:((precision==2)?6:20);
facets=(precision==1)?6:((precision==2)?20:50);
segments=facets*threadTurns;
$fn=max(20,facets);
module screwThread(stR,stZ,stPitch,stDepth,stWidth,stTurns,stN,stSmooth,stFacets)
{
  stSegs=stFacets*stTurns;
  union()
    {
      for(t=[0:stN-1])
        {
          rotate([0,0,360*t/stN])
            translate([stR,0,stZ])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
          rotate([0,0,360*t/stN+stTurns*360])
            translate([stR,0,stZ+stPitch*stN*stTurns])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
        }
      for(tw=[0:stSegs-1])
        {
          translate([0,0,stZ])
            union()
            {
              for(t=[0:stN-1])
                {
                  translate([0,0,stPitch*stN*tw/stSegs*stTurns])
                    rotate([0,0,360*(t/stN)+tw*360*stTurns/stSegs])
                    rotate([atan(stPitch*stN/(stR*2*3.1416)),0,0])
                    rotate_extrude(convexity=10,angle=360/stFacets,$fn=50)
                    translate([stR,0,0])
                    scale([stDepth,stWidth/2,0])circle(r=1,$fn=stSmooth);
                }
            }
        }
    }
}

module capThread()
{
  screwThread(innerDiameter/2,wallThickness+innerDepth-threadStart-threadTurns*threadPitch*threads-threadWidth/2,threadPitch,threadDepth,threadWidth,threadTurns,threads,smoothness,facets);
}
union()
{
  capThread();
  rotate_extrude(convexity=10)
  polygon(points=[
                  [innerDiameter/2, 0],
                  [innerDiameter/2+wallThickness, 0],
                  [innerDiameter/2+wallThickness, innerDepth+wallThickness],
                  [innerDiameter/2, innerDepth+wallThickness]
                  ]);
  rotate_extrude(convexity=10)
  polygon(points=[
                  [jointPlugInnerR, 0],
                  [skirtInnerR, 0],
                  [innerDiameter/2, wallThickness],
                  [jointPlugInnerR, wallThickness]
                  ]);
  translate([0,0,-jointHeight])
  difference()
  {
    cylinder(h=jointHeight, r=jointPlugR);
    cylinder(h=jointHeight, r=jointPlugInnerR);
  }
  union()
  {
    rotate_extrude(convexity=10)
    polygon(points=[
                    [skirtOuterR, -jointHeight],
                    [skirtOuterR, 0],
                    [skirtInnerR, 0],
                    [skirtInnerR, -jointHeight]
                    ]);
    intersection()
    {
      translate([0, 0, -jointHeight])
        cylinder(h=jointHeight, r=skirtInnerR + collarThreadDepth + 1);
      translate([0, 0, -jointHeight])
        screwThread(skirtInnerR,
                    0,
                    collarThreadPitch,
                    collarThreadDepth,
                    collarThreadWidth,
                    collarThreadTurns,
                    collarThreadStarts,
                    smoothness,
                    facets);
    }
  }
  difference()
  {
    cylinder(h=sealHeight,r=innerDiameter/2-sealGap);
    cylinder(h=sealHeight+1,r=innerDiameter/2-sealGap-sealThickness);
  }
  for(ridge=[0:ridges-1])
    {
      hull()
        {
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,wallThickness*5/4])
            sphere(r=wallThickness/4,$fn=smoothness);
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,innerDepth+wallThickness*3/4])
            sphere(r=wallThickness/4,$fn=smoothness);
        }
    }
    }

phone-stl "${stl}"

Pièce de fermeture

Pièce qui prend la place de l’entonnoir sur un bidon plein. Une fois le bidon rempli, on déclipse l’entonnoir et on clipse cette pièce à sa place : le bidon est scellé et peut être stocké sans avoir à toucher au bouchon fileté qui reste vissé en permanence.

Réutilisation intégrale du collet de l’entonnoir. Même coque (collar-shell), même filetage mâle (collar-male-thread), même gorge annulaire (collar-groove). Seul le plancher change : plein au lieu d’être percé, puisque la raison d’être est précisément de couper le passage de l’eau.

Ce qu’on enlève par rapport à funnel-collar :

  • le trou central de collar-floor (plus besoin de laisser passer l’eau) ;
  • collar-floor-chamfer (existait pour ponter le trou vers la paroi intérieure du cône — sans cône ni trou, plus de porte-à-faux à supporter à l’impression).

Ce qu’on garde inchangé : le joint fibre plat 30×38×2 vient toujours se poser sur le plancher du collet et est écrasé par le manchon du bouchon comme côté entonnoir. Le plancher plein ajoute juste une seconde barrière : même si le joint fuit légèrement (bidon couché, secoué au stockage), l’eau ne passe pas.

Pièce unique MIEUXA/Netto : comme l’entonnoir, elle ne porte pas de filetage bouteille — son alésage est commun aux deux standards de col, et le filetage mâle qu’elle porte (côté bouchon-entonnoir) est défini dans thread-params indépendamment du standard.

cylinder(h=wallThickness, r=jointBoreR);

difference()
{
  union()
    {
      rotate_extrude(convexity=10)
      polygon(points=[
                      [jointBoreR, 0],
                      [collarOuterR, 0],
                      [collarOuterR, collarHeight],
                      [jointBoreR, collarHeight]
                      ]);
      screwThread(collarOuterR,
                  collarThreadBottomZ,
                  collarThreadPitch,
                  collarThreadDepth,
                  collarThreadWidth,
                  collarThreadTurns,
                  collarThreadStarts,
                  smoothness,
                  facets);
      cylinder(h=wallThickness, r=jointBoreR);
    }
  rotate_extrude(convexity=10)
  polygon(points=[
                  [collarGrooveInnerR, collarGrooveBottomZ],
                  [collarOuterR + 1,   collarGrooveBottomZ],
                  [collarOuterR + 1,   collarGrooveTopZ],
                  [collarGrooveInnerR, collarGrooveTopZ]
                  ]);
}

Chaîne noweb : funnel-globals pour satisfaire wallThickness (utilisé par closing-floor et par collarGrooveTopZ dans joint-geometry) et threadTurns (utilisé par cap-computed pour calculer segments) ; joint-params pour la géométrie du collet et du filetage ; cap-computed pour les presets de maillage ; screw-thread pour le module générique appelé par collar-male-thread. Pas besoin de funnel-params (ni funnel-cone ni collar-floor-chamfer ne sont appelés).

precision=3;
wallThickness=2;
threadTurns=1;
jointWall=2.5;
jointPlugR=funnelHoleDiameter/2-0.3;
jointPlugInnerR=jointPlugR-jointWall;
gasketID=30;
gasketOD=38;
gasketThickness=2.0;
gasketCompression=0.3;
gasketSeatClearance=0.2;
gasketRetentionLip=0.5;
collarThreadPitch=2.0;        // mm/turn — pas du filetage
collarThreadTurns=2;          // tours d'engagement
collarThreadStarts=1;         // single-start
collarThreadDepth=1.0;        // profondeur radiale du filet (mm)
collarThreadWidth=1.2;        // largeur axiale du profil de filet (mm)
collarSkirtClearance=0.3;     // jeu radial entre crête mâle et flanc lisse de la jupe (mm)
collarGrooveDepthExtra=0.5;   // surcreusement de la gorge sous le mur du collet (mm)
collarGrooveAxialExtra=0.5;   // jeu axial entre filet femelle et plancher de la gorge (mm)
collarThreadAboveGasket=3.7;  // hauteur du pied du filet mâle au-dessus du joint (mm)
collarWall=2.5;               // épaisseur paroi du collet (mm)
collarDiskGap=1.0;            // jeu axial disque bouchon / haut du collet (mm)
// Rayons
jointBoreR=gasketOD/2+gasketSeatClearance;
collarFloorHoleR=gasketID/2-gasketRetentionLip;
collarOuterR=jointBoreR+collarWall;
// Crête mâle au-dessus du mur du collet ; flanc lisse de la jupe au-delà,
// décalé d'un jeu collarSkirtClearance pour que les deux crêtes
// s'engagent sans frotter sur la paroi opposée.
threadCrestR=collarOuterR+collarThreadDepth;
skirtInnerR=threadCrestR+collarSkirtClearance;
skirtOuterR=skirtInnerR+jointWall;
femaleCrestR=skirtInnerR-collarThreadDepth;
// Gorge surcreusée par rapport au mur du collet pour absorber la
// crête du filet femelle quand celle-ci y descend en fin de course.
collarGrooveInnerR=collarOuterR-collarGrooveDepthExtra;

// Cotes axiales
collarThreadAxialExtent=collarThreadPitch*collarThreadTurns;
// Plafond de la gorge = pied du filet mâle. Au moment où le filet
// femelle atteint cette cote en descendant, le manchon a écrasé
// le joint de gasketCompression.
collarGrooveTopZ=wallThickness+gasketThickness+collarThreadAboveGasket;
collarThreadBottomZ=collarGrooveTopZ;
collarThreadTopZ=collarThreadBottomZ+collarThreadAxialExtent;
collarHeight=collarThreadTopZ;
collarGrooveBottomZ=collarGrooveTopZ-collarThreadAxialExtent-collarGrooveAxialExtra;

// Hauteur du manchon — cote du bottom de la jupe sous le disque du
// bouchon, calée sur la compression du joint à l'arrêt.
jointHeight=collarHeight+collarDiskGap-(wallThickness+gasketThickness-gasketCompression);
smoothness=(precision==1)?4:((precision==2)?6:20);
facets=(precision==1)?6:((precision==2)?20:50);
segments=facets*threadTurns;
$fn=max(20,facets);
module screwThread(stR,stZ,stPitch,stDepth,stWidth,stTurns,stN,stSmooth,stFacets)
{
  stSegs=stFacets*stTurns;
  union()
    {
      for(t=[0:stN-1])
        {
          rotate([0,0,360*t/stN])
            translate([stR,0,stZ])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
          rotate([0,0,360*t/stN+stTurns*360])
            translate([stR,0,stZ+stPitch*stN*stTurns])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
        }
      for(tw=[0:stSegs-1])
        {
          translate([0,0,stZ])
            union()
            {
              for(t=[0:stN-1])
                {
                  translate([0,0,stPitch*stN*tw/stSegs*stTurns])
                    rotate([0,0,360*(t/stN)+tw*360*stTurns/stSegs])
                    rotate([atan(stPitch*stN/(stR*2*3.1416)),0,0])
                    rotate_extrude(convexity=10,angle=360/stFacets,$fn=50)
                    translate([stR,0,0])
                    scale([stDepth,stWidth/2,0])circle(r=1,$fn=stSmooth);
                }
            }
        }
    }
}
difference()
{
  union()
    {
      rotate_extrude(convexity=10)
      polygon(points=[
                      [jointBoreR, 0],
                      [collarOuterR, 0],
                      [collarOuterR, collarHeight],
                      [jointBoreR, collarHeight]
                      ]);
      screwThread(collarOuterR,
                  collarThreadBottomZ,
                  collarThreadPitch,
                  collarThreadDepth,
                  collarThreadWidth,
                  collarThreadTurns,
                  collarThreadStarts,
                  smoothness,
                  facets);
      cylinder(h=wallThickness, r=jointBoreR);
    }
  rotate_extrude(convexity=10)
  polygon(points=[
                  [collarGrooveInnerR, collarGrooveBottomZ],
                  [collarOuterR + 1,   collarGrooveBottomZ],
                  [collarOuterR + 1,   collarGrooveTopZ],
                  [collarGrooveInnerR, collarGrooveTopZ]
                  ]);
}

Ajout d’un tuyau de trop-plein et d’un coude

Ajoute à l’entonnoir un petit tuyau de trop-plein incliné qui sort par la paroi du cône à 45° vers le bas-réel. Quand le bidon est plein, l’excédent d’eau monte dans l’entonnoir jusqu’à ce qu’il rencontre l’embouchure de ce tuyau, y tombe par gravité, et est évacué par la sortie extérieure. Une rallonge cylindrique imprimée séparément s’emboîte à friction sur le bout du tuyau pour prolonger l’évacuation jusqu’au bidon suivant. Une troisième pièce, le coude, s’emboîte à son tour sur la rallonge et redresse le flux à la verticale pour qu’il tombe d’aplomb dans l’ouverture du bidon suivant.

Le tuyau traverse la paroi du cône et est entièrement orienté à 45° : impression sans support, drainage par gravité assuré.

Stratégie géométrique

Le tuyau intégré est modélisé comme deux cylindres coaxiaux inclinés :

  • le cylindre extérieur (paroi du tuyau) est la matière qu’on ajoute ;
  • le cylindre intérieur (canal creux) est la matière qu’on retire.

Le cylindre intérieur joue d’ailleurs deux rôles simultanément dans l’opération booléenne : il creuse le canal dans la paroi et perce le trou dans la paroi du cône là où le tuyau la traverse — une seule opération difference() pour les deux.

Le cylindre extérieur est lui aussi recoupé par la cavité intérieure du cône, pour ne garder que la portion utile (paroi du cône et au-delà) sans intrusion dans l’intérieur du cône.

Paramètres du tuyau

Diamètre de canal 14mm, paroi 1mm (↔ Ø extérieur 16mm), longueur 40mm depuis l’axe. Le centre du pivot est à pipeCenterZ=-34 (en coordonnées code ; plus négatif = plus haut en réel). L’angle de pente est donné par pipeSlope=1 (rise/run = tan de l’angle ; 1 → 45°).

pipeHoleDiameter=14;
pipeWallThickness=1;
pipeLength=50;
pipeCenterZ=-34;
pipeSlope=1;

Module pipe() partagé

Un seul module factorise les deux cylindres (paroi et canal). Il prend un rayon en argument, et optionnellement un extra (rallongement aux deux extrémités) pour garantir un overlap propre en opération booléenne — on l’utilise sur le canal pour éviter les faces coplanaires qui font tousser CGAL.

La chaîne de transformations : translate au pivot, rotate autour de Y pour l’inclinaison, translate local en X pour l’overlap, rotate 90° Y pour coucher le cylindre le long de X.

module pipe(r, extra=0)
{
  translate([0,0,pipeCenterZ])
    rotate([0,-atan(pipeSlope),0])
    translate([-extra,0,0])
    rotate([0,90,0])
    cylinder(h=pipeLength+2*extra, r=r);
}

Clippage de la paroi par la cavité du cône

La paroi du tuyau commence à X=0 (sur l’axe du cône) : elle traverse donc la moitié intérieure du cône avant de sortir par la paroi. On ne veut garder que la portion qui est dans la paroi et à l’extérieur, pas celle qui pénètre l’intérieur. On soustrait donc la cavité intérieure du cône de la paroi du tuyau.

Deux subtilités :

  1. le coin interne de la paroi (côté axe, côté haut-réel) plonge au-dessus du haut du cône matière — au-delà de où la cavité s’arrête normalement. On étend le cône de soustraction de 15mm au-delà du haut, en gardant la même pente, pour clipper ce débordement ;
  2. au-dessus du bas-réel du cône (i.e. côté sortie étroite), le sommet incliné de la paroi pourrait dépasser dans l’ouverture. Un cylindre droit de rayon funnelHoleDiameter/2 prolonge la soustraction au-dessus pour le couvrir.

union()
{
  translate([0,0,-funnelHeight-15])
    cylinder(h=funnelHeight+15.1,
             r1=funnelTopDiameter/2 + 15*(funnelTopDiameter-funnelHoleDiameter)/(2*funnelHeight),
             r2=funnelHoleDiameter/2);
  translate([0,0,-0.1])
    cylinder(h=20,r=funnelHoleDiameter/2);
}

Entonnoir avec tuyau intégré

Pièce finale côté entonnoir : le cône de l’entonnoir + le collet fileté + le petit tuyau de trop-plein. Une seule pièce commune aux deux bidons : les cotes du cône sont fixées en constantes dans funnel-params (indépendantes de innerDiameter), et tout le collet/filetage dérive déjà de constantes. Le manchon du bouchon (qui diffère entre MIEUXA et Netto) s’adapte à l’alésage commun du collet. Joint fibre plat 30x38x2mm déposé sur le plancher du collet avant vissage de l’entonnoir sur le filet mâle.

Assemblage final en un difference() : cône + collet + (paroi du tuyau recoupée par la cavité), le tout moins le canal (qui perce à la fois la paroi du cône au passage et l’intérieur de la paroi du tuyau).

pipeHoleDiameter=14;
pipeWallThickness=1;
pipeLength=50;
pipeCenterZ=-34;
pipeSlope=1;
module pipe(r, extra=0)
{
  translate([0,0,pipeCenterZ])
    rotate([0,-atan(pipeSlope),0])
    translate([-extra,0,0])
    rotate([0,90,0])
    cylinder(h=pipeLength+2*extra, r=r);
}
difference()
{
  union()
    {
      translate([0,0,-funnelHeight])
      difference()
      {
        cylinder(h=funnelHeight, r1=funnelTopDiameter/2+wallThickness, r2=funnelHoleDiameter/2+wallThickness);
        translate([0,0,-0.1])
          cylinder(h=funnelHeight+0.2, r1=funnelTopDiameter/2, r2=funnelHoleDiameter/2);
      }
    difference()
    {
      union()
        {
          rotate_extrude(convexity=10)
          polygon(points=[
                          [jointBoreR, 0],
                          [collarOuterR, 0],
                          [collarOuterR, collarHeight],
                          [jointBoreR, collarHeight]
                          ]);
          screwThread(collarOuterR,
                      collarThreadBottomZ,
                      collarThreadPitch,
                      collarThreadDepth,
                      collarThreadWidth,
                      collarThreadTurns,
                      collarThreadStarts,
                      smoothness,
                      facets);
          difference()
          {
            cylinder(h=wallThickness,r=jointBoreR);
            cylinder(h=wallThickness*3,r=collarFloorHoleR,center=true);
          }
          collarChamferAngle=50;
          funnelInnerSlope=(funnelTopDiameter-funnelHoleDiameter)/(2*funnelHeight);
          chamferDrPerDz=1/tan(collarChamferAngle);
          chamferDepth=(funnelHoleDiameter/2-collarFloorHoleR)/(chamferDrPerDz-funnelInnerSlope);
          rotate_extrude(convexity=10)
          polygon(points=[
                          [collarFloorHoleR,0],
                          [funnelHoleDiameter/2,0],
                          [funnelHoleDiameter/2+funnelInnerSlope*chamferDepth,-chamferDepth]
                          ]);
        }
      rotate_extrude(convexity=10)
      polygon(points=[
                      [collarGrooveInnerR, collarGrooveBottomZ],
                      [collarOuterR + 1,   collarGrooveBottomZ],
                      [collarOuterR + 1,   collarGrooveTopZ],
                      [collarGrooveInnerR, collarGrooveTopZ]
                      ]);
    }
        difference()
        {
          pipe(pipeHoleDiameter/2+pipeWallThickness);
          union()
          {
            translate([0,0,-funnelHeight-15])
              cylinder(h=funnelHeight+15.1,
                       r1=funnelTopDiameter/2 + 15*(funnelTopDiameter-funnelHoleDiameter)/(2*funnelHeight),
                       r2=funnelHoleDiameter/2);
            translate([0,0,-0.1])
              cylinder(h=20,r=funnelHoleDiameter/2);
          }
              }
    }
  pipe(pipeHoleDiameter/2, extra=0.1);
}

wallThickness et threadTurns sont définis ici pour satisfaire les blocs noweb-inclus (joint-params, cap-computed, screw-thread) qui les attendent, mais leurs valeurs n’influencent pas le rendu du funnel — le filetage n’est pas appelé, et wallThickness (identique entre MIEUXA et Netto de toute façon) sert uniquement à l’épaisseur du plancher et à la position axiale de la gorge du collet.

wallThickness=2;
threadTurns=1;

precision=3;
wallThickness=2;
threadTurns=1;
funnelSlope=0.68;
funnelGenLength=70;
funnelHoleDiameter=39;
funnelHeight=funnelGenLength/sqrt(funnelSlope*funnelSlope+1);
funnelTopDiameter=2*(funnelSlope*funnelHeight+funnelHoleDiameter/2);
jointWall=2.5;
jointPlugR=funnelHoleDiameter/2-0.3;
jointPlugInnerR=jointPlugR-jointWall;
gasketID=30;
gasketOD=38;
gasketThickness=2.0;
gasketCompression=0.3;
gasketSeatClearance=0.2;
gasketRetentionLip=0.5;
collarThreadPitch=2.0;        // mm/turn — pas du filetage
collarThreadTurns=2;          // tours d'engagement
collarThreadStarts=1;         // single-start
collarThreadDepth=1.0;        // profondeur radiale du filet (mm)
collarThreadWidth=1.2;        // largeur axiale du profil de filet (mm)
collarSkirtClearance=0.3;     // jeu radial entre crête mâle et flanc lisse de la jupe (mm)
collarGrooveDepthExtra=0.5;   // surcreusement de la gorge sous le mur du collet (mm)
collarGrooveAxialExtra=0.5;   // jeu axial entre filet femelle et plancher de la gorge (mm)
collarThreadAboveGasket=3.7;  // hauteur du pied du filet mâle au-dessus du joint (mm)
collarWall=2.5;               // épaisseur paroi du collet (mm)
collarDiskGap=1.0;            // jeu axial disque bouchon / haut du collet (mm)
// Rayons
jointBoreR=gasketOD/2+gasketSeatClearance;
collarFloorHoleR=gasketID/2-gasketRetentionLip;
collarOuterR=jointBoreR+collarWall;
// Crête mâle au-dessus du mur du collet ; flanc lisse de la jupe au-delà,
// décalé d'un jeu collarSkirtClearance pour que les deux crêtes
// s'engagent sans frotter sur la paroi opposée.
threadCrestR=collarOuterR+collarThreadDepth;
skirtInnerR=threadCrestR+collarSkirtClearance;
skirtOuterR=skirtInnerR+jointWall;
femaleCrestR=skirtInnerR-collarThreadDepth;
// Gorge surcreusée par rapport au mur du collet pour absorber la
// crête du filet femelle quand celle-ci y descend en fin de course.
collarGrooveInnerR=collarOuterR-collarGrooveDepthExtra;

// Cotes axiales
collarThreadAxialExtent=collarThreadPitch*collarThreadTurns;
// Plafond de la gorge = pied du filet mâle. Au moment où le filet
// femelle atteint cette cote en descendant, le manchon a écrasé
// le joint de gasketCompression.
collarGrooveTopZ=wallThickness+gasketThickness+collarThreadAboveGasket;
collarThreadBottomZ=collarGrooveTopZ;
collarThreadTopZ=collarThreadBottomZ+collarThreadAxialExtent;
collarHeight=collarThreadTopZ;
collarGrooveBottomZ=collarGrooveTopZ-collarThreadAxialExtent-collarGrooveAxialExtra;

// Hauteur du manchon — cote du bottom de la jupe sous le disque du
// bouchon, calée sur la compression du joint à l'arrêt.
jointHeight=collarHeight+collarDiskGap-(wallThickness+gasketThickness-gasketCompression);
smoothness=(precision==1)?4:((precision==2)?6:20);
facets=(precision==1)?6:((precision==2)?20:50);
segments=facets*threadTurns;
$fn=max(20,facets);
module screwThread(stR,stZ,stPitch,stDepth,stWidth,stTurns,stN,stSmooth,stFacets)
{
  stSegs=stFacets*stTurns;
  union()
    {
      for(t=[0:stN-1])
        {
          rotate([0,0,360*t/stN])
            translate([stR,0,stZ])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
          rotate([0,0,360*t/stN+stTurns*360])
            translate([stR,0,stZ+stPitch*stN*stTurns])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
        }
      for(tw=[0:stSegs-1])
        {
          translate([0,0,stZ])
            union()
            {
              for(t=[0:stN-1])
                {
                  translate([0,0,stPitch*stN*tw/stSegs*stTurns])
                    rotate([0,0,360*(t/stN)+tw*360*stTurns/stSegs])
                    rotate([atan(stPitch*stN/(stR*2*3.1416)),0,0])
                    rotate_extrude(convexity=10,angle=360/stFacets,$fn=50)
                    translate([stR,0,0])
                    scale([stDepth,stWidth/2,0])circle(r=1,$fn=stSmooth);
                }
            }
        }
    }
}
pipeHoleDiameter=14;
pipeWallThickness=1;
pipeLength=50;
pipeCenterZ=-34;
pipeSlope=1;
module pipe(r, extra=0)
{
  translate([0,0,pipeCenterZ])
    rotate([0,-atan(pipeSlope),0])
    translate([-extra,0,0])
    rotate([0,90,0])
    cylinder(h=pipeLength+2*extra, r=r);
}
difference()
{
  union()
    {
      translate([0,0,-funnelHeight])
      difference()
      {
        cylinder(h=funnelHeight, r1=funnelTopDiameter/2+wallThickness, r2=funnelHoleDiameter/2+wallThickness);
        translate([0,0,-0.1])
          cylinder(h=funnelHeight+0.2, r1=funnelTopDiameter/2, r2=funnelHoleDiameter/2);
      }
    difference()
    {
      union()
        {
          rotate_extrude(convexity=10)
          polygon(points=[
                          [jointBoreR, 0],
                          [collarOuterR, 0],
                          [collarOuterR, collarHeight],
                          [jointBoreR, collarHeight]
                          ]);
          screwThread(collarOuterR,
                      collarThreadBottomZ,
                      collarThreadPitch,
                      collarThreadDepth,
                      collarThreadWidth,
                      collarThreadTurns,
                      collarThreadStarts,
                      smoothness,
                      facets);
          difference()
          {
            cylinder(h=wallThickness,r=jointBoreR);
            cylinder(h=wallThickness*3,r=collarFloorHoleR,center=true);
          }
          collarChamferAngle=50;
          funnelInnerSlope=(funnelTopDiameter-funnelHoleDiameter)/(2*funnelHeight);
          chamferDrPerDz=1/tan(collarChamferAngle);
          chamferDepth=(funnelHoleDiameter/2-collarFloorHoleR)/(chamferDrPerDz-funnelInnerSlope);
          rotate_extrude(convexity=10)
          polygon(points=[
                          [collarFloorHoleR,0],
                          [funnelHoleDiameter/2,0],
                          [funnelHoleDiameter/2+funnelInnerSlope*chamferDepth,-chamferDepth]
                          ]);
        }
      rotate_extrude(convexity=10)
      polygon(points=[
                      [collarGrooveInnerR, collarGrooveBottomZ],
                      [collarOuterR + 1,   collarGrooveBottomZ],
                      [collarOuterR + 1,   collarGrooveTopZ],
                      [collarGrooveInnerR, collarGrooveTopZ]
                      ]);
    }
        difference()
        {
          pipe(pipeHoleDiameter/2+pipeWallThickness);
          union()
          {
            translate([0,0,-funnelHeight-15])
              cylinder(h=funnelHeight+15.1,
                       r1=funnelTopDiameter/2 + 15*(funnelTopDiameter-funnelHoleDiameter)/(2*funnelHeight),
                       r2=funnelHoleDiameter/2);
            translate([0,0,-0.1])
              cylinder(h=20,r=funnelHoleDiameter/2);
          }
              }
    }
  pipe(pipeHoleDiameter/2, extra=0.1);
}

https://ipfs.konubinix.eu/p/bafybeicjqtyivy5ifhwwl75qwyb3quoespmsynmlskcjay3hoc7gfyolra?funnel.stl?nil

https://ipfs.konubinix.eu/p/bafybeicjqtyivy5ifhwwl75qwyb3quoespmsynmlskcjay3hoc7gfyolra?funnel.stl?nil

https://ipfs.konubinix.eu/p/bafybeicjqtyivy5ifhwwl75qwyb3quoespmsynmlskcjay3hoc7gfyolra?funnel.stl?nil

https://ipfs.konubinix.eu/p/bafybeicjqtyivy5ifhwwl75qwyb3quoespmsynmlskcjay3hoc7gfyolra?funnel.stl?nil

Réglages slicer (Bambu Studio, A1 mini)

À l’impression, l’intérieur du cône se retrouve envahi de fils qui ressemblent à du stringing classique, mais le vrai diagnostic est double. La géométrie SCAD est saine — tout se passe côté slicer.

Cause 1 : position de la couture sur le bord du trou du tuyau

Sur les couches qui traversent le trou du tuyau, le pourtour du cône est coupé en deux — un mur côté “gauche” du trou et un mur côté “droit”. Bambu Studio place la couture (seam) par défaut à la position la plus proche du point de départ précédent — ce qui tombe souvent pile au bord du trou, là où le mur est extrêmement fin voire absent.

Conséquence : au démarrage du périmètre, le filament est déposé sur rien (pas de mur en-dessous pour accrocher la première goutte). La buse tourne en continuant à extruder un “collier” censé reposer sur la couche du dessous — mais comme le départ n’a pas pris, tout le début du périmètre se fait tirer vers l’intérieur de la cavité. Ça ne se rattrape que quand la buse rejoint une zone de mur franc plus loin.

Fix principal :

  • Quality → Seam → Seam position : passer de Aligned (défaut) à Rear. Force la couture à l’arrière du modèle — à positionner dans la plate opposé à la sortie du tuyau. Règle le problème en un réglage.

Alternatives si *Rear ne colle pas avec ton orientation sur le plateau* :

  • Painted Seam : outil “Seam painting” dans Bambu Studio → on peint manuellement la couture sur le dos du cône, loin du trou.
  • Scarf joint seam (option récente Bambu Studio) : fade progressivement l’extrusion au début du périmètre. Masque la couture et améliore l’accroche. Plus un pansement qu’un fix — la cause racine (position) reste.
Cause 2 : stringing pendant les travel moves

Indépendamment du seam, le trou du tuyau coupe le pourtour en deux segments à chaque couche concernée. Le printhead doit donc traverser la cavité pour passer d’un segment à l’autre, et le PLA coule pendant le travel. Sur A1 mini direct drive, le stringing est normalement rare mais la géométrie le favorise ici.

Ordre de priorité des settings à activer :

  1. Quality → Travel → “Avoid crossing walls” : coche. Force le printhead à contourner le pourtour au lieu de couper à travers la cavité. Rallonge le G-code mais c’est ce qu’on veut ici. Suffit seul dans la plupart des cas.
  2. Filament → Setting Overrides → Retraction length : défaut 0.8mm sur A1 mini → 1.0-1.2mm si le #1 ne suffit pas.
  3. Quality → Travel → “Z hop when retracting” : Normal lift, 0.4mm. Évite que la buse accroche les fils déjà déposés.
  4. Quality → Travel → “Wipe while retracting” : coche.
  5. Filament → Temperature → Nozzle temp : 220 → 210°C (PLA Bambu Basic). Dernier recours.
Protocole de test
  1. Appliquer Seam position = Rear en premier (cause 1 = diagnostic principal). Orienter le funnel dans la plate avec le tuyau face à soi pour que “rear” tombe à l’opposé.
  2. Si fils persistent, ajouter Avoid crossing walls.
  3. Si encore des fils, remonter la liste cause 2 un setting à la fois.
  4. En cas de doute, Bambu Studio → Preview → Layer slider pour inspecter les paths au niveau du trou du tuyau (print_Z ~10-22mm) : si les lignes d’extrusion passent par le vide, c’est la cause 1 ; si ce sont les travels, c’est la cause 2.

Rallonge

Cylindre creux qui coulisse à friction sur le bout du petit tuyau sortant de l’entonnoir, pour rallonger l’évacuation jusqu’au bidon suivant. Son Ø intérieur = Ø extérieur du petit tuyau + pipeTolerance (jeu radial d’emboîtement). Sa paroi reprend la même épaisseur pipeWallThickness que celle du petit tuyau. Identique pour MIEUXA et Netto (le petit tuyau a les mêmes dimensions dans les deux entonnoirs).

Pour améliorer l’accroche, on ajoute un bump radial sur la face intérieure de la rallonge, près de son bord : une petite crête annulaire qui pince le petit tuyau en friction quand la rallonge est glissée dessus. Profondeur légèrement supérieure à pipeTolerance pour créer une interférence contrôlée (compression élastique du plastique pendant l’insertion, puis retenue).

pipeTolerance=0.3;

extensionInnerR=pipeHoleDiameter/2+pipeWallThickness+pipeTolerance;
extensionOuterR=extensionInnerR+pipeWallThickness;
extensionLength=90;
extensionBumpDepth=0.5;       // pincement radial du bump (≈ pipeTolerance + 0.2 d'interférence)
extensionBumpWidth=1.5;       // largeur axiale du bump
extensionBumpZ=extensionLength-2;  // position axiale (près du bord haut)

difference()
{
  cylinder(h=extensionLength,r=extensionOuterR);
  rotate_extrude()
    polygon(points=[
                    [0, -0.1],
                    [extensionInnerR, -0.1],
                    [extensionInnerR, extensionBumpZ-extensionBumpWidth/2],
                    [extensionInnerR-extensionBumpDepth, extensionBumpZ],
                    [extensionInnerR, extensionBumpZ+extensionBumpWidth/2],
                    [extensionInnerR, extensionLength+0.1],
                    [0, extensionLength+0.1]
                    ]);
}

precision=3;
pipeHoleDiameter=14;
pipeWallThickness=1;
pipeLength=50;
pipeCenterZ=-34;
pipeSlope=1;
pipeTolerance=0.3;
smoothness=(precision==1)?4:((precision==2)?6:20);
facets=(precision==1)?6:((precision==2)?20:50);
segments=facets*threadTurns;
$fn=max(20,facets);
extensionInnerR=pipeHoleDiameter/2+pipeWallThickness+pipeTolerance;
extensionOuterR=extensionInnerR+pipeWallThickness;
extensionLength=90;
extensionBumpDepth=0.5;       // pincement radial du bump (≈ pipeTolerance + 0.2 d'interférence)
extensionBumpWidth=1.5;       // largeur axiale du bump
extensionBumpZ=extensionLength-2;  // position axiale (près du bord haut)

difference()
{
  cylinder(h=extensionLength,r=extensionOuterR);
  rotate_extrude()
    polygon(points=[
                    [0, -0.1],
                    [extensionInnerR, -0.1],
                    [extensionInnerR, extensionBumpZ-extensionBumpWidth/2],
                    [extensionInnerR-extensionBumpDepth, extensionBumpZ],
                    [extensionInnerR, extensionBumpZ+extensionBumpWidth/2],
                    [extensionInnerR, extensionLength+0.1],
                    [0, extensionLength+0.1]
                    ]);
}

Coude

Pièce finale de la chaîne d’évacuation : un tube creux coudé qui se glisse sur le bout de la rallonge — exactement comme la rallonge se glisse sur le bout du tuyau sortant de l’entonnoir — et qui redresse à la verticale le flux sortant à 45°. L’eau tombe alors d’aplomb dans l’ouverture du bidon suivant.

Raison d’être une pièce séparée :

  • l’entonnoir doit rester imprimable sans support, donc le tuyau qui en sort reste droit à 45° ;
  • le coude, imprimé seul, se positionne au slicer dans la meilleure orientation (segment de sortie vertical sur le plateau, arc et segment d’entrée en l’air, tous les angles ≤ 45°) — pas de support nécessaire.

Emboîtement : le Ø intérieur du segment d’entrée est calé sur le Ø extérieur de la rallonge + pipeTolerance, avec le même bump radial de retenue que celui de la rallonge (recopié du pattern extension-body). Une fois emboîté, le coude tourne librement autour de l’axe de la rallonge — ce qui permet d’ajuster à la main la direction dans laquelle tombe la sortie.

Géométrie. Trois segments unifiés puis creusés par difference() :

  • segment d’entrée cylindrique, orienté selon l’axe du tuyau (45°) ;
  • arc de tore sur 45° (rotate_extrude(angle=45) d’un disque à distance elbowBendRadius de l’axe), qui fait pivoter le flux ;
  • segment de sortie cylindrique vertical, court (quelques cm, juste ce qu’il faut pour laisser l’eau se détacher proprement).

Le canal intérieur est tracé par une seconde union au rayon intérieur, rallongée de 0.1mm à chaque extrémité (pattern extra=0.1 déjà utilisé pour le module pipe()) pour éviter les faces coplanaires qui font tousser CGAL. Sur le segment d’entrée, le canal intérieur est tracé par un rotate_extrude d’un polygone qui porte le bump radial de retenue.

elbowBendRadius=20;
elbowInletLength=30;
elbowOutletLength=40;
elbowBumpDepth=0.5;
elbowBumpWidth=1.5;

elbowAngle=atan(pipeSlope);
elbowInletInnerR=extensionOuterR+pipeTolerance;
elbowInletOuterR=elbowInletInnerR+pipeWallThickness;
elbowBumpZ=elbowInletLength-2;

difference()
{
  union()
    {
      translate([elbowBendRadius,0,-elbowOutletLength])
        cylinder(h=elbowOutletLength+0.1,r=elbowInletOuterR);
      rotate([90,0,0])
        rotate_extrude(angle=elbowAngle,convexity=10)
        translate([elbowBendRadius,0])
        circle(r=elbowInletOuterR);
      translate([elbowBendRadius*cos(elbowAngle),0,elbowBendRadius*sin(elbowAngle)])
        rotate([0,-elbowAngle,0])
        translate([0,0,-0.1])
        cylinder(h=elbowInletLength+0.1,r=elbowInletOuterR);
    }
  union()
    {
      translate([elbowBendRadius,0,-elbowOutletLength-0.1])
        cylinder(h=elbowOutletLength+0.2,r=elbowInletInnerR);
      rotate([90,0,0])
        rotate_extrude(angle=elbowAngle,convexity=10)
        translate([elbowBendRadius,0])
        circle(r=elbowInletInnerR);
      translate([elbowBendRadius*cos(elbowAngle),0,elbowBendRadius*sin(elbowAngle)])
        rotate([0,-elbowAngle,0])
        rotate_extrude(convexity=10)
        polygon(points=[
                        [0,-0.1],
                        [elbowInletInnerR,-0.1],
                        [elbowInletInnerR,elbowBumpZ-elbowBumpWidth/2],
                        [elbowInletInnerR-elbowBumpDepth,elbowBumpZ],
                        [elbowInletInnerR,elbowBumpZ+elbowBumpWidth/2],
                        [elbowInletInnerR,elbowInletLength+0.1],
                        [0,elbowInletLength+0.1]
                        ]);
    }
}

precision=3;
pipeHoleDiameter=14;
pipeWallThickness=1;
pipeLength=50;
pipeCenterZ=-34;
pipeSlope=1;
pipeTolerance=0.3;
elbowBendRadius=20;
elbowInletLength=30;
elbowOutletLength=40;
elbowBumpDepth=0.5;
elbowBumpWidth=1.5;
smoothness=(precision==1)?4:((precision==2)?6:20);
facets=(precision==1)?6:((precision==2)?20:50);
segments=facets*threadTurns;
$fn=max(20,facets);
extensionInnerR=pipeHoleDiameter/2+pipeWallThickness+pipeTolerance;
extensionOuterR=extensionInnerR+pipeWallThickness;
elbowAngle=atan(pipeSlope);
elbowInletInnerR=extensionOuterR+pipeTolerance;
elbowInletOuterR=elbowInletInnerR+pipeWallThickness;
elbowBumpZ=elbowInletLength-2;

difference()
{
  union()
    {
      translate([elbowBendRadius,0,-elbowOutletLength])
        cylinder(h=elbowOutletLength+0.1,r=elbowInletOuterR);
      rotate([90,0,0])
        rotate_extrude(angle=elbowAngle,convexity=10)
        translate([elbowBendRadius,0])
        circle(r=elbowInletOuterR);
      translate([elbowBendRadius*cos(elbowAngle),0,elbowBendRadius*sin(elbowAngle)])
        rotate([0,-elbowAngle,0])
        translate([0,0,-0.1])
        cylinder(h=elbowInletLength+0.1,r=elbowInletOuterR);
    }
  union()
    {
      translate([elbowBendRadius,0,-elbowOutletLength-0.1])
        cylinder(h=elbowOutletLength+0.2,r=elbowInletInnerR);
      rotate([90,0,0])
        rotate_extrude(angle=elbowAngle,convexity=10)
        translate([elbowBendRadius,0])
        circle(r=elbowInletInnerR);
      translate([elbowBendRadius*cos(elbowAngle),0,elbowBendRadius*sin(elbowAngle)])
        rotate([0,-elbowAngle,0])
        rotate_extrude(convexity=10)
        polygon(points=[
                        [0,-0.1],
                        [elbowInletInnerR,-0.1],
                        [elbowInletInnerR,elbowBumpZ-elbowBumpWidth/2],
                        [elbowInletInnerR-elbowBumpDepth,elbowBumpZ],
                        [elbowInletInnerR,elbowBumpZ+elbowBumpWidth/2],
                        [elbowInletInnerR,elbowInletLength+0.1],
                        [0,elbowInletLength+0.1]
                        ]);
    }
}

Abandon

Au final, le projet ne vaut pas le coup. La pile de problèmes techniques empilés les uns sur les autres dépasse largement le bénéfice attendu (remplir des bidons sans les déplacer) :

  • STL non-manifolds. L’assemblage du bouchon plugged accumule des faces coplanaires entre le manchon, le disque et la bague d’étanchéité. CGAL produit des arêtes non-manifolds que le slicer doit réparer à l’aveugle, et chaque tentative de fix géométrique en déplace de nouvelles ailleurs.
  • Difficultés d’impression. Le bord intérieur de la bague est en deçà du trou central du disque, donc il flotte au premier layer en plug-down. Toutes les corrections (chanfrein, embed par epsilon, extension du disque) introduisent soit un nouveau porte-à-faux, soit une nouvelle face coplanaire — on déplace le problème sans le résoudre.
  • Étanchéité incertaine. Même si la pièce s’imprimait proprement, l’étanchéité combine un joint fibre, une bague imprimée pressant la lèvre du col, et un filetage à course limitée qui doit s’arrêter pile au bon moment. Trop de surfaces à calibrer, chacune sensible à ±0.1 mm d’impression. Le risque de fuite sur un bidon plein laissé plusieurs jours est réel.

Le coût (temps de design, itérations d’impression, calibration des cotes pour chaque standard de col) excède le gain pratique d’un remplissage en cascade. On revient au remplissage manuel séquentiel avec les bouchons d’origine.

Annexe

Dérivation des paramètres du cône

Les paramètres funnelSlope et funnelGenLength pilotent la géométrie du cône. La génératrice d’un tronc de cône de hauteur h et de différence de rayon Δr = r₁ − r₂ suit le théorème de Pythagore appliqué au triangle rectangle (Δr, h) :

L = √( Δr² + h² )   avec Δr = funnelTopDiameter/2 − funnelHoleDiameter/2

Avec la contrainte de pente constante Δr / h = funnelSlope, on substitue Δr = funnelSlope × h. En isolant h puis D, on obtient les expressions directes des deux paramètres SCAD dérivés :

h = funnelGenLength / √(funnelSlope² + 1)
D = 2 × (funnelSlope × h + funnelHoleDiameter/2)

Modules SCAD partagés

Toutes les pièces finales (anneaux de test, bouchons filetés nus, bouchons avec entonnoir, entonnoirs séparés, pièces de fermeture) sont construites à partir d’un petit vocabulaire de primitives partagées définies ici une seule fois. L’objectif — remplir plusieurs bidons sans bouger — nécessite à terme :

  • un filetage qui s’adapte au col du bidon cible (MIEUXA ou Netto) ;
  • un corps de bouchon qui porte ce filetage et referme le col ;
  • une bague d’étanchéité intérieure qui scelle contre la lèvre du col.

Chaque élément a son bloc paramétré ci-dessous, instancié par les pièces finales selon leurs besoins via noweb.

Résolution d’impression

precision sélectionne un preset de qualité de maillage ($fn, nombre de facettes des extrusions, nombre de sphères par spire du filetage). On tourne à precision=1 pour les anneaux de test (rapide, un peu facetté mais suffisant pour confirmer un standard de filetage), precision=3 pour les pièces définitives.

smoothness=(precision==1)?4:((precision==2)?6:20);
facets=(precision==1)?6:((precision==2)?20:50);
segments=facets*threadTurns;
$fn=max(20,facets);

Filetage (screwThread)

Module générique de filetage hélicoïdal — n’importe quel rayon, n’importe quel pas, n’importe quel nombre de tours. Implémentation : on discrétise la spire en petits segments de rotate_extrude d’un profil elliptique (sphère écrasée en scale), placés tout au long d’une hélice. Deux sphères aux extrémités pour arrondir le départ et l’arrêt du filet (évite les angles vifs qui accrochent pendant le vissage).

C’est le bloc critique du projet : c’est ce qui permet au bouchon imprimé de se visser proprement sur le col du bidon du commerce. Les paramètres (rayon, pas, profondeur, largeur, nombre de tours) sont réglés par bidon cible dans params-mieuxa / params-netto.

module screwThread(stR,stZ,stPitch,stDepth,stWidth,stTurns,stN,stSmooth,stFacets)
{
  stSegs=stFacets*stTurns;
  union()
    {
      for(t=[0:stN-1])
        {
          rotate([0,0,360*t/stN])
            translate([stR,0,stZ])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
          rotate([0,0,360*t/stN+stTurns*360])
            translate([stR,0,stZ+stPitch*stN*stTurns])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
        }
      for(tw=[0:stSegs-1])
        {
          translate([0,0,stZ])
            union()
            {
              for(t=[0:stN-1])
                {
                  translate([0,0,stPitch*stN*tw/stSegs*stTurns])
                    rotate([0,0,360*(t/stN)+tw*360*stTurns/stSegs])
                    rotate([atan(stPitch*stN/(stR*2*3.1416)),0,0])
                    rotate_extrude(convexity=10,angle=360/stFacets,$fn=50)
                    translate([stR,0,0])
                    scale([stDepth,stWidth/2,0])circle(r=1,$fn=stSmooth);
                }
            }
        }
    }
}

Wrapper capThread()

Sucre syntaxique : capThread() instancie le module générique screwThread avec tous les paramètres de filetage du bouchon (innerDiameter, threadPitch, threadDepth, etc.) déjà câblés. Évite de répéter la longue liste d’arguments chaque fois qu’un bouchon en a besoin.

module screwThread(stR,stZ,stPitch,stDepth,stWidth,stTurns,stN,stSmooth,stFacets)
{
  stSegs=stFacets*stTurns;
  union()
    {
      for(t=[0:stN-1])
        {
          rotate([0,0,360*t/stN])
            translate([stR,0,stZ])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
          rotate([0,0,360*t/stN+stTurns*360])
            translate([stR,0,stZ+stPitch*stN*stTurns])
            scale([stDepth,stDepth,stWidth/2])sphere(r=1,$fn=stSmooth);
        }
      for(tw=[0:stSegs-1])
        {
          translate([0,0,stZ])
            union()
            {
              for(t=[0:stN-1])
                {
                  translate([0,0,stPitch*stN*tw/stSegs*stTurns])
                    rotate([0,0,360*(t/stN)+tw*360*stTurns/stSegs])
                    rotate([atan(stPitch*stN/(stR*2*3.1416)),0,0])
                    rotate_extrude(convexity=10,angle=360/stFacets,$fn=50)
                    translate([stR,0,0])
                    scale([stDepth,stWidth/2,0])circle(r=1,$fn=stSmooth);
                }
            }
        }
    }
}

module capThread()
{
  screwThread(innerDiameter/2,wallThickness+innerDepth-threadStart-threadTurns*threadPitch*threads-threadWidth/2,threadPitch,threadDepth,threadWidth,threadTurns,threads,smoothness,facets);
}

Corps commun du bouchon (cap-body-common)

Éléments partagés par tous les bouchons fermés (bouchons nus, bouchons avec entonnoir) : le filetage, une paroi cylindrique externe qui porte le filetage, un chanfrein de raccord entre la paroi et le dessus, et des stries verticales extérieures pour la préhension au vissage. N’inclut pas le disque de fermeture du dessus (variable selon la variante — plein, percé pour recevoir un entonnoir, etc.) ni la bague d’étanchéité.

capThread();

difference()
{
  union()
    {
      translate([0,0,innerDepth/2+wallThickness])
        cylinder(h=innerDepth,r=(innerDiameter)/2+wallThickness,center=true);

      translate([0,0,wallThickness])
        rotate_extrude(convexity = 10)
        translate([innerDiameter/2, 0, 0])
        circle(r = wallThickness, $fn = smoothness);
    }
  cylinder(h=innerDepth*4,r=innerDiameter/2,center=true);
}

translate([0,0,wallThickness*1.5])
rotate([180,0,0])
rotate_extrude(convexity = 10)
translate([innerDiameter/2-wallThickness/2, 0, 0])
difference()
{
  square(wallThickness,center=false);
  circle(r = wallThickness/2, $fn = smoothness);
}

for(ridge=[0:ridges-1])
  {
    hull()
      {
        rotate([0,0,360/ridges*ridge])
          translate([innerDiameter/2+wallThickness,0,wallThickness*5/4])
          sphere(r=wallThickness/4,$fn=smoothness);
        rotate([0,0,360/ridges*ridge])
          translate([innerDiameter/2+wallThickness,0,innerDepth+wallThickness*3/4])
          sphere(r=wallThickness/4,$fn=smoothness);
      }
  }

Bouchon nu (cap-body)

Bouchon complet fermé : cap-body-common + un disque plein de fermeture + la bague d’étanchéité. C’est le bouchon “basique” qu’on imprime pour boucher un bidon qu’on ne veut pas remplir. Utilisé dans les bouchons de référence MIEUXA/Netto pour vérifier qu’un standard de filetage donné s’adapte bien au col avant d’investir dans la variante avec entonnoir.

union()
{
  capThread();

  difference()
  {
    union()
      {
        translate([0,0,innerDepth/2+wallThickness])
          cylinder(h=innerDepth,r=(innerDiameter)/2+wallThickness,center=true);

        translate([0,0,wallThickness])
          rotate_extrude(convexity = 10)
          translate([innerDiameter/2, 0, 0])
          circle(r = wallThickness, $fn = smoothness);
      }
    cylinder(h=innerDepth*4,r=innerDiameter/2,center=true);
  }

  translate([0,0,wallThickness*1.5])
  rotate([180,0,0])
  rotate_extrude(convexity = 10)
  translate([innerDiameter/2-wallThickness/2, 0, 0])
  difference()
  {
    square(wallThickness,center=false);
    circle(r = wallThickness/2, $fn = smoothness);
  }

  for(ridge=[0:ridges-1])
    {
      hull()
        {
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,wallThickness*5/4])
            sphere(r=wallThickness/4,$fn=smoothness);
          rotate([0,0,360/ridges*ridge])
            translate([innerDiameter/2+wallThickness,0,innerDepth+wallThickness*3/4])
            sphere(r=wallThickness/4,$fn=smoothness);
        }
    }
    translate([0,0,wallThickness/2])cylinder(h=wallThickness,r=innerDiameter/2,center=true);
  difference()
  {
    cylinder(h=sealHeight,r=innerDiameter/2-sealGap);
    cylinder(h=sealHeight+1,r=innerDiameter/2-sealGap-sealThickness);
  }
      }

Bague d’étanchéité (cap-joint)

Anneau intérieur qui descend du dessous du bouchon et qui vient sceller contre la lèvre du col du bidon pendant le vissage du bouchon sur le bidon. C’est l’étanchéité bouchon/bidon, distincte du joint fibre plat qui, lui, assure l’étanchéité bouchon/entonnoir à l’interface filetée. Dimensions réglées par sealHeight, sealThickness et sealGap dans les params du bidon.

difference()
{
  cylinder(h=sealHeight,r=innerDiameter/2-sealGap);
  cylinder(h=sealHeight+1,r=innerDiameter/2-sealGap-sealThickness);
}

Jupe de test pour la bague d’étanchéité

Petite pièce de test isolée : juste la bague d’étanchéité seule, avec un plancher de base. Utile pour valider rapidement que les paramètres sealHeight=/=sealThickness=/=sealGap donnent bien un ajustement étanche sur le col, sans imprimer un bouchon entier.

$fn=50;

union()
{
  // cylinder(h=wallThickness,r=innerDiameter/2-sealGap);
  difference()
  {
    cylinder(h=sealHeight,r=innerDiameter/2-sealGap);
    cylinder(h=sealHeight+1,r=innerDiameter/2-sealGap-sealThickness);
  }
    }

Notes pointant ici