vibe-openscad

tier-2-offset-handle-mug / gpt-5.4-nano-2026-03-17

7 run / 累計 127.0s / 累計 $0.0110

openai gpt 5.4 nano 2026-03-17 bare

bare (provider default effort)

run detail →
render
bare render of tier-2-offset-handle-mug
loading STL…
success 12.7s · 273/1723t · $0.0011
SCAD (171 lines)
// Mug with handle (OpenSCAD)

$fn = 128;

// Parameters
outer_d = 80;      // outer diameter
inner_d = 70;      // inner diameter
t_wall  = 5;       // wall thickness (given)
h_mug   = 90;      // mug height
t_bottom= 6;       // bottom thickness

handle_w = 25;     // inner space width (x-direction)
handle_h = 30;     // inner space height (z-direction)
handle_center_z = h_mug/2; // center near mid height

handle_thickness = 5; // approximate handle wall thickness (for solidness)
handle_outer_r = handle_h/2; // radius used for outer half-cylinder

module mug() {
  difference() {
    // Outer cylinder with bottom thickness included
    cylinder(d=outer_d, h=h_mug, center=false);

    // Inner cavity: leave bottom thickness
    translate([0,0,t_bottom])
      cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
  }
}

module handle() {
  // D-shaped handle:
  // - Outside: half-cylinder along YZ plane (flat side toward mug center)
  // - Inside: remove a slightly smaller half-cylinder to make an opening with
  //            inner cross-section approximating 30mm(height) x 25mm(width)
  //
  // We'll place the handle on +X side only.
  // Coordinate note:
  // - Mug axis: Z
  // - We attach handle so it extends in +X direction.
  // - Half-cylinder is generated along Y (so width is along Y) and radius is Z.

  // Choose radius so that inner height matches ~handle_h
  r_outer = handle_outer_r; // ~ handle_h/2
  r_inner = r_outer - handle_thickness;

  // Depth (along Y) is handle_w, using half-cylinder which naturally spans 2*r.
  // We'll limit width by intersecting with a Y slab of size handle_w.
  // Also, orient the half-cylinder so its flat side is toward -X (mug center),
  // and its curved side is toward +X.

  // Position: touch at mug outer surface on +X
  mug_r = outer_d/2;

  // Half-cylinder centered in Y at 0.
  // Flat side aligned so that curved outer surface is at +X near mug surface.
  // We'll create half-cylinder with centerline at X = mug_r.
  x_attach = mug_r; // flat face starts at X=mug_r, curved extends to +X.

  translate([x_attach, 0, handle_center_z])
    rotate([0,90,0])  // make cylinder axis along X initially, then adjust to half-cylinder along X
      // We'll build using a half-cylinder via intersection with a half-space.
      difference() {
        // Outer half-cylinder (curved outer, flat inner side)
        intersection() {
          // Full cylinder (axis along X after rotate)
          rotate([0,0,0])
            translate([0,0,0])
              cylinder(r=r_outer, h=outer_d*0.4, center=false);

          // Keep only the +X half of the cylinder relative to its center
          // For a cylinder (axis along X), half selection is in YZ, not X.
          // Instead, we create "D" in cross-section by selecting Y range:
          // - The flat side is created by clipping on Y: keep Y >= 0 for half.
          translate([0, -r_outer, -r_outer])
            cube([outer_d*0.4, 2*r_outer, 2*r_outer], center=false);
        }

        // Inner cavity in the handle: subtract smaller half-cylinder
        intersection() {
          cylinder(r=r_inner, h=outer_d*0.4, center=false);

          translate([0, -r_inner, -r_inner])
            cube([outer_d*0.4, 2*r_inner, 2*r_inner], center=false);
        }

        // Limit the overall handle width (inner space target ~handle_w) by slicing in Y.
        // (Applied after subtraction; keeps walls from extending too far.)
        translate([0, -(handle_w/2 + handle_thickness), -(r_outer)])
          cube([outer_d*0.4, handle_w + 2*handle_thickness, 2*r_outer], center=false);
      }
}

module handle_v2() {
  // More controlled D-shape:
  // Create a "D" cross-section in YZ, extruded along X into the +X side,
  // then subtract an inner opening with the requested width/height.
  //
  // D-shape in 2D: outer half-circle + flat line.
  // We use rotate_extrude-like by using linear_extrude of 2D polygons.

  mug_r = outer_d/2;

  // X extrusion length: make sure it grips the mug wall firmly
  x_len = 20;

  // 2D coordinates:
  // - Flat side is at X=0 in 2D space; curved side extends to +Y.
  // But for our 2D, we will define in (y,z) plane.
  // Let half-circle be centered at y = 0, z = 0? Better:
  // Define 2D in (Y,Z):
  // outer half-circle radius r_outer centered at (Y=0, Z=0),
  // keep only Y>=0 to form the D with flat at Y=0.
  r_outer = handle_h/2;
  r_inner = r_outer - handle_thickness;

  // Width control: inner opening width is handle_w (in Y).
  // We'll subtract an inner half-oval limited to handle_w in Y by clipping.
  // Then outer will be clipped similarly to avoid overly long width.

  linear_extrude(height=x_len)
    translate([mug_r, 0, handle_center_z])  // move to mug side and height
      rotate([0,0,0])  // keep
      // Build outer D in YZ (extruded along X)
      difference() {
        // Outer D polygon (in YZ plane)
        // Outer half-circle from z=-r_outer..+r_outer with y>=0
        polygon(concat(
          // Arc points (counter-clockwise), y from 0..r
          [for (i=[0:60]) 
            let(t = -90 + i*(180/60))
            [ r_outer*cos(t), r_outer*sin(t) ]],
          // Close with flat edge along y=0 from top to bottom
          [[0, r_outer],[0,-r_outer]]
        ));

        // Inner opening: subtract smaller D, clipped by requested inner width in Y
        translate([0,0])
          intersection() {
            // Inner D
            polygon(concat(
              [for (i=[0:60])
                let(t = -90 + i*(180/60))
                [ r_inner*cos(t), r_inner*sin(t) ]],
              [[0, r_inner],[0,-r_inner]]
            ));
            // Clip to requested inner width around y= (0..handle_w)
            // Since D uses y>=0, we only keep 0..handle_w.
            translate([0,0])
              square([handle_w, 2*r_outer + 1], center=false);
          }
      }
}

module mug_with_handle() {
  union() {
    mug();

    // Attach handle only on +X side:
    // The handle geometry is placed at X = mug outer radius.
    // Ensure it overlaps slightly into the mug wall region to avoid gaps.
    // We'll union by overlapping in X by a small amount.
    translate([0,0,0])
      union() {
        // Choose handle_v2 for controlled D shape.
        // Slight negative X offset to guarantee connection (overlap) with mug wall.
        translate([-1, 0, 0]) handle_v2();
      }
  }
}

mug_with_handle();
パラメータ (8)
スライダを動かすと openscad-wasm で再レンダリング

prompt

OpenSCAD で、向きが指定されたマグカップを作ってください。

本体:
- 外径 80mm、内径 70mm(肉厚 5mm)、高さ 90mm の円筒形マグ
- 底面の厚みは 6mm
- 内側は飲み物を入れられるよう中空

取手(handle):
- マグの **+X 軸方向の側面のみ** に付ける(他の方向には付けない)
- 取手は D 字形状(外側が半円、本体側が直線)で、本体外周にしっかり接続する
- 取手の内側の空間は高さ 30mm × 幅 25mm(指 3 本ぐらい入る)
- 取手はマグの高さ方向の中央付近に付ける
- 本体と取手は確実に union され、マニフォールド(隙間や宙吊りなし)を保つ

完成したコード全体を ```openscad ... ``` のフェンスで囲んで出力してください。コードのみで、追加の説明は不要です。

variant: effort

low success
low
13.6s · 273/1461t · $0.0009
SCAD (134 lines)
// Mug + handle (OpenSCAD)
// Parameters
$fn = 128;

cup_outer_d = 80;
cup_inner_d = 70;
cup_outer_r = cup_outer_d/2;
cup_inner_r = cup_inner_d/2;

cup_height = 90;
bottom_thickness = 6;
wall_thickness = (cup_outer_d - cup_inner_d)/2; // 5mm

handle_width = 25;   // width (Y direction)
handle_inner_h = 30; // height along Z
handle_outer_d = 20;  // D-shape outer half-circle diameter
handle_thickness = wall_thickness; // connector thickness into the cup

handle_center_z = cup_height/2; // middle of mug height

// --- Main mug body (hollow cylinder with bottom) ---
module mug_body() {
    difference() {
        // Outer cylinder
        cylinder(h=cup_height, r=cup_outer_r);

        // Inner cavity: starts at bottom thickness, leaving bottom solid
        translate([0,0,bottom_thickness])
            cylinder(h=cup_height-bottom_thickness, r=cup_inner_r);
    }
}

// --- D-shaped handle aligned at +X side ---
// D-shape: half-circle outer, flat side toward cup, connected to cup wall.
module handle_plus_x() {
    // Place handle at +X side of the mug
    // The handle's flat face should attach to the mug outer surface.
    // For a half-cylinder with radius R, center line is at +X = cup_outer_r + R
    // (flat face at x = cup_outer_r).
    R = handle_outer_d/2;

    // Ensure connector has enough thickness into/out of the cup without gaps.
    // We'll build handle as a solid half-cylinder extruded in Y (width),
    // then subtract the rectangular inner void.
    //
    // Local coordinates for handle solid:
    // - X axis extends outward from the mug
    // - Y axis is width
    // - Z axis is height
    //
    // We'll create a half-cylinder "D" using intersection of:
    // - a full cylinder (radius R) with diameter along X,
    // - a half-space (Y? no, we need flat side in X, so use X>=0 half-space via translate and cube)
    //
    // Better: Create half-cylinder by intersecting with a cube plane that keeps the "outer" half.
    // The half we want has flat face toward the cup (negative X side of handle local).
    // Build in local frame where flat face is at x=0 and outer is x in [0..R*?].
    // We'll then translate to attach at cup_outer_r.

    // Handle local origin: at flat face centerline (x=0 plane), centered in Y, at z=0
    // We'll place the handle by translating so that local z=0 aligns to desired center height.

    // Solid outer half-D + width extrusion
    module outer_half_d() {
        // Base block to define the solid volume shape
        // We'll generate a half-cylinder with its flat face at x=0:
        // Use intersection between a cylinder centered at (x=R, y=0) and a half-space x>=0.
        // Cylinder axis is along Z; but we want D shape in X-Y plane with width in Y.
        // Use cylinder axis along Z and treat X-Y as plane: cylinder has radius R in X-Y.
        // Then "half" is selected by keeping points with X>=0 in local space.
        intersection() {
            // Full cylinder in X-Y with radius R, spanning Z thickness=handle_inner_h + some allowance
            // We'll span enough and later subtract inner cavity.
            cylinder(h=handle_inner_h + 2*handle_thickness, r=R, center=false);

            // Keep only points with X>=0 by using an infinite half-space approximation with cube:
            // Since cylinder already occupies x in [center_x-R .. center_x+R] relative to its own center,
            // we shift cylinder center so that its leftmost point touches x=0.
        }
    }

    // The above intersection needs correct placement: We'll do it explicitly without that inner module.

    // Build in local space:
    // - Cylinder axis along Z, centered in Y at 0, centered in X at R so that its flat face at x=0
    // - Keep only x >= 0 by intersecting with a large cube that includes only x>=0.
    // Then limit Z to full handle thickness region, and also limit Y width with a bounding cube.
    union() {
        // Outer half-D shell
        difference() {
            // Outer shape (half-cylinder with width limited)
            intersection() {
                // Half of cylinder in X-Y plane:
                // Cylinder centered at (R,0) in X-Y, axis along Z
                translate([R, 0, 0])
                    cylinder(h=handle_inner_h + 2*handle_thickness, r=R, center=false);

                // Keep only x >= 0 (local)
                translate([0, -handle_width/2, 0])
                    cube([R + handle_thickness + 1, handle_width, handle_inner_h + 2*handle_thickness], center=false);
            }

            // Cut inner cavity (rectangular) to create D-shape opening
            // Local cavity: x from (handle_thickness) to (R + ...) to remove
            // y within handle_width, z within handle_inner_h
            translate([handle_thickness, -handle_width/2, handle_thickness])
                cube([R + handle_thickness + 2, handle_width, handle_inner_h], center=false);
        }

        // Connector to ensure solid union with the mug body:
        // Add a block that overlaps the mug exterior so there is no "宙吊り"/gap.
        // This block spans a bit into the cup wall and outwards.
        connector_len_x = 3.0; // overlap depth into mug
        translate([cup_outer_r - handle_thickness/2, -handle_width/2, handle_center_z - handle_inner_h/2])
            cube([handle_thickness + connector_len_x, handle_width, handle_inner_h], center=false);
    }

    // Note: union() includes both outer and connector; outer already has half-D shape.
}

// --- Final assembly ---
difference() {
    union() {
        mug_body();

        // Place handle at +X side.
        // Handle flat face at cup_outer_r in world coordinates.
        // Local handle flat plane is at x=0; outer half extends positive X.
        // So world x = cup_outer_r + 0 for flat face.
        translate([cup_outer_r, 0, 0])
            translate([0, 0, handle_center_z - handle_inner_h/2])
                handle_plus_x();
    }
}
medium (default) success
medium
12.7s · 273/1723t · $0.0011
SCAD (171 lines)
// Mug with handle (OpenSCAD)

$fn = 128;

// Parameters
outer_d = 80;      // outer diameter
inner_d = 70;      // inner diameter
t_wall  = 5;       // wall thickness (given)
h_mug   = 90;      // mug height
t_bottom= 6;       // bottom thickness

handle_w = 25;     // inner space width (x-direction)
handle_h = 30;     // inner space height (z-direction)
handle_center_z = h_mug/2; // center near mid height

handle_thickness = 5; // approximate handle wall thickness (for solidness)
handle_outer_r = handle_h/2; // radius used for outer half-cylinder

module mug() {
  difference() {
    // Outer cylinder with bottom thickness included
    cylinder(d=outer_d, h=h_mug, center=false);

    // Inner cavity: leave bottom thickness
    translate([0,0,t_bottom])
      cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
  }
}

module handle() {
  // D-shaped handle:
  // - Outside: half-cylinder along YZ plane (flat side toward mug center)
  // - Inside: remove a slightly smaller half-cylinder to make an opening with
  //            inner cross-section approximating 30mm(height) x 25mm(width)
  //
  // We'll place the handle on +X side only.
  // Coordinate note:
  // - Mug axis: Z
  // - We attach handle so it extends in +X direction.
  // - Half-cylinder is generated along Y (so width is along Y) and radius is Z.

  // Choose radius so that inner height matches ~handle_h
  r_outer = handle_outer_r; // ~ handle_h/2
  r_inner = r_outer - handle_thickness;

  // Depth (along Y) is handle_w, using half-cylinder which naturally spans 2*r.
  // We'll limit width by intersecting with a Y slab of size handle_w.
  // Also, orient the half-cylinder so its flat side is toward -X (mug center),
  // and its curved side is toward +X.

  // Position: touch at mug outer surface on +X
  mug_r = outer_d/2;

  // Half-cylinder centered in Y at 0.
  // Flat side aligned so that curved outer surface is at +X near mug surface.
  // We'll create half-cylinder with centerline at X = mug_r.
  x_attach = mug_r; // flat face starts at X=mug_r, curved extends to +X.

  translate([x_attach, 0, handle_center_z])
    rotate([0,90,0])  // make cylinder axis along X initially, then adjust to half-cylinder along X
      // We'll build using a half-cylinder via intersection with a half-space.
      difference() {
        // Outer half-cylinder (curved outer, flat inner side)
        intersection() {
          // Full cylinder (axis along X after rotate)
          rotate([0,0,0])
            translate([0,0,0])
              cylinder(r=r_outer, h=outer_d*0.4, center=false);

          // Keep only the +X half of the cylinder relative to its center
          // For a cylinder (axis along X), half selection is in YZ, not X.
          // Instead, we create "D" in cross-section by selecting Y range:
          // - The flat side is created by clipping on Y: keep Y >= 0 for half.
          translate([0, -r_outer, -r_outer])
            cube([outer_d*0.4, 2*r_outer, 2*r_outer], center=false);
        }

        // Inner cavity in the handle: subtract smaller half-cylinder
        intersection() {
          cylinder(r=r_inner, h=outer_d*0.4, center=false);

          translate([0, -r_inner, -r_inner])
            cube([outer_d*0.4, 2*r_inner, 2*r_inner], center=false);
        }

        // Limit the overall handle width (inner space target ~handle_w) by slicing in Y.
        // (Applied after subtraction; keeps walls from extending too far.)
        translate([0, -(handle_w/2 + handle_thickness), -(r_outer)])
          cube([outer_d*0.4, handle_w + 2*handle_thickness, 2*r_outer], center=false);
      }
}

module handle_v2() {
  // More controlled D-shape:
  // Create a "D" cross-section in YZ, extruded along X into the +X side,
  // then subtract an inner opening with the requested width/height.
  //
  // D-shape in 2D: outer half-circle + flat line.
  // We use rotate_extrude-like by using linear_extrude of 2D polygons.

  mug_r = outer_d/2;

  // X extrusion length: make sure it grips the mug wall firmly
  x_len = 20;

  // 2D coordinates:
  // - Flat side is at X=0 in 2D space; curved side extends to +Y.
  // But for our 2D, we will define in (y,z) plane.
  // Let half-circle be centered at y = 0, z = 0? Better:
  // Define 2D in (Y,Z):
  // outer half-circle radius r_outer centered at (Y=0, Z=0),
  // keep only Y>=0 to form the D with flat at Y=0.
  r_outer = handle_h/2;
  r_inner = r_outer - handle_thickness;

  // Width control: inner opening width is handle_w (in Y).
  // We'll subtract an inner half-oval limited to handle_w in Y by clipping.
  // Then outer will be clipped similarly to avoid overly long width.

  linear_extrude(height=x_len)
    translate([mug_r, 0, handle_center_z])  // move to mug side and height
      rotate([0,0,0])  // keep
      // Build outer D in YZ (extruded along X)
      difference() {
        // Outer D polygon (in YZ plane)
        // Outer half-circle from z=-r_outer..+r_outer with y>=0
        polygon(concat(
          // Arc points (counter-clockwise), y from 0..r
          [for (i=[0:60]) 
            let(t = -90 + i*(180/60))
            [ r_outer*cos(t), r_outer*sin(t) ]],
          // Close with flat edge along y=0 from top to bottom
          [[0, r_outer],[0,-r_outer]]
        ));

        // Inner opening: subtract smaller D, clipped by requested inner width in Y
        translate([0,0])
          intersection() {
            // Inner D
            polygon(concat(
              [for (i=[0:60])
                let(t = -90 + i*(180/60))
                [ r_inner*cos(t), r_inner*sin(t) ]],
              [[0, r_inner],[0,-r_inner]]
            ));
            // Clip to requested inner width around y= (0..handle_w)
            // Since D uses y>=0, we only keep 0..handle_w.
            translate([0,0])
              square([handle_w, 2*r_outer + 1], center=false);
          }
      }
}

module mug_with_handle() {
  union() {
    mug();

    // Attach handle only on +X side:
    // The handle geometry is placed at X = mug outer radius.
    // Ensure it overlaps slightly into the mug wall region to avoid gaps.
    // We'll union by overlapping in X by a small amount.
    translate([0,0,0])
      union() {
        // Choose handle_v2 for controlled D shape.
        // Slight negative X offset to guarantee connection (overlap) with mug wall.
        translate([-1, 0, 0]) handle_v2();
      }
  }
}

mug_with_handle();
high render_error render
render_error
52.0s · 273/7349t · $0.0044
openscad stl render failed (exit 1): ERROR: Parser error: syntax error in file ../../../../../tmp/render-WMbUOf/input.scad, line 47
Can't parse file '/tmp/render-WMbUOf/input.scad'!

SCAD (65 lines)
$fn = 96;

// ---------------- Mug parameters ----------------
mug_outer_d = 80;
mug_inner_d = 70;      // inner diameter
mug_h       = 90;
mug_bottom  = 6;      // bottom thickness

// ---------------- Handle parameters ----------------
handle_opening_h = 30; // inner space height
handle_opening_w = 25; // inner space width (Y direction)

handle_wall   = 6;     // used to size outer D and side walls
attach_overlap = 0.5;  // ensure overlap with mug for robust union
eps = 0.2;

// Handle will be centered around the mug's height center
z_center = mug_h / 2;

// ---------------- Geometry helpers ----------------
module half_cyl_y(r, ylen, x_center, z_center) {
    // Cylinder axis is along Y; D shape is created by keeping only x >= x_center.
    intersection() {
        translate([x_center, 0, z_center])
            rotate([90, 0, 0])
                cylinder(r = r, h = ylen, center = true);

        translate([x_center, -ylen/2, z_center - r])
            cube([r, ylen, 2*r], center = false);
    }
}

module d_handle() {
    outer_r = handle_opening_h/2 + handle_wall; // outer semicircle radius
    inner_r = handle_opening_h/2;               // target semicircle radius for inner opening

    outer_ylen = handle_opening_w + 2*handle_wall;
    inner_ylen = handle_opening_w;

    mug_outer_r = mug_outer_d/2;
    x_flat_out  = mug_outer_r - attach_overlap; // outer D flat face plane
    x_flat_in   = x_flat_out + handle_wall;    // inner cavity flat face plane

    outer = half_cyl_y(outer_r, outer_ylen, x_flat_out, z_center);
    inner = half_cyl_y(inner_r - eps, inner_ylen - eps, x_flat_in, z_center);

    difference() { outer; inner; }
}

module mug_body() {
    difference() {
        cylinder(d = mug_outer_d, h = mug_h);

        // hollow interior
        translate([0, 0, mug_bottom])
            cylinder(d = mug_inner_d, h = mug_h - mug_bottom + 0.1);
    }
}

// ---------------- Assembly ----------------
union() {
    mug_body();
    // attach only on +X side by construction of the handle (x >= flat face plane)
    d_handle();
}
maxnot run
xhighnot run

iteration: iter-png

i1 success
iter 1
14.7s · 2661/1911t · $0.0013
diff vs bare: +153/-139
- // Mug with handle (OpenSCAD)
+ // Mug + +X side D-handle (OpenSCAD)
+ // Dimensions (mm):
+ // Mug: outer Ø80, inner Ø70 (wall 5), height 90, bottom thickness 6
+ // Handle: only on +X side, D-shape (outer half-cylinder with flat inner side),
+ // inner void ~ 30 (height) x 25 (width in Y). Positioned around middle of mug height.
  
  $fn = 128;
  
- // Parameters
- outer_d = 80;      // outer diameter
- inner_d = 70;      // inner diameter
- t_wall  = 5;       // wall thickness (given)
- h_mug   = 90;      // mug height
- t_bottom= 6;       // bottom thickness
+ // ---- Mug parameters ----
+ outer_d = 80;
+ inner_d = 70;
+ t_wall  = (outer_d - inner_d)/2; // 5
+ h_mug   = 90;
+ t_bottom= 6;
  
- handle_w = 25;     // inner space width (x-direction)
- handle_h = 30;     // inner space height (z-direction)
- handle_center_z = h_mug/2; // center near mid height
+ mug_outer_r = outer_d/2;
+ mug_inner_r = inner_d/2;
  
- handle_thickness = 5; // approximate handle wall thickness (for solidness)
- handle_outer_r = handle_h/2; // radius used for outer half-cylinder
+ // ---- Handle parameters ----
+ handle_void_w = 25;   // width in Y (requested)
+ handle_void_h = 30;   // height in Z (requested)
  
- module mug() {
-   difference() {
-     // Outer cylinder with bottom thickness included
-     cylinder(d=outer_d, h=h_mug, center=false);
+ handle_center_z = h_mug/2;
  
-     // Inner cavity: leave bottom thickness
-     translate([0,0,t_bottom])
-       cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
-   }
- }
+ // Handle grip depth along +X (how far it extends from the mug)
+ handle_depth = 20;
  
- module handle() {
-   // D-shaped handle:
-   // - Outside: half-cylinder along YZ plane (flat side toward mug center)
-   // - Inside: remove a slightly smaller half-cylinder to make an opening with
-   //            inner cross-section approximating 30mm(height) x 25mm(width)
-   //
-   // We'll place the handle on +X side only.
-   // Coordinate note:
-   // - Mug axis: Z
-   // - We attach handle so it extends in +X direction.
-   // - Half-cylinder is generated along Y (so width is along Y) and radius is Z.
+ // Handle wall thickness (for D shell)
+ handle_wall = 4;
  
-   // Choose radius so that inner height matches ~handle_h
-   r_outer = handle_outer_r; // ~ handle_h/2
-   r_inner = r_outer - handle_thickness;
+ // How much extra overlap to avoid manifold gaps
+ overlap = 0.6;
  
-   // Depth (along Y) is handle_w, using half-cylinder which naturally spans 2*r.
-   // We'll limit width by intersecting with a Y slab of size handle_w.
-   // Also, orient the half-cylinder so its flat side is toward -X (mug center),
-   // and its curved side is toward +X.
+ // For D-shape outer half-circle radius in YZ:
+ r_outer = handle_void_h/2 + handle_wall;   // gives outer size; inner will be r_inner = r_outer-handle_wall
+ r_inner = r_outer - handle_wall;
  
-   // Position: touch at mug outer surface on +X
-   mug_r = outer_d/2;
+ // Mug contact plane for handle:
+ x_mug_outer = mug_outer_r; // x = +40 touches outer wall
  
-   // Half-cylinder centered in Y at 0.
-   // Flat side aligned so that curved outer surface is at +X near mug surface.
-   // We'll create half-cylinder with centerline at X = mug_r.
-   x_attach = mug_r; // flat face starts at X=mug_r, curved extends to +X.
+ // Small helper: 2D half-circle polygon (Y-Z plane) with flat at Y=0
+ module half_circle_2d(r=10, yflip=false) {
+   // Creates points for a semicircle in +Y direction, from Z=-r..+r, with Y>=0
+   pts = [
+     for (i=[0:64])
+       let(
+         a = -90 + i*(180/64),
+         y = r*cos(a),   // cos maps -90..+90 -> 0..r..0
+         z = r*sin(a)    // sin maps -90..+90 -> -r..+r
+       )
+       [yflip ? -y : y, z]
+   ];
+   // Close with flat edge on Y=0 (connect ends)
+   polygon(concat(pts, [[0,-r],[0,r]]));
+ }
  
-   translate([x_attach, 0, handle_center_z])
-     rotate([0,90,0])  // make cylinder axis along X initially, then adjust to half-cylinder along X
-       // We'll build using a half-cylinder via intersection with a half-space.
-       difference() {
-         // Outer half-cylinder (curved outer, flat inner side)
-         intersection() {
-           // Full cylinder (axis along X after rotate)
-           rotate([0,0,0])
-             translate([0,0,0])
-               cylinder(r=r_outer, h=outer_d*0.4, center=false);
+ // Build D-shaped handle as an extrusion along X (depth).
+ // D is defined in YZ cross-section:
+ // - Outer: semicircle (Y>=0) with flat at Y=0
+ // - Inner void: smaller semicircle (Y>=0) with flat at Y=0
+ // Then we clip in Y (width) to ~handle_void_w and in Z to ~handle_void_h.
+ module handle_d() {
  
-           // Keep only the +X half of the cylinder relative to its center
-           // For a cylinder (axis along X), half selection is in YZ, not X.
-           // Instead, we create "D" in cross-section by selecting Y range:
-           // - The flat side is created by clipping on Y: keep Y >= 0 for half.
-           translate([0, -r_outer, -r_outer])
-             cube([outer_d*0.4, 2*r_outer, 2*r_outer], center=false);
-         }
+   // Cross-section extents:
+   // We'll clip the D in Y to control void width.
+   // Our semicircle is centered at Y=0; flat at Y=0 implies curved bulges toward +Y.
+   // For inner void width ~25, we clip y from 0..(handle_void_w).
+   clip_y_max = handle_void_w; // void width along Y
  
-         // Inner cavity in the handle: subtract smaller half-cylinder
-         intersection() {
-           cylinder(r=r_inner, h=outer_d*0.4, center=false);
+   // For Z we center at handle_center_z with requested height
+   z0 = handle_center_z - handle_void_h/2;
+   z1 = handle_center_z + handle_void_h/2;
  
-           translate([0, -r_inner, -r_inner])
-             cube([outer_d*0.4, 2*r_inner, 2*r_inner], center=false);
+   // Outer half-circle in YZ plane, extruded along +X.
+   // We position the flat face at Y=0 and place the semicircle bulge toward +Y.
+   difference() {
+     // Outer handle (D shell)
+     translate([x_mug_outer - overlap, 0, 0])
+       linear_extrude(height=handle_depth, center=false)
+         translate([0,0,0]) // already in YZ in 2D
+           intersection() {
+             half_circle_2d(r=r_outer, yflip=false);
+             // Clip to achieve target void width/shape (keep only y in [0..clip_y_max])
+             // and allow enough thickness above/below due to wall thickness.
+             // We'll clip in the 2D stage by intersecting with a rectangle.
+             translate([0,0])
+               square([clip_y_max, 2*r_outer + 1], center=false);
+           }
+ 
+     // Inner void: subtract a smaller D (so wall remains)
+     translate([x_mug_outer - overlap, 0, 0])
+       // Move inner void so its Z span is centered correctly
+       translate([0,0,0])
+       linear_extrude(height=handle_depth + 2*overlap, center=false)
+         intersection() {
+           // Smaller half-circle
+           half_circle_2d(r=r_inner, yflip=false);
+           // Clip in Y to target width:
+           square([clip_y_max, 2*r_inner + 1], center=false);
          }
  
-         // Limit the overall handle width (inner space target ~handle_w) by slicing in Y.
-         // (Applied after subtraction; keeps walls from extending too far.)
-         translate([0, -(handle_w/2 + handle_thickness), -(r_outer)])
-           cube([outer_d*0.4, handle_w + 2*handle_thickness, 2*r_outer], center=false);
-       }
+     // Finally clip inner void in Z to exact requested height by subtracting a 3D box
+     // (easier than precise 2D Z offsets).
+     translate([x_mug_outer - overlap - 0.5, 0, z0])
+       cube([handle_depth + 2*overlap + 1, clip_y_max, handle_void_h], center=false);
+   }
  }
  
- module handle_v2() {
-   // More controlled D-shape:
-   // Create a "D" cross-section in YZ, extruded along X into the +X side,
-   // then subtract an inner opening with the requested width/height.
-   //
-   // D-shape in 2D: outer half-circle + flat line.
-   // We use rotate_extrude-like by using linear_extrude of 2D polygons.
+ // Place handle so its flat face is toward mug center (i.e., at Y=0 plane).
+ // Requirement: attach only to +X side, and connect to mug outer wall.
+ // We'll also ensure it doesn't appear on -X.
+ module mug_with_handle() {
+   union() {
+     // Mug body: outer cylinder minus inner cavity, leaving bottom thickness.
+     difference() {
+       cylinder(d=outer_d, h=h_mug, center=false);
+       translate([0,0,t_bottom])
+         cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
+     }
  
-   mug_r = outer_d/2;
+     // Handle: build D handle shell and clip in X to ensure only +X attachment.
+     // Position so D is centered at mug's middle height.
+     translate([0,0,0]) {
+       // The handle_d() is centered around handle_center_z by subtractive Z box,
+       // but its outer half-cylinder extrusion exists from Z=-r_outer..+r_outer around origin.
+       // So we shift Z of the whole handle to mug center.
+       translate([0,0,handle_center_z]) {
+         // Our handle_d() expects Z clipping box at absolute z0..z1 in mug coords,
+         // so we implement by shifting handle_z logic inside handle_d via absolute z0/z1
+         // already computed. Thus set origin accordingly: handle_d includes absolute z0/z1
+         // but we are shifting; to avoid double-shift, redefine approach:
+         // We'll implement a dedicated shifted handle below (simpler).
  
-   // X extrusion length: make sure it grips the mug wall firmly
-   x_len = 20;
+       }
+     }
+   }
+ }
  
-   // 2D coordinates:
-   // - Flat side is at X=0 in 2D space; curved side extends to +Y.
-   // But for our 2D, we will define in (y,z) plane.
-   // Let half-circle be centered at y = 0, z = 0? Better:
-   // Define 2D in (Y,Z):
-   // outer half-circle radius r_outer centered at (Y=0, Z=0),
-   // keep only Y>=0 to form the D with flat at Y=0.
-   r_outer = handle_h/2;
-   r_inner = r_outer - handle_thickness;
+ // Dedicated handle with correct Z placement (no double-shift):
+ module handle_d_placed() {
+   clip_y_max = handle_void_w;
+   // Z limits in absolute coordinates of mug
+   z0 = handle_center_z - handle_void_h/2;
  
-   // Width control: inner opening width is handle_w (in Y).
-   // We'll subtract an inner half-oval limited to handle_w in Y by clipping.
-   // Then outer will be clipped similarly to avoid overly long width.
+   // We'll create outer and inner D extrusions centered around Z=0,
+   // then shift to z0 appropriately using linear_extrude base at Z=-r_outer.
+   // Since half_circle_2d spans Z in [-r_outer, +r_outer] (in 2D), shifting by handle_center_z
+   // will put the Z center at handle_center_z.
+   difference() {
+     // Outer
+     translate([x_mug_outer - overlap, 0, handle_center_z])
+       linear_extrude(height=handle_depth, center=false)
+         intersection() {
+           half_circle_2d(r=r_outer, yflip=false);
+           square([clip_y_max, 2*r_outer + 1], center=false);
+         }
  
-   linear_extrude(height=x_len)
-     translate([mug_r, 0, handle_center_z])  // move to mug side and height
-       rotate([0,0,0])  // keep
-       // Build outer D in YZ (extruded along X)
-       difference() {
-         // Outer D polygon (in YZ plane)
-         // Outer half-circle from z=-r_outer..+r_outer with y>=0
-         polygon(concat(
-           // Arc points (counter-clockwise), y from 0..r
-           [for (i=[0:60]) 
-             let(t = -90 + i*(180/60))
-             [ r_outer*cos(t), r_outer*sin(t) ]],
-           // Close with flat edge along y=0 from top to bottom
-           [[0, r_outer],[0,-r_outer]]
-         ));
+     // Inner: smaller D
+     translate([x_mug_outer - overlap, 0, handle_center_z])
+       linear_extrude(height=handle_depth + 2*overlap, center=false)
+         intersection() {
+           half_circle_2d(r=r_inner, yflip=false);
+           square([clip_y_max, 2*r_inner + 1], center=false);
+         }
  
-         // Inner opening: subtract smaller D, clipped by requested inner width in Y
-         translate([0,0])
-           intersection() {
-             // Inner D
-             polygon(concat(
-               [for (i=[0:60])
-                 let(t = -90 + i*(180/60))
-                 [ r_inner*cos(t), r_inner*sin(t) ]],
-               [[0, r_inner],[0,-r_inner]]
-             ));
-             // Clip to requested inner width around y= (0..handle_w)
-             // Since D uses y>=0, we only keep 0..handle_w.
-             translate([0,0])
-               square([handle_w, 2*r_outer + 1], center=false);
-           }
-       }
+     // Ensure inner void height is exactly handle_void_h
+     // (Also prevents the inner D from exceeding requested vertical size.)
+     translate([x_mug_outer - overlap - 0.5, 0, z0])
+       cube([handle_depth + 2*overlap + 1, clip_y_max, handle_void_h], center=false);
+   }
  }
  
- module mug_with_handle() {
+ module mug_with_handle_fixed() {
    union() {
-     mug();
- 
-     // Attach handle only on +X side:
-     // The handle geometry is placed at X = mug outer radius.
-     // Ensure it overlaps slightly into the mug wall region to avoid gaps.
-     // We'll union by overlapping in X by a small amount.
-     translate([0,0,0])
-       union() {
-         // Choose handle_v2 for controlled D shape.
-         // Slight negative X offset to guarantee connection (overlap) with mug wall.
-         translate([-1, 0, 0]) handle_v2();
-       }
+     difference() {
+       cylinder(d=outer_d, h=h_mug, center=false);
+       translate([0,0,t_bottom])
+         cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
+     }
+     // Add handle only on +X side
+     handle_d_placed();
    }
  }
  
- mug_with_handle();
+ mug_with_handle_fixed();
SCAD (185 lines)
// Mug + +X side D-handle (OpenSCAD)
// Dimensions (mm):
// Mug: outer Ø80, inner Ø70 (wall 5), height 90, bottom thickness 6
// Handle: only on +X side, D-shape (outer half-cylinder with flat inner side),
// inner void ~ 30 (height) x 25 (width in Y). Positioned around middle of mug height.

$fn = 128;

// ---- Mug parameters ----
outer_d = 80;
inner_d = 70;
t_wall  = (outer_d - inner_d)/2; // 5
h_mug   = 90;
t_bottom= 6;

mug_outer_r = outer_d/2;
mug_inner_r = inner_d/2;

// ---- Handle parameters ----
handle_void_w = 25;   // width in Y (requested)
handle_void_h = 30;   // height in Z (requested)

handle_center_z = h_mug/2;

// Handle grip depth along +X (how far it extends from the mug)
handle_depth = 20;

// Handle wall thickness (for D shell)
handle_wall = 4;

// How much extra overlap to avoid manifold gaps
overlap = 0.6;

// For D-shape outer half-circle radius in YZ:
r_outer = handle_void_h/2 + handle_wall;   // gives outer size; inner will be r_inner = r_outer-handle_wall
r_inner = r_outer - handle_wall;

// Mug contact plane for handle:
x_mug_outer = mug_outer_r; // x = +40 touches outer wall

// Small helper: 2D half-circle polygon (Y-Z plane) with flat at Y=0
module half_circle_2d(r=10, yflip=false) {
  // Creates points for a semicircle in +Y direction, from Z=-r..+r, with Y>=0
  pts = [
    for (i=[0:64])
      let(
        a = -90 + i*(180/64),
        y = r*cos(a),   // cos maps -90..+90 -> 0..r..0
        z = r*sin(a)    // sin maps -90..+90 -> -r..+r
      )
      [yflip ? -y : y, z]
  ];
  // Close with flat edge on Y=0 (connect ends)
  polygon(concat(pts, [[0,-r],[0,r]]));
}

// Build D-shaped handle as an extrusion along X (depth).
// D is defined in YZ cross-section:
// - Outer: semicircle (Y>=0) with flat at Y=0
// - Inner void: smaller semicircle (Y>=0) with flat at Y=0
// Then we clip in Y (width) to ~handle_void_w and in Z to ~handle_void_h.
module handle_d() {

  // Cross-section extents:
  // We'll clip the D in Y to control void width.
  // Our semicircle is centered at Y=0; flat at Y=0 implies curved bulges toward +Y.
  // For inner void width ~25, we clip y from 0..(handle_void_w).
  clip_y_max = handle_void_w; // void width along Y

  // For Z we center at handle_center_z with requested height
  z0 = handle_center_z - handle_void_h/2;
  z1 = handle_center_z + handle_void_h/2;

  // Outer half-circle in YZ plane, extruded along +X.
  // We position the flat face at Y=0 and place the semicircle bulge toward +Y.
  difference() {
    // Outer handle (D shell)
    translate([x_mug_outer - overlap, 0, 0])
      linear_extrude(height=handle_depth, center=false)
        translate([0,0,0]) // already in YZ in 2D
          intersection() {
            half_circle_2d(r=r_outer, yflip=false);
            // Clip to achieve target void width/shape (keep only y in [0..clip_y_max])
            // and allow enough thickness above/below due to wall thickness.
            // We'll clip in the 2D stage by intersecting with a rectangle.
            translate([0,0])
              square([clip_y_max, 2*r_outer + 1], center=false);
          }

    // Inner void: subtract a smaller D (so wall remains)
    translate([x_mug_outer - overlap, 0, 0])
      // Move inner void so its Z span is centered correctly
      translate([0,0,0])
      linear_extrude(height=handle_depth + 2*overlap, center=false)
        intersection() {
          // Smaller half-circle
          half_circle_2d(r=r_inner, yflip=false);
          // Clip in Y to target width:
          square([clip_y_max, 2*r_inner + 1], center=false);
        }

    // Finally clip inner void in Z to exact requested height by subtracting a 3D box
    // (easier than precise 2D Z offsets).
    translate([x_mug_outer - overlap - 0.5, 0, z0])
      cube([handle_depth + 2*overlap + 1, clip_y_max, handle_void_h], center=false);
  }
}

// Place handle so its flat face is toward mug center (i.e., at Y=0 plane).
// Requirement: attach only to +X side, and connect to mug outer wall.
// We'll also ensure it doesn't appear on -X.
module mug_with_handle() {
  union() {
    // Mug body: outer cylinder minus inner cavity, leaving bottom thickness.
    difference() {
      cylinder(d=outer_d, h=h_mug, center=false);
      translate([0,0,t_bottom])
        cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
    }

    // Handle: build D handle shell and clip in X to ensure only +X attachment.
    // Position so D is centered at mug's middle height.
    translate([0,0,0]) {
      // The handle_d() is centered around handle_center_z by subtractive Z box,
      // but its outer half-cylinder extrusion exists from Z=-r_outer..+r_outer around origin.
      // So we shift Z of the whole handle to mug center.
      translate([0,0,handle_center_z]) {
        // Our handle_d() expects Z clipping box at absolute z0..z1 in mug coords,
        // so we implement by shifting handle_z logic inside handle_d via absolute z0/z1
        // already computed. Thus set origin accordingly: handle_d includes absolute z0/z1
        // but we are shifting; to avoid double-shift, redefine approach:
        // We'll implement a dedicated shifted handle below (simpler).

      }
    }
  }
}

// Dedicated handle with correct Z placement (no double-shift):
module handle_d_placed() {
  clip_y_max = handle_void_w;
  // Z limits in absolute coordinates of mug
  z0 = handle_center_z - handle_void_h/2;

  // We'll create outer and inner D extrusions centered around Z=0,
  // then shift to z0 appropriately using linear_extrude base at Z=-r_outer.
  // Since half_circle_2d spans Z in [-r_outer, +r_outer] (in 2D), shifting by handle_center_z
  // will put the Z center at handle_center_z.
  difference() {
    // Outer
    translate([x_mug_outer - overlap, 0, handle_center_z])
      linear_extrude(height=handle_depth, center=false)
        intersection() {
          half_circle_2d(r=r_outer, yflip=false);
          square([clip_y_max, 2*r_outer + 1], center=false);
        }

    // Inner: smaller D
    translate([x_mug_outer - overlap, 0, handle_center_z])
      linear_extrude(height=handle_depth + 2*overlap, center=false)
        intersection() {
          half_circle_2d(r=r_inner, yflip=false);
          square([clip_y_max, 2*r_inner + 1], center=false);
        }

    // Ensure inner void height is exactly handle_void_h
    // (Also prevents the inner D from exceeding requested vertical size.)
    translate([x_mug_outer - overlap - 0.5, 0, z0])
      cube([handle_depth + 2*overlap + 1, clip_y_max, handle_void_h], center=false);
  }
}

module mug_with_handle_fixed() {
  union() {
    difference() {
      cylinder(d=outer_d, h=h_mug, center=false);
      translate([0,0,t_bottom])
        cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
    }
    // Add handle only on +X side
    handle_d_placed();
  }
}

mug_with_handle_fixed();
i2 success
iter 2
10.5s · 2849/1544t · $0.0011
diff vs i1: +108/-146
- // Mug + +X side D-handle (OpenSCAD)
- // Dimensions (mm):
- // Mug: outer Ø80, inner Ø70 (wall 5), height 90, bottom thickness 6
- // Handle: only on +X side, D-shape (outer half-cylinder with flat inner side),
- // inner void ~ 30 (height) x 25 (width in Y). Positioned around middle of mug height.
+ // Mug +X-side D-handle (revised to match prompt)
  
- $fn = 128;
+ // ---- Quality ----
+ $fn = 160;
  
  // ---- Mug parameters ----
- outer_d = 80;
- inner_d = 70;
- t_wall  = (outer_d - inner_d)/2; // 5
- h_mug   = 90;
- t_bottom= 6;
+ outer_d   = 80;           // outer diameter
+ inner_d   = 70;           // inner diameter
+ wall      = (outer_d-inner_d)/2; // =5
+ h_mug     = 90;           // mug height
+ t_bottom  = 6;           // bottom thickness
  
- mug_outer_r = outer_d/2;
- mug_inner_r = inner_d/2;
- 
  // ---- Handle parameters ----
- handle_void_w = 25;   // width in Y (requested)
- handle_void_h = 30;   // height in Z (requested)
- 
+ handle_void_w = 25;      // width of inner void (Y direction)
+ handle_void_h = 30;      // height of inner void (Z direction)
  handle_center_z = h_mug/2;
  
- // Handle grip depth along +X (how far it extends from the mug)
- handle_depth = 20;
+ // Handle should connect to +X side ONLY.
+ // We'll attach it so the handle extends only from x = mug outer radius to x = mug outer radius + depth,
+ // and we also clip it to x>=mug_outer_r so it doesn't appear on -X side.
+ handle_depth = 22;       // extrusion depth along +X
+ handle_wall  = 4;        // thickness of handle shell in the D cross-section
  
- // Handle wall thickness (for D shell)
- handle_wall = 4;
+ // Avoid tiny non-manifold overlaps: ensure a small overlap when unioning/subtracting.
+ eps = 0.6;
  
- // How much extra overlap to avoid manifold gaps
- overlap = 0.6;
+ // ---- Derived mug radii ----
+ mug_outer_r = outer_d/2; // 40
+ mug_inner_r = inner_d/2; // 35
  
- // For D-shape outer half-circle radius in YZ:
- r_outer = handle_void_h/2 + handle_wall;   // gives outer size; inner will be r_inner = r_outer-handle_wall
- r_inner = r_outer - handle_wall;
- 
- // Mug contact plane for handle:
- x_mug_outer = mug_outer_r; // x = +40 touches outer wall
- 
- // Small helper: 2D half-circle polygon (Y-Z plane) with flat at Y=0
- module half_circle_2d(r=10, yflip=false) {
-   // Creates points for a semicircle in +Y direction, from Z=-r..+r, with Y>=0
+ // ---- 2D semicircle with flat at y=0, bulge toward +Y ----
+ module semicircle_with_flat(r=10) {
+   // returns polygon in (Y,Z) plane with flat edge at Y=0
    pts = [
-     for (i=[0:64])
+     for (i=[0:80])
        let(
-         a = -90 + i*(180/64),
-         y = r*cos(a),   // cos maps -90..+90 -> 0..r..0
-         z = r*sin(a)    // sin maps -90..+90 -> -r..+r
+         a = -90 + i*(180/80),
+         y = r*cos(a),     // 0..r..0
+         z = r*sin(a)      // -r..+r
        )
-       [yflip ? -y : y, z]
+       [y, z]
    ];
-   // Close with flat edge on Y=0 (connect ends)
-   polygon(concat(pts, [[0,-r],[0,r]]));
+   // close with flat edge
+   polygon(concat(pts, [[0, -r],[0, r]]));
  }
  
- // Build D-shaped handle as an extrusion along X (depth).
- // D is defined in YZ cross-section:
- // - Outer: semicircle (Y>=0) with flat at Y=0
- // - Inner void: smaller semicircle (Y>=0) with flat at Y=0
- // Then we clip in Y (width) to ~handle_void_w and in Z to ~handle_void_h.
- module handle_d() {
+ // ---- D-handle as hollow D-shell extruded in +X ----
+ // Cross-section in YZ: outer semicircle (bulge +Y) with flat at Y=0,
+ // inner semicircle reduced by handle_wall, also flat at Y=0.
+ // Then we clip to requested inner void rectangle by subtracting a box.
+ module handle_d_shell() {
+   // Outer D "radius" in YZ. Inner void width at flat-to-flat should be handle_void_w.
+   // Because both outer/inner are clipped from y=0..clip_y_max, this controls the void width.
+   // Choose outer radius so that inner radius - clipping gives adequate wall.
+   // We'll directly size by clip bounds and inner radius.
+   //
+   // For better match: let inner semicircle radius be handle_void_h/2,
+   // and then outer semicircle radius = inner + handle_wall.
+   r_inner = handle_void_h/2;
+   r_outer = r_inner + handle_wall;
  
-   // Cross-section extents:
-   // We'll clip the D in Y to control void width.
-   // Our semicircle is centered at Y=0; flat at Y=0 implies curved bulges toward +Y.
-   // For inner void width ~25, we clip y from 0..(handle_void_w).
-   clip_y_max = handle_void_w; // void width along Y
+   // Inner void rectangle is width handle_void_w (Y) and height handle_void_h (Z).
+   // We will subtract this box after forming D-shell, ensuring exact dimensions.
+   clip_y_max = handle_void_w;
  
-   // For Z we center at handle_center_z with requested height
+   // Z placement centered at handle_center_z.
    z0 = handle_center_z - handle_void_h/2;
-   z1 = handle_center_z + handle_void_h/2;
  
-   // Outer half-circle in YZ plane, extruded along +X.
-   // We position the flat face at Y=0 and place the semicircle bulge toward +Y.
+   // Build shell at local origin where:
+   // - flat face is at Y=0
+   // - Z is centered at 0
+   // Later we translate to mug coordinates.
    difference() {
-     // Outer handle (D shell)
-     translate([x_mug_outer - overlap, 0, 0])
+     // Outer D shell
+     translate([mug_outer_r - eps, 0, handle_center_z])
        linear_extrude(height=handle_depth, center=false)
-         translate([0,0,0]) // already in YZ in 2D
-           intersection() {
-             half_circle_2d(r=r_outer, yflip=false);
-             // Clip to achieve target void width/shape (keep only y in [0..clip_y_max])
-             // and allow enough thickness above/below due to wall thickness.
-             // We'll clip in the 2D stage by intersecting with a rectangle.
-             translate([0,0])
-               square([clip_y_max, 2*r_outer + 1], center=false);
-           }
+         intersection() {
+           semicircle_with_flat(r=r_outer);
+           // Clip in Y to control overall "grip" width of the D
+           square([clip_y_max, 2*r_outer + 1], center=false);
+         }
  
-     // Inner void: subtract a smaller D (so wall remains)
-     translate([x_mug_outer - overlap, 0, 0])
-       // Move inner void so its Z span is centered correctly
-       translate([0,0,0])
-       linear_extrude(height=handle_depth + 2*overlap, center=false)
+     // Inner void: subtract smaller D-shape
+     translate([mug_outer_r - eps, 0, handle_center_z])
+       linear_extrude(height=handle_depth + 2*eps, center=false)
          intersection() {
-           // Smaller half-circle
-           half_circle_2d(r=r_inner, yflip=false);
-           // Clip in Y to target width:
+           semicircle_with_flat(r=r_inner);
            square([clip_y_max, 2*r_inner + 1], center=false);
          }
  
-     // Finally clip inner void in Z to exact requested height by subtracting a 3D box
-     // (easier than precise 2D Z offsets).
-     translate([x_mug_outer - overlap - 0.5, 0, z0])
-       cube([handle_depth + 2*overlap + 1, clip_y_max, handle_void_h], center=false);
+     // Ensure void height exactly handle_void_h by cutting with a box
+     translate([mug_outer_r - eps - 0.2, 0, z0])
+       cube([handle_depth + 2*eps + 0.4, clip_y_max, handle_void_h], center=false);
+ 
+     // Extra cut: ensure no negative-Y void extension and also keep manifold sane
+     // (flat at Y=0 means void shouldn't go to negative Y)
    }
  }
  
- // Place handle so its flat face is toward mug center (i.e., at Y=0 plane).
- // Requirement: attach only to +X side, and connect to mug outer wall.
- // We'll also ensure it doesn't appear on -X.
- module mug_with_handle() {
-   union() {
-     // Mug body: outer cylinder minus inner cavity, leaving bottom thickness.
-     difference() {
-       cylinder(d=outer_d, h=h_mug, center=false);
-       translate([0,0,t_bottom])
-         cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
-     }
- 
-     // Handle: build D handle shell and clip in X to ensure only +X attachment.
-     // Position so D is centered at mug's middle height.
-     translate([0,0,0]) {
-       // The handle_d() is centered around handle_center_z by subtractive Z box,
-       // but its outer half-cylinder extrusion exists from Z=-r_outer..+r_outer around origin.
-       // So we shift Z of the whole handle to mug center.
-       translate([0,0,handle_center_z]) {
-         // Our handle_d() expects Z clipping box at absolute z0..z1 in mug coords,
-         // so we implement by shifting handle_z logic inside handle_d via absolute z0/z1
-         // already computed. Thus set origin accordingly: handle_d includes absolute z0/z1
-         // but we are shifting; to avoid double-shift, redefine approach:
-         // We'll implement a dedicated shifted handle below (simpler).
- 
-       }
-     }
+ // Ensure handle ONLY appears on +X side:
+ // 1) Position handle so its "inner base" is on mug's outer radius.
+ // 2) Clip by x >= mug_outer_r so it cannot protrude to -X.
+ module handle_with_x_clip() {
+   difference() {
+     handle_d_shell();
+     // Clip away any part with x < mug_outer_r
+     translate([mug_outer_r/2, 0, handle_center_z])
+       cube([mug_outer_r + eps, 200, 200], center=true);
+     // (This cube centered at mug_outer_r/2 will cover x from 0..mug_outer_r+eps,
+     // but since we need to remove x < mug_outer_r, we instead rely on
+     // subtracting a large block that covers the -X side.)
    }
  }
  
- // Dedicated handle with correct Z placement (no double-shift):
- module handle_d_placed() {
-   clip_y_max = handle_void_w;
-   // Z limits in absolute coordinates of mug
-   z0 = handle_center_z - handle_void_h/2;
- 
-   // We'll create outer and inner D extrusions centered around Z=0,
-   // then shift to z0 appropriately using linear_extrude base at Z=-r_outer.
-   // Since half_circle_2d spans Z in [-r_outer, +r_outer] (in 2D), shifting by handle_center_z
-   // will put the Z center at handle_center_z.
+ // ---- Mug body ----
+ module mug_body() {
    difference() {
-     // Outer
-     translate([x_mug_outer - overlap, 0, handle_center_z])
-       linear_extrude(height=handle_depth, center=false)
-         intersection() {
-           half_circle_2d(r=r_outer, yflip=false);
-           square([clip_y_max, 2*r_outer + 1], center=false);
-         }
- 
-     // Inner: smaller D
-     translate([x_mug_outer - overlap, 0, handle_center_z])
-       linear_extrude(height=handle_depth + 2*overlap, center=false)
-         intersection() {
-           half_circle_2d(r=r_inner, yflip=false);
-           square([clip_y_max, 2*r_inner + 1], center=false);
-         }
- 
-     // Ensure inner void height is exactly handle_void_h
-     // (Also prevents the inner D from exceeding requested vertical size.)
-     translate([x_mug_outer - overlap - 0.5, 0, z0])
-       cube([handle_depth + 2*overlap + 1, clip_y_max, handle_void_h], center=false);
+     cylinder(d=outer_d, h=h_mug, center=false);
+     translate([0,0,t_bottom])
+       cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
    }
  }
  
- module mug_with_handle_fixed() {
-   union() {
-     difference() {
-       cylinder(d=outer_d, h=h_mug, center=false);
-       translate([0,0,t_bottom])
-         cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
+ // ---- Final ----
+ union() {
+   mug_body();
+ 
+   // Place handle at +X side:
+   // In this model, the handle's flat face is at Y=0 (i.e., centered on +Y side).
+   // To center the handle symmetrically around the Y axis, shift it by -handle_void_w/2 in Y
+   // is *not* requested by prompt; prompt says inner space width 25 (likely along Y),
+   // but doesn't require centering around Y=0. However typical handle should be centered.
+   //
+   // We'll center it: void should span Y=[-handle_void_w/2, +handle_void_w/2].
+   // Our D currently spans Y in [0, handle_void_w] due to flat at Y=0.
+   // So shift by -handle_void_w/2 to center it around Y=0.
+   translate([0, -handle_void_w/2, 0]) {
+     // Clip to +X only using an explicit intersection with x>=mug_outer_r
+     intersection() {
+       handle_with_x_clip();
+ 
+       // keep only x>=mug_outer_r
+       translate([mug_outer_r + (handle_depth/2), 0, handle_center_z])
+         cube([handle_depth + mug_outer_r + 50, 200, h_mug + 100], center=true);
      }
-     // Add handle only on +X side
-     handle_d_placed();
    }
- }
- 
- mug_with_handle_fixed();
+ }
SCAD (147 lines)
// Mug +X-side D-handle (revised to match prompt)

// ---- Quality ----
$fn = 160;

// ---- Mug parameters ----
outer_d   = 80;           // outer diameter
inner_d   = 70;           // inner diameter
wall      = (outer_d-inner_d)/2; // =5
h_mug     = 90;           // mug height
t_bottom  = 6;           // bottom thickness

// ---- Handle parameters ----
handle_void_w = 25;      // width of inner void (Y direction)
handle_void_h = 30;      // height of inner void (Z direction)
handle_center_z = h_mug/2;

// Handle should connect to +X side ONLY.
// We'll attach it so the handle extends only from x = mug outer radius to x = mug outer radius + depth,
// and we also clip it to x>=mug_outer_r so it doesn't appear on -X side.
handle_depth = 22;       // extrusion depth along +X
handle_wall  = 4;        // thickness of handle shell in the D cross-section

// Avoid tiny non-manifold overlaps: ensure a small overlap when unioning/subtracting.
eps = 0.6;

// ---- Derived mug radii ----
mug_outer_r = outer_d/2; // 40
mug_inner_r = inner_d/2; // 35

// ---- 2D semicircle with flat at y=0, bulge toward +Y ----
module semicircle_with_flat(r=10) {
  // returns polygon in (Y,Z) plane with flat edge at Y=0
  pts = [
    for (i=[0:80])
      let(
        a = -90 + i*(180/80),
        y = r*cos(a),     // 0..r..0
        z = r*sin(a)      // -r..+r
      )
      [y, z]
  ];
  // close with flat edge
  polygon(concat(pts, [[0, -r],[0, r]]));
}

// ---- D-handle as hollow D-shell extruded in +X ----
// Cross-section in YZ: outer semicircle (bulge +Y) with flat at Y=0,
// inner semicircle reduced by handle_wall, also flat at Y=0.
// Then we clip to requested inner void rectangle by subtracting a box.
module handle_d_shell() {
  // Outer D "radius" in YZ. Inner void width at flat-to-flat should be handle_void_w.
  // Because both outer/inner are clipped from y=0..clip_y_max, this controls the void width.
  // Choose outer radius so that inner radius - clipping gives adequate wall.
  // We'll directly size by clip bounds and inner radius.
  //
  // For better match: let inner semicircle radius be handle_void_h/2,
  // and then outer semicircle radius = inner + handle_wall.
  r_inner = handle_void_h/2;
  r_outer = r_inner + handle_wall;

  // Inner void rectangle is width handle_void_w (Y) and height handle_void_h (Z).
  // We will subtract this box after forming D-shell, ensuring exact dimensions.
  clip_y_max = handle_void_w;

  // Z placement centered at handle_center_z.
  z0 = handle_center_z - handle_void_h/2;

  // Build shell at local origin where:
  // - flat face is at Y=0
  // - Z is centered at 0
  // Later we translate to mug coordinates.
  difference() {
    // Outer D shell
    translate([mug_outer_r - eps, 0, handle_center_z])
      linear_extrude(height=handle_depth, center=false)
        intersection() {
          semicircle_with_flat(r=r_outer);
          // Clip in Y to control overall "grip" width of the D
          square([clip_y_max, 2*r_outer + 1], center=false);
        }

    // Inner void: subtract smaller D-shape
    translate([mug_outer_r - eps, 0, handle_center_z])
      linear_extrude(height=handle_depth + 2*eps, center=false)
        intersection() {
          semicircle_with_flat(r=r_inner);
          square([clip_y_max, 2*r_inner + 1], center=false);
        }

    // Ensure void height exactly handle_void_h by cutting with a box
    translate([mug_outer_r - eps - 0.2, 0, z0])
      cube([handle_depth + 2*eps + 0.4, clip_y_max, handle_void_h], center=false);

    // Extra cut: ensure no negative-Y void extension and also keep manifold sane
    // (flat at Y=0 means void shouldn't go to negative Y)
  }
}

// Ensure handle ONLY appears on +X side:
// 1) Position handle so its "inner base" is on mug's outer radius.
// 2) Clip by x >= mug_outer_r so it cannot protrude to -X.
module handle_with_x_clip() {
  difference() {
    handle_d_shell();
    // Clip away any part with x < mug_outer_r
    translate([mug_outer_r/2, 0, handle_center_z])
      cube([mug_outer_r + eps, 200, 200], center=true);
    // (This cube centered at mug_outer_r/2 will cover x from 0..mug_outer_r+eps,
    // but since we need to remove x < mug_outer_r, we instead rely on
    // subtracting a large block that covers the -X side.)
  }
}

// ---- Mug body ----
module mug_body() {
  difference() {
    cylinder(d=outer_d, h=h_mug, center=false);
    translate([0,0,t_bottom])
      cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
  }
}

// ---- Final ----
union() {
  mug_body();

  // Place handle at +X side:
  // In this model, the handle's flat face is at Y=0 (i.e., centered on +Y side).
  // To center the handle symmetrically around the Y axis, shift it by -handle_void_w/2 in Y
  // is *not* requested by prompt; prompt says inner space width 25 (likely along Y),
  // but doesn't require centering around Y=0. However typical handle should be centered.
  //
  // We'll center it: void should span Y=[-handle_void_w/2, +handle_void_w/2].
  // Our D currently spans Y in [0, handle_void_w] due to flat at Y=0.
  // So shift by -handle_void_w/2 to center it around Y=0.
  translate([0, -handle_void_w/2, 0]) {
    // Clip to +X only using an explicit intersection with x>=mug_outer_r
    intersection() {
      handle_with_x_clip();

      // keep only x>=mug_outer_r
      translate([mug_outer_r + (handle_depth/2), 0, handle_center_z])
        cube([handle_depth + mug_outer_r + 50, 200, h_mug + 100], center=true);
    }
  }
}
i3 success
iter 3
10.9s · 2482/1406t · $0.0010
diff vs i2: +106/-117
- // Mug +X-side D-handle (revised to match prompt)
+ // Mug (OD 80, ID 70, wall 5, height 90, bottom thickness 6)
+ // +X side only D-handle (hollow), manifold-safe union
  
- // ---- Quality ----
- $fn = 160;
+ $fn = 180;
  
- // ---- Mug parameters ----
- outer_d   = 80;           // outer diameter
- inner_d   = 70;           // inner diameter
- wall      = (outer_d-inner_d)/2; // =5
- h_mug     = 90;           // mug height
- t_bottom  = 6;           // bottom thickness
+ // --- Mug params ---
+ outer_d   = 80;
+ inner_d   = 70;
+ h_mug     = 90;
+ t_bottom  = 6;
  
- // ---- Handle parameters ----
- handle_void_w = 25;      // width of inner void (Y direction)
- handle_void_h = 30;      // height of inner void (Z direction)
- handle_center_z = h_mug/2;
+ mug_outer_r = outer_d/2; // 40
+ mug_inner_r = inner_d/2; // 35
  
- // Handle should connect to +X side ONLY.
- // We'll attach it so the handle extends only from x = mug outer radius to x = mug outer radius + depth,
- // and we also clip it to x>=mug_outer_r so it doesn't appear on -X side.
+ // --- Handle params ---
+ handle_inner_w = 25;     // inner space width (Y)
+ handle_inner_h = 30;     // inner space height (Z)
+ handle_center_z = h_mug/2; // around middle
+ 
  handle_depth = 22;       // extrusion depth along +X
- handle_wall  = 4;        // thickness of handle shell in the D cross-section
+ handle_shell = 4;        // handle shell thickness (radial in YZ)
  
- // Avoid tiny non-manifold overlaps: ensure a small overlap when unioning/subtracting.
- eps = 0.6;
+ // D cross-section definition in YZ:
+ // - D is "outer half-circle + flat line" (flat at Y = 0, bulge to +Y)
+ // - Inner void is another D scaled by handle_shell
+ // - Then overall handle is clipped by a rectangle to ensure exact void (Y width, Z height).
+ module d_shell_2d(r_outer, r_inner, y_void_max, z_void_min, z_void_max) {
+   // 2D D surfaces (in YZ plane):
+   // Outer D: semicircle bulging +Y with flat at Y=0
+   // Inner D: semicircle bulging +Y with flat at Y=0
+   module semicircle_with_flat_2d(r) {
+     // returns polygon in (Y,Z), flat edge at Y=0, centered in Z around 0, bulge +Y
+     pts = [
+       for (i=[0:120])
+         let(
+           a = -90 + i*(180/120),
+           y = r*cos(a),  // 0..r
+           z = r*sin(a)   // -r..r
+         )
+         [y,z]
+     ];
+     // close the flat edge
+     polygon(concat(pts, [[0,-r],[0,r]]));
+   }
  
- // ---- Derived mug radii ----
- mug_outer_r = outer_d/2; // 40
- mug_inner_r = inner_d/2; // 35
- 
- // ---- 2D semicircle with flat at y=0, bulge toward +Y ----
- module semicircle_with_flat(r=10) {
-   // returns polygon in (Y,Z) plane with flat edge at Y=0
-   pts = [
-     for (i=[0:80])
-       let(
-         a = -90 + i*(180/80),
-         y = r*cos(a),     // 0..r..0
-         z = r*sin(a)      // -r..+r
-       )
-       [y, z]
-   ];
-   // close with flat edge
-   polygon(concat(pts, [[0, -r],[0, r]]));
+   // Build 2D handle shell as difference of outer minus inner,
+   // then clip in Y and Z to get intended void dimensions.
+   // Note: clipping is performed after 3D extrusion in the caller using a box.
+   difference() {
+     semicircle_with_flat_2d(r_outer);
+     semicircle_with_flat_2d(r_inner);
+   }
  }
  
- // ---- D-handle as hollow D-shell extruded in +X ----
- // Cross-section in YZ: outer semicircle (bulge +Y) with flat at Y=0,
- // inner semicircle reduced by handle_wall, also flat at Y=0.
- // Then we clip to requested inner void rectangle by subtracting a box.
- module handle_d_shell() {
-   // Outer D "radius" in YZ. Inner void width at flat-to-flat should be handle_void_w.
-   // Because both outer/inner are clipped from y=0..clip_y_max, this controls the void width.
-   // Choose outer radius so that inner radius - clipping gives adequate wall.
-   // We'll directly size by clip bounds and inner radius.
-   //
-   // For better match: let inner semicircle radius be handle_void_h/2,
-   // and then outer semicircle radius = inner + handle_wall.
-   r_inner = handle_void_h/2;
-   r_outer = r_inner + handle_wall;
+ // 3D D-handle shell with exact hollow void size.
+ // Local coordinate system:
+ // - Mug axis: X
+ // - Y is tangential around mug
+ // - Z is vertical
+ // Local origin for handle placement: at (x = mug_outer_r) plane will be handled outside.
+ module handle_d() {
+   // We set inner D semicircle radius so that its max width matches handle_inner_w.
+   // For a semicircle with flat at Y=0, the bulge extent in Y equals radius.
+   // So bulge extent should be handle_inner_w. That means r_inner = handle_inner_w.
+   // But we also need a rectangular hole width; we'll clip hole exactly anyway.
+   // We'll use r_inner = handle_inner_w/2 only if void width is measured flat-to-flat.
+   // The prompt states "幅25mm" for the inner space; typical interpretation here:
+   // the maximum Y spread equals 25. We'll satisfy that by using r_inner=25.
+   r_inner = handle_inner_w;
+   r_outer = r_inner + handle_shell;
  
-   // Inner void rectangle is width handle_void_w (Y) and height handle_void_h (Z).
-   // We will subtract this box after forming D-shell, ensuring exact dimensions.
-   clip_y_max = handle_void_w;
+   // Z void extents:
+   z0 = handle_center_z - handle_inner_h/2;
+   z1 = handle_center_z + handle_inner_h/2;
  
-   // Z placement centered at handle_center_z.
-   z0 = handle_center_z - handle_void_h/2;
+   // Local Z centered at 0 for D shapes; we will shift the local geometry by z0 later.
+   // Create shell in local coordinates where Z center=0.
+   // Then cut exact Z range and Y range using boxes.
  
-   // Build shell at local origin where:
-   // - flat face is at Y=0
-   // - Z is centered at 0
-   // Later we translate to mug coordinates.
    difference() {
-     // Outer D shell
-     translate([mug_outer_r - eps, 0, handle_center_z])
+     // Outer shell extruded along +X from x=0..handle_depth
+     translate([0,0,z0])
        linear_extrude(height=handle_depth, center=false)
-         intersection() {
-           semicircle_with_flat(r=r_outer);
-           // Clip in Y to control overall "grip" width of the D
-           square([clip_y_max, 2*r_outer + 1], center=false);
-         }
+         d_shell_2d(r_outer=r_outer, r_inner=r_inner,
+                    y_void_max=handle_inner_w,
+                    z_void_min=z0, z_void_max=z1);
  
-     // Inner void: subtract smaller D-shape
-     translate([mug_outer_r - eps, 0, handle_center_z])
-       linear_extrude(height=handle_depth + 2*eps, center=false)
-         intersection() {
-           semicircle_with_flat(r=r_inner);
-           square([clip_y_max, 2*r_inner + 1], center=false);
-         }
+     // Inner void exact cut: subtract rectangle "void window" from within the D shell
+     // so that interior space is height 30 and width 25.
+     // We'll cut in a way that results in a D-shaped cavity (semi-circle + flat)
+     // but bounded to the requested dimensions.
+     translate([-1, -100, z0 - 0.5])
+       cube([handle_depth + 2, 200, handle_inner_h + 1], center=false);
  
-     // Ensure void height exactly handle_void_h by cutting with a box
-     translate([mug_outer_r - eps - 0.2, 0, z0])
-       cube([handle_depth + 2*eps + 0.4, clip_y_max, handle_void_h], center=false);
- 
-     // Extra cut: ensure no negative-Y void extension and also keep manifold sane
-     // (flat at Y=0 means void shouldn't go to negative Y)
+     // Additionally, ensure the handle cavity doesn't create negative-Y extension beyond flat side.
+     // (Flat side at Y=0 in the D cross-section.)
+     // Cut everything at Y<0 within the handle depth.
+     translate([-1, -100, z0 - 0.5])
+       cube([handle_depth + 2, 100, handle_inner_h + 1], center=false);
    }
  }
  
- // Ensure handle ONLY appears on +X side:
- // 1) Position handle so its "inner base" is on mug's outer radius.
- // 2) Clip by x >= mug_outer_r so it cannot protrude to -X.
- module handle_with_x_clip() {
-   difference() {
-     handle_d_shell();
-     // Clip away any part with x < mug_outer_r
-     translate([mug_outer_r/2, 0, handle_center_z])
-       cube([mug_outer_r + eps, 200, 200], center=true);
-     // (This cube centered at mug_outer_r/2 will cover x from 0..mug_outer_r+eps,
-     // but since we need to remove x < mug_outer_r, we instead rely on
-     // subtracting a large block that covers the -X side.)
-   }
- }
- 
- // ---- Mug body ----
+ // Mug body (solid cup with hollow interior)
  module mug_body() {
    difference() {
      cylinder(d=outer_d, h=h_mug, center=false);
+     // Subtract inner cylinder leaving bottom thickness t_bottom
      translate([0,0,t_bottom])
        cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
    }
  }
  
- // ---- Final ----
- union() {
-   mug_body();
+ module mug_with_handle() {
+   // Place handle on +X side only.
+   // We position handle starting at x = mug_outer_r - small_eps so it unions reliably.
+   // Then we clip to x>=mug_outer_r to avoid showing on -X.
+   small_eps = 0.6;
  
-   // Place handle at +X side:
-   // In this model, the handle's flat face is at Y=0 (i.e., centered on +Y side).
-   // To center the handle symmetrically around the Y axis, shift it by -handle_void_w/2 in Y
-   // is *not* requested by prompt; prompt says inner space width 25 (likely along Y),
-   // but doesn't require centering around Y=0. However typical handle should be centered.
-   //
-   // We'll center it: void should span Y=[-handle_void_w/2, +handle_void_w/2].
-   // Our D currently spans Y in [0, handle_void_w] due to flat at Y=0.
-   // So shift by -handle_void_w/2 to center it around Y=0.
-   translate([0, -handle_void_w/2, 0]) {
-     // Clip to +X only using an explicit intersection with x>=mug_outer_r
-     intersection() {
-       handle_with_x_clip();
+   union() {
+     mug_body();
  
-       // keep only x>=mug_outer_r
-       translate([mug_outer_r + (handle_depth/2), 0, handle_center_z])
-         cube([handle_depth + mug_outer_r + 50, 200, h_mug + 100], center=true);
-     }
+     // Handle placement:
+     // local handle_d() is built with x from 0..handle_depth.
+     // We'll translate it so that x=0 is at the mug outer surface plane (x=mug_outer_r).
+     translate([mug_outer_r - small_eps, 0, 0])
+       intersection() {
+         handle_d();
+         // Keep only +X side region
+         translate([-(mug_outer_r - small_eps), 0, 0])
+           cube([mug_outer_r*2, 200, h_mug + 60], center=false);
+       }
    }
- }
+ }
+ 
+ // Ensure manifoldness by using a single union at top level
+ mug_with_handle();
SCAD (136 lines)
// Mug (OD 80, ID 70, wall 5, height 90, bottom thickness 6)
// +X side only D-handle (hollow), manifold-safe union

$fn = 180;

// --- Mug params ---
outer_d   = 80;
inner_d   = 70;
h_mug     = 90;
t_bottom  = 6;

mug_outer_r = outer_d/2; // 40
mug_inner_r = inner_d/2; // 35

// --- Handle params ---
handle_inner_w = 25;     // inner space width (Y)
handle_inner_h = 30;     // inner space height (Z)
handle_center_z = h_mug/2; // around middle

handle_depth = 22;       // extrusion depth along +X
handle_shell = 4;        // handle shell thickness (radial in YZ)

// D cross-section definition in YZ:
// - D is "outer half-circle + flat line" (flat at Y = 0, bulge to +Y)
// - Inner void is another D scaled by handle_shell
// - Then overall handle is clipped by a rectangle to ensure exact void (Y width, Z height).
module d_shell_2d(r_outer, r_inner, y_void_max, z_void_min, z_void_max) {
  // 2D D surfaces (in YZ plane):
  // Outer D: semicircle bulging +Y with flat at Y=0
  // Inner D: semicircle bulging +Y with flat at Y=0
  module semicircle_with_flat_2d(r) {
    // returns polygon in (Y,Z), flat edge at Y=0, centered in Z around 0, bulge +Y
    pts = [
      for (i=[0:120])
        let(
          a = -90 + i*(180/120),
          y = r*cos(a),  // 0..r
          z = r*sin(a)   // -r..r
        )
        [y,z]
    ];
    // close the flat edge
    polygon(concat(pts, [[0,-r],[0,r]]));
  }

  // Build 2D handle shell as difference of outer minus inner,
  // then clip in Y and Z to get intended void dimensions.
  // Note: clipping is performed after 3D extrusion in the caller using a box.
  difference() {
    semicircle_with_flat_2d(r_outer);
    semicircle_with_flat_2d(r_inner);
  }
}

// 3D D-handle shell with exact hollow void size.
// Local coordinate system:
// - Mug axis: X
// - Y is tangential around mug
// - Z is vertical
// Local origin for handle placement: at (x = mug_outer_r) plane will be handled outside.
module handle_d() {
  // We set inner D semicircle radius so that its max width matches handle_inner_w.
  // For a semicircle with flat at Y=0, the bulge extent in Y equals radius.
  // So bulge extent should be handle_inner_w. That means r_inner = handle_inner_w.
  // But we also need a rectangular hole width; we'll clip hole exactly anyway.
  // We'll use r_inner = handle_inner_w/2 only if void width is measured flat-to-flat.
  // The prompt states "幅25mm" for the inner space; typical interpretation here:
  // the maximum Y spread equals 25. We'll satisfy that by using r_inner=25.
  r_inner = handle_inner_w;
  r_outer = r_inner + handle_shell;

  // Z void extents:
  z0 = handle_center_z - handle_inner_h/2;
  z1 = handle_center_z + handle_inner_h/2;

  // Local Z centered at 0 for D shapes; we will shift the local geometry by z0 later.
  // Create shell in local coordinates where Z center=0.
  // Then cut exact Z range and Y range using boxes.

  difference() {
    // Outer shell extruded along +X from x=0..handle_depth
    translate([0,0,z0])
      linear_extrude(height=handle_depth, center=false)
        d_shell_2d(r_outer=r_outer, r_inner=r_inner,
                   y_void_max=handle_inner_w,
                   z_void_min=z0, z_void_max=z1);

    // Inner void exact cut: subtract rectangle "void window" from within the D shell
    // so that interior space is height 30 and width 25.
    // We'll cut in a way that results in a D-shaped cavity (semi-circle + flat)
    // but bounded to the requested dimensions.
    translate([-1, -100, z0 - 0.5])
      cube([handle_depth + 2, 200, handle_inner_h + 1], center=false);

    // Additionally, ensure the handle cavity doesn't create negative-Y extension beyond flat side.
    // (Flat side at Y=0 in the D cross-section.)
    // Cut everything at Y<0 within the handle depth.
    translate([-1, -100, z0 - 0.5])
      cube([handle_depth + 2, 100, handle_inner_h + 1], center=false);
  }
}

// Mug body (solid cup with hollow interior)
module mug_body() {
  difference() {
    cylinder(d=outer_d, h=h_mug, center=false);
    // Subtract inner cylinder leaving bottom thickness t_bottom
    translate([0,0,t_bottom])
      cylinder(d=inner_d, h=h_mug - t_bottom, center=false);
  }
}

module mug_with_handle() {
  // Place handle on +X side only.
  // We position handle starting at x = mug_outer_r - small_eps so it unions reliably.
  // Then we clip to x>=mug_outer_r to avoid showing on -X.
  small_eps = 0.6;

  union() {
    mug_body();

    // Handle placement:
    // local handle_d() is built with x from 0..handle_depth.
    // We'll translate it so that x=0 is at the mug outer surface plane (x=mug_outer_r).
    translate([mug_outer_r - small_eps, 0, 0])
      intersection() {
        handle_d();
        // Keep only +X side region
        translate([-(mug_outer_r - small_eps), 0, 0])
          cube([mug_outer_r*2, 200, h_mug + 60], center=false);
      }
  }
}

// Ensure manifoldness by using a single union at top level
mug_with_handle();