/* OpenSCAD 3D Text Generator — Nightly v2 ---------------------------------------- Requires an OpenSCAD Development Snapshot / Nightly with: - textmetrics() - fontmetrics() Main improvements over the original: * Real text bounding boxes via textmetrics() * Real multi-line layout (up to 5 Customizer-friendly lines) * Functional left/center/right line alignment * Optional true mm-based character tracking (no text(spacing!=1)) * Automatic line spacing from fontmetrics().interline * Base sizing/placement based on actual rendered glyph extents * Auto rounded rectangular sign and automatically positioned stand * Separate engraving depth, safety floor and through-cut option * Optional chamfer on engraved text as well as embossed text * Correct upright placement on Z=0 using measured geometry * Diagnostics for requested/resolved font and model dimensions Units: millimetres unless otherwise stated. */ /* [Tekst] */ // Første tekstlinje Tekst = "3D PRINT"; // Ekstra linjer. Tom streng = linjen ignoreres. Linje_2 = ""; Linje_3 = ""; Linje_4 = ""; Linje_5 = ""; // Skrifttypens familienavn, som vist i OpenSCAD Font List Font_Navn = "Impact"; // Skrifttype-stil Font_Stil = "Regular"; // ["Regular", "Bold", "Italic", "Bold Italic"] // Nominel fontstørrelse Tekst_Stoerrelse = 30; // [5:1:200] // Dybde/højde på embossed eller fritstående tekst Tekst_Dybde = 5; // [0.5:0.5:50] // Ekstra afstand MELLEM tegn i mm. 0 bruger fontens normale shaping/kerning. // Når værdien er forskellig fra 0, placeres glyffer individuelt for forudsigelig fysisk tracking. Bogstav_Afstand_mm = 0; // [-2:0.1:10] // Baseline-afstand mellem linjer som faktor af fontens designede interline. Linje_Afstand = 1.0; // [0.5:0.05:3] // Justering af linjer inden for den bredeste tekstlinje Justering = "center"; // ["left", "center", "right"] // Sprog/script sendes videre til OpenSCADs tekst-shaping Sprog = "en"; Script = "latin"; /* [Base & Bagplade] */ // 0: Ingen, 1: Boble/outline, 2: Konveks hull, 3: Rektangulært skilt, 4: Stand/plint under teksten Base_Type = 2; // [0, 1, 2, 3, 4] // 0: Embossed, 1: Engraved Print_Mode = 0; // [0, 1] // Margin omkring tekst for base type 1/2 og ved auto-dimensionering Base_Margin = 6; // [0:0.5:50] // Basens tykkelse Base_Dybde = 3; // [0.5:0.5:50] /* [Rektangulært skilt - Type 3] */ // Automatisk størrelse efter tekstens faktiske bounding box Rektangel_Auto = true; // Manuelle dimensioner bruges når Rektangel_Auto = false Manuel_Base_Bredde = 150; // [10:1:400] Manuel_Base_Hoejde = 50; // [10:1:300] // Vertikal forskydning af rektanglet Manuel_Base_Y_Forskydning = 0; // [-100:1:100] // Radius på skiltets hjørner Base_Hjoerne_Radius = 3; // [0:0.5:30] /* [Stand / plint - Type 4] */ // Automatisk bredde efter teksten Stand_Auto_Bredde = true; // Bruges som bredde hvis Stand_Auto_Bredde = false Stand_Bredde = 150; // [10:1:400] // Plintens højde i modellens 2D-plan Stand_Hoejde = 12; // [2:0.5:100] // Hvor meget plinten overlapper tekstens nederste del for sikker union Stand_Overlap = 2.0; // [0.2:0.2:20] // Ekstra Y-forskydning af plinten Stand_Y_Forskydning = 0; // [-50:0.5:50] // Afrunding på plinten Stand_Hjoerne_Radius = 2; // [0:0.5:20] /* [Embossed Bevel] */ // Top-chamfer på embossed/fritstående tekst. 0 = fra. Bevel_Tykkelse = 0.8; // [0:0.1:5] // Antal stepped offset-lag Bevel_Trin = 4; // [1:1:16] /* [Engraving] */ // Faktisk graveringsdybde. Uafhængig af Tekst_Dybde. Graverings_Dybde = 1.2; // [0.1:0.1:20] // false = behold minimum bundtykkelse, true = skær helt igennem basen Gravering_Gennem = false; // Sikker bundtykkelse når Gravering_Gennem = false Minimum_Bundtykkelse = 0.8; // [0:0.1:10] // Chamfer omkring graveringsåbningen. 0 = lodrette kanter. Gravering_Bevel = 0.4; // [0:0.1:5] Gravering_Bevel_Trin = 3; // [1:1:16] /* [Effekter & Orientering] */ // Falsk italic via shear i grader Shear_Vinkel = 0; // [-45:1:45] // 0 = fladt, 1 = stående med nederste målte kant præcist på Z=0 Orientering = 1; // [0, 1] /* [Kvalitet & System] */ // Kurveopløsning for tekst og afrundinger Kurve_Oploesning = 64; // [12:4:160] // Lille overlap/tolerance mod coplanar/non-manifold geometri Epsilon = 0.02; // [0.005:0.005:0.1] // Vis diagnostiske echo()-linjer i konsollen Diagnostik = true; $fn = Kurve_Oploesning; // ----------------------------------------------------------------------------- // NIGHTLY METRICS & HJÆLPEFUNKTIONER // ----------------------------------------------------------------------------- _Fuld_Font = str(Font_Navn, ":style=", Font_Stil); _Linjer_Raa = [for (s = [Tekst, Linje_2, Linje_3, Linje_4, Linje_5]) if (s != "") s]; // Sørger for defineret geometri/metrics selv hvis alle tekstfelter tømmes. _Linjer = len(_Linjer_Raa) > 0 ? _Linjer_Raa : [" "]; _FontMetrics = fontmetrics(size = Tekst_Stoerrelse, font = _Fuld_Font); function _tm(s) = textmetrics( text = s, size = Tekst_Stoerrelse, font = _Fuld_Font, halign = "left", valign = "baseline", spacing = 1, direction = "ltr", language = Sprog, script = Script ); // Position af tegn nr. i ved ægte fysisk tracking. // Hvert tegn placeres efter forrige tegns advance + Bogstav_Afstand_mm. function _glyph_x(s, i) = i <= 0 ? 0 : _glyph_x(s, i - 1) + _tm(s[i - 1]).advance.x + Bogstav_Afstand_mm; function _glyph_bbox(s, i) = let(t = _tm(s[i]), x = _glyph_x(s, i)) [ x + t.position.x, t.position.y, x + t.position.x + t.size.x, t.position.y + t.size.y ]; // Tight ink-bounding-box for én linje: [xmin, ymin, xmax, ymax] function _line_bbox(s) = Bogstav_Afstand_mm == 0 ? let(t = _tm(s)) [t.position.x, t.position.y, t.position.x + t.size.x, t.position.y + t.size.y] : len(s) == 0 ? [0, 0, 0, 0] : [ min([for (i = [0 : len(s) - 1]) _glyph_bbox(s, i)[0]]), min([for (i = [0 : len(s) - 1]) _glyph_bbox(s, i)[1]]), max([for (i = [0 : len(s) - 1]) _glyph_bbox(s, i)[2]]), max([for (i = [0 : len(s) - 1]) _glyph_bbox(s, i)[3]]) ]; function _line_width(s) = let(b = _line_bbox(s)) b[2] - b[0]; function _line_step() = _FontMetrics.interline * Linje_Afstand; function _block_width() = max([for (s = _Linjer) _line_width(s)]); function _raw_line_y(i) = -i * _line_step(); function _raw_block_ymin() = min([ for (i = [0 : len(_Linjer) - 1]) _line_bbox(_Linjer[i])[1] + _raw_line_y(i) ]); function _raw_block_ymax() = max([ for (i = [0 : len(_Linjer) - 1]) _line_bbox(_Linjer[i])[3] + _raw_line_y(i) ]); function _block_y_shift() = -(_raw_block_ymin() + _raw_block_ymax()) / 2; // Placér hver linje inden for bredeste linjes centrerede blok. function _line_x_shift(s) = let(b = _line_bbox(s), bw = _block_width()) Justering == "left" ? -bw / 2 - b[0] : Justering == "right" ? bw / 2 - b[2] : -(b[0] + b[2]) / 2; function _placed_line_bbox(i) = let( s = _Linjer[i], b = _line_bbox(s), dx = _line_x_shift(s), dy = _raw_line_y(i) + _block_y_shift() ) [b[0] + dx, b[1] + dy, b[2] + dx, b[3] + dy]; // Bounding box efter shear. Y ændres ikke af denne shear-transform. function _sheared_line_bbox(i) = let( b = _placed_line_bbox(i), k = tan(Shear_Vinkel), xs = [ b[0] + k * b[1], b[0] + k * b[3], b[2] + k * b[1], b[2] + k * b[3] ] ) [min(xs), b[1], max(xs), b[3]]; function _text_bbox() = [ min([for (i = [0 : len(_Linjer) - 1]) _sheared_line_bbox(i)[0]]), min([for (i = [0 : len(_Linjer) - 1]) _sheared_line_bbox(i)[1]]), max([for (i = [0 : len(_Linjer) - 1]) _sheared_line_bbox(i)[2]]), max([for (i = [0 : len(_Linjer) - 1]) _sheared_line_bbox(i)[3]]) ]; function _text_width() = let(b = _text_bbox()) b[2] - b[0]; function _text_height() = let(b = _text_bbox()) b[3] - b[1]; function _rect_width() = Rektangel_Auto ? _text_width() + 2 * Base_Margin : Manuel_Base_Bredde; function _rect_height() = Rektangel_Auto ? _text_height() + 2 * Base_Margin : Manuel_Base_Hoejde; function _stand_width() = Stand_Auto_Bredde ? _text_width() + 2 * Base_Margin : Stand_Bredde; function _stand_top() = _text_bbox()[1] + Stand_Overlap + Stand_Y_Forskydning; function _stand_center_y() = _stand_top() - Stand_Hoejde / 2; // Approksimeret/sikker 2D-bbox for basen. function _base_bbox() = let(tb = _text_bbox()) Base_Type == 1 || Base_Type == 2 ? [tb[0] - Base_Margin, tb[1] - Base_Margin, tb[2] + Base_Margin, tb[3] + Base_Margin] : Base_Type == 3 ? [-_rect_width()/2, Manuel_Base_Y_Forskydning - _rect_height()/2, _rect_width()/2, Manuel_Base_Y_Forskydning + _rect_height()/2] : Base_Type == 4 ? [-_stand_width()/2, _stand_top() - Stand_Hoejde, _stand_width()/2, _stand_top()] : tb; function _scene_ymin() = Base_Type == 0 ? _text_bbox()[1] : Print_Mode == 0 ? min(_base_bbox()[1], _text_bbox()[1]) : _base_bbox()[1]; function _eff_text_bevel() = min(max(Bevel_Tykkelse, 0), max(Tekst_Dybde - Epsilon, 0)); function _max_safe_engrave() = max(Base_Dybde - Minimum_Bundtykkelse, Epsilon); function _eff_engrave_depth() = Gravering_Gennem ? Base_Dybde + 2 * Epsilon : min(Graverings_Dybde, _max_safe_engrave()); function _eff_engrave_bevel() = min(max(Gravering_Bevel, 0), _eff_engrave_depth()); // ----------------------------------------------------------------------------- // 2D TEKST // ----------------------------------------------------------------------------- module shear_transform(vinkel) { multmatrix([ [1, tan(vinkel), 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]) children(); } module _draw_line(s) { if (Bogstav_Afstand_mm == 0) { // Bevarer fontens normale HarfBuzz shaping, kerning og ligaturer. text( text = s, size = Tekst_Stoerrelse, font = _Fuld_Font, halign = "left", valign = "baseline", spacing = 1, direction = "ltr", language = Sprog, script = Script ); } else { // Forudsigelig ekstra fysisk tracking i mm. // Tradeoff: tegn shaper ikke længere som én samlet streng. for (i = [0 : len(s) - 1]) { translate([_glyph_x(s, i), 0]) text( text = s[i], size = Tekst_Stoerrelse, font = _Fuld_Font, halign = "left", valign = "baseline", spacing = 1, direction = "ltr", language = Sprog, script = Script ); } } } module draw_2d_text() { shear_transform(Shear_Vinkel) { for (i = [0 : len(_Linjer) - 1]) { s = _Linjer[i]; translate([ _line_x_shift(s), _raw_line_y(i) + _block_y_shift() ]) _draw_line(s); } } } // ----------------------------------------------------------------------------- // 3D TEKST / EMBOSS BEVEL // ----------------------------------------------------------------------------- module draw_3d_text() { b = _eff_text_bevel(); if (b > Epsilon && Bevel_Trin > 0) { body_h = max(Epsilon, Tekst_Dybde - b); step_h = b / Bevel_Trin; // Hovedkrop linear_extrude(height = body_h + Epsilon, convexity = 10) draw_2d_text(); // Stepped top-chamfer: konstant lateral/vertikal ratio ~1:1. for (i = [0 : Bevel_Trin - 1]) { d = (i + 1) * b / Bevel_Trin; translate([0, 0, body_h + i * step_h]) linear_extrude(height = step_h + Epsilon, convexity = 10) offset(delta = -d) draw_2d_text(); } } else { linear_extrude(height = Tekst_Dybde, convexity = 10) draw_2d_text(); } } // ----------------------------------------------------------------------------- // BASE // ----------------------------------------------------------------------------- module rounded_rect_2d(w, h, r) { rr = min(max(r, 0), max(min(w, h) / 2 - Epsilon, 0)); if (rr > Epsilon) { offset(r = rr) square([max(w - 2*rr, Epsilon), max(h - 2*rr, Epsilon)], center = true); } else { square([w, h], center = true); } } module draw_2d_base() { if (Base_Type == 1) { // Boble/outline følger den faktiske tekstgeometri. offset(r = Base_Margin) draw_2d_text(); } else if (Base_Type == 2) { // Stram konveks plade rundt om alle tegn/linjer. offset(r = Base_Margin) hull() draw_2d_text(); } else if (Base_Type == 3) { translate([0, Manuel_Base_Y_Forskydning]) rounded_rect_2d(_rect_width(), _rect_height(), Base_Hjoerne_Radius); } else if (Base_Type == 4) { // Plintens top ligger ved tekstens FAKTISKE nederste ink-kant + overlap. translate([0, _stand_center_y()]) rounded_rect_2d(_stand_width(), Stand_Hoejde, Stand_Hjoerne_Radius); } } // ----------------------------------------------------------------------------- // ENGRAVING CUTTER // ----------------------------------------------------------------------------- module engraving_cutter() { depth = _eff_engrave_depth(); bevel = _eff_engrave_bevel(); bottom_z = Gravering_Gennem ? -Epsilon : Base_Dybde - depth; // Nominel tekstprofil gennem hele den valgte dybde. translate([0, 0, bottom_z - Epsilon]) linear_extrude(height = depth + 2*Epsilon, convexity = 10) draw_2d_text(); // Ekstra udvidelse nær overfladen giver en chamfer/countersink-effekt. if (bevel > Epsilon && Gravering_Bevel_Trin > 0) { step_h = bevel / Gravering_Bevel_Trin; for (i = [0 : Gravering_Bevel_Trin - 1]) { d = (i + 1) * bevel / Gravering_Bevel_Trin; translate([0, 0, Base_Dybde - bevel + i * step_h]) linear_extrude(height = step_h + Epsilon, convexity = 10) offset(delta = d) draw_2d_text(); } } } // ----------------------------------------------------------------------------- // ASSEMBLY // ----------------------------------------------------------------------------- module assemble_scene() { if (Base_Type == 0) { // Engraving giver ingen mening uden base; vi genererer derfor ren tekst. draw_3d_text(); } else if (Print_Mode == 0) { union() { linear_extrude(height = Base_Dybde, convexity = 10) draw_2d_base(); // Minimal overlap gør union robust uden synlig geometrisk ændring. translate([0, 0, Base_Dybde - Epsilon]) draw_3d_text(); } } else { difference() { linear_extrude(height = Base_Dybde, convexity = 10) draw_2d_base(); engraving_cutter(); } } } // Korrekt placering ved stående print: // Efter rotate([90,0,0]) bliver oprindelig Y til ny Z. // Derfor flyttes modellen med -scene_ymin efter rotationen. module apply_orientation() { if (Orientering == 1) { translate([0, 0, -_scene_ymin()]) rotate([90, 0, 0]) children(); } else { children(); } } module diagnostics() { if (Diagnostik) { echo("--- 3D TEXT GENERATOR / NIGHTLY v2 ---"); echo(Requested_Font = _Fuld_Font); echo(Resolved_Font_Family = _FontMetrics.font.family); echo(Resolved_Font_Style = _FontMetrics.font.style); echo(Active_Lines = _Linjer); echo(Text_BBox_mm = _text_bbox()); echo(Text_Size_mm = [_text_width(), _text_height()]); if (Base_Type != 0) echo(Base_BBox_mm = _base_bbox()); if (Bogstav_Afstand_mm != 0) echo("NOTE: mm-tracking placerer tegn individuelt; ligaturer og par-kerning fra den samlede streng bruges derfor ikke."); if (Bevel_Tykkelse > _eff_text_bevel() + Epsilon) echo("NOTE: Bevel_Tykkelse er automatisk begrænset af Tekst_Dybde."); if (!Gravering_Gennem && Graverings_Dybde > _eff_engrave_depth() + Epsilon) echo("NOTE: Graverings_Dybde er automatisk reduceret for at bevare Minimum_Bundtykkelse."); if (Base_Type == 0 && Print_Mode == 1) echo("NOTE: Print_Mode=Engraved ignoreres når Base_Type=0."); } } // ----------------------------------------------------------------------------- // MAIN // ----------------------------------------------------------------------------- diagnostics(); apply_orientation() assemble_scene();