Skip to content

Instantly share code, notes, and snippets.

@KoStard
Last active March 30, 2026 21:32
Show Gist options
  • Select an option

  • Save KoStard/d46c91a12660ff9760dbba8714117254 to your computer and use it in GitHub Desktop.

Select an option

Save KoStard/d46c91a12660ff9760dbba8714117254 to your computer and use it in GitHub Desktop.
// Tissot-Style Golden Watch with Leather Strap
// ForgeCAD model — Z-up, all dimensions in mm
// === Parameters ===
const caseDia = param("Case Diameter", 40, { min: 34, max: 46, unit: "mm" });
const caseThick = param("Case Thickness", 10, { min: 7, max: 14, unit: "mm" });
const strapW = param("Strap Width", 20, { min: 16, max: 24, unit: "mm" });
const strapLen = param("Strap Length", 80, { min: 60, max: 110, unit: "mm" });
const caseR = caseDia / 2;
const bezelR = caseR - 1;
const dialR = bezelR - 1.5;
const wallThick = 2;
const dialDepth = 3;
// === Colors ===
const GOLD = '#C5A855';
const GOLD_DARK = '#846d2f';
const LEATHER = '#5C3A1E';
const LEATHER_DARK = '#3A2210';
const DIAL_SILVER = '#C8CDD4';
const HAND_GOLD = '#D4AF37';
// ========================================
// WATCH CASE — hollowed, open on top so dial is visible
// ========================================
// Full outer cylinder
const caseOuter = cylinder(caseThick, caseR, caseR, 64, true);
// Cavity carved from the top — leaves a floor (wallThick) at the bottom
// and thin walls around the sides
const cavityDepth = caseThick - wallThick;
const cavity = cylinder(cavityDepth + 0.1, dialR + 0.5, dialR + 0.5, 64, true)
.translate(0, 0, (caseThick - cavityDepth) / 2 + 0.05);
const caseBody = difference(caseOuter, cavity)
.color(GOLD)
.material({ metalness: 0.85, roughness: 0.15 });
// Bezel ring on top edge
const bezelRing = difference(
cylinder(1.2, caseR, caseR, 64, true),
cylinder(1.4, bezelR, bezelR, 64, true)
).translate(0, 0, caseThick / 2 - 0.4)
.color(GOLD_DARK)
.material({ metalness: 0.9, roughness: 0.1 });
// Case back
const caseBack = cylinder(0.8, caseR - 0.5, caseR - 0.5, 64, true)
.translate(0, 0, -caseThick / 2 - 0.3)
.color(GOLD_DARK)
.material({ metalness: 0.8, roughness: 0.2 });
// ========================================
// DIAL — sits on the cavity floor
// ========================================
const dialZ = caseThick / 2 - dialDepth;
const dial = cylinder(0.5, dialR, dialR, 64, true)
.translate(0, 0, dialZ)
.color(DIAL_SILVER)
.material({ metalness: 0.4, roughness: 0.3 });
const minuteTrack = difference(
cylinder(0.3, dialR, dialR, 64, true),
cylinder(0.5, dialR - 1.2, dialR - 1.2, 64, true)
).translate(0, 0, dialZ + 0.4)
.color(GOLD)
.material({ metalness: 0.8, roughness: 0.15 });
// ========================================
// HOUR MARKERS
// ========================================
const markers = [];
for (let i = 0; i < 12; i++) {
const angle = (i * 30) * Math.PI / 180;
const markerR = dialR - 3.5;
const markerLen = i % 3 === 0 ? 3.5 : 2.2;
const markerW = i % 3 === 0 ? 1.2 : 0.8;
const marker = box(markerW, markerLen, 0.6, true)
.rotate(0, 0, -i * 30)
.translate(
Math.sin(angle) * markerR,
Math.cos(angle) * markerR,
dialZ + 0.6
);
markers.push({
name: `Marker ${i === 0 ? 12 : i}`,
shape: marker,
color: GOLD,
});
}
// ========================================
// WATCH HANDS — inside cavity, joint-controlled
// ========================================
const handZ = dialZ + 0.8;
const hourHand = box(1.6, 10, 0.8, true)
.translate(0, 5, handZ);
const minuteHand = box(1.1, 14.5, 0.6, true)
.translate(0, 7.25, handZ + 0.2);
const secondHand = box(0.4, 15.5, 0.3, true)
.translate(0, 6, handZ + 0.4);
const centerPin = cylinder(1.5, 1.2, 1.2, 24, true)
.translate(0, 0, handZ + 0.6)
.color(GOLD)
.material({ metalness: 0.95, roughness: 0.05 });
// ========================================
// JOINTS — interactive hand rotation
// ========================================
jointsView({
joints: [
{
name: "Hours",
child: "Hour Hand",
type: "revolute",
axis: [0, 0, -1],
pivot: [0, 0, handZ],
min: 0, max: 360, default: 60,
unit: "°",
},
{
name: "Minutes",
child: "Minute Hand",
type: "revolute",
axis: [0, 0, -1],
pivot: [0, 0, handZ + 0.2],
min: 0, max: 360, default: 150,
unit: "°",
},
{
name: "Seconds",
child: "Second Hand",
type: "revolute",
axis: [0, 0, -1],
pivot: [0, 0, handZ + 0.4],
min: 0, max: 360, default: 220,
unit: "°",
},
],
animations: [
{
name: "Tick",
duration: 60,
loop: true,
continuous: true,
keyframes: [
{ at: 0, values: { "Hours": 0, "Minutes": 0, "Seconds": 0 } },
{ at: 1, values: { "Hours": 30, "Minutes": 360, "Seconds": 21600 } },
],
},
],
});
// ========================================
// CROWN
// ========================================
const crownBody = cylinder(4, 2.5, 2.5, 16, true)
.rotate(0, 90, 0)
.translate(caseR + 2.5, 0, 0)
.color(GOLD)
.material({ metalness: 0.85, roughness: 0.2 });
const crownRidges = [];
for (let i = -1; i <= 1; i++) {
const ridge = difference(
cylinder(0.5, 2.8, 2.8, 16, true),
cylinder(0.6, 2.3, 2.3, 16, true)
).rotate(0, 90, 0)
.translate(caseR + 2.5 + i * 1.2, 0, 0);
crownRidges.push({
name: `Crown Ridge ${i + 2}`,
shape: ridge,
color: GOLD_DARK,
});
}
const crownGuard = box(3, 4, 4, true)
.translate(caseR + 0.5, 0, 0)
.color(GOLD)
.material({ metalness: 0.85, roughness: 0.15 });
// ========================================
// LUGS
// ========================================
const lugs = [];
const lugOffsetX = strapW / 2 - 2;
const lugOffsetY = caseR - 2;
for (let side = 0; side < 2; side++) {
for (let lr = -1; lr <= 1; lr += 2) {
const ySign = side === 0 ? 1 : -1;
const lug = box(5, 8, caseThick * 0.7, true)
.translate(lr * lugOffsetX, ySign * (lugOffsetY + 3), 0);
lugs.push({
name: `Lug ${side === 0 ? 'Top' : 'Bot'} ${lr < 0 ? 'L' : 'R'}`,
shape: lug,
color: GOLD,
});
}
const bar = cylinder(7, 0.6, 0.6, 12, true)
.rotate(0, 90, 0)
.translate(0, (side === 0 ? 1 : -1) * (lugOffsetY + 5), 0);
lugs.push({
name: `Bar ${side === 0 ? 'Top' : 'Bot'}`,
shape: bar,
color: GOLD_DARK,
});
}
// ========================================
// TOP STRAP — buckle, no holes
// ========================================
const strapTop = box(strapW, strapLen, 2.5, true)
.translate(0, caseR + strapLen / 2 + 3, -1)
.color(LEATHER)
.material({ roughness: 0.85, metalness: 0 });
const buckleY = caseR + strapLen + 5;
const buckleFrame = difference(
box(strapW + 4, 8, 2, true),
box(strapW - 2, 5, 3, true)
).translate(0, buckleY, -0.5)
.color(GOLD)
.material({ metalness: 0.85, roughness: 0.15 });
const bucklePin = cylinder(strapW + 2, 0.5, 0.5, 12, true)
.rotate(0, 90, 0)
.translate(0, buckleY - 2, 0.5)
.color(GOLD_DARK)
.material({ metalness: 0.9, roughness: 0.1 });
const keeperTop = difference(
box(strapW + 2, 4, 4, true),
box(strapW, 3, 3, true)
).translate(0, caseR + strapLen * 0.4, -1)
.color(LEATHER_DARK)
.material({ roughness: 0.8, metalness: 0 });
// ========================================
// BOTTOM STRAP — holes + curved pointed tip
// ========================================
const halfW = strapW / 2;
const strapProfile = path()
.moveTo(-halfW, 0)
.lineTo(-halfW, -strapLen)
.arcTo(-halfW + 3, -strapLen - 6, 8, false)
.arcTo(0, -strapLen - 12, 12, false)
.arcTo(halfW - 3, -strapLen - 6, 12, false)
.arcTo(halfW, -strapLen, 8, false)
.lineTo(halfW, 0)
.close();
const strapBottomSolid = strapProfile.extrude(2.5)
.translate(0, -(caseR + 3), -2.25);
const strapHoles = [];
for (let i = 0; i < 5; i++) {
const hole = cylinder(3, 1.2, 1.2, 12, true)
.translate(0, -(caseR + 20 + i * 10), -1);
strapHoles.push(hole);
}
const strapBotWithHoles = difference(strapBottomSolid, ...strapHoles);
// ========================================
// BRANDING
// ========================================
const brandText = text2d('TISSOT', { size: 3, align: 'center', baseline: 'center' })
.extrude(0.3)
.translate(0, 6, dialZ + 0.3)
.color(GOLD_DARK);
const swissMade = text2d('SWISS MADE', { size: 1.5, align: 'center', baseline: 'center' })
.extrude(0.2)
.translate(0, -10, dialZ + 0.3)
.color('#8890A0');
const yearText = text2d('1853', { size: 1.8, align: 'center', baseline: 'center' })
.extrude(0.2)
.translate(0, 3, dialZ + 0.3)
.color('#8890A0');
// ========================================
// DATE WINDOW — real calendar day
// ========================================
const today = new Date();
const dayNum = today.getDate().toString();
const dateWindow = box(4.5, 3.5, 1, true)
.translate(11, 0, dialZ + 0.3)
.color('#FFFFFF');
const dateFrame = difference(
box(5.5, 4.5, 0.6, true),
box(4.5, 3.5, 1, true)
).translate(11, 0, dialZ + 0.6)
.color(GOLD)
.material({ metalness: 0.85, roughness: 0.15 });
const dateText = text2d(dayNum, { size: 2.2, align: 'center', baseline: 'center' })
.extrude(0.2)
.translate(11, 0, dialZ + 0.5)
.color('#1A1A1A');
// ========================================
// GLASS CRYSTAL — transparent lid
// ========================================
const crystal = cylinder(0.8, bezelR - 0.3, bezelR - 0.3, 64, true)
.translate(0, 0, caseThick / 2 - 0.2)
.color('#E8EDF2')
.material({ opacity: 0.052, metalness: 0.1, roughness: 0.05 });
// ========================================
// SCENE
// ========================================
scene({
background: { top: '#1a1a2e', bottom: '#0a0a14' },
// camera: {
// position: [55, -75, 90],
// target: [0, 5, 0],
// fov: 42,
// },
environment: {
preset: 'studio',
intensity: 0.6,
},
lights: [
// Soft ambient fill so nothing goes full black
{ type: 'ambient', color: '#c8cdd4', intensity: 0.15 },
// Key light — warm, upper-right, casts shadow
{ type: 'directional', position: [80, -60, 120], target: [0, 0, 0], color: '#fff4e0', intensity: 1.8, castShadow: true },
// Rim/back light — cool, for edge separation
{ type: 'directional', position: [-60, 40, 80], target: [0, 0, 0], color: '#b0c4de', intensity: 0.7 },
// Low fill — warm bounce from below to light the strap
{ type: 'point', position: [0, -30, -30], color: '#ffe8cc', intensity: 0.6, distance: 200, decay: 1.5 },
// Accent highlight on the crown side
{ type: 'point', position: [50, 0, 20], color: '#ffd700', intensity: 0.4, distance: 120, decay: 2 },
],
postProcessing: {
bloom: { intensity: 0.3, threshold: 0.85, radius: 0.3 },
vignette: { darkness: 0.5, offset: 0.4 },
toneMappingExposure: 1.3,
},
});
// ========================================
// RETURN ALL PARTS
// ========================================
return [
{ name: "Case Body", shape: caseBody },
{ name: "Bezel Ring", shape: bezelRing },
{ name: "Case Back", shape: caseBack },
{ name: "Dial", shape: dial },
{ name: "Minute Track", shape: minuteTrack },
...markers,
{ name: "Hour Hand", shape: hourHand, color: HAND_GOLD },
{ name: "Minute Hand", shape: minuteHand, color: HAND_GOLD },
{ name: "Second Hand", shape: secondHand, color: GOLD_DARK },
{ name: "Center Pin", shape: centerPin },
{ name: "Crown", shape: crownBody },
...crownRidges,
{ name: "Crown Guard", shape: crownGuard },
...lugs,
{ name: "Strap Top", shape: strapTop },
{ name: "Buckle Frame", shape: buckleFrame },
{ name: "Buckle Pin", shape: bucklePin },
{ name: "Keeper Top", shape: keeperTop },
{ name: "Strap Bottom", shape: strapBotWithHoles, color: LEATHER },
{ name: "Tissot Text", shape: brandText },
{ name: "Swiss Made", shape: swissMade },
{ name: "Year 1853", shape: yearText },
{ name: "Date Window", shape: dateWindow },
{ name: "Date Frame", shape: dateFrame },
{ name: "Date Number", shape: dateText },
{ name: "Crystal", shape: crystal },
];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment