OpenSCAD-Generators/universal-sign/universal-sign.scad

1358 lines
No EOL
37 KiB
OpenSCAD

/*
===============================================================================
UNIVERSAL SIGN GENERATOR - OpenSCAD Nightly
Version 1.0 - 2026-09
-------------------------------------------------------------------------------
Selvstændig parametrisk skiltegenerator til:
- 3D-print
- CNC-fræsning / 2.5D-gravering
- Laserskæring / lasergravering
Designmål:
* Flere tekstlinjer med individuel font, størrelse, spacing og finjustering.
* Nightly textmetrics() + fontmetrics() til reel fontmåling og auto-sizing.
* Moderne em-baseret fontstørrelse kan vælges.
* Mange pladeformer, konstruktioner og monteringsformer kan kombineres.
* Raised / engraved / cut-through / inlay tekst.
* Raised / engraved / cut-through kant.
* 2D outputs til SVG/DXF og 3D outputs til STL/3MF.
* Ingen eksterne libraries.
NIGHTLY:
textmetrics() / fontmetrics() er fortsat en udviklingsfunktion i aktuelle
snapshots. I GUI: Preferences -> Features -> textmetrics.
CLI: openscad-nightly --enable=textmetrics ...
FALLBACK:
Sæt Use_Nightly_Metrics=false hvis du vil åbne filen i en ældre OpenSCAD.
Auto-sizing bliver da en tilnærmelse og em-mode bør ikke anvendes.
FONT-FILER:
Projektspecifikke .ttf/.otf kan registreres med en literal use<>-linje,
eksempel:
use <fonts/MyFont.ttf>
Derefter anvendes fontens logiske navn i Line_*_Font.
OBS:
OpenSCAD arbejder uden fysiske enheder; værdierne er tænkt som millimeter.
===============================================================================
*/
/* [Output] */
// Primært output
Output_Mode = "3D"; // ["3D","2D Cut Outline","2D Engrave Text","2D Text/Inlay","2D Border","2D Fold Score"]
// Grundkonstruktion
Construction = "Flat Plate"; // ["Flat Plate","Desk L-Stand","Desk Wedge","Desk Slot Stand","A-Frame Tent","Foldable Sheet Blank","Stake Sign"]
// Hvilken del vises/eksporteres i 3D
Part_Selection = "Assembly"; // ["Assembly","Face only","Support only","Text insert only"]
// Brug præcis Nightly fontmåling
Use_Nightly_Metrics = true;
/* [Skiltform] */
Plate_Shape = "Rounded Rectangle"; // ["Rectangle","Square","Rounded Rectangle","Capsule","Circle","Oval","Triangle","Diamond","Hexagon","Octagon","Shield","Arch","Tag","Arrow Right","Pennant"]
Auto_Size_Plate = true;
// Bruges hvis Auto_Size_Plate=false
Manual_Width = 160; // [20:1:600]
Manual_Height = 70; // [20:1:400]
// Minimum ved auto-size
Minimum_Width = 70; // [10:1:600]
Minimum_Height = 35; // [10:1:400]
// Luft omkring tekstblokken
Padding_X = 14; // [0:0.5:80]
Padding_Y = 10; // [0:0.5:80]
// Ekstra konservativ sikkerhed for former med smalle hjørner
Shape_Text_Safety = 1.00; // [0.5:0.01:1.2]
// Hjørner / formdetaljer
Corner_Radius = 8; // [0:0.5:60]
Tag_Cut = 10; // [1:0.5:60]
Arrow_Head_Fraction = 0.30; // [0.15:0.01:0.50]
Arch_Radius_Fraction = 0.40; // [0.20:0.01:0.50]
// Pladetykkelse
Plate_Thickness = 3.0; // [0.4:0.1:30]
// Kantprofil
Edge_Profile = "Straight"; // ["Straight","Chamfered"]
Bevel_Size = 0.8; // [0:0.1:5]
/* [Tekst - generelt] */
Font_Sizing_Mode = "size"; // ["size","em"]
Text_Mode = "Raised"; // ["Raised","Engraved","Cut-through","Inlay","None"]
Text_HAlign = "center"; // ["left","center","right"]
Text_Direction = "ltr"; // ["ltr","rtl","ttb","btt"]
Text_Language = "da";
Text_Script = "latin";
// Afstand mellem line-boxes ud over fontens interline
Line_Gap = 2.0; // [-5:0.25:30]
Leading_Scale = 1.00; // [0.5:0.01:2.0]
// Global flytning af tekstblokken
Text_Offset_X = 0; // [-100:0.5:100]
Text_Offset_Y = 0; // [-100:0.5:100]
// Ved fast pladestørrelse: nedskaler automatisk hvis teksten ikke kan være der
Scale_Text_Down_To_Fit = true;
Text_Global_Scale = 1.0; // [0.1:0.01:3]
// Relief / gravering
Text_Raised_Height = 1.0; // [0.1:0.1:10]
Text_Engrave_Depth = 0.8; // [0.1:0.1:20]
// Inlay
Inlay_Depth = 1.2; // [0.2:0.1:20]
Inlay_XY_Clearance = 0.15; // [0:0.01:1]
Inlay_Z_Clearance = 0.10; // [0:0.01:1]
/* [Tekstlinje 1] */
Line_1_Text = "MAKERSPACE";
Line_1_Size = 18; // [1:0.5:150]
Line_1_Font = "Liberation Sans:style=Bold";
Line_1_Spacing = 1.0; // [0.5:0.01:2]
Line_1_X = 0; // [-100:0.5:100]
Line_1_Y = 0; // [-100:0.5:100]
/* [Tekstlinje 2] */
Line_2_Text = "NÆSTVED";
Line_2_Size = 11; // [1:0.5:150]
Line_2_Font = "Liberation Sans";
Line_2_Spacing = 1.0; // [0.5:0.01:2]
Line_2_X = 0; // [-100:0.5:100]
Line_2_Y = 0; // [-100:0.5:100]
/* [Tekstlinje 3] */
Line_3_Text = "";
Line_3_Size = 9; // [1:0.5:150]
Line_3_Font = "Liberation Sans";
Line_3_Spacing = 1.0; // [0.5:0.01:2]
Line_3_X = 0; // [-100:0.5:100]
Line_3_Y = 0; // [-100:0.5:100]
/* [Tekstlinje 4] */
Line_4_Text = "";
Line_4_Size = 8; // [1:0.5:150]
Line_4_Font = "Liberation Sans";
Line_4_Spacing = 1.0; // [0.5:0.01:2]
Line_4_X = 0; // [-100:0.5:100]
Line_4_Y = 0; // [-100:0.5:100]
/* [Tekstlinje 5] */
Line_5_Text = "";
Line_5_Size = 8; // [1:0.5:150]
Line_5_Font = "Liberation Sans";
Line_5_Spacing = 1.0; // [0.5:0.01:2]
Line_5_X = 0; // [-100:0.5:100]
Line_5_Y = 0; // [-100:0.5:100]
/* [Tekstlinje 6] */
Line_6_Text = "";
Line_6_Size = 8; // [1:0.5:150]
Line_6_Font = "Liberation Sans";
Line_6_Spacing = 1.0; // [0.5:0.01:2]
Line_6_X = 0; // [-100:0.5:100]
Line_6_Y = 0; // [-100:0.5:100]
/* [Tekstlinje 7] */
Line_7_Text = "";
Line_7_Size = 8; // [1:0.5:150]
Line_7_Font = "Liberation Sans";
Line_7_Spacing = 1.0; // [0.5:0.01:2]
Line_7_X = 0; // [-100:0.5:100]
Line_7_Y = 0; // [-100:0.5:100]
/* [Tekstlinje 8] */
Line_8_Text = "";
Line_8_Size = 8; // [1:0.5:150]
Line_8_Font = "Liberation Sans";
Line_8_Spacing = 1.0; // [0.5:0.01:2]
Line_8_X = 0; // [-100:0.5:100]
Line_8_Y = 0; // [-100:0.5:100]
/* [Kant / border] */
Border_Style = "None"; // ["None","Raised","Engraved","Cut-through"]
Border_Width = 2.0; // [0.3:0.1:20]
Border_Inset = 3.0; // [0:0.1:30]
Border_Raised_Height = 0.8; // [0.1:0.1:10]
Border_Engrave_Depth = 0.6; // [0.1:0.1:20]
/* [Montering] */
Mounting_Style = "None"; // ["None","2 Hanging Holes","2 Side Holes","4 Corner Holes","2 Countersunk","4 Countersunk","Lanyard Hole","Zip-tie Slots","Keyhole Pair","Magnet Pair","Magnet Quad"]
Mount_Edge = 10; // [2:0.5:80]
Hole_Diameter = 4.0; // [1:0.1:30]
Countersink_Diameter = 8.0; // [2:0.1:40]
Countersink_Depth = 1.8; // [0.2:0.1:10]
// Magnetlommer skæres fra bagsiden
Magnet_Diameter = 10.2; // [2:0.1:40]
Magnet_Depth = 2.2; // [0.2:0.1:20]
Magnet_Spacing = 70; // [5:1:400]
// Nøglehulslommer skæres fra bagsiden
Keyhole_Head_Diameter = 8.5; // [2:0.1:30]
Keyhole_Neck_Width = 4.0; // [1:0.1:20]
Keyhole_Length = 14; // [4:0.5:50]
Keyhole_Depth = 2.0; // [0.2:0.1:20]
// Kabelbinder-slots
Zip_Slot_Length = 12; // [3:0.5:50]
Zip_Slot_Width = 3.5; // [1:0.1:20]
/* [Bordskilte / standere] */
Desk_Angle = 72; // [45:1:90]
Base_Thickness = 4; // [1:0.5:30]
Base_Depth = 45; // [10:1:150]
Base_Width_Extra = 16; // [0:1:100]
Base_Front_Lip = 8; // [0:1:50]
// Giver fysisk overlap ved monolitiske standere
Base_Embed = 1.5; // [0.2:0.1:10]
// Slotstander
Slot_Clearance = 0.30; // [0:0.05:2]
Slot_Depth = 2.5; // [0.5:0.1:20]
// Wedge
Wedge_Depth = 35; // [5:1:150]
Wedge_Height_Fraction = 0.65; // [0.2:0.01:0.95]
Wedge_Width_Fraction = 0.70; // [0.2:0.01:1]
// A-frame
Tent_Angle = 70; // [45:1:85]
Tent_Top_Bar = true;
Tent_Top_Bar_Diameter = 5; // [1:0.5:20]
/* [Foldbart plademateriale] */
// Foldable Sheet Blank er lavet til fx karton, PP, tynd plast, finer mv.
Fold_Hinge_Band = 4; // [0:0.5:30]
Fold_Score_Width = 0.35; // [0.1:0.05:3]
Fold_Corner_Radius = 4; // [0:0.5:30]
Fold_Sheet_Thickness = 1.0; // [0.2:0.1:10]
Fold_Text_On_Both_Panels = true;
/* [Stake sign] */
Stake_Width = 18; // [5:1:80]
Stake_Length = 90; // [20:1:400]
Stake_Tip_Length = 25; // [5:1:100]
/* [Kvalitet] */
Curve_FN = 96; // [12:4:360]
Text_FN = 32; // [8:2:128]
Arch_Segments = 48; // [12:2:180]
EPS = 0.02;
/* ---------------------------------------------------------------------------
DATA + FONT METRICS
--------------------------------------------------------------------------- */
raw_lines = [
[Line_1_Text, Line_1_Size, Line_1_Font, Line_1_Spacing, Line_1_X, Line_1_Y],
[Line_2_Text, Line_2_Size, Line_2_Font, Line_2_Spacing, Line_2_X, Line_2_Y],
[Line_3_Text, Line_3_Size, Line_3_Font, Line_3_Spacing, Line_3_X, Line_3_Y],
[Line_4_Text, Line_4_Size, Line_4_Font, Line_4_Spacing, Line_4_X, Line_4_Y],
[Line_5_Text, Line_5_Size, Line_5_Font, Line_5_Spacing, Line_5_X, Line_5_Y],
[Line_6_Text, Line_6_Size, Line_6_Font, Line_6_Spacing, Line_6_X, Line_6_Y],
[Line_7_Text, Line_7_Size, Line_7_Font, Line_7_Spacing, Line_7_X, Line_7_Y],
[Line_8_Text, Line_8_Size, Line_8_Font, Line_8_Spacing, Line_8_X, Line_8_Y]
];
active_lines = [for (l = raw_lines) if (len(l[0]) > 0) l];
function sum_vec(v, i=0) =
i >= len(v) ? 0 : v[i] + sum_vec(v, i+1);
function sum_first(v, count, i=0) =
i >= count ? 0 : v[i] + sum_first(v, count, i+1);
function line_tm(l) =
Font_Sizing_Mode == "em"
? textmetrics(
text=l[0], em=l[1], font=l[2],
direction=Text_Direction, language=Text_Language, script=Text_Script,
halign="center", valign="center", spacing=l[3])
: textmetrics(
text=l[0], size=l[1], font=l[2],
direction=Text_Direction, language=Text_Language, script=Text_Script,
halign="center", valign="center", spacing=l[3]);
function line_fm(l) =
Font_Sizing_Mode == "em"
? fontmetrics(em=l[1], font=l[2])
: fontmetrics(size=l[1], font=l[2]);
function fallback_width(l) =
max([0.01, len(l[0]) * l[1] * 0.62 * l[3]]);
function fallback_height(l) =
max([0.01, l[1]]);
function line_width(l) =
Use_Nightly_Metrics ? max([0.01, line_tm(l).size[0]]) : fallback_width(l);
function line_height(l) =
Use_Nightly_Metrics ? max([0.01, line_tm(l).size[1]]) : fallback_height(l);
function line_interline(l) =
Use_Nightly_Metrics
? max([0.01, line_fm(l).interline])
: l[1] * 1.22;
line_boxes = [
for (l = active_lines)
max([line_height(l), line_interline(l) * Leading_Scale])
];
text_block_width =
len(active_lines) > 0
? max([for (l = active_lines) line_width(l)])
: 0.01;
text_block_height =
len(active_lines) > 0
? sum_vec(line_boxes) + max([0, len(active_lines)-1]) * Line_Gap
: 0.01;
function line_center_y(i) =
text_block_height/2
- sum_first(line_boxes, i)
- i * Line_Gap
- line_boxes[i]/2;
function text_anchor_x() =
Text_HAlign == "left" ? -text_block_width/2 :
Text_HAlign == "right" ? text_block_width/2 :
0;
/* ---------------------------------------------------------------------------
SHAPE MATH
--------------------------------------------------------------------------- */
// Faktor for den nogenlunde rektangulære "safe text area" inde i formen.
function native_shape_safe(shape) =
shape == "Circle" ? [0.80, 0.80] :
shape == "Oval" ? [0.84, 0.78] :
shape == "Triangle" ? [0.62, 0.54] :
shape == "Diamond" ? [0.65, 0.65] :
shape == "Hexagon" ? [0.82, 0.82] :
shape == "Octagon" ? [0.88, 0.88] :
shape == "Shield" ? [0.76, 0.74] :
shape == "Arch" ? [0.88, 0.82] :
shape == "Arrow Right" ? [0.66, 0.82] :
shape == "Pennant" ? [0.70, 0.82] :
[1.0, 1.0];
safe_factor = native_shape_safe(Plate_Shape) * Shape_Text_Safety;
mount_feature_d =
Mounting_Style == "Magnet Pair" || Mounting_Style == "Magnet Quad"
? Magnet_Diameter
: Mounting_Style == "Keyhole Pair"
? Keyhole_Head_Diameter
: max([Hole_Diameter, Countersink_Diameter]);
mount_min_w =
Mounting_Style == "None" || Mounting_Style == "Lanyard Hole"
? 0
: 2 * (Mount_Edge + mount_feature_d/2 + 2);
mount_min_h =
Mounting_Style == "None"
? 0
: 2 * (Mount_Edge + mount_feature_d/2 + 2);
auto_width_raw =
max([
Minimum_Width,
(text_block_width + 2*Padding_X) / max([0.1, safe_factor[0]]),
mount_min_w
]);
auto_height_raw =
max([
Minimum_Height,
(text_block_height + 2*Padding_Y) / max([0.1, safe_factor[1]]),
mount_min_h
]);
pre_w = Auto_Size_Plate ? auto_width_raw : Manual_Width;
pre_h = Auto_Size_Plate ? auto_height_raw : Manual_Height;
square_side = max([pre_w, pre_h]);
plate_w =
Plate_Shape == "Square" || Plate_Shape == "Circle"
? square_side
: pre_w;
plate_h =
Plate_Shape == "Square" || Plate_Shape == "Circle"
? square_side
: pre_h;
usable_text_w =
max([0.1, (plate_w - 2*Padding_X) * safe_factor[0]]);
usable_text_h =
max([0.1, (plate_h - 2*Padding_Y) * safe_factor[1]]);
auto_fit_scale =
Scale_Text_Down_To_Fit
? min([
1,
usable_text_w / max([0.01, text_block_width]),
usable_text_h / max([0.01, text_block_height])
])
: 1;
final_text_scale = auto_fit_scale * Text_Global_Scale;
/* ---------------------------------------------------------------------------
VALIDATION + DIAGNOSTICS
--------------------------------------------------------------------------- */
module validate_parameters() {
assert(plate_w > 0 && plate_h > 0, "Plate dimensions must be positive.");
assert(Plate_Thickness > 0, "Plate_Thickness must be > 0.");
assert(Corner_Radius >= 0, "Corner_Radius cannot be negative.");
assert(Bevel_Size >= 0, "Bevel_Size cannot be negative.");
assert(final_text_scale > 0, "Text scale must be > 0.");
if (Text_Mode == "Engraved")
assert(Text_Engrave_Depth <= Plate_Thickness + EPS,
"Text_Engrave_Depth is larger than Plate_Thickness.");
if (Text_Mode == "Inlay") {
assert(Inlay_Depth <= Plate_Thickness + EPS,
"Inlay_Depth is larger than Plate_Thickness.");
assert(Inlay_Depth > Inlay_Z_Clearance,
"Inlay_Z_Clearance must be smaller than Inlay_Depth.");
}
if (Border_Style == "Engraved")
assert(Border_Engrave_Depth <= Plate_Thickness + EPS,
"Border_Engrave_Depth is larger than Plate_Thickness.");
if (Mounting_Style == "Magnet Pair" || Mounting_Style == "Magnet Quad")
assert(Magnet_Depth < Plate_Thickness,
"Magnet_Depth must be smaller than Plate_Thickness.");
if (Mounting_Style == "Keyhole Pair")
assert(Keyhole_Depth < Plate_Thickness,
"Keyhole_Depth must be smaller than Plate_Thickness.");
if (Construction == "Desk Slot Stand")
assert(Slot_Depth < Base_Thickness,
"Slot_Depth must be smaller than Base_Thickness.");
children();
}
echo("=== Universal Sign Generator / Nightly ===");
echo("OpenSCAD version", version());
echo("Use_Nightly_Metrics", Use_Nightly_Metrics);
echo("Plate shape", Plate_Shape);
echo("Construction", Construction);
echo("Plate size", [plate_w, plate_h, Plate_Thickness]);
echo("Text block raw", [text_block_width, text_block_height]);
echo("Final text scale", final_text_scale);
echo("Mounting", Mounting_Style);
if (!Use_Nightly_Metrics)
echo("WARNING: Nightly metrics disabled: text auto-sizing is approximate.");
if (final_text_scale < 0.65)
echo("WARNING: Text has been scaled below 65%. Consider a larger sign or smaller text.");
if (Text_Mode == "Raised" && Construction == "Foldable Sheet Blank")
echo("NOTE: Foldable Sheet Blank is primarily intended for 2D cut/engrave workflows.");
/* ---------------------------------------------------------------------------
2D SHAPES
--------------------------------------------------------------------------- */
module rounded_rect_2d(w, h, r) {
rr = min([max([0, r]), w/2, h/2]);
if (rr <= EPS)
square([w, h], center=true);
else
offset(r=rr, $fn=Curve_FN)
square([max([EPS, w-2*rr]), max([EPS, h-2*rr])], center=true);
}
module capsule_2d(w, h) {
if (w >= h) {
r = h/2;
hull() {
translate([-(w-h)/2, 0]) circle(r=r, $fn=Curve_FN);
translate([ +(w-h)/2, 0]) circle(r=r, $fn=Curve_FN);
}
} else {
r = w/2;
hull() {
translate([0, -(h-w)/2]) circle(r=r, $fn=Curve_FN);
translate([0, +(h-w)/2]) circle(r=r, $fn=Curve_FN);
}
}
}
module ellipse_2d(w, h) {
scale([w/2, h/2])
circle(r=1, $fn=Curve_FN);
}
module regular_polygon_2d(w, h, n, rot=90) {
polygon(points=[
for (i=[0:n-1])
[w/2*cos(rot + i*360/n), h/2*sin(rot + i*360/n)]
]);
}
module arch_2d(w, h) {
r = min([w/2, h * Arch_Radius_Fraction]);
cy = h/2 - r;
pts = concat(
[[-w/2, -h/2], [w/2, -h/2], [w/2, cy]],
[for (i=[0:Arch_Segments])
let(a = i * 180 / Arch_Segments)
[r*cos(a), cy + r*sin(a)]
]
);
polygon(points=pts);
}
module shield_2d(w, h) {
polygon(points=[
[-w/2, h*0.34],
[-w*0.34, h/2],
[ w*0.34, h/2],
[ w/2, h*0.34],
[ w*0.43, -h*0.18],
[ 0, -h/2],
[-w*0.43, -h*0.18]
]);
}
module tag_2d(w, h) {
c = min([Tag_Cut, w/3, h/2]);
polygon(points=[
[-w/2+c, -h/2],
[ w/2-c, -h/2],
[ w/2, -h/2+c],
[ w/2, h/2-c],
[ w/2-c, h/2],
[-w/2+c, h/2],
[-w/2, h/2-c],
[-w/2, -h/2+c]
]);
}
module arrow_right_2d(w, h) {
ah = w * Arrow_Head_Fraction;
polygon(points=[
[-w/2, -h/2],
[ w/2-ah, -h/2],
[ w/2-ah, -h/2],
[ w/2, 0],
[ w/2-ah, h/2],
[-w/2, h/2]
]);
}
module pennant_2d(w, h) {
polygon(points=[
[-w/2, -h/2],
[ w*0.20, -h/2],
[ w/2, 0],
[ w*0.20, h/2],
[-w/2, h/2]
]);
}
module face_outline_2d() {
if (Plate_Shape == "Rectangle")
square([plate_w, plate_h], center=true);
else if (Plate_Shape == "Square")
square([plate_w, plate_h], center=true);
else if (Plate_Shape == "Rounded Rectangle")
rounded_rect_2d(plate_w, plate_h, Corner_Radius);
else if (Plate_Shape == "Capsule")
capsule_2d(plate_w, plate_h);
else if (Plate_Shape == "Circle")
circle(d=plate_w, $fn=Curve_FN);
else if (Plate_Shape == "Oval")
ellipse_2d(plate_w, plate_h);
else if (Plate_Shape == "Triangle")
polygon(points=[
[-plate_w/2, -plate_h/2],
[ plate_w/2, -plate_h/2],
[0, plate_h/2]
]);
else if (Plate_Shape == "Diamond")
polygon(points=[
[0, plate_h/2],
[plate_w/2, 0],
[0, -plate_h/2],
[-plate_w/2, 0]
]);
else if (Plate_Shape == "Hexagon")
regular_polygon_2d(plate_w, plate_h, 6, 0);
else if (Plate_Shape == "Octagon")
regular_polygon_2d(plate_w, plate_h, 8, 22.5);
else if (Plate_Shape == "Shield")
shield_2d(plate_w, plate_h);
else if (Plate_Shape == "Arch")
arch_2d(plate_w, plate_h);
else if (Plate_Shape == "Tag")
tag_2d(plate_w, plate_h);
else if (Plate_Shape == "Arrow Right")
arrow_right_2d(plate_w, plate_h);
else if (Plate_Shape == "Pennant")
pennant_2d(plate_w, plate_h);
else
rounded_rect_2d(plate_w, plate_h, Corner_Radius);
}
module inset_outline_2d(d) {
offset(delta=-d, $fn=Curve_FN)
face_outline_2d();
}
/* ---------------------------------------------------------------------------
TEXT GEOMETRY
--------------------------------------------------------------------------- */
module one_line_text_2d(l) {
if (Font_Sizing_Mode == "em")
text(
text=l[0],
em=l[1],
font=l[2],
direction=Text_Direction,
language=Text_Language,
script=Text_Script,
halign=Text_HAlign,
valign="center",
spacing=l[3],
$fn=Text_FN
);
else
text(
text=l[0],
size=l[1],
font=l[2],
direction=Text_Direction,
language=Text_Language,
script=Text_Script,
halign=Text_HAlign,
valign="center",
spacing=l[3],
$fn=Text_FN
);
}
module text_block_2d() {
translate([Text_Offset_X, Text_Offset_Y])
scale([final_text_scale, final_text_scale])
if (len(active_lines) > 0)
for (i=[0:len(active_lines)-1]) {
l = active_lines[i];
translate([
text_anchor_x() + l[4],
line_center_y(i) + l[5]
])
one_line_text_2d(l);
}
}
module text_recess_shape_2d() {
if (Text_Mode == "Inlay")
offset(delta=Inlay_XY_Clearance, $fn=Curve_FN)
text_block_2d();
else
text_block_2d();
}
/* ---------------------------------------------------------------------------
BORDER
--------------------------------------------------------------------------- */
module border_ring_2d() {
if (Border_Width > EPS)
difference() {
inset_outline_2d(Border_Inset);
inset_outline_2d(Border_Inset + Border_Width);
}
}
/* ---------------------------------------------------------------------------
MOUNTING - 2D CUT SHAPES
--------------------------------------------------------------------------- */
function x_edge_pos() = max([0, plate_w/2 - Mount_Edge]);
function y_edge_pos() = max([0, plate_h/2 - Mount_Edge]);
module round_hole_2d(x, y, d=Hole_Diameter) {
translate([x,y])
circle(d=d, $fn=Curve_FN);
}
module slot_2d(x, y, length, width, vertical=true) {
translate([x,y])
hull() {
if (vertical) {
translate([0, -(length-width)/2]) circle(d=width, $fn=Curve_FN);
translate([0, +(length-width)/2]) circle(d=width, $fn=Curve_FN);
} else {
translate([-(length-width)/2, 0]) circle(d=width, $fn=Curve_FN);
translate([+(length-width)/2, 0]) circle(d=width, $fn=Curve_FN);
}
}
}
module through_mounts_2d() {
xe = x_edge_pos();
ye = y_edge_pos();
if (Mounting_Style == "2 Hanging Holes") {
round_hole_2d(-xe, ye);
round_hole_2d( xe, ye);
}
else if (Mounting_Style == "2 Side Holes") {
round_hole_2d(-xe, 0);
round_hole_2d( xe, 0);
}
else if (Mounting_Style == "4 Corner Holes" ||
Mounting_Style == "4 Countersunk") {
for (x=[-xe,xe], y=[-ye,ye])
round_hole_2d(x,y);
}
else if (Mounting_Style == "2 Countersunk") {
round_hole_2d(-xe, ye);
round_hole_2d( xe, ye);
}
else if (Mounting_Style == "Lanyard Hole") {
round_hole_2d(0, ye);
}
else if (Mounting_Style == "Zip-tie Slots") {
slot_2d(-xe, 0, Zip_Slot_Length, Zip_Slot_Width, true);
slot_2d( xe, 0, Zip_Slot_Length, Zip_Slot_Width, true);
}
}
module keyhole_2d() {
// Stort indføringshul nederst, smal kanal opad.
union() {
translate([0, -Keyhole_Length/2 + Keyhole_Head_Diameter/2])
circle(d=Keyhole_Head_Diameter, $fn=Curve_FN);
translate([0, Keyhole_Length/4])
square([Keyhole_Neck_Width, Keyhole_Length], center=true);
}
}
/* ---------------------------------------------------------------------------
3D BASE PLATE
--------------------------------------------------------------------------- */
module plate_solid_3d() {
b = min([
max([0, Bevel_Size]),
max([0, Plate_Thickness/2 - EPS])
]);
if (Edge_Profile == "Straight" || b <= EPS) {
linear_extrude(height=Plate_Thickness)
face_outline_2d();
} else {
// Chamfer begge sider uden Minkowski: hurtigere og robust på mange former.
union() {
hull() {
translate([0,0,0])
linear_extrude(height=EPS)
inset_outline_2d(b);
translate([0,0,b])
linear_extrude(height=EPS)
face_outline_2d();
}
if (Plate_Thickness > 2*b)
translate([0,0,b])
linear_extrude(height=Plate_Thickness-2*b)
face_outline_2d();
hull() {
translate([0,0,Plate_Thickness-b])
linear_extrude(height=EPS)
face_outline_2d();
translate([0,0,Plate_Thickness-EPS])
linear_extrude(height=EPS)
inset_outline_2d(b);
}
}
}
}
module through_mounts_3d() {
translate([0,0,-EPS])
linear_extrude(height=Plate_Thickness + 2*EPS)
through_mounts_2d();
}
module countersinks_3d() {
xe = x_edge_pos();
ye = y_edge_pos();
positions =
Mounting_Style == "2 Countersunk"
? [[-xe,ye],[xe,ye]]
: Mounting_Style == "4 Countersunk"
? [[-xe,-ye],[-xe,ye],[xe,-ye],[xe,ye]]
: [];
if (len(positions) > 0)
for (p = positions)
translate([p[0], p[1], Plate_Thickness-Countersink_Depth])
cylinder(
h=Countersink_Depth + EPS,
d1=Hole_Diameter,
d2=Countersink_Diameter,
$fn=Curve_FN
);
}
module backside_mount_recesses_3d() {
xe = x_edge_pos();
ye = y_edge_pos();
if (Mounting_Style == "Magnet Pair") {
mx = min([Magnet_Spacing/2, xe]);
for (x=[-mx,mx])
translate([x,0,-EPS])
cylinder(h=Magnet_Depth+EPS, d=Magnet_Diameter, $fn=Curve_FN);
}
else if (Mounting_Style == "Magnet Quad") {
for (x=[-xe,xe], y=[-ye,ye])
translate([x,y,-EPS])
cylinder(h=Magnet_Depth+EPS, d=Magnet_Diameter, $fn=Curve_FN);
}
else if (Mounting_Style == "Keyhole Pair") {
for (x=[-xe,xe])
translate([x, ye - Keyhole_Length/2, -EPS])
linear_extrude(height=Keyhole_Depth+EPS)
keyhole_2d();
}
}
/* ---------------------------------------------------------------------------
FACE: BASE + SUBTRACTIONS + RELIEF
--------------------------------------------------------------------------- */
module face_core_3d() {
difference() {
plate_solid_3d();
// Gennemgående monteringshuller / slots
through_mounts_3d();
// Forsænkning på forsiden
countersinks_3d();
// Magneter / keyholes fra bagsiden
backside_mount_recesses_3d();
// Tekst
if (Text_Mode == "Engraved")
translate([0,0,Plate_Thickness-Text_Engrave_Depth])
linear_extrude(height=Text_Engrave_Depth+EPS)
text_block_2d();
else if (Text_Mode == "Cut-through")
translate([0,0,-EPS])
linear_extrude(height=Plate_Thickness+2*EPS)
text_block_2d();
else if (Text_Mode == "Inlay")
translate([0,0,Plate_Thickness-Inlay_Depth])
linear_extrude(height=Inlay_Depth+EPS)
text_recess_shape_2d();
// Border
if (Border_Style == "Engraved")
translate([0,0,Plate_Thickness-Border_Engrave_Depth])
linear_extrude(height=Border_Engrave_Depth+EPS)
border_ring_2d();
else if (Border_Style == "Cut-through")
translate([0,0,-EPS])
linear_extrude(height=Plate_Thickness+2*EPS)
border_ring_2d();
}
}
module face_relief_3d() {
if (Text_Mode == "Raised")
translate([0,0,Plate_Thickness-EPS])
linear_extrude(height=Text_Raised_Height+EPS)
text_block_2d();
if (Border_Style == "Raised")
translate([0,0,Plate_Thickness-EPS])
linear_extrude(height=Border_Raised_Height+EPS)
border_ring_2d();
}
module sign_face_3d() {
union() {
face_core_3d();
face_relief_3d();
}
}
module inlay_insert_3d(on_face=false) {
insert_h = max([EPS, Inlay_Depth-Inlay_Z_Clearance]);
z0 =
on_face
? Plate_Thickness-Inlay_Depth+Inlay_Z_Clearance
: 0;
translate([0,0,z0])
linear_extrude(height=insert_h)
text_block_2d();
}
/* ---------------------------------------------------------------------------
DESK / SUPPORT GEOMETRY
--------------------------------------------------------------------------- */
module desk_base_3d(with_slot=false) {
bw = plate_w + Base_Width_Extra;
difference() {
translate([0, (Base_Depth-Base_Front_Lip)/2, Base_Thickness/2])
cube([bw, Base_Depth+Base_Front_Lip, Base_Thickness], center=true);
if (with_slot)
translate([
0,
0,
Base_Thickness - Slot_Depth/2 + EPS
])
cube([
plate_w + 2*Slot_Clearance,
Plate_Thickness + 2*Slot_Clearance,
Slot_Depth + 2*EPS
], center=true);
}
}
// Sign_face er modelleret i XY. Denne transform drejer bundkanten ned på bordet.
module posed_face(angle=90, y0=0, z0=0) {
translate([0,y0,z0])
rotate([angle,0,0])
translate([0,plate_h/2,0])
children();
}
// Ekstrudér en polygon i YZ langs X.
module prism_x(width, yz_points) {
rotate([0,90,0])
linear_extrude(height=width, center=true, convexity=10)
polygon(points=[
for (p=yz_points) [-p[1], p[0]]
]);
}
module l_stand_support_3d() {
desk_base_3d(false);
}
module desk_l_stand_3d(include_face=true, include_support=true) {
if (include_support)
l_stand_support_3d();
if (include_face)
posed_face(90, 0, Base_Thickness-Base_Embed)
sign_face_3d();
}
module wedge_support_3d() {
support_w = plate_w * Wedge_Width_Fraction;
rise_len = plate_h * Wedge_Height_Fraction;
tip_y = rise_len * cos(Desk_Angle);
tip_z = Base_Thickness + rise_len * sin(Desk_Angle) - Base_Embed;
desk_base_3d(false);
prism_x(
support_w,
[
[0, Base_Thickness-EPS],
[Wedge_Depth, Base_Thickness-EPS],
[tip_y, tip_z]
]
);
}
module desk_wedge_3d(include_face=true, include_support=true) {
if (include_support)
wedge_support_3d();
if (include_face)
posed_face(Desk_Angle, 0, Base_Thickness-Base_Embed)
sign_face_3d();
}
module slot_support_3d() {
desk_base_3d(true);
}
module desk_slot_stand_3d(include_face=true, include_support=true) {
if (include_support)
slot_support_3d();
if (include_face)
posed_face(
90,
0,
Base_Thickness-Slot_Depth
)
sign_face_3d();
}
module tent_face_pair_3d() {
a = Tent_Angle;
half_spread = plate_h * cos(a);
top_z = plate_h * sin(a);
// Forside
posed_face(a, -half_spread, 0)
sign_face_3d();
// Bagside: spejling over Y giver læsbar tekst fra modsatte side.
mirror([0,1,0])
posed_face(a, -half_spread, 0)
sign_face_3d();
if (Tent_Top_Bar)
translate([0,0,top_z])
rotate([0,90,0])
cylinder(
h=plate_w,
d=Tent_Top_Bar_Diameter,
center=true,
$fn=Curve_FN
);
}
/* ---------------------------------------------------------------------------
FOLDABLE SHEET BLANK
--------------------------------------------------------------------------- */
fold_blank_h = 2*plate_h + Fold_Hinge_Band;
module fold_blank_outline_2d() {
rounded_rect_2d(
plate_w,
fold_blank_h,
min([Fold_Corner_Radius, plate_w/2, fold_blank_h/2])
);
}
module fold_score_2d() {
// To fold-linjer skaber en lille hinge-band. Ved band=0 bliver de samme linje.
y1 = -Fold_Hinge_Band/2;
y2 = Fold_Hinge_Band/2;
translate([0,y1])
square([plate_w-2*Fold_Corner_Radius, Fold_Score_Width], center=true);
if (Fold_Hinge_Band > EPS)
translate([0,y2])
square([plate_w-2*Fold_Corner_Radius, Fold_Score_Width], center=true);
}
module fold_text_2d() {
panel_offset = (plate_h + Fold_Hinge_Band)/2;
translate([0, panel_offset])
text_block_2d();
if (Fold_Text_On_Both_Panels)
translate([0,-panel_offset])
rotate(180)
text_block_2d();
}
module fold_blank_3d() {
linear_extrude(height=Fold_Sheet_Thickness)
difference() {
fold_blank_outline_2d();
if (Text_Mode == "Cut-through")
fold_text_2d();
}
}
/* ---------------------------------------------------------------------------
STAKE SIGN
--------------------------------------------------------------------------- */
module stake_2d() {
y_top = -plate_h/2 + EPS;
y_shoulder = -plate_h/2 - (Stake_Length-Stake_Tip_Length);
y_tip = -plate_h/2 - Stake_Length;
polygon(points=[
[-Stake_Width/2, y_top],
[ Stake_Width/2, y_top],
[ Stake_Width/2, y_shoulder],
[0, y_tip],
[-Stake_Width/2, y_shoulder]
]);
}
module stake_body_3d() {
union() {
sign_face_3d();
linear_extrude(height=Plate_Thickness)
stake_2d();
}
}
/* ---------------------------------------------------------------------------
2D OUTPUTS
--------------------------------------------------------------------------- */
module face_cut_outline_2d() {
difference() {
face_outline_2d();
// Kun features der faktisk går igennem pladen.
through_mounts_2d();
if (Text_Mode == "Cut-through")
text_block_2d();
if (Border_Style == "Cut-through")
border_ring_2d();
}
}
module stake_cut_outline_2d() {
difference() {
union() {
face_outline_2d();
stake_2d();
}
through_mounts_2d();
if (Text_Mode == "Cut-through")
text_block_2d();
if (Border_Style == "Cut-through")
border_ring_2d();
}
}
module output_2d_cut() {
if (Construction == "Foldable Sheet Blank")
difference() {
fold_blank_outline_2d();
if (Text_Mode == "Cut-through")
fold_text_2d();
}
else if (Construction == "Stake Sign")
stake_cut_outline_2d();
else
face_cut_outline_2d();
}
module output_2d_text() {
if (Construction == "Foldable Sheet Blank")
fold_text_2d();
else
text_block_2d();
}
/* ---------------------------------------------------------------------------
3D CONSTRUCTION DISPATCH
--------------------------------------------------------------------------- */
module output_3d() {
want_face =
Part_Selection == "Assembly" ||
Part_Selection == "Face only";
want_support =
Part_Selection == "Assembly" ||
Part_Selection == "Support only";
if (Part_Selection == "Text insert only") {
inlay_insert_3d(false);
}
else if (Construction == "Flat Plate") {
if (want_face)
if (Text_Mode == "Inlay" && Part_Selection == "Assembly") {
sign_face_3d();
inlay_insert_3d(true);
} else {
sign_face_3d();
}
}
else if (Construction == "Desk L-Stand") {
desk_l_stand_3d(want_face, want_support);
}
else if (Construction == "Desk Wedge") {
desk_wedge_3d(want_face, want_support);
}
else if (Construction == "Desk Slot Stand") {
desk_slot_stand_3d(want_face, want_support);
}
else if (Construction == "A-Frame Tent") {
if (Part_Selection == "Support only") {
if (Tent_Top_Bar) {
top_z = plate_h * sin(Tent_Angle);
translate([0,0,top_z])
rotate([0,90,0])
cylinder(
h=plate_w,
d=Tent_Top_Bar_Diameter,
center=true,
$fn=Curve_FN
);
}
} else {
tent_face_pair_3d();
}
}
else if (Construction == "Foldable Sheet Blank") {
if (want_face)
fold_blank_3d();
}
else if (Construction == "Stake Sign") {
if (want_face)
stake_body_3d();
}
else {
sign_face_3d();
}
}
/* ---------------------------------------------------------------------------
MAIN
--------------------------------------------------------------------------- */
validate_parameters() {
if (Output_Mode == "3D")
output_3d();
else if (Output_Mode == "2D Cut Outline")
output_2d_cut();
else if (Output_Mode == "2D Engrave Text")
output_2d_text();
else if (Output_Mode == "2D Text/Inlay")
output_2d_text();
else if (Output_Mode == "2D Border")
border_ring_2d();
else if (Output_Mode == "2D Fold Score")
if (Construction == "Foldable Sheet Blank")
fold_score_2d();
else
output_3d();
}