OpenSCAD-Generators/universal-clock-dial/universal-clock-dial.scad

694 lines
No EOL
26 KiB
OpenSCAD

/*
===============================================================================
UNIVERSAL CLOCK DIAL GENERATOR — OpenSCAD Nightly
Version 1.0 — 2026-09
Requires a recent OpenSCAD Nightly / development snapshot because text layout
deliberately uses textmetrics(). No external libraries are required.
Designed for:
• 3D printing
• laser cutting / engraving
• CNC routing / engraving
• vinyl / plotter workflows via 2D export
Recommended workflow:
1. Select parameters in the Customizer.
2. Use Output_Mode = "3D" for a printable dial.
3. Use 2D_CUT / 2D_ENGRAVE / 2D_MARKS / 2D_TEXT for SVG/DXF export.
4. Use F6 render before final export.
textmetrics() notes:
We do not assume text() starts at [0,0], and we do not use string length as
a width estimate. The helpers below use textmetrics().position and .size to
center and fit the actual ink bounds of each rendered string.
===============================================================================
*/
/* [Output] */
Output_Mode = "3D"; // [3D,2D_CUT,2D_ENGRAVE,2D_MARKS,2D_TEXT,2D_PREVIEW]
Preview_Colors = true;
Explode_3D = false;
Explode_Distance = 8; // [0:1:40]
/* [Dial shape] */
Dial_Shape = "Round"; // [Round,Oval,Rectangle,RoundedRectangle,Polygon,Star]
Dial_Diameter = 180; // [30:1:500]
Dial_Width = 180; // [30:1:500]
Dial_Height = 180; // [30:1:500]
Corner_Radius = 18; // [0:0.5:100]
Polygon_Sides = 8; // [3:1:24]
Polygon_Rotation = 22.5; // [-180:1:180]
Star_Points = 12; // [3:1:36]
Star_Inner_Ratio = 0.88; // [0.1:0.01:0.99]
Shape_Margin = 0; // [-20:0.5:40]
/* [3D body] */
Dial_Thickness = 3.0; // [0.4:0.1:20]
Back_Recess_Enable = false;
Back_Recess_Diameter = 58; // [5:1:200]
Back_Recess_Depth = 1.2; // [0:0.1:10]
Edge_Bevel_Enable = false;
Edge_Bevel = 0.8; // [0:0.1:5]
/* [Center / movement hole] */
Center_Hole_Enable = true;
Center_Hole_Type = "Round"; // [Round,Square,D_Shaft,None]
Center_Hole_Diameter = 8.2; // [0.5:0.1:30]
Center_Hole_Square = 8.2; // [0.5:0.1:30]
D_Shaft_Flat = 1.8; // [0:0.1:10]
Center_Hole_X = 0; // [-50:0.1:50]
Center_Hole_Y = 0; // [-50:0.1:50]
/* [Outer rings and borders] */
Outer_Rim_Enable = true;
Outer_Rim_Inset = 4; // [0:0.5:40]
Outer_Rim_Width = 1.2; // [0.2:0.1:10]
Outer_Rim_Mode = "Raised"; // [Raised,Engraved,None]
Outer_Rim_Height = 0.7; // [0.1:0.1:5]
Outer_Rim_Depth = 0.5; // [0.1:0.1:5]
Inner_Ring_Enable = true;
Inner_Ring_Radius = 58; // [5:0.5:240]
Inner_Ring_Width = 0.7; // [0.1:0.1:8]
Inner_Ring_Mode = "Engraved"; // [Raised,Engraved,None]
/* [Minute / second track] */
Track_Enable = true;
Track_Count = 60; // [1:1:240]
Track_Radius = 78; // [5:0.5:240]
Track_Major_Every = 5; // [1:1:60]
Track_Major_Style = "Line"; // [Line,Dot,Pill,Triangle,Diamond,None]
Track_Minor_Style = "Line"; // [Line,Dot,Pill,Triangle,Diamond,None]
Track_Major_Length = 6; // [0.5:0.25:30]
Track_Minor_Length = 3; // [0.25:0.25:20]
Track_Major_Width = 1.6; // [0.2:0.1:8]
Track_Minor_Width = 0.7; // [0.1:0.1:5]
Track_Major_Diameter = 3.2; // [0.5:0.1:12]
Track_Minor_Diameter = 1.5; // [0.3:0.1:8]
Track_Rotation = 0; // [-180:1:180]
Track_Mode = "Raised"; // [Raised,Engraved,Through,None]
Track_Height = 0.7; // [0.1:0.1:5]
Track_Depth = 0.5; // [0.1:0.1:5]
/* [Hour markers] */
Hour_Markers_Enable = false;
Hour_Marker_Count = 12; // [1:1:36]
Hour_Marker_Radius = 66; // [5:0.5:240]
Hour_Marker_Style = "Pill"; // [Line,Dot,Pill,Triangle,Diamond,None]
Hour_Marker_Length = 8; // [0.5:0.25:30]
Hour_Marker_Width = 2.4; // [0.2:0.1:12]
Hour_Marker_Diameter = 4; // [0.5:0.1:15]
Hour_Marker_Rotation = 0; // [-180:1:180]
Hour_Marker_Mode = "Raised"; // [Raised,Engraved,Through,None]
Hour_Marker_Height = 0.8; // [0.1:0.1:5]
Hour_Marker_Depth = 0.5; // [0.1:0.1:5]
/* [Hour numerals] */
Numbers_Enable = true;
Number_Set = "Arabic_1_12"; // [Arabic_1_12,Arabic_0_11,Roman,Quarter_Arabic,Quarter_Roman,Words,None]
Number_Radius = 61; // [5:0.5:240]
Number_Font = "Liberation Sans:style=Bold";
Number_Size = 18; // [2:0.5:80]
Number_Max_Width = 22; // [2:0.5:80]
Number_Max_Height = 18; // [2:0.5:80]
Number_Spacing = 1.0; // [0.5:0.05:2]
Number_Orientation = "Upright"; // [Upright,Radial,Tangent]
Number_Rotation = 0; // [-180:1:180]
Number_Mode = "Raised"; // [Raised,Engraved,Through,None]
Number_Height = 0.8; // [0.1:0.1:5]
Number_Depth = 0.5; // [0.1:0.1:5]
/* [Custom text] */
Top_Text_Enable = true;
Top_Text = "MAKERSPACE";
Top_Text_Y = 27; // [-200:0.5:200]
Top_Text_Font = "Liberation Sans:style=Bold";
Top_Text_Size = 8; // [1:0.5:50]
Top_Text_Max_Width = 70; // [5:1:300]
Top_Text_Max_Height = 12; // [2:0.5:100]
Top_Text_Mode = "Engraved"; // [Raised,Engraved,Through,None]
Bottom_Text_Enable = true;
Bottom_Text = "NÆSTVED";
Bottom_Text_Y = -30; // [-200:0.5:200]
Bottom_Text_Font = "Liberation Sans";
Bottom_Text_Size = 6; // [1:0.5:50]
Bottom_Text_Max_Width = 55; // [5:1:300]
Bottom_Text_Max_Height = 10; // [2:0.5:100]
Bottom_Text_Mode = "Engraved"; // [Raised,Engraved,Through,None]
Center_Text_Enable = false;
Center_Text = "QUARTZ";
Center_Text_X = 0; // [-200:0.5:200]
Center_Text_Y = -15; // [-200:0.5:200]
Center_Text_Font = "Liberation Sans";
Center_Text_Size = 5; // [1:0.5:50]
Center_Text_Max_Width = 45; // [5:1:300]
Center_Text_Max_Height = 8; // [2:0.5:100]
Center_Text_Mode = "Engraved"; // [Raised,Engraved,Through,None]
Text_Raised_Height = 0.7; // [0.1:0.1:5]
Text_Engrave_Depth = 0.45; // [0.1:0.1:5]
/* [Subdial 1] */
Subdial1_Enable = false;
Subdial1_X = 0; // [-200:0.5:200]
Subdial1_Y = -35; // [-200:0.5:200]
Subdial1_Radius = 19; // [4:0.5:80]
Subdial1_Count = 60; // [1:1:120]
Subdial1_Major_Every = 5; // [1:1:60]
Subdial1_Label = "SECONDS";
Subdial1_Label_Size = 3.5; // [1:0.25:20]
Subdial1_Center_Hole = 2.2; // [0:0.1:20]
/* [Subdial 2] */
Subdial2_Enable = false;
Subdial2_X = -38; // [-200:0.5:200]
Subdial2_Y = 0; // [-200:0.5:200]
Subdial2_Radius = 17; // [4:0.5:80]
Subdial2_Count = 24; // [1:1:120]
Subdial2_Major_Every = 4; // [1:1:60]
Subdial2_Label = "24 H";
Subdial2_Label_Size = 3.5; // [1:0.25:20]
Subdial2_Center_Hole = 2.2; // [0:0.1:20]
/* [Subdial 3] */
Subdial3_Enable = false;
Subdial3_X = 38; // [-200:0.5:200]
Subdial3_Y = 0; // [-200:0.5:200]
Subdial3_Radius = 17; // [4:0.5:80]
Subdial3_Count = 31; // [1:1:120]
Subdial3_Major_Every = 5; // [1:1:60]
Subdial3_Label = "DATE";
Subdial3_Label_Size = 3.5; // [1:0.25:20]
Subdial3_Center_Hole = 2.2; // [0:0.1:20]
/* [Subdial appearance] */
Subdial_Ring_Width = 0.65; // [0.1:0.05:5]
Subdial_Tick_Width = 0.45; // [0.1:0.05:3]
Subdial_Tick_Length = 2.0; // [0.2:0.1:8]
Subdial_Major_Length = 3.2; // [0.2:0.1:10]
Subdial_Mode = "Engraved"; // [Raised,Engraved,Through,None]
Subdial_Depth = 0.35; // [0.1:0.05:3]
Subdial_Height = 0.5; // [0.1:0.05:3]
Subdial_Font = "Liberation Sans";
/* [Decorative center] */
Center_Ring_Enable = true;
Center_Ring_Radius = 8; // [1:0.5:50]
Center_Ring_Width = 0.8; // [0.1:0.1:6]
Center_Ring_Mode = "Engraved"; // [Raised,Engraved,None]
/* [Mounting / auxiliary holes] */
Aux_Holes_Enable = false;
Aux_Hole_Diameter = 3.2; // [0.5:0.1:20]
Aux_Holes = [[0,75],[0,-75]];
/* [Manufacturing compensation] */
Cut_Kerf_Compensation = 0; // [-1:0.01:1]
Engrave_Expansion = 0; // [-1:0.01:1]
Min_Feature = 0.2; // [0.05:0.05:3]
/* [Quality] */
Curve_Fragments = 128; // [24:8:512]
Text_Fragments = 24; // [8:4:128]
$fn = Curve_Fragments;
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
function clamp(v, lo, hi) = min(max(v, lo), hi);
function safe_div(a,b,fallback=1) = abs(b) < 0.000001 ? fallback : a/b;
function polar(r,a) = [r*sin(a), r*cos(a)];
function is_major(i,every) = every > 0 && i % every == 0;
ROMAN = ["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];
WORDS = ["TWELVE","ONE","TWO","THREE","FOUR","FIVE","SIX","SEVEN","EIGHT","NINE","TEN","ELEVEN"];
function hour_label(i) =
Number_Set == "Arabic_1_12" ? str(i == 0 ? 12 : i) :
Number_Set == "Arabic_0_11" ? str(i) :
Number_Set == "Roman" ? ROMAN[i] :
Number_Set == "Quarter_Arabic" ? (i==0?"12":i==3?"3":i==6?"6":i==9?"9":"") :
Number_Set == "Quarter_Roman" ? (i==0?"XII":i==3?"III":i==6?"VI":i==9?"IX":"") :
Number_Set == "Words" ? WORDS[i] : "";
function star_pts(n, ro, ri, rot=0) =
[for(i=[0:2*n-1])
let(a = rot + i*180/n, r = (i%2==0 ? ro : ri))
[r*sin(a), r*cos(a)]];
// Nightly textmetrics(): tight ink-bounds fit scale.
function text_fit_scale(s, nominal_size, font, max_w, max_h, spacing=1) =
let(m = textmetrics(text=s, size=nominal_size, font=font,
halign="left", valign="baseline", spacing=spacing),
sx = safe_div(max_w, max(m.size.x, 0.0001), 1),
sy = safe_div(max_h, max(m.size.y, 0.0001), 1))
min(1, sx, sy);
module tight_text_2d(s, nominal_size=10, font="Liberation Sans",
max_w=1000, max_h=1000, spacing=1) {
if (s != "") {
m = textmetrics(text=s, size=nominal_size, font=font,
halign="left", valign="baseline", spacing=spacing);
fit = min(1,
safe_div(max_w, max(m.size.x,0.0001),1),
safe_div(max_h, max(m.size.y,0.0001),1));
scale([fit,fit])
translate([-m.position.x - m.size.x/2,
-m.position.y - m.size.y/2])
text(text=s, size=nominal_size, font=font,
halign="left", valign="baseline", spacing=spacing,
$fn=Text_Fragments);
}
}
module ring2d(radius, width) {
w = max(width, Min_Feature);
difference() {
circle(r=radius + w/2);
circle(r=max(0.001, radius - w/2));
}
}
module rounded_rect_2d(w,h,r) {
rr = min(max(r,0), min(w,h)/2);
if (rr <= 0) square([w,h], center=true);
else offset(r=rr) square([max(0.01,w-2*rr), max(0.01,h-2*rr)], center=true);
}
module raw_dial_shape_2d(delta=0) {
if (Dial_Shape == "Round")
circle(r=Dial_Diameter/2 + Shape_Margin + delta);
else if (Dial_Shape == "Oval")
scale([(Dial_Width/2 + Shape_Margin + delta),
(Dial_Height/2 + Shape_Margin + delta)]) circle(r=1);
else if (Dial_Shape == "Rectangle")
square([Dial_Width + 2*(Shape_Margin+delta),
Dial_Height + 2*(Shape_Margin+delta)], center=true);
else if (Dial_Shape == "RoundedRectangle")
rounded_rect_2d(Dial_Width + 2*(Shape_Margin+delta),
Dial_Height + 2*(Shape_Margin+delta),
max(0,Corner_Radius + delta));
else if (Dial_Shape == "Polygon")
rotate(Polygon_Rotation)
circle(r=Dial_Diameter/2 + Shape_Margin + delta, $fn=Polygon_Sides);
else if (Dial_Shape == "Star")
polygon(star_pts(Star_Points,
Dial_Diameter/2 + Shape_Margin + delta,
(Dial_Diameter/2 + Shape_Margin + delta)*Star_Inner_Ratio,
Polygon_Rotation));
}
module dial_cut_outline_2d() {
delta = Cut_Kerf_Compensation/2;
raw_dial_shape_2d(delta);
}
module center_hole_2d(extra=0) {
translate([Center_Hole_X,Center_Hole_Y]) {
if (Center_Hole_Enable && Center_Hole_Type == "Round")
circle(d=max(0.01,Center_Hole_Diameter + 2*extra));
else if (Center_Hole_Enable && Center_Hole_Type == "Square")
square([Center_Hole_Square+2*extra,Center_Hole_Square+2*extra],center=true);
else if (Center_Hole_Enable && Center_Hole_Type == "D_Shaft") {
d = Center_Hole_Diameter + 2*extra;
flat = clamp(D_Shaft_Flat,0,d*0.9);
intersection() {
circle(d=d);
translate([-d/2,-d/2]) square([d-flat,d]);
}
}
}
}
module aux_holes_2d(extra=0) {
if (Aux_Holes_Enable)
for(p=Aux_Holes)
translate(p) circle(d=max(0.01,Aux_Hole_Diameter + 2*extra));
}
module all_through_holes_2d() {
center_hole_2d(Cut_Kerf_Compensation/2);
aux_holes_2d(Cut_Kerf_Compensation/2);
subdial_holes_2d(Cut_Kerf_Compensation/2);
}
// -----------------------------------------------------------------------------
// Marker primitives
// -----------------------------------------------------------------------------
module marker_shape_2d(style="Line", len=5, width=1, diameter=2) {
w = max(width, Min_Feature);
d = max(diameter, Min_Feature);
if (style == "Line")
square([w,max(len,Min_Feature)],center=true);
else if (style == "Pill")
hull() {
translate([0,-max(0,len-d)/2]) circle(d=d);
translate([0, max(0,len-d)/2]) circle(d=d);
}
else if (style == "Dot")
circle(d=d);
else if (style == "Triangle")
polygon([[0,len/2],[-w,-len/2],[w,-len/2]]);
else if (style == "Diamond")
polygon([[0,len/2],[w,0],[0,-len/2],[-w,0]]);
}
module radial_marker_2d(radius, angle, style, len, width, diameter) {
p = polar(radius,angle);
translate(p) rotate(-angle) marker_shape_2d(style,len,width,diameter);
}
module track_geometry_2d() {
if (Track_Enable && Track_Mode != "None")
for(i=[0:Track_Count-1]) {
major = is_major(i,Track_Major_Every);
a = Track_Rotation + i*360/Track_Count;
style = major ? Track_Major_Style : Track_Minor_Style;
len = major ? Track_Major_Length : Track_Minor_Length;
wid = major ? Track_Major_Width : Track_Minor_Width;
dia = major ? Track_Major_Diameter : Track_Minor_Diameter;
radial_marker_2d(Track_Radius,a,style,len,wid,dia);
}
}
module hour_marker_geometry_2d() {
if (Hour_Markers_Enable && Hour_Marker_Mode != "None")
for(i=[0:Hour_Marker_Count-1])
radial_marker_2d(Hour_Marker_Radius,
Hour_Marker_Rotation + i*360/Hour_Marker_Count,
Hour_Marker_Style,
Hour_Marker_Length,
Hour_Marker_Width,
Hour_Marker_Diameter);
}
// -----------------------------------------------------------------------------
// Numerals / text
// -----------------------------------------------------------------------------
module numeral_at(i) {
s = hour_label(i);
if (s != "") {
a = Number_Rotation + i*30;
p = polar(Number_Radius,a);
orient = Number_Orientation == "Radial" ? -a :
Number_Orientation == "Tangent" ? -a+90 : 0;
translate(p)
rotate(orient)
tight_text_2d(s, Number_Size, Number_Font,
Number_Max_Width, Number_Max_Height,
Number_Spacing);
}
}
module number_geometry_2d() {
if (Numbers_Enable && Number_Set != "None" && Number_Mode != "None")
for(i=[0:11]) numeral_at(i);
}
module top_text_2d() {
if (Top_Text_Enable && Top_Text_Mode != "None")
translate([0,Top_Text_Y])
tight_text_2d(Top_Text,Top_Text_Size,Top_Text_Font,
Top_Text_Max_Width,Top_Text_Max_Height);
}
module bottom_text_2d() {
if (Bottom_Text_Enable && Bottom_Text_Mode != "None")
translate([0,Bottom_Text_Y])
tight_text_2d(Bottom_Text,Bottom_Text_Size,Bottom_Text_Font,
Bottom_Text_Max_Width,Bottom_Text_Max_Height);
}
module center_text_2d() {
if (Center_Text_Enable && Center_Text_Mode != "None")
translate([Center_Text_X,Center_Text_Y])
tight_text_2d(Center_Text,Center_Text_Size,Center_Text_Font,
Center_Text_Max_Width,Center_Text_Max_Height);
}
module all_custom_text_2d() {
top_text_2d();
bottom_text_2d();
center_text_2d();
}
// -----------------------------------------------------------------------------
// Rings / subdials
// -----------------------------------------------------------------------------
module outer_rim_2d() {
// Generalized inset border: offset inward from actual dial outline.
if (Outer_Rim_Enable && Outer_Rim_Mode != "None")
difference() {
offset(delta=-Outer_Rim_Inset + Outer_Rim_Width/2) raw_dial_shape_2d();
offset(delta=-Outer_Rim_Inset - Outer_Rim_Width/2) raw_dial_shape_2d();
}
}
module inner_ring_2d() {
if (Inner_Ring_Enable && Inner_Ring_Mode != "None")
ring2d(Inner_Ring_Radius,Inner_Ring_Width);
}
module center_ring_2d() {
if (Center_Ring_Enable && Center_Ring_Mode != "None")
translate([Center_Hole_X,Center_Hole_Y]) ring2d(Center_Ring_Radius,Center_Ring_Width);
}
module subdial_2d(x,y,r,count,major_every,label,label_size,hole_d) {
translate([x,y]) {
ring2d(r,Subdial_Ring_Width);
for(i=[0:count-1]) {
major = is_major(i,major_every);
a = i*360/count;
rr = r - (major ? Subdial_Major_Length : Subdial_Tick_Length)/2;
radial_marker_2d(rr,a,"Line",
major ? Subdial_Major_Length : Subdial_Tick_Length,
Subdial_Tick_Width,Subdial_Tick_Width);
}
if (label != "")
translate([0,-r*0.42])
tight_text_2d(label,label_size,Subdial_Font,r*1.35,label_size*1.35);
}
}
module subdial_holes_2d(extra=0) {
if (Subdial1_Enable && Subdial1_Center_Hole > 0)
translate([Subdial1_X,Subdial1_Y]) circle(d=Subdial1_Center_Hole+2*extra);
if (Subdial2_Enable && Subdial2_Center_Hole > 0)
translate([Subdial2_X,Subdial2_Y]) circle(d=Subdial2_Center_Hole+2*extra);
if (Subdial3_Enable && Subdial3_Center_Hole > 0)
translate([Subdial3_X,Subdial3_Y]) circle(d=Subdial3_Center_Hole+2*extra);
}
module subdial_geometry_2d() {
if (Subdial_Mode != "None") {
if (Subdial1_Enable) subdial_2d(Subdial1_X,Subdial1_Y,Subdial1_Radius,Subdial1_Count,Subdial1_Major_Every,Subdial1_Label,Subdial1_Label_Size,Subdial1_Center_Hole);
if (Subdial2_Enable) subdial_2d(Subdial2_X,Subdial2_Y,Subdial2_Radius,Subdial2_Count,Subdial2_Major_Every,Subdial2_Label,Subdial2_Label_Size,Subdial2_Center_Hole);
if (Subdial3_Enable) subdial_2d(Subdial3_X,Subdial3_Y,Subdial3_Radius,Subdial3_Count,Subdial3_Major_Every,Subdial3_Label,Subdial3_Label_Size,Subdial3_Center_Hole);
}
}
// -----------------------------------------------------------------------------
// Group by manufacturing treatment
// -----------------------------------------------------------------------------
module raised_geometry_2d() {
if (Track_Mode == "Raised") track_geometry_2d();
if (Hour_Marker_Mode == "Raised") hour_marker_geometry_2d();
if (Number_Mode == "Raised") number_geometry_2d();
if (Outer_Rim_Mode == "Raised") outer_rim_2d();
if (Inner_Ring_Mode == "Raised") inner_ring_2d();
if (Center_Ring_Mode == "Raised") center_ring_2d();
if (Subdial_Mode == "Raised") subdial_geometry_2d();
if (Top_Text_Mode == "Raised") top_text_2d();
if (Bottom_Text_Mode == "Raised") bottom_text_2d();
if (Center_Text_Mode == "Raised") center_text_2d();
}
module engraved_geometry_2d() {
if (Track_Mode == "Engraved") track_geometry_2d();
if (Hour_Marker_Mode == "Engraved") hour_marker_geometry_2d();
if (Number_Mode == "Engraved") number_geometry_2d();
if (Outer_Rim_Mode == "Engraved") outer_rim_2d();
if (Inner_Ring_Mode == "Engraved") inner_ring_2d();
if (Center_Ring_Mode == "Engraved") center_ring_2d();
if (Subdial_Mode == "Engraved") subdial_geometry_2d();
if (Top_Text_Mode == "Engraved") top_text_2d();
if (Bottom_Text_Mode == "Engraved") bottom_text_2d();
if (Center_Text_Mode == "Engraved") center_text_2d();
}
module through_decoration_2d() {
if (Track_Mode == "Through") track_geometry_2d();
if (Hour_Marker_Mode == "Through") hour_marker_geometry_2d();
if (Number_Mode == "Through") number_geometry_2d();
if (Subdial_Mode == "Through") subdial_geometry_2d();
if (Top_Text_Mode == "Through") top_text_2d();
if (Bottom_Text_Mode == "Through") bottom_text_2d();
if (Center_Text_Mode == "Through") center_text_2d();
}
module all_cutouts_2d() {
all_through_holes_2d();
through_decoration_2d();
}
module all_marks_2d() {
track_geometry_2d();
hour_marker_geometry_2d();
outer_rim_2d();
inner_ring_2d();
center_ring_2d();
subdial_geometry_2d();
}
module all_text_2d() {
number_geometry_2d();
all_custom_text_2d();
}
// -----------------------------------------------------------------------------
// 3D construction
// -----------------------------------------------------------------------------
module raw_body_3d() {
b = min(max(Edge_Bevel,0), Dial_Thickness/2);
if (Edge_Bevel_Enable && b > 0.01) {
// True geometric top/bottom bevel using hulls between an inset profile
// and the full profile; works for all supported dial outlines.
hull() {
linear_extrude(height=0.01) offset(delta=-b) raw_dial_shape_2d();
translate([0,0,b]) linear_extrude(height=0.01) raw_dial_shape_2d();
}
translate([0,0,b])
linear_extrude(height=max(0.01,Dial_Thickness-2*b)) raw_dial_shape_2d();
hull() {
translate([0,0,Dial_Thickness-b-0.01])
linear_extrude(height=0.01) raw_dial_shape_2d();
translate([0,0,Dial_Thickness-0.01])
linear_extrude(height=0.01) offset(delta=-b) raw_dial_shape_2d();
}
} else
linear_extrude(height=Dial_Thickness) raw_dial_shape_2d();
}
module base_body_3d() {
difference() {
raw_body_3d();
// Through holes and through-decoration get a small Z overcut.
translate([0,0,-0.2])
linear_extrude(height=Dial_Thickness+0.4)
all_cutouts_2d();
if (Back_Recess_Enable && Back_Recess_Depth > 0)
translate([Center_Hole_X,Center_Hole_Y,-0.01])
cylinder(d=Back_Recess_Diameter,
h=min(Back_Recess_Depth,Dial_Thickness-0.05));
// Engraving cut from front face.
translate([0,0,max(0,Dial_Thickness-max(Track_Depth,Hour_Marker_Depth,Number_Depth,Outer_Rim_Depth,Subdial_Depth,Text_Engrave_Depth)-0.01)])
linear_extrude(height=max(Track_Depth,Hour_Marker_Depth,Number_Depth,Outer_Rim_Depth,Subdial_Depth,Text_Engrave_Depth)+0.02)
engraved_geometry_2d();
}
}
module raised_features_3d() {
// Features share one user-visible top height policy: each category may
// request its own height, so they are extruded separately.
z0 = Dial_Thickness + (Explode_3D ? Explode_Distance : 0);
if (Track_Mode == "Raised")
translate([0,0,z0]) linear_extrude(height=Track_Height) track_geometry_2d();
if (Hour_Marker_Mode == "Raised")
translate([0,0,z0]) linear_extrude(height=Hour_Marker_Height) hour_marker_geometry_2d();
if (Number_Mode == "Raised")
translate([0,0,z0]) linear_extrude(height=Number_Height) number_geometry_2d();
if (Outer_Rim_Mode == "Raised")
translate([0,0,z0]) linear_extrude(height=Outer_Rim_Height) outer_rim_2d();
if (Inner_Ring_Mode == "Raised")
translate([0,0,z0]) linear_extrude(height=Outer_Rim_Height) inner_ring_2d();
if (Center_Ring_Mode == "Raised")
translate([0,0,z0]) linear_extrude(height=Outer_Rim_Height) center_ring_2d();
if (Subdial_Mode == "Raised")
translate([0,0,z0]) linear_extrude(height=Subdial_Height) subdial_geometry_2d();
if (Top_Text_Mode == "Raised")
translate([0,0,z0]) linear_extrude(height=Text_Raised_Height) top_text_2d();
if (Bottom_Text_Mode == "Raised")
translate([0,0,z0]) linear_extrude(height=Text_Raised_Height) bottom_text_2d();
if (Center_Text_Mode == "Raised")
translate([0,0,z0]) linear_extrude(height=Text_Raised_Height) center_text_2d();
}
module clock_dial_3d() {
if (Preview_Colors) {
color([0.82,0.82,0.82]) base_body_3d();
color([0.12,0.12,0.12]) raised_features_3d();
} else {
base_body_3d();
raised_features_3d();
}
}
// -----------------------------------------------------------------------------
// 2D export views
// -----------------------------------------------------------------------------
module export_2d_cut() {
difference() {
dial_cut_outline_2d();
all_cutouts_2d();
}
}
module export_2d_engrave() {
if (Engrave_Expansion == 0)
engraved_geometry_2d();
else
offset(delta=Engrave_Expansion) engraved_geometry_2d();
}
module export_2d_marks() { all_marks_2d(); }
module export_2d_text() { all_text_2d(); }
module export_2d_preview() {
color([0.85,0.85,0.85]) export_2d_cut();
color([0.15,0.15,0.15]) export_2d_engrave();
color([0.05,0.25,0.75]) raised_geometry_2d();
}
// -----------------------------------------------------------------------------
// Diagnostics
// -----------------------------------------------------------------------------
echo("Universal Clock Dial Generator — Nightly textmetrics edition");
echo("Output",Output_Mode,"Shape",Dial_Shape);
echo("Center hole",Center_Hole_Enable,Center_Hole_Type,Center_Hole_Diameter);
if (Numbers_Enable && Number_Set != "None") {
_m12 = textmetrics(text=hour_label(0), size=Number_Size, font=Number_Font,
halign="left",valign="baseline",spacing=Number_Spacing);
echo("12-label textmetrics position",_m12.position,"size",_m12.size,"advance",_m12.advance);
}
// -----------------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------------
if (Output_Mode == "3D")
clock_dial_3d();
else if (Output_Mode == "2D_CUT")
export_2d_cut();
else if (Output_Mode == "2D_ENGRAVE")
export_2d_engrave();
else if (Output_Mode == "2D_MARKS")
export_2d_marks();
else if (Output_Mode == "2D_TEXT")
export_2d_text();
else if (Output_Mode == "2D_PREVIEW")
export_2d_preview();