/* ========================================================================= Abat-jour parametrique ------------------------------------------------------------------------- Compatible MakerWorld Parametric Model Maker. Toutes les cotes sont en millimetres. La silhouette est definie par 7 rayons relatifs, repartis regulierement du bas vers le haut, relies par une spline de Catmull-Rom a tension reglable. Le profil est ensuite renormalise pour que le corps, une fois epaissi, tienne exactement dans diam_max : impossible de deborder du plateau en bougeant les curseurs. ========================================================================= */ /* ------------------------------------------------------------------------- MODE VASE -- le meilleur rendu pour un abat-jour, et de loin. Une paroi d'une seule extrusion laisse passer la lumiere et n'a aucune couture. 1.6 mm de PLA, non : c'est opaque, et le remplissage projette des ombres. Le modele reste un SOLIDE FERME, toujours. Le mode vase est un reglage du slicer, pas une propriete du maillage : on lui donne un tube ferme a paroi mince, il n'en garde que le contour exterieur. Le dessus plat perce n'est PAS un obstacle -- c'est meme la clef. On imprime l'abat-jour RETOURNE, plateau sur le plateau : 1. le slicer imprime d'abord les couches pleines du fond -- c'est le plateau perce, donc le support de la douille, imprime plein et solide ; 2. puis il passe en spirale pour tout le corps. C'est exactement ce que fait le modele de reference. Le "second solide a la taille d'une E27" qu'on trouve dans son .3mf, c'est le renfort ci-dessous : 0.8 mm de paroi ne porte pas une douille, mais epaissir tout le corps ruinerait le rendu. On n'epaissit donc que l'anneau de la douille, et comme il tient entier dans les couches pleines du fond, il ne casse pas la spirale. Le mode vase ne se regle pas ici : c'est une case du slicer. Ce fichier n'a qu'a produire le bon solide. Recette : epaisseur = 0.8 (deux largeurs de buse ; le slicer n'en gardera qu'une, mais le solide doit rester assez epais pour que le contour exterieur tombe juste) dessus = "plat perce" renfort_ep = 0 (c'est le defaut -- voir plus bas) Dans le slicer : retourner l'objet, cocher le Mode vase spirale. Rien d'autre : mesure faite sur le maillage, chaque couche n'a qu'un seul ilot, la spirale passe partout. renfort_ep > 0 est la seule chose qui complique ce tableau : la couronne ne touche pas la paroi, donc sur les couches ou les deux existent il y a deux ilots, et il faut les couvrir en couches pleines. Le fichier affiche le nombre exact au rendu. Les stries : le pas des couches en cree deja de tres fines. Les modeliser par-dessus (pas 2.1 mm, creux 0.35) reproduit la texture large du modele de reference, qui elle est bien de la geometrie. ------------------------------------------------------------------------- */ /* [Silhouette] */ // La silhouette se dessine A LA SOURIS dans le configurateur web du projet // (index.html) : on tire les points, on ajoute, on supprime, la 3D suit en // direct, et un bouton recopie le bloc de parametres a coller ici. Le // customizer d'OpenSCAD ne sait afficher que des curseurs -- c'est une limite // de MakerWorld, pas un choix. Les curseurs ci-dessous sont donc la sortie de // l'atelier, pas l'atelier. // Hauteur totale hauteur = 200; // [80:5:400] // Diametre hors tout, epaisseur comprise diam_max = 240; // [60:5:400] // Nombre de points de controle reellement utilises, du bas vers le haut. // Les rayons au-dela sont ignores : c'est ainsi qu'on "supprime" un point. // Moins de points = courbe plus douce, plus de points = plus de controle. n_points = 7; // [3:1:9] // Rayon au bord bas r0 = 58; // [5:1:100] r1 = 88; // [5:1:100] r2 = 100; // [5:1:100] r3 = 87; // [5:1:100] r4 = 96; // [5:1:100] r5 = 81; // [5:1:100] // Rayon au bord haut quand n_points = 7 r6 = 58; // [5:1:100] // Utilises seulement si n_points vaut 8 ou 9 r7 = 70; // [5:1:100] r8 = 55; // [5:1:100] /* [Silhouette] */ // Tension de la courbe : 0 = segments droits, 100 = tres arrondi. // Celle-ci agit TOUJOURS, y compris sur les formes toutes faites. tension = 60; // [0:1:100] /* [Nervures] */ // Type de relief nervures = "helicoidales"; // [aucune, verticales, helicoidales, ondulation, facettes] // Nombre de nervures (ou de vagues pour l'ondulation) n_nervures = 24; // [3:1:64] // Profondeur, en pourcentage du rayon profondeur = 4; // [0:0.5:18] // Torsion totale sur la hauteur, pour les nervures helicoidales torsion = 90; // [-360:5:360] // Rainure creusee dans chaque nervure rainure = 30; // [0:1:100] // Fondu aux deux bords, en pourcentage de la hauteur fondu = 10; // [0:1:40] /* [Stries fines] */ // Fines rainures horizontales, comme sur un vase imprime en mode vase. Elles // se cumulent avec les nervures ci-dessus : elles ne les remplacent pas. // ATTENTION : elles imposent la finesse verticale et coutent donc cher en // triangles. Cumulees avec des nervures profondes, il faut monter le budget. stries = "non"; // [non, oui] // Pas vertical, en mm. Le modele qui a servi de reference mesure 2.1 mm, // ce qui correspond a 10 couches de 0.2 -- autrement dit a sa texture // d'impression, pas a sa geometrie. pas_stries = 2.1; // [0.6:0.1:8] // Profondeur crete a creux, en mm. Reference : 0.35. La strie ne creuse que // la peau exterieure, la paroi passe donc de epaisseur a epaisseur - creux. creux_stries = 0.35; // [0.05:0.05:1.2] // Echantillons verticaux par strie. 4 place deja l'erreur de corde sous // 0.05 mm, soit un quart de couche : monter au-dela ne change rien a // l'impression, seulement au rendu a l'ecran. pts_par_strie = 6; // [4:1:12] /* [Dessus et douille] */ // Terminaison haute dessus = "plat perce"; // [plat perce, plat plein, ouvert] // Diametre du percage central. E27 ~ 40, E14 ~ 28, GU10 ~ 35, cable ~ 10 percage = 40; // [8:1:120] // Rayon de l'arrondi entre le corps et le plateau conge = 10; // [0:1:40] // RENFORT DE DOUILLE. Couronne qui double le plateau par l'interieur, la ou // se serre la douille : 0.8 mm de paroi ne la porterait pas, et epaissir tout // le corps ruinerait le rendu. En "complet" elle fait partie du maillage. // Diametre exterieur. Ramene au bord du plateau s'il est plus petit : au-dela // commence le conge, qui s'echappe vers le bas, il n'y a rien a renforcer. renfort_diam = 90; // [20:1:240] // Epaisseur ajoutee. 0 par defaut, et ce n'est pas un oubli : le plateau est // pris en SANDWICH entre la douille et sa bague, donc sollicite en // compression, ou 1.6 mm de PLA suffit largement. Le renfort a en revanche un // cout reel : il ne touche pas la paroi, et sur les couches ou les deux // existent la coupe donne deux ilots que le mode vase ne sait pas relier -- // il faut alors couvrir toute la zone en couches pleines. Sans lui, un seul // ilot partout. A monter si tu veux une portee franche sous la bague. renfort_ep = 0; // [0:0.2:8] /* [Fabrication] */ // PREREGLAGE. Il accorde a lui seul l'epaisseur, le dessus et les stries -- // les trois reglages qui doivent aller ensemble pour que ca s'imprime bien. // Epaisseur de paroi. 1.6 pour une impression ordinaire ; 0.8 pour le Mode // vase spirale du slicer (voir l'entete du fichier). epaisseur = 1.6; // [0.4:0.1:6] // Facettes par nervure. C'EST LE REGLAGE QUI COMPTE. // Ce n'est pas le cercle qu'il faut resoudre, c'est la nervure. Avec 24 // nervures et 240 facettes sur le tour, chaque nervure n'est decrite que // par 10 segments : la crete sort en escalier, meme si le cercle, lui, est // parfaitement lisse. En dessous de 12 c'est un polygone, 16 est correct, // 20 est lisse. facettes_par_nervure = 28; // [8:1:72] // Plancher, quand il n'y a pas de nervure ou qu'elles sont peu nombreuses. facettes_mini = 180; // [96:12:480] // Plancher vertical, quand il n'y a pas de torsion. finesse_mini = 90; // [30:5:200] // Budget de triangles. Les deux resolutions sont derivees des nervures, donc // une combinaison extreme (64 nervures + 270 deg de torsion) reclamerait des // millions de facettes et plusieurs minutes de calcul -- au-dela de ce que // MakerWorld accepte. Ce budget les reduit ensemble, en gardant leur rapport. budget_triangles = 500000; // [100000:50000:2500000] /* [Hidden] */ // ATTENTION A L'ORDRE. OpenSCAD evalue undef > 0 comme faux sans rien dire : // une variable utilisee avant sa definition fait silencieusement basculer un // test dans la mauvaise branche. Rmax, AMP_MAX et PROF doivent donc etre // definis AVANT FAC0 et FIN0, qui s'en servent. STRIES_ON = (stries == "oui"); Rmax = diam_max / 2 - epaisseur; // GARDE-FOU. Au fond d'un creux de nervure, le rayon de courbure vaut // Rmax / (1 + amplitude * n^2). Quand il descend sous l'epaisseur, la paroi // interieure se retourne sur elle-meme : le maillage reste apparie (le // controle d'etancheite ne voit rien) mais il est auto-intersecte, et le // slicer en fait n'importe quoi. On borne donc l'amplitude. AMP_MAX = 100 * (Rmax / (1.5 * epaisseur) - 1) / (n_nervures * n_nervures); PROF = min(profondeur, max(0, AMP_MAX)); // Resolution angulaire effective : assez de segments pour decrire la nervure, // jamais moins que le plancher, et bornee pour ne pas exploser le calcul. FAC0 = min(1800, max(facettes_mini, (nervures == "aucune" || PROF <= 0) ? 0 : n_nervures * facettes_par_nervure)); // Resolution VERTICALE. Une nervure helicoidale decale sa phase avec la // hauteur : le motif traverse (n_nervures x torsion / 360) periodes de bas en // haut, et il faut les echantillonner comme on echantillonne le tour. Sans ca, // augmenter les facettes ne sert a rien -- on raffine l'axe qui n'est pas le // goulot. Mesure : a torsion 0 l'angle median entre facettes vaut 2 deg, a // 270 deg il monte a 26 deg sans que le nombre de facettes n'y change rien. // Stries. Definies AVANT FIN0, qui s'en sert -- cf. l'avertissement sur // l'ordre en tete de bloc. L'amplitude est bornee pour laisser au moins une // buse de paroi au fond de la strie. AST = STRIES_ON ? min(creux_stries, max(0, epaisseur - 0.35)) : 0; FIN_ST = (AST > 0) ? ceil(hauteur / pas_stries * pts_par_strie) : 0; FIN0 = min(4000, max(finesse_mini, FIN_ST, (nervures == "helicoidales" && PROF > 0) ? ceil(n_nervures * abs(torsion) / 360 * facettes_par_nervure) : 0)); // Reduction si le budget est depasse. Sans strie, les deux axes decrivent le // meme motif et se raboteent a parts egales. Avec stries, la finesse // verticale devient un PLANCHER : sous-echantillonner une strie ne l'adoucit // pas, elle se replie en moire -- pire que pas de strie du tout. Toute la // reduction passe donc sur l'angulaire, et si ca ne suffit pas c'est le // budget qu'il faut monter, pas la finesse qu'il faut baisser. TRI0 = 4 * (FIN0 + 16) * FAC0; KB = min(1, budget_triangles / TRI0); // Les affectations de haut niveau sont evaluees meme si le resultat n'est pas // rendu : sans ce garde-fou, demander le seul renfort calculerait quand meme // le polyedre complet de l'abat-jour. // ATTENTION : le garde-fou ne touche QUE l'angulaire. Le renfort se cale sur // le plateau, dont la hauteur sort du conge, donc de la meridienne : rogner // aussi la finesse verticale le decalait de 3.6 mm -- le renfort flottait // sous le plateau qu'il est cense doubler. FAC = max(96, floor(FAC0 * ((FIN_ST > 0) ? KB : sqrt(KB)))); FIN = max(40, floor(FIN0 * ((FIN_ST > 0) ? 1 : sqrt(KB)))); R_TOUS = [r0, r1, r2, r3, r4, r5, r6, r7, r8]; NP = max(3, min(9, n_points)); R = [for (i = [0:NP-1]) R_TOUS[i]]; NS = NP - 1; EPS = 0.0001; // ---------- silhouette : Catmull-Rom sur points equidistants ---------- function brut(t) = let(u = max(0, min(1 - EPS, t)) * NS, i = floor(u), f = u - i, p0 = R[max(0, i - 1)], p1 = R[i], p2 = R[min(NS, i + 1)], p3 = R[min(NS, i + 2)], c = tension / 100 * 0.5, m1 = c * (p2 - p0), m2 = c * (p3 - p1), f2 = f * f, f3 = f2 * f) (2*f3 - 3*f2 + 1) * p1 + (f3 - 2*f2 + f) * m1 + (-2*f3 + 3*f2) * p2 + (f3 - f2) * m2; // Le rayon reellement atteint combine la silhouette ET le relief : normaliser // sur la seule silhouette laisserait les nervures deborder du plateau. // L'ondulation varie selon la hauteur, les autres reliefs selon l'angle : // on echantillonne finement la direction ou la frequence est la plus haute. NT = (nervures == "ondulation") ? max(80, 8 * n_nervures) : 40; NA = (nervures == "ondulation") ? 1 : max(48, 6 * n_nervures); function maxcombine() = max([for (i = [0:NT], j = [0:NA-1]) brut(i/NT) * relief(i/NT, 360*j/NA)]); ECH = Rmax / maxcombine(); Rd = percage / 2; Rfin = (dessus == "plat plein") ? 0 : Rd; function rayon(t) = brut(t) * ECH; // ---------- meridienne exterieure : corps, puis conge, puis plateau ------ corps = [for (i = [0:FIN]) [rayon(i/FIN), i/FIN * hauteur, i/FIN]]; Ra = corps[FIN][0]; Za = corps[FIN][1]; Rb = corps[FIN - 1][0]; Zb = corps[FIN - 1][1]; TH0 = atan2(Ra - Rb, Za - Zb); // degres, tangente / verticale NC = 14; conge_pts = (dessus == "ouvert") ? [] : [for (i = [1:NC]) let(th = TH0 + (-90 - TH0) * (i / NC)) [Ra + conge * (cos(th) - cos(TH0)), Za + conge * (sin(TH0) - sin(th)), 1]]; // ---------- renfort de douille, porte par la meridienne ----------------- // Il ne peut PAS etre ajoute par CSG : unir une couronne a un polyedre de // 450 000 triangles prend plus de deux minutes sous CGAL, et MakerWorld // tourne sur OpenSCAD 2021, donc sans Manifold. On epaissit donc la peau // interieure du plateau, en deca de RE -- meme resultat, un seul polyedre, // aucun cout. Z_PLAT = (dessus == "ouvert") ? hauteur : conge_pts[NC - 1][1]; R_PLAT = (dessus == "ouvert") ? rayon(1) : conge_pts[NC - 1][0]; RENFORT_ON = (dessus != "ouvert") && renfort_ep > 0; RE = max(Rfin + 3, min(renfort_diam / 2, R_PLAT - 1)); // Deux points au lieu d'un : sans le second, l'epaississement ne s'arreterait // nulle part et deviendrait un biseau courant sur tout le plateau. 0.6 mm // d'ecart, bien au-dessus du seuil du filtre de points confondus. PLAT_REN = RENFORT_ON && RE > Rfin + 2 && RE < R_PLAT - 0.5; plateau_pts = (dessus == "ouvert") ? [] : PLAT_REN ? [[RE, Z_PLAT, 1], [RE - 0.6, Z_PLAT, 1], [Rfin, Z_PLAT, 1]] : [[Rfin, Z_PLAT, 1]]; // Deux points confondus (conge a 0, plateau de largeur nulle) produiraient un // anneau de facettes degenerees : le maillage cesserait d'etre etanche. BRUTE = concat(corps, conge_pts, plateau_pts); MER = [for (i = [0:len(BRUTE)-1]) if (i == 0 || norm([BRUTE[i][0] - BRUTE[i-1][0], BRUTE[i][1] - BRUTE[i-1][1]]) > 0.002) BRUTE[i]]; M = len(MER); // ---------- paroi interieure : decalage selon la normale 2D ------------- function voisin(i, d) = MER[max(0, min(M - 1, i + d))]; function normale(i) = let(a = voisin(i, -1), b = voisin(i, 1), dr = b[0] - a[0], dz = b[1] - a[1], L = max(EPS, sqrt(dr*dr + dz*dz))) [dz / L, -dr / L]; INT = [for (i = [0:M-1]) let(n = normale(i), // Sur le plateau et en deca de RE, la peau interieure descend de // renfort_ep de plus : c'est le renfort. ren = PLAT_REN && MER[i][1] > Z_PLAT - 0.002 && MER[i][0] < RE - 0.3) [max(0, MER[i][0] - n[0] * epaisseur), MER[i][1] - n[1] * epaisseur - (ren ? renfort_ep : 0), MER[i][2]]]; // boucle fermee : exterieur a l'aller, interieur au retour LOOP = concat(MER, [for (i = [M-1 : -1 : 0]) INT[i]]); ML = len(LOOP); // ---------- facteur de relief ------------------------------------------ function enveloppe(t) = let(fd = max(0.001, fondu / 100)) max(0, min(1, t / fd, (1 - t) / fd)); function relief(t, a) = (nervures == "aucune" || PROF <= 0) ? 1 : let(amp = PROF / 100, a2 = amp * rainure / 100, env = enveloppe(t)) (env <= 0) ? 1 : (nervures == "ondulation") ? 1 + env * amp * cos(360 * t * n_nervures) : let(psi = a - ((nervures == "helicoidales") ? torsion * t : 0)) (nervures == "facettes") ? let(x = (psi * n_nervures / 360) - floor(psi * n_nervures / 360)) 1 + env * amp * (abs(2*x - 1) * 2 - 1) : 1 + env * (amp * cos(n_nervures * psi) + a2 * cos(2 * n_nervures * psi + 180)); // ---------- stries fines ------------------------------------------------ // Creusees UNIQUEMENT sur la peau exterieure. Les porter aussi a l'interieur // retournerait la paroi : au fond d'une strie le rayon de courbure vaut // pas^2 / (4 pi^2 creux), soit 0.32 mm pour 2.1 / 0.35 -- cinq fois moins que // l'epaisseur. Le maillage resterait apparie, mais auto-intersecte. // (1 - cos)/2 reste dans [0,1] : la strie ne fait que rentrer, jamais sortir. // Le diametre hors tout ne bouge donc pas et la normalisation reste juste. function fondu_strie(t) = max(0, min(1, t / 0.02, (1 - t) / 0.02)); function strie(z, t) = (AST <= 0) ? 0 : AST * fondu_strie(t) * (1 - cos(360 * z / pas_stries)) / 2; // ---------- polyedre ---------------------------------------------------- // i < M : peau exterieure. Au-dela, on revient par l'interieur, qui reste // lisse. PTS = [for (i = [0:ML-1], j = [0:FAC-1]) let(a = 360 * j / FAC, r = LOOP[i][0] * relief(LOOP[i][2], a) - ((i < M) ? strie(LOOP[i][1], LOOP[i][2]) : 0)) [r * cos(a), r * sin(a), LOOP[i][1]]]; function idx(i, j) = (i % ML) * FAC + (j % FAC); FACES = [for (i = [0:ML-1], j = [0:FAC-1], k = [0:1]) (k == 0) ? [idx(i, j), idx(i+1, j), idx(i+1, j+1)] : [idx(i, j), idx(i+1, j+1), idx(i, j+1)]]; module abatjour() { polyhedron(points = PTS, faces = FACES, convexity = 8); } // MakerWorld appelle mw_plate_1(). Hors MakerWorld, le rendu direct suffit. module mw_plate_1() { abatjour(); } // Le renfort et la paroi ne se touchent pas : entre les deux il y a le vide de // la cavite. Sur les couches ou les deux existent, la coupe donne DEUX ilots, // et une spirale ne sait pas les relier d'un trait continu. Ces couches-la // doivent donc rester dans les couches PLEINES du fond, qui elles sont // tranchees normalement. Mesure sur le maillage : deux ilots depuis epaisseur // jusqu'a epaisseur + renfort_ep, un seul au-dessus. if (RENFORT_ON) echo(str("MODE VASE : mettre assez de couches pleines en fond pour couvrir ", epaisseur + renfort_ep, " mm, soit ", ceil((epaisseur + renfort_ep) / 0.2), " couches en 0.2 mm. En dessous, la spirale casse.")); mw_plate_1();