Josef Průša

Parametrické modelování v OpenSCADu

07. 09. 2018 | by Michal Altair Valášek

V minulém návodu jsme si vymodelovali náhradní díl ke kancelářské židli a už v něm jsem čtenáře nabádal, ať si zkusí proměnné v kódu modifikovat. Jednou z hlavních výhod OpenSCADU proti běžným 3D modelovacím programům je totiž snadné parametrické modelování. Tedy schopnost, kdy pomocí jednoduché změny proměnných zásadním způsobem ovlivníte vlastnosti modelu. A to nejenom jeho rozměry, ale celkově jakékoliv chování.

Ukážeme si to na příkladu věci, kterou na 3D tiskárně tisknu velice často: držák ve tvaru „S“ (často označovaný jako S-bracket). Jsem velkým příznivcem zavěšování věcí pod poličky, desky stolu a podobně. Síťový switch, USB hub, čtečka karet, externí CD mechanika… to vše je při zavěšení tímto způsobem na dosah ruky, na pevném místě a nezabírá mnoho prostoru.

Na tomto obrázku je zařízení (modrá krabička) přimontováno ke stolu pomocí červených držáků. Jejich vlastnosti a rozměry jsou pro každé zařízení různé. Pro menší a lehčí zařízení stačí menší síla stěny a délka ploch, které budou držet zařízení na místě. Těžší zařízení pak bude vyžadovat robustnější konstrukci a třeba i větší počet děr na šrouby, kterými budou držáky přidělané ke stolu. Pro každé zařízení pak budou třeba držáky o odpovídající šířce atd.

To je typické zadání pro parametrické modelování v OpenSCADu. Ukážeme si, jak vytvořit jeden model a pomocí změn jeho parametrů dosahovat výrazně odlišných výsledků.

Důležitá poznámka: v článku rozebírám části kódu separátně, abych se vyhnul nutnosti stále opakovat již napsaný kód, což by vedlo akorát k tomu, že by článek byl jeden dlouhý kus kódu. Pokud chcete funkční kód, sjeďte na poslední kapitoly nebo použijte odkaz v posledním odstavci ke stažení vzorového kódu.

Kulatá kostka

Jedním z nejpoužívanějších konstruktů při navrhování modelů je kvádr (cube) se zaoblenými rohy. Typicky se v OpenSCADu generuje pomocí funkce hull. Ta vytvoří objekt, který těsně „obaluje” všechny objekty, které jsou jejími potomky.

Tyto čtyři válce tvoří zakulacené rohy našeho kvádru. Pokud na ně aplikujeme funkci hull, vznikne požadované těleso:

Pokud bychom chtěli zakulacené nejenom rohy v průmětu, ale všechny hrany, mohli bychom místo čtyř válců použít osm koulí (tzn. v každém rohu dvě koule nad sebou) a dosáhnout požadovaného efektu podobným způsobem. My se ale spokojíme s první variantou, a protože toto těleso budeme používat pravidelně, vytvoříme si modul (tak OpenSCAD říká tomu, co se v jiných programovacích jazycích obvykle jmenuje „metoda”), který nám na požádání vytvoří kvádr o daných rozměrech s daným zakřivením rohů.

Modul se bude jmenovat rcube, jako „rounded cube” (zakulacená kostka) a jeho zdrojový kód vypadá následovně:

module rcube(size, radius) {
    hull() {
        translate([radius, radius]) cylinder(r = radius, h = size[2]);
        translate([size[0] - radius, radius]) cylinder(r = radius, h = size[2]);
        translate([size[0] - radius, size[1] - radius]) cylinder(r = radius, h = size[2]);
        translate([radius, size[1] - radius]) cylinder(r = radius, h = size[2]);
    }
}

Modul se používá stejně, jako vestavěný příkaz cube, pouze bere jeden parametr navíc – zakřivení rohů.

Při návrhu složitějších modelů často potřebujeme, aby různé rohy měly různý poloměr zakřivení. Snadná pomoci – nechť je parametr radius vektor, který bude obsahovat čtyři různé hodnoty zakřivení, následovně:

module rcube(size, radius) {
    hull() {
        translate([radius[0], radius[0]]) cylinder(r = radius[0], h = size[2]);
        translate([size[0] - radius[1], radius[1]]) cylinder(r = radius[1], h = size[2]);
        translate([size[0] - radius[2], size[1] - radius[2]]) cylinder(r = radius[2], h = size[2]);
        translate([radius[3], size[1] - radius[3]]) cylinder(r = radius[3], h = size[2]);
    }
}

 

Pokud ke kódu výše přidáme volání modulu „rcube([100, 50, 10], [5, 10, 15, 20]);“ (bez uvozovek) můžeme vytvořit následující tvar:

Náš modul už je schopnější, ale nedokáže si poradit s tím, že některé rohy mají být ostré. Pokud zadáte hodnotu zakřivení 0, příslušný roh se bude ignorovat. Modifikujeme tedy kód tak, aby v případě nuly do příslušného rohu neudělal válec, ale kvádr [1, 1, n]:

 
module rcube(size, radius) {
    hull() {
        // BL
        if(radius[0] == 0) cube([1, 1, size[2]]);
        else translate([radius[0], radius[0]]) cylinder(r = radius[0], h = size[2]);
        // BR
        if(radius[1] == 0) translate([size[0] - 1, 0]) cube([1, 1, size[2]]);
        else translate([size[0] - radius[1], radius[1]]) cylinder(r = radius[1], h = size[2]);
        // TR
        if(radius[2] == 0) translate([size[0] - 1, size[1] - 1])cube([1, 1, size[2]]);
        else translate([size[0] - radius[2], size[1] - radius[2]]) cylinder(r = radius[2], h = size[2]);
        // TL
        if(radius[3] == 0) translate([0, size[1] - 1]) cube([1, 1, size[2]]);
        else translate([radius[3], size[1] - radius[3]]) cylinder(r = radius[3], h = size[2]);
    }
}

Pokud nyní přidáme řádek „rcube([100, 50, 10], [15, 0, 15, 0]);“ (opět bez uvozovek), vygeneruje nám kvádr se dvěma rohy ostrými a dvěma kulatými:

Pojďme se nyní zaměřit na pohodlí programátora, který bude funkci využívat – už jenom proto, že to budeme zejména my sami. Bylo by hezké, kdyby parametr radius byl univerzální:

  • Je-li to jednoduchá proměnná (skalár), určuje poloměr zakřivení, který je pro všechny čtyři rohy stejný.
  • Je-li to vektor o dvou členech, určuje první číslo zakřivení předních rohů, druhé pak zadních.
  • Je-li to vektor o čtyřech členech, určují čísla zakřivení všech rohů počínaje levým předním a pokračuje proti směru hodinových ručiček.

Délku vektoru zjistíme pomocí funkce len(). Jedná-li se o skalár, vrátí undef, jinak délku vektoru. Toho můžeme využít a pokud je délka jiná než 4, zavolá modul sám sebe s odvozenými parametry:

Pojďme se nyní zaměřit na pohodlí programátora, který bude funkci využívat – kdyby už jenom protože to budeme zejména my sami. Bylo by hezké, kdyby parametr radius byl univerzální:

  • Je-li to jednoduchá proměnná (skalár), určuje poloměr zakřivení, který je pro všechny čtyři rohy stejný.
  • Je-li to vektor o dvou členech, určuje první číslo zakřivení předních rohů, druhé pak zadních.
  • Je-li to vektor o čtyřech členech, určují čísla zakřivení všech rohů počínaje levým předním a pokračuje proti směru hodinových ručiček.

Délku vektoru zjistíme pomocí funkce len(). Jedná-li se o skalár, vrátí undef, jinak délku vektoru. Toho můžeme využít a pokud je délka jiná než 4, zavolá modul sám sebe s odvozenými parametry:

 

module rcube(size, radius) {
    if(len(radius) == undef) {
        // Stejné zakřivení ve všech rozích
        rcube(size, [radius, radius, radius, radius]);
    } else if(len(radius) == 2) {
        // Rozdílné zakřivení nahoře a dole
        rcube(size, [radius[0], radius[0], radius[1], radius[1]]);
    } else if(len(radius) == 4) {
        // V každém rohu jiné zakřivení
        hull() {
            // Spodní levý roh
            if(radius[0] == 0) cube([1, 1, size[2]]);
            else translate([radius[0], radius[0]]) cylinder(r = radius[0], h = size[2]);
            // Spodní pravý roh
            if(radius[1] == 0) translate([size[0] - 1, 0]) cube([1, 1, size[2]]);
            else translate([size[0] - radius[1], radius[1]]) cylinder(r = radius[1], h = size[2]);
            // Horní pravý roh
            if(radius[2] == 0) translate([size[0] - 1, size[1] - 1])cube([1, 1, size[2]]);
            else translate([size[0] - radius[2], size[1] - radius[2]]) cylinder(r = radius[2], h = size[2]);
            // Horní levý roh
            if(radius[3] == 0) translate([0, size[1] - 1]) cube([1, 1, size[2]]);
            else translate([radius[3], size[1] - radius[3]]) cylinder(r = radius[3], h = size[2]);
        }
    } else {
        echo("CHYBA: Nesprávná délka parametru radius. Očekáváno celé číslo nebo vektor s délkou 2 nebo 4.");
    }
}

Modelujeme držák

Proč tolik péče o zakulacené kvádry? Protože podíváme-li se na náš držák podrobněji, nejde o nic jiného, než o tři kvádry (z toho dva s různě zaoblenými rohy), jak je vidno na následujícím obrázku, který jej zachycuje z profilu:

S využitím poslední uvedené iterace modulu rcube (viz výše) můžeme parametricky vymodelovat držák následujícím kódem, který je potřeba umístit před kód z minulé kapitoly:

width = 30;
height = 30;
top_length = 30;
bottom_length = 20;
number_of_holes = 2;
wall_thickness = 3;
hole_diameter = 4;
 
$fn = 16;
 
rcube([bottom_length + wall_thickness, wall_thickness, width], [wall_thickness / 2, wall_thickness / 2, 0, wall_thickness / 2]);
translate([bottom_length, wall_thickness]) cube([wall_thickness, height, width]);
translate([bottom_length, height]) rcube([top_length + wall_thickness, wall_thickness, width], [0, wall_thickness / 2, 0, 0]);

 

Vyrenderovaný výsledek pak vypadá následovně:

Držák je hezký a v podstatě hotový pro případ, kdy jej chceme zespodu na desku stolu přilepit (což je mnohdy zcela postačující řešení). Nicméně chceme-li jej přišroubovat, potřebujeme v něm vyrobit díry.

Ale kolik? Pro malé držáky stačí jeden šroub, ale lepší jsou dva, aby se díl kolem jednoho šroubu neotáčel. Pro větší a těžší zařízení je pak třeba větších držáků s větší plochou a také větší množství děr. Náš model by tedy měl umožnit různé konfigurace. Kupříkladu libovolný počet od žádné (jako výše) po šest, jak vidno na následujícím obrázku:

Principiálně jde o tentýž problém, který jsme řešili u modulu rcube, kdy jsme se podle délky vektoru rozhodovali, co budou jeho hodnoty znamenat. Kompletní kód generátoru libovolných držáků vypadá takto:

//deklarování parametrů - šířka, výška, délka horní strany, délka spodní strany, počet děr, tloušťka stěn, průměr děr
width = 30;
height = 30;
top_length = 30;
bottom_length = 20;
number_of_holes = 2;
wall_thickness = 10;
hole_diameter = 4;
 
fudge = 1;
$fn = 16;
 
rcube([bottom_length + wall_thickness, wall_thickness, width], [wall_thickness / 2, wall_thickness / 2, 0, wall_thickness / 2]);
translate([bottom_length, wall_thickness]) cube([wall_thickness, height, width]);
translate([bottom_length, height]) difference() {
    rcube([top_length + wall_thickness, wall_thickness, width], [0, wall_thickness / 2, 0, 0]);
    if(number_of_holes == 0) {
        // Nic nedělat, již je hotovo
    } else if(number_of_holes == 1) {
        // Jedna díra uprostřed
        translate([wall_thickness + top_length * .50, -fudge, width * .50]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
    } else if(number_of_holes == 2) {
        // Dvě diagonální díry
        translate([wall_thickness + top_length * .30, -fudge, width * .30]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .70, -fudge, width * .70]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
    } else if(number_of_holes == 3) {
        // Tři díry poskládané do trojúhelníku
        translate([wall_thickness + top_length * .30, -fudge, width * .30]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .30, -fudge, width * .70]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .70, -fudge, width * .50]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
    } else if(number_of_holes == 4) {
        // Čtyři díry v rozích
        translate([wall_thickness + top_length * .30, -fudge, width * .30]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .70, -fudge, width * .70]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .30, -fudge, width * .70]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .70, -fudge, width * .30]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
    } else if(number_of_holes == 5) {
        // Čtyři díry v rozích a jedna uprostřed
        translate([wall_thickness + top_length * .25, -fudge, width * .25]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .75, -fudge, width * .75]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .25, -fudge, width * .75]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .75, -fudge, width * .25]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .50, -fudge, width * .50]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
    } else if(number_of_holes == 6) {
        // Šest děr
        translate([wall_thickness + top_length * .25, -fudge, width * .25]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .50, -fudge, width * .25]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .75, -fudge, width * .25]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .25, -fudge, width * .75]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .50, -fudge, width * .75]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        translate([wall_thickness + top_length * .75, -fudge, width * .75]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
    } else {
        echo("CHYBA: Nepodporovaný počet děr. Očekáváno 0-6");
    }
}


Další úpravy

Nyní už máme docela schopný generátor, pomocí kterého lze vytvořit ledacos. Z čistě praktických důvodů doporučuji kód pro generování držáku umístit do modulu, který bude možné volat s různými parametry. Často chceme vytvořit držáků více, třeba i s různými parametry. Posunout objekty lze funkcí „translate“ (viz kód níže). S funkcionalitou vyvedenou do modulu nám k tomu stačí jediný řádek. Například obrázek výše (s řadou držáků s různými počty děr) byl vygenerován pomocí následujícího kódu.

 
fudge = 1;
$fn = 16;
 
translate([000, 0]) bracket(30, 30, 30, 30, number_of_holes = 1);
translate([080, 0]) bracket(30, 30, 30, 30, number_of_holes = 2);
translate([160, 0]) bracket(30, 30, 30, 30, number_of_holes = 3);
translate([240, 0]) bracket(30, 30, 30, 30, number_of_holes = 4);
translate([320, 0]) bracket(30, 30, 30, 30, number_of_holes = 5);
translate([400, 0]) bracket(30, 30, 30, 30, number_of_holes = 6);
 
module bracket(width, height, top_length, bottom_length, number_of_holes = 2, wall_thickness = 3, hole_diameter = 4) {
    rcube([bottom_length + wall_thickness, wall_thickness, width], [wall_thickness / 2, wall_thickness / 2, 0, wall_thickness / 2]);
    translate([bottom_length, wall_thickness]) cube([wall_thickness, height, width]);
    translate([bottom_length, height]) difference() {
        rcube([top_length + wall_thickness, wall_thickness, width], [0, wall_thickness / 2, 0, 0]);
        if(number_of_holes == 0) {
            // Nic nedělat, již je hotovo
        } else if(number_of_holes == 1) {
            // Jedna díra uprostřed
            translate([wall_thickness + top_length * .50, -fudge, width * .50]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        } else if(number_of_holes == 2) {
            // Dvě diagonální díry
            translate([wall_thickness + top_length * .30, -fudge, width * .30]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .70, -fudge, width * .70]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        } else if(number_of_holes == 3) {
            // Tři díry poskládané do trojúhelníku
            translate([wall_thickness + top_length * .30, -fudge, width * .30]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .30, -fudge, width * .70]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .70, -fudge, width * .50]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        } else if(number_of_holes == 4) {
            // Čtyři díry v rozích
            translate([wall_thickness + top_length * .30, -fudge, width * .30]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .70, -fudge, width * .70]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .30, -fudge, width * .70]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .70, -fudge, width * .30]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        } else if(number_of_holes == 5) {
            // Čtyři díry v rozích a jedna uprostřed
            translate([wall_thickness + top_length * .25, -fudge, width * .25]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .75, -fudge, width * .75]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .25, -fudge, width * .75]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .75, -fudge, width * .25]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .50, -fudge, width * .50]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        } else if(number_of_holes == 6) {
            // Šest děr
            translate([wall_thickness + top_length * .25, -fudge, width * .25]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .50, -fudge, width * .25]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .75, -fudge, width * .25]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .25, -fudge, width * .75]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .50, -fudge, width * .75]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
            translate([wall_thickness + top_length * .75, -fudge, width * .75]) rotate([-90, 0, 0]) cylinder(d = hole_diameter, h = wall_thickness + 2 * fudge);
        } else {
            echo("CHYBA: Nepodporovaný počet děr. Očekáváno 0-6");
        }
    }
}
 
module rcube(size, radius) {
    if(len(radius) == undef) {
        // Stejné zakřivení ve všech rozích
        rcube(size, [radius, radius, radius, radius]);
    } else if(len(radius) == 2) {
        // Rozdílné zakřivení nahoře a dole
        rcube(size, [radius[0], radius[0], radius[1], radius[1]]);
    } else if(len(radius) == 4) {
        // Rozdílné zakřivení v různých rozích
        hull() {
            // Spodní levý roh
            if(radius[0] == 0) cube([1, 1, size[2]]);
            else translate([radius[0], radius[0]]) cylinder(r = radius[0], h = size[2]);
            // Spodní pravý roh
            if(radius[1] == 0) translate([size[0] - 1, 0]) cube([1, 1, size[2]]);
            else translate([size[0] - radius[1], radius[1]]) cylinder(r = radius[1], h = size[2]);
            // Horní pravý roh
            if(radius[2] == 0) translate([size[0] - 1, size[1] - 1])cube([1, 1, size[2]]);
            else translate([size[0] - radius[2], size[1] - radius[2]]) cylinder(r = radius[2], h = size[2]);
            // Horní levý roh
            if(radius[3] == 0) translate([0, size[1] - 1]) cube([1, 1, size[2]]);
            else translate([radius[3], size[1] - radius[3]]) cylinder(r = radius[3], h = size[2]);
        }
    } else {
        echo("CHYBA: Nesprávná délka parametru radius. Očekáváno celé číslo nebo vektor s délkou 2 nebo 4.");
    }
}

Slovo závěrem

Tentokrát jsme si ukázali výrazně pokročilejší látku než v předchozím článku. Podstatou bylo si ukázat, kde tkví jedna z předností OpenSCADu – tedy v možnosti rychlého parametrického generování objektů na základě vložení několika proměnných. Snad vám přijde tenhle kód, potažmo objekt, k užitku a třeba se jej rozhodnete dál modifikovat.

Hotový kód nabízíme i ke stažení zde.


Originální 3D tiskárna Prusa i3

Podívejte se na originální Prusa i3 MK3 3D tiskárnu!
Nejlevnější je už od 19 990 Kč

Průša 3D tiskárny na faceboku