Conception et impression d’une bonde de piscine

Lors d’une folle après midi d’utilisation de la piscine, la bonde de fond de piscine a cédé.

Je pars pour imprimer sa remplaçante.

Trois solutions s’offrent à moi :

  • concevoir la pièce humainement (c’est comme avec l’arrivée des vélo electriques, on se sent obligé de trouver un adjectif pour definir l’ancien monde…)
  • trouver la pièce sur https://www.thingiverse.com/
  • concevoir la pièce avec l’aide de l’IA

Je n’ai pas trouvé la bonne pièce sur https://www.thingiverse.com/, donc je lance mon Claude et le MCP sur mon FreeCad.
Je lui envoie la photo ainsi que les deux dimensions clés : le diametre et l’entre axe.
Je lui indique aussi de chanfreiner les trous de vis et d’ajouter un renfort en étoile.

Mon explication pour les renforts…

La nouvelle bonde imprimée, PETG, cloisons 99%

bonde_v3_parametrique.py FreeCAD / Python
import FreeCAD as App, Part, math, os

# ================= PARAMETRES =================
D_OUT     = 174.0   # diametre total
ENTRAXE   = 158.0   # entraxe des 2 trous de vis
T         = 5.0     # epaisseur de la grille
D_SCREW   = 6.0     # percage des trous de vis
D_CSK     = 10.0    # diametre d'ouverture du fraisage (tete fraisee 9 mm)
D_CENTER  = 26.0    # diametre du trou central
R_HUB_OUT = 21.0    # rayon exterieur du moyeu plein
RIM_W     = 14.0    # largeur de la couronne exterieure (portee, SANS renfort)
N_RINGS   = 4       # anneaux de fentes
N_WEBS    = 6       # nervures radiales
WEB_W     = 6.0     # largeur des nervures radiales
WALL_R    = 5.0     # nervure circulaire entre deux anneaux
CH_EDGE   = 0.8     # chanfrein du bord exterieur
RIB_H     = 5.0     # hauteur des renforts en sous-face
RIB_W     = 6.0     # largeur des renforts radiaux
RIB_TAPER = 5.0     # longueur du raccord a 45 deg
RIB_MARGIN= 3.0     # retrait des renforts avant la couronne de portee
# ==============================================

R_OUT  = D_OUT/2.0
R_HOLE = ENTRAXE/2.0
R_GRID = R_OUT - RIM_W          # 73.0 : debut de la couronne de portee
R_RIB_END = R_GRID - RIB_MARGIN  # les renforts s'arretent ici
CSK_D  = (D_CSK - D_SCREW)/2.0  # profondeur du fraisage (cone a 90 deg)

name = "Bonde174v3"
if name in App.listDocuments(): App.closeDocument(name)
doc = App.newDocument(name)

pitch  = (R_GRID - R_HUB_OUT) / N_RINGS
slot_w = pitch - WALL_R

def sector(ri, ro, a1, a2):
    pie = Part.makeCylinder(ro, T+4, App.Vector(0,0,-2), App.Vector(0,0,1), a2-a1)
    pie.rotate(App.Vector(0,0,0), App.Vector(0,0,1), a1)
    return pie.cut(Part.makeCylinder(ri, T+8, App.Vector(0,0,-4)))

# ---------- grille ----------
cut = None
for i in range(N_RINGS):
    rm = R_HUB_OUT + i*pitch + WALL_R/2.0 + slot_w/2.0
    half = math.degrees((WEB_W/2.0 + slot_w/2.0) / rm)
    for k in range(N_WEBS):
        a0 = 360.0*k/N_WEBS
        a1, a2 = a0 + half, a0 + 360.0/N_WEBS - half
        s = sector(rm - slot_w/2.0, rm + slot_w/2.0, a1, a2)
        for aa in (a1, a2):
            t = math.radians(aa)
            s = s.fuse(Part.makeCylinder(slot_w/2.0, T+4,
                       App.Vector(rm*math.cos(t), rm*math.sin(t), -2)))
        cut = s if cut is None else cut.fuse(s)

shape = Part.makeCylinder(R_OUT, T).cut(cut.removeSplitter())

# ---------- renforts en sous-face (z = -RIB_H .. 0) ----------
ribs = Part.makeCylinder(R_HUB_OUT, RIB_H, App.Vector(0,0,-RIB_H))      # anneau central
ribs = ribs.cut(Part.makeCylinder(D_CENTER/2.0, RIB_H+4, App.Vector(0,0,-RIB_H-2)))
for k in range(N_WEBS):
    b = Part.makeBox(R_GRID+2, RIB_W, RIB_H, App.Vector(0, -RIB_W/2.0, -RIB_H))
    b.rotate(App.Vector(0,0,0), App.Vector(0,0,1), 360.0*k/N_WEBS)
    ribs = ribs.fuse(b)
ribs = ribs.cut(Part.makeCylinder(D_CENTER/2.0, RIB_H+4, App.Vector(0,0,-RIB_H-2)))
# raccord a 45 deg : on retire tout ce qui sort du cone -> hauteur nulle a R_GRID
outside = Part.makeCylinder(R_OUT+10, RIB_H, App.Vector(0,0,-RIB_H)).cut(
          Part.makeCone(R_RIB_END-RIB_TAPER, R_RIB_END, RIB_H,
                        App.Vector(0,0,-RIB_H), App.Vector(0,0,1)))
ribs = ribs.cut(outside)
shape = shape.fuse(ribs).removeSplitter()

# ---------- percages ----------
shape = shape.cut(Part.makeCylinder(D_CENTER/2.0, T+RIB_H+4, App.Vector(0,0,-RIB_H-2)))
for k in range(2):
    t = math.pi*k
    x, y = R_HOLE*math.cos(t), R_HOLE*math.sin(t)
    shape = shape.cut(Part.makeCylinder(D_SCREW/2.0, T+4, App.Vector(x,y,-2)))

# fraisage : chanfrein sur l'arete des percages, au niveau de la face superieure
ar = [e for e in shape.Edges
      if len(e.Vertexes) == 1
      and abs(e.BoundBox.ZMax - T) < 1e-6 and abs(e.BoundBox.ZMin - T) < 1e-6
      and abs(e.Length - 2*math.pi*(D_SCREW/2.0)) < 0.5]
print("aretes de percage sur la face superieure : %d (attendu 2)" % len(ar))
assert len(ar) == 2, "selection d'aretes incorrecte"
shape = shape.makeChamfer(CSK_D, ar)

def chamfer(sh, r, z, size):
    es = [e for e in sh.Edges if len(e.Vertexes)==1
          and abs(e.BoundBox.ZMax-z)<1e-6 and abs(e.Length-2*math.pi*r)<0.5]
    if not es: return sh
    try: return sh.makeChamfer(size, es)
    except Exception as ex:
        print("chanfrein ignore:", ex); return sh

shape = chamfer(shape, R_OUT, T, CH_EDGE)
shape = chamfer(shape, R_OUT, 0.0, CH_EDGE)
shape = chamfer(shape, D_CENTER/2.0, T, CH_EDGE)

obj = doc.addObject("Part::Feature", "Bonde"); obj.Shape = shape; doc.recompute()

# ---------- controles ----------
print("--- CONTROLE V3 ---")
bb = shape.BoundBox
sec = shape.section(Part.makePlane(400,400, App.Vector(-200,-200, T/2.0)))
print("diametre reel        : %.3f mm" % (2*max((v.Point.x**2+v.Point.y**2)**.5 for v in sec.Vertexes)))
print("hauteur totale       : %.3f mm  (Z de %.2f a %.2f)" % (bb.ZLength, bb.ZMin, bb.ZMax))
print("epaisseur grille     : %.3f mm | renfort : %.3f mm" % (T, -bb.ZMin))
cen = sorted({(round(f.BoundBox.Center.x,2), round(f.BoundBox.Center.y,2)) for f in shape.Faces
              if hasattr(f.Surface,"Radius") and abs(f.Surface.Radius-D_SCREW/2.0)<1e-6})
print("entraxe              : %.3f mm  %s" % (math.hypot(cen[-1][0]-cen[0][0], cen[-1][1]-cen[0][1]), cen))
csk = sorted({round(f.Surface.Radius,3) for f in shape.Faces
              if f.Surface.__class__.__name__=="Cone" and f.BoundBox.ZMax > T-0.01
              and abs(f.BoundBox.Center.x) > 10})
print("--- profil du fraisage (trou a x=+%.0f) ---" % R_HOLE)
for zc in [0.5, 1.5, 2.5, 3.0, 3.5, 4.0, 4.5, 4.95]:
    pl = Part.makePlane(400, 400, App.Vector(-200, -200, zc))
    sc = shape.section(pl)
    rmin = 1e9
    for e in sc.Edges:
        for pt in e.discretize(60):
            d = math.hypot(pt.x - R_HOLE, pt.y)
            if d < 12: rmin = min(rmin, d)
    if rmin < 1e8:
        print("  z=%4.2f mm  ->  diametre du percage = %6.3f mm" % (zc, 2*rmin))
print("attendu : %.1f mm jusqu'a z=%.1f puis elargissement jusqu'a %.1f mm en z=%.1f"
      % (D_SCREW, T-CSK_D, D_CSK, T))
# la couronne de portee doit etre strictement plane en sous-face
under = [f for f in shape.Faces if abs(f.BoundBox.ZMax) < 1e-6 and abs(f.BoundBox.ZMin) < 1e-6]
rmin_under = min((min((v.Point.x**2+v.Point.y**2)**.5 for v in f.Vertexes)) for f in under)
low = [f for f in shape.Faces if f.BoundBox.ZMin < -1e-6]
rmax_rib = max(max((v.Point.x**2+v.Point.y**2)**.5 for v in f.Vertexes) for f in low)
print("renfort : rayon max  = %.3f mm   (cible %.1f, couronne a %.1f)" % (rmax_rib, R_RIB_END, R_GRID))
print("retrait reel avant la couronne : %.3f mm" % (R_GRID - rmax_rib))
print("sous-face plane de r=%.3f a r=%.1f mm  -> disque exterieur intact : %s"
      % (rmax_rib, R_OUT, rmax_rib <= R_RIB_END + 1e-6))
print("solide valide        : %s | solides : %d | faces : %d" % (shape.isValid(), len(shape.Solids), len(shape.Faces)))
print("volume               : %.1f cm3" % (shape.Volume/1000.0))

out = r"C:\chemin\vers\ton\dossier"
doc.saveAs(os.path.join(out, "bonde_v3_174_entraxe158.FCStd"))
import MeshPart
m = MeshPart.meshFromShape(Shape=shape, LinearDeflection=0.04, AngularDeflection=0.15, Relative=False)
m.write(os.path.join(out, "bonde_v3_174_entraxe158.stl"))
print("STL : %d triangles" % m.CountFacets)
try:
    import FreeCADGui as Gui
    d = r"C:\chemin\vers\dossier"
    v = Gui.activeDocument().activeView()
    v.viewAxonometric(); Gui.SendMsgToActiveView("ViewFit"); v.saveImage(d+r"\v3_haut.png", 1200, 1000, "White")
    v.viewRear(); v.viewAxonometric()
    import FreeCAD
    v.setCameraOrientation(App.Rotation(App.Vector(1,0,0), 200).multiply(App.Rotation(App.Vector(0,0,1), 30)))
    Gui.SendMsgToActiveView("ViewFit"); v.saveImage(d+r"\v3_bas.png", 1200, 1000, "White")
except Exception as ex: print("vue:", ex)

La bonde réinstallée

Et ma contribution sur Thingiverse : https://www.thingiverse.com/thing:7410017

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *