/* ========================================================================= 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 // (https://studio-cocha.com/lampshade) : 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:120] // 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] // Changements de sens de l'helice : 0 = helice simple, 1 = chevron (la // nervure tourne dans un sens jusqu'a mi-hauteur, puis revient), 2 et plus = // zigzag. Chaque troncon garde la meme pente : la torsion se partage entre eux. inversions = 0; // [0:1:8] // Fondu au changement de sens : 0 = pointe nette (chevron), 100 = le virage // s'etale sur la moitie de chaque troncon, la nervure se courbe au lieu de // casser. fondu_inversion = 0; // [0:1:100] // Rainure creusee dans chaque nervure rainure = 30; // [0:1:100] // Fondu aux deux bouts de chaque zone, en pourcentage de la hauteur fondu = 10; // [0:1:40] /* [Zones de relief] */ // Les nervures ne vivent que dans ces zones, en pourcentage de la hauteur, du // bas vers le haut ; le reste de la paroi reste lisse. Une seule zone de 0 a // 100 = des nervures partout. Les zones au-dela de nb_zones sont ignorees. nb_zones = 1; // [1:1:4] zone1_de = 0; // [0:1:100] zone1_a = 100; // [0:1:100] zone2_de = 0; // [0:1:100] zone2_a = 0; // [0:1:100] zone3_de = 0; // [0:1:100] zone3_a = 0; // [0:1:100] zone4_de = 0; // [0:1:100] zone4_a = 0; // [0:1:100] /* [Ajours] */ // Des ouvertures a travers la paroi : la lumiere passe par elles. Elles sont // decoupees dans la grille du maillage lui-meme (pas de difference(), le // solide reste ferme et le rendu reste immediat), et suivent l'helice comme // les nervures. Pas de mode vase avec des ajours : la spirale serait coupee. // "voronoi" : des cellules organiques, une graine par cellule du motif. Leurs // bords sont obliques : la grille est plus fine, et les sommets voisins d'un // bord y sont glisses. Le calcul est plus long (compter une a deux minutes). ajours = "aucun"; // [aucun, fentes, fenetres, points, voronoi] // Ouvertures sur un tour, par rangee ouvertures_par_tour = 16; // [4:1:48] // Rangees d'ouvertures sur la hauteur rangees = 6; // [1:1:16] // Part de sa cellule qu'occupe une ouverture, en pourcentage taille_ouvertures = 50; // [20:1:80] /* [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 = 3; // [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"); AJOURS_ON = (ajours != "aucun"); VORO = (ajours == "voronoi"); // Les zones de relief, en liste. Definies ICI, avant ECH : maxcombine() passe // par relief() puis enveloppe(), qui les lit. ZONES = [[zone1_de, zone1_a], [zone2_de, zone2_a], [zone3_de, zone3_a], [zone4_de, zone4_a]]; 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 ne coute plus de rangs : la // grille elle-meme est tordue le long de l'helice (voir PTS), chaque colonne // garde donc la meme phase du relief de bas en haut et la crete suit une // colonne au lieu de couper la grille en biais. Avant, a court de budget, les // deux axes etaient rabotes ensemble et la crete sortait en dents de scie. // Seule l'ondulation, dont le relief court le long de la hauteur, reclame des // rangs par periode. // 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 == "ondulation" && PROF > 0) ? n_nervures * facettes_par_nervure : 0)); // Troncons de l'helice : un de plus que de changements de sens. JAMBES = (nervures == "helicoidales") ? inversions + 1 : 1; // Reduction si le budget est depasse. Un axe porte le relief, l'autre n'a // qu'a dessiner une forme lisse : le second garde ce qu'il lui faut, le // premier prend le reste du budget. // - Ondulation : le cercle garde ses colonnes, les rangs suivent les vagues // (jamais sous les stries, qui sous-echantillonnees se replient en moire). // - Tout le reste : la silhouette garde ses rangs, les colonnes suivent les // nervures. // 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 : hors ondulation, 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. ONDUL = (nervures == "ondulation" && PROF > 0); FAC_B = max(96, ONDUL ? FAC0 : min(FAC0, floor(budget_triangles / (4 * (FIN0 + 16))))); // Avec des ajours, la grille doit tomber pile sur le motif : colonnes multiple // du double des ouvertures par tour (une rangee en quinconce se decale d'une // demi-cellule), au moins 4 facettes par cellule dans chaque sens. Le voronoi // n'a pas de quinconce mais des bords obliques : 12 facettes par graine. // Graines par tour en voronoi : le curseur, ou plus sur une lampe large, pour // qu'aucune cellule ne depasse 30 mm (voir la section voronoi). NC_V = max(ouvertures_par_tour, ceil(PI * diam_max / 30)); PAS_C = VORO ? NC_V : 2 * ouvertures_par_tour; MIN_C = VORO ? 12 * NC_V : 4 * PAS_C; FAC = AJOURS_ON ? ceil(max(FAC_B, MIN_C) / PAS_C) * PAS_C : FAC_B; FIN_B = max(40, ONDUL ? max(FIN_ST, min(FIN0, floor(budget_triangles / (4 * FAC)) - 16)) : FIN0); // Un changement de sens doit tomber pile sur un rang, sinon la pointe du // chevron est rognee : on arrondit au multiple du nombre de troncons. // Avec des ajours, les rangs sont aussi un multiple des rangees. function pgcd(a, b) = (b == 0) ? a : pgcd(b, a % b); PAS_R = rangees * JAMBES / pgcd(rangees, JAMBES); // Un bord voronoi court dans tous les sens : ses facettes sont carrees (aussi // hautes que larges au plus large), sinon les rangs le dessinent en escalier. FIN_CARRE = VORO ? ceil(hauteur * FAC / (PI * diam_max)) : 0; FIN = AJOURS_ON ? ceil(max(FIN_B, FIN_CARRE, (VORO ? 12 : 4) * rangees) / PAS_R) * PAS_R : ceil(FIN_B / JAMBES) * JAMBES; 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 ------------------------------------------ // Chaque zone monte et descend sur la longueur du fondu (au plus la moitie de // la zone) ; des zones qui se chevauchent se fondent simplement. ZONES est // defini en tete du bloc cache : maxcombine() s'en sert bien avant ici. function enveloppe_zone(t, z) = let(de = min(z[0], z[1]) / 100, a = max(z[0], z[1]) / 100, fd = max(0.001, min(fondu / 100, (a - de) / 2))) (a <= de) ? 0 : max(0, min(1, (t - de) / fd, (a - t) / fd)); function enveloppe(t) = max([for (k = [0 : max(1, min(4, nb_zones)) - 1]) enveloppe_zone(t, ZONES[k])]); // De combien l'helice a tourne a la hauteur t, en degres. En zigzag, chaque // troncon tourne dans un sens puis dans l'autre, a la meme pente. // Avec fondu_inversion, chaque pointe interieure est remplacee par une // parabole tangente aux deux troncons (meme position, meme pente) : la // nervure se courbe au lieu de casser, et rien ne bouge hors de la fenetre. function decalage(t) = (nervures != "helicoidales") ? 0 : let(u = max(0, min(1, t)) * JAMBES, k = min(JAMBES - 1, floor(u)), f = u - k, S = torsion / JAMBES, net = S * ((k % 2 == 0) ? f : 1 - f), w = fondu_inversion / 100 * 0.5, c = round(u), o = u - c, b = S * w / 2 + S * o * o / (2 * max(w, 0.0001))) (w <= 0 || c <= 0 || c >= JAMBES || abs(o) >= w) ? net : ((c % 2 == 1) ? S - b : b); // psi : la PHASE, c'est-a-dire l'angle mesure dans le repere de la nervure // (l'helice deja deroulee par decalage()). function relief(t, psi) = (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) : (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. // Chaque anneau est tourne de decalage(t) : la colonne j garde la phase p du // bas en haut, donc suit la nervure. La paroi etant de revolution, la rotation // ne deplace pas la silhouette ; la peau interieure partage les t de la peau // exterieure, donc sa rotation, et le solide reste ferme. PTS0 = [for (i = [0:ML-1], j = [0:FAC-1]) let(p = 360 * j / FAC, a = p + decalage(LOOP[i][2]), r = LOOP[i][0] * relief(LOOP[i][2], p) - ((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) + FAC) % FAC; // ---------- ajours ------------------------------------------------------ // Le motif se repete sur des cellules de CW colonnes par CR rangs. Une // ouverture laisse toujours au moins une facette de paroi de chaque cote, // donc jamais les bords haut et bas. CW = FAC / ouvertures_par_tour; CR = FIN / rangees; T_AJ = taille_ouvertures / 100; L_AJ = (ajours == "fentes") ? T_AJ * 0.5 : (ajours == "points") ? T_AJ * 0.5 : T_AJ; H_AJ = (ajours == "fentes") ? 0.8 : (ajours == "points") ? T_AJ * 0.5 : T_AJ; QUINCONCE = (ajours != "fentes"); OUV_C = max(1, min(CW - 2, round(L_AJ * CW))); OUV_R = max(1, min(CR - 2, round(H_AJ * CR))); DEB_C = floor((CW - OUV_C) / 2); DEB_R = floor((CR - OUV_R) / 2); // ---------- voronoi ------------------------------------------------------ // Le meme calcul que l'atelier (lib/lampshade/perforation.ts), pas a pas, pour // que les deux ouvrent les memes cellules : memes graines (un hachage entier, // exact en double, faute d'operateurs binaires ici), meme champ, memes passes. // Imprimable par construction : nervures d'au moins 1.6 mm, jamais une cellule // plus large que 30 mm autour de la lampe (le haut d'une ouverture est un pont // court : une lampe large recoit plus de cellules), bords haut et bas pleins. NC_AJ = NC_V; NR_AJ = rangees; CWMM = PI * diam_max / NC_AJ; // largeur d'une cellule au plus large, mm CHMM = hauteur / NR_AJ; // hauteur d'une cellule, mm STRUT = max(1.6, min(CWMM, CHMM) * 0.35 * ((80 + 20 - taille_ouvertures) / 80)); // Rayon des coins arrondis : les nervures se rejoignent en branches courbes. ROND = min(4, min(CWMM, CHMM) * 0.2); RIM = 0.35 * CHMM; // bande pleine a chaque bord function h01(a, b, s) = let(m = 2147483647, h0 = (a * 73856093 + b * 19349663 + s * 83492791) % m, h1 = (h0 * 48271) % m, h2 = (h1 * 48271) % m, h3 = (h2 * 48271) % m) h3 / m; SEM = !VORO ? [] : [for (r = [0:NR_AJ-1]) [for (c = [0:NC_AJ-1]) [c + 0.5 + (h01(c, r, 1) * 2 - 1) * 0.3, r + 0.5 + (h01(c, r, 2) * 2 - 1) * 0.3]]]; // Positif dans une ouverture, negatif dans la paroi, en mm du bord. Dans la // cellule de la graine la plus proche, la distance a chaque bord est celle a // la mediatrice avec une graine voisine ; elles sont combinees par un minimum // ADOUCI (log-somme-exp) : rien ne change le long d'un bord, mais aux coins la // distance baisse de ROND au plus, d'ou des ouvertures aux coins ronds. La // largeur d'une cellule suit le rayon de la lampe a cette hauteur, pour que // les nervures soient mesurees en vrais millimetres. function somme(v, i = 0) = (i >= len(v)) ? 0 : v[i] + somme(v, i + 1); function champ(row, col) = let(u = col / (FAC / NC_AJ), v = row / (FIN / NR_AJ), bc = floor(u), br = floor(v), i0 = min(FIN - 1, max(0, floor(row))), fl = min(1, max(0, row - i0)), lw = CWMM * (corps[i0][0] * (1 - fl) + corps[i0 + 1][0] * fl) / Rmax, pts = [for (dr = [-1:1], dc = [-1:1]) if (br + dr >= 0 && br + dr < NR_AJ) let(w = ((bc + dc) % NC_AJ + NC_AJ) % NC_AJ, s = SEM[br + dr][w]) [(s[0] + (bc + dc - w) - u) * lw, (s[1] - v) * CHMM]], ds = [for (p = pts) norm(p)], n = [for (k = [0:len(pts)-1]) if (ds[k] == min(ds)) k][0], z = v * CHMM, // Les bandes pleines des bords entrent dans le minimum doux : une // ouverture rejoint un bord en courbe, jamais en coupe plate. bords = concat( [for (k = [0:len(pts)-1]) if (k != n) let(dx = pts[k][0] - pts[n][0], dy = pts[k][1] - pts[n][1]) ((pts[k][0] + pts[n][0]) / 2 * dx + (pts[k][1] + pts[n][1]) / 2 * dy) / norm([dx, dy])], [z - RIM + STRUT / 2, hauteur - RIM - z + STRUT / 2]), cl = min(bords)) cl - ROND * ln(somme([for (b = bords) exp(-(b - cl) / ROND)])) - STRUT / 2; FORCE = !VORO ? [] : [for (r = [0:FIN-1]) [for (j = [0:FAC-1]) champ(r + 0.5, j + 0.5)]]; // Deux ouvertures ne se touchent jamais par un coin seul : la plus faible des // deux se ferme. Chaque passe lit la grille d'avant la passe, comme l'atelier. function wr(j) = ((j % FAC) + FAC) % FAC; function coin(o, r0, j0, r, j) = (r0 < 0 || r0 > FIN - 2) ? false : let(a = o[r0][wr(j0)], b = o[r0][wr(j0 + 1)], c = o[r0 + 1][wr(j0)], d = o[r0 + 1][wr(j0 + 1)], fa = FORCE[r0][wr(j0)], fb = FORCE[r0][wr(j0 + 1)], fc = FORCE[r0 + 1][wr(j0)], fd = FORCE[r0 + 1][wr(j0 + 1)], ad = (a && d && !b && !c) ? ((fa < fd) ? [r0, wr(j0)] : [r0 + 1, wr(j0 + 1)]) : undef, bc = (b && c && !a && !d) ? ((fb < fc) ? [r0, wr(j0 + 1)] : [r0 + 1, wr(j0)]) : undef) ad == [r, wr(j)] || bc == [r, wr(j)]; function a_fermer(o, r, j) = coin(o, r - 1, j - 1, r, j) || coin(o, r - 1, j, r, j) || coin(o, r, j - 1, r, j) || coin(o, r, j, r, j); function passe(o) = [for (r = [0:FIN-1]) [for (j = [0:FAC-1]) o[r][j] && !a_fermer(o, r, j)]]; function fixe(o) = let(n = passe(o)) (n == o) ? o : fixe(n); OUVERT = !VORO ? [] : fixe([for (r = [0:FIN-1]) [for (j = [0:FAC-1]) FORCE[r][j] > 0]]); // De combien glisser le sommet (rang, colonne) pour le poser sur un bord, en // facettes : PAS_NEWTON pas de Newton (un bord aux coins ronds se courbe), // rien au-dela de 0.75 facette, jamais plus de 0.49 dans un sens. PAS_NEWTON = 3; function borne(x) = max(-0.49, min(0.49, x)); function newton(row, col, d, n) = (n == 0) ? d : let(v = champ(row + d[0], col + d[1]), up = champ(row + d[0] + 0.5, col + d[1]) - champ(row + d[0] - 0.5, col + d[1]), ac = champ(row + d[0], col + d[1] + 0.5) - champ(row + d[0], col + d[1] - 0.5), s = up * up + ac * ac) (s == 0) ? undef : let(e = [d[0] - v * up / s, d[1] - v * ac / s]) (max(abs(e[0]), abs(e[1])) > 0.75) ? undef : newton(row, col, e, n - 1); function glisse(row, col) = let(d = newton(row, col, [0, 0], PAS_NEWTON)) (d == undef) ? undef : [borne(d[0]), borne(d[1])]; GLISSE = !VORO ? [] : [for (r = [0:FIN]) [for (j = [0:FAC-1]) (r >= 1 && r <= FIN - 1) ? glisse(r, j) : undef]]; // Deux voisins qui glissent l'un vers l'autre peuvent retourner un triangle. // Les deux peaux coupent chaque case selon des diagonales opposees : aucun des // quatre coins d'une case pleine ne doit passer sous AIRE_MIN (une case // ouverte n'a pas de triangle). Dans une case pliee, le coin qui a glisse le // plus loin (le premier a egalite) glisse RETRAIT fois moins, passe apres // passe, et plus du tout apres ETAPES (comme l'atelier, SNAP_GUARD). AIRE_MIN = 0.1; RETRAIT = 0.8; ETAPES = 12; function pas(e, r, j) = let(g = GLISSE[r][wr(j)], k = e[r][wr(j)]) (g == undef || k >= ETAPES) ? [0, 0] : g * pow(RETRAIT, k); function pt(e, r, j) = [r, j] + pas(e, r, j); function aire(p, q, s) = (q[0] - p[0]) * (s[1] - p[1]) - (q[1] - p[1]) * (s[0] - p[0]); function plie(e, r, j) = let(a = pt(e, r, j), b = pt(e, r + 1, j), c = pt(e, r + 1, j + 1), d = pt(e, r, j + 1)) !OUVERT[r][wr(j)] && (aire(d, a, b) < AIRE_MIN || aire(a, b, c) < AIRE_MIN || aire(b, c, d) < AIRE_MIN || aire(c, d, a) < AIRE_MIN); // Le coin a raccourcir d'une case pliee : [rang, colonne], ou undef. COINS = [[0, 0], [1, 0], [1, 1], [0, 1]]; function mobile(e, r, j) = r >= 1 && r <= FIN - 1 && GLISSE[r][wr(j)] != undef && e[r][wr(j)] < ETAPES; function plus_loin(e, r0, j0, k = 0, choix = undef, loin = -1) = (k == 4) ? choix : let(r = r0 + COINS[k][0], j = j0 + COINS[k][1], d = mobile(e, r, j) ? norm(pas(e, r, j)) : -1) (mobile(e, r, j) && d > loin) ? plus_loin(e, r0, j0, k + 1, [r, wr(j)], d) : plus_loin(e, r0, j0, k + 1, choix, loin); function coupable(e, r0, j0) = plie(e, r0, j0) ? plus_loin(e, r0, j0) : undef; function monte(e) = [for (r = [0:FIN]) [for (j = [0:FAC-1]) (mobile(e, r, j) && (coupable(e, r - 1, j - 1) == [r, j] || coupable(e, r - 1, j) == [r, j] || coupable(e, r, j - 1) == [r, j] || coupable(e, r, j) == [r, j])) ? e[r][j] + 1 : e[r][j]]]; function stabilise(e) = let(n = monte(e)) (n == e) ? e : stabilise(n); ETAGE = !VORO ? [] : stabilise([for (r = [0:FIN]) [for (j = [0:FAC-1]) 0]]); // Un point d'une peau (0 exterieure, 1 interieure) a un rang et une colonne // fractionnaires, lu entre ses quatre voisins de la grille non deplacee. function anneau(peau, row) = (peau == 0) ? row : ML - 1 - row; function echantillon(peau, row, col) = let(r0 = min(FIN - 1, max(0, floor(row))), fr = min(1, max(0, row - r0)), c0 = floor(col), fc = col - c0, ca = wr(c0), cb = (ca + 1) % FAC, a0 = anneau(peau, r0) * FAC, a1 = anneau(peau, r0 + 1) * FAC, lo = PTS0[a0 + ca] * (1 - fc) + PTS0[a0 + cb] * fc, hi = PTS0[a1 + ca] * (1 - fc) + PTS0[a1 + cb] * fc) lo * (1 - fr) + hi * fr; PTS = !VORO ? PTS0 : [for (i = [0:ML-1], j = [0:FAC-1]) let(peau = (i <= FIN - 1) ? 0 : 1, r = (i >= 1 && i <= FIN - 1) ? i : (i >= ML - FIN && i <= ML - 2) ? ML - 1 - i : -1, g = (r >= 1) ? GLISSE[r][j] : undef, p = (g == undef) ? undef : pas(ETAGE, r, j)) (g == undef) ? PTS0[i * FAC + j] : echantillon(peau, r + p[0], j + p[1])]; // Le quad du rang de corps r, colonne j, est-il une ouverture ? function trou(r, j) = (!AJOURS_ON || r < 0 || r >= FIN) ? false : VORO ? OUVERT[r][wr(j)] : let(b = floor(r / CR), dans = r - b * CR, dec = (QUINCONCE && b % 2 == 1) ? CW / 2 : 0, col = ((((j % FAC) + FAC) % FAC + dec) % CW + CW) % CW) (b < rangees && dans >= DEB_R && dans < DEB_R + OUV_R && col >= DEB_C && col < DEB_C + OUV_C); // L'anneau i de la boucle porte le rang de corps i sur la peau exterieure, // et le rang 2M-2-i sur la peau interieure (parcourue a l'envers). function rang(i) = (i < FIN) ? i : (i >= ML - 1 - FIN && i <= ML - 2) ? ML - 2 - i : -1; function EXT(r, j) = idx(r, j); function INTR(r, j) = idx(ML - 1 - r, j); FACES_PAROI = [for (i = [0:ML-1], j = [0:FAC-1], k = [0:1]) if (!trou(rang(i), j)) (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)]]; // Un mur sur chaque bord d'ouverture qui touche de la paroi pleine. function mur(p, q, qi, pi) = [[p, qi, q], [p, pi, qi]]; MURS = !AJOURS_ON ? [] : [for (r = [0:FIN-1], j = [0:FAC-1]) if (trou(r, j)) each concat( trou(r - 1, j) ? [] : mur(EXT(r, j), EXT(r, j+1), INTR(r, j+1), INTR(r, j)), trou(r, j + 1) ? [] : mur(EXT(r, j+1), EXT(r+1, j+1), INTR(r+1, j+1), INTR(r, j+1)), trou(r + 1, j) ? [] : mur(EXT(r+1, j+1), EXT(r+1, j), INTR(r+1, j), INTR(r+1, j+1)), trou(r, j - 1) ? [] : mur(EXT(r+1, j), EXT(r, j), INTR(r, j), INTR(r+1, j)))]; FACES = concat(FACES_PAROI, MURS); 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();