499 lines
17 KiB
OpenSCAD
499 lines
17 KiB
OpenSCAD
/*
|
|
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();
|