-
SJCAM M20 Camera: Tour Easy Seat Mount
The general idea is to replace this:
With this:
Thereby solving two problems:
- Pitifully small battery capacity
- Wobbly camera support
The battery is an Anker PowerCore 13000 Power Bank plugged into the M20’s USB port. Given that SJCAM’s 1 A·h batteries barely lasted for a typical hour of riding, the 13 A·h PowerCore will definitely outlast my legs. The four blue dots just ahead of the strap around the battery show it’s fully charged and the blue light glowing through the case around the M20 indicates it’s turned on.
The solid model has four parts:
Which, as always, incorporates improvements based on the actual hardware on the bike.
A strap-and-buckle belt harvested from a defunct water pack holds the battery into the cradle and the cradle onto the rack, with a fuzzy velcro strip stuck to the bottom to prevent sliding:
The shell around the camera is basically a box minus the camera:
The shell builds as three separate slabs, with the center section having cutouts ahead of the camera’s projections to let it slide into place:
The new shell version is 30.5 mm thick, so a 40 mm screw will stick out maybe 5 mm beyond the nylon locknut. I trust the screws will get lost in the visual noise of the bike.
A peg sticking out behind the USB jack anchors the cable in place:
The front slab and center top have curves matching the M20 case:
The camera model has a tidy presentation option:
And an ugly option to knock the protruberances out of the shell:
The square-ish post on the base fits into an angled socket in the clamp around the seat rail:
The numbers correspond to the “Look Angle” of the socket pointing the camera toward overtaking traffic. The -20° in the first clamp shows a bit too much rack:
It may not matter, though, as sometimes you want to remember what’s on the right:
FWIW, the track veering off onto the grass came from a fat-tire bike a few days earlier. Most of the rail trail had cleared by the time we tried it, with some ice and snow in rock cuts and shaded areas.
Contrary to the first picture, I later remounted the camera under the seat rail with its top side downward. The M20 has a “rotate video” mode for exactly that situation, which I forgot to turn off in the fancy new mount, so I rotated the pix afterward.
A 3 mm screw extends upward through the hole in the socket to meet a threaded brass insert epoxied into the shell base, as shown in the uglified M20 model. Despite appearances, the hole is perpendicular to both the socket and the shell, so you can tweak the Look Angle without reprinting the shell.
All in all, the mount works well. We await better riding weather …
The OpenSCAD source code as a GitHub Gist:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters// SJCAM M20 Camera Mount for Tour Easy seat back rail // Ed Nisley – KE4ZNU // 2019-02 /* [Layout Options] */ Layout = "Fit"; // [Show,Fit,Build] Part = "Shell"; // [Cradle,Shell,Clamp,ShellSections,M20,Interposer,Battery,Buttons] LookAngle = [0,5,-25]; // camera angle, looking backwards /* [Extrusion Parameters] */ ThreadWidth = 0.40; ThreadThick = 0.25; HoleWindage = 0.2; Protrusion = 0.1; //—– // Dimensions /* [Hidden] */ ID = 0; OD = 1; LENGTH = 2; ClampScrew = [5.0,10.0,50.0]; // ID=thread OD=washer LENGTH=total ClampInsert = [5.0,7.5,10.5]; // brass insert MountScrew = [3.0,7.0,23]; // ID=thread OD=washer LENGTH=tune to fit clamp arch MountInsert = [3.0,4.95,8.0]; // ID=screw OD, OD=knurl dia EmbossDepth = 2*ThreadThick + Protrusion; // recess depth + Protrusion beyond surface DebossHeight = EmbossDepth; // text height + Protrusion into part Projection = 10; // stick-out to punch through shell sides & suchlike SupportColor = "Yellow"; FadeColor = "Green"; FadeAlpha = 0.25; //—– // Useful routines function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit); module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2); FixDia = Dia / cos(180/Sides); cylinder(r=(FixDia + HoleWindage)/2, h=Height, $fn=Sides); } //—– // M20 Camera // Looks backwards from seat = usual right-hand coordinates work fine // X parallel to bike frame, Y parallel to seat strut, Z true vertical M20 = [24.5,40.5,54.0]; M20tm = 4.0; // chord height at top of case M20tr = (pow(M20tm,2) + pow(M20.y,2)/4) / (2*M20tm); // … radius echo(str("Top radius: ",M20tr)); M20TopSides = 3*3*4; echo(str(" … sides: ",M20TopSides)); M20fm = 1.0; // chord height at front of case M20fr = (pow(M20fm,2) + pow(M20.y,2)/4) / (2*M20fm); // … radius echo(str("Front radius: ",M20fr)); M20FrontSides = ceil(M20fr / M20tr * M20TopSides); // make arc sides match up echo(str(" … sides: ",M20FrontSides)); Lens = [19.0,22.5,5.5]; // ID=optical element, OD=tube LensBezel = [23.0,24.5,2.5]; // ID=lens tube, OD=bezel LensOffset = [-M20fm,0,41.5]; // bottom of case to lens centerline LensCap = [Lens[OD],24.5,4.5]; // silicone lens cap Spkr = [0.75,M20.y,14.3]; // speaker recess below LCD Switch = [8.0,1.0,38.0]; // selection switches SwitchOffset = [9.0,0,0]; // from rear to center of switches Jack = [10.0,0.1,36.0]; // jack and MicroSD card access, slightly enlarged JackOffset = [10.0,0,30.0]; // rear, bottom to center of jack block USB = [JackOffset.x – Jack.x/2,20.0,10.0]; // strut under USB plug USBOffset = [0,0,33.5]; // bottom to center of jack SDCard = [2.0,0.1,12.0]; // SD Card slot SDOffset = [9.0,0,20.0]; // bottom, rear to center of slot Button = [8.5,10.5,M20tm]; // ID = button, OD = bezel ButtonOC = 18.0; // on-center Y separation, assume X centered Screen = [0.1,31,24]; // LCD on rear face ScreenOffset = [0,0,33]; BarLEDs = [0.1 + M20fm,12.0,5.0]; // Bar LEDs on front face BarLEDsOffset = [-M20fm,0,12.5]; PwrLED = [3.5,3.5,0.1 + M20tm]; // power LED on top PwrLEDOffset = [2.5,0,0]; RearLEDs = [1.0,2.0,0.1]; // charge and power LED openings above LCD RearLEDsOffset = [0,13.0/2,M20tm + 3.0]; // .. from top center of case module Buttons(KO) { for (j = [-1,1]) translate([0,j*ButtonOC/2,0]) { cylinder(d=Button[OD],h=Button[LENGTH],$fn=12); if (KO) translate([0,0,M20tm]) cylinder(d1=Button[OD],d2=1.5*Button[OD],h=Button.z,$fn=12); } } module M20Shape(Knockout = false) { difference() { intersection() { translate([0,0,M20.z/2 – M20tr]) // top curve rotate([0,90,0]) rotate(180/M20TopSides) cylinder(r=M20tr,h=2*(M20.x + Protrusion),$fn=M20TopSides,center=true); translate([M20.x/2 – M20fr,0,0]) rotate(180/M20FrontSides) cylinder(r=M20fr,h=2*M20.z,$fn=M20FrontSides,center=true); cube(M20,center=true); } translate([Spkr.x/2 – M20.x/2 – Protrusion,0,Spkr.z/2 – Protrusion/2 – M20.z/2]) cube(Spkr + [Protrusion,2*Protrusion,Protrusion],center=true); } translate([M20.x/2,0,-M20.z/2] + LensOffset) rotate([0,90,0]) cylinder(d=Lens[OD] + HoleWindage,h=(Knockout ? Projection : Lens[LENGTH]),$fn=4*4*3,center=false); translate([M20.x/2 + M20fm/2,0,-M20.z/2] + LensOffset) // lens bezel rotate([0,90,0]) cylinder(d1=LensBezel[OD],d2=Lens[OD],h=LensBezel[LENGTH],$fn=4*4*3,center=false); translate([-M20.x/2 + SwitchOffset.x, // side switches -(Switch.y + M20.y – Protrusion)/2, 0]) cube(Switch + [0,Protrusion,0] + (Knockout ? [0,Projection,0] : [0,0,0]),center=true); if (Knockout) translate([(M20.x/2 – M20fm)/2,-M20.y/2,0]) // side switch slide-in clearance cube([M20.x/2 – M20fm,2*Switch.y,Switch.z],center=true); translate([-M20.x/2 + JackOffset.x, (Jack.y + M20.y – Protrusion)/2, JackOffset.z – M20.z/2]) cube(Jack + [0,Protrusion,0] + (Knockout ? [0,Projection,0] : [0,0,0]),center=true); translate([0,0,M20.z/2 – M20tm]) // top control buttons Buttons(Knockout); if (Knockout) translate([(M20.x – M20fm)/4,0,M20.z/2 – M20tm + Button[LENGTH]/2]) // slide-in button clearance cube([(M20.x – M20fm)/2,ButtonOC + Button[OD],Button[LENGTH]],center=true); translate([-(M20.x + Screen.x – Protrusion)/2,0,-M20.z/2] + ScreenOffset) cube(Screen + [Protrusion,0,0] + (Knockout ? [Projection,0,0] : [0,0,0]),center=true); for (j = [-1,1]) translate([-M20.x/2 + Protrusion,j*RearLEDsOffset.y,M20.z/2 – RearLEDsOffset.z]) rotate([0,-90,0]) rotate(180/6) PolyCyl(RearLEDs[OD],Knockout ? Projection : RearLEDs[LENGTH],6); translate([M20.x/2 + BarLEDs.x/2,0,-M20.z/2] + BarLEDsOffset) cube(BarLEDs + (Knockout ? [Projection,0,0] : [0,0,0]),center=true); translate([0,0,M20.z/2 – M20tm] + PwrLEDOffset) rotate(180/8) PolyCyl(PwrLED[OD],(Knockout ? Projection : PwrLED[LENGTH]),8); if (Knockout) { translate([0,0,-M20.z/2]) rotate([180,0,0]) { // mounting screw PolyCyl(MountScrew[ID],MountScrew[LENGTH],6); translate([0,0,MountScrew[LENGTH] – Protrusion]) PolyCyl(MountScrew[OD],MountScrew[ID] + 4*ThreadThick,6); // SHCS head is about 1 ID long } translate([0,0,-(M20.z/2 + MountInsert[LENGTH] + 4*ThreadWidth – Protrusion)]) PolyCyl(MountInsert[OD],MountInsert[LENGTH] + 4*ThreadWidth,6); // insert inside Interposer } } //—– // Shell // Wraps around camera NomWall = 3.0; ShellWall = [IntegerMultiple(NomWall,ThreadThick), IntegerMultiple(NomWall,ThreadWidth), IntegerMultiple(NomWall,ThreadWidth)]; ShellRadius = ShellWall.x; ShellSides = 8; ShellOA = M20 + 2*ShellWall; echo(str("Shell OA: ",ShellOA)); Interposer = [M20.x – M20fm,M20.x – M20fm,10.0]; // if you can't be smart, be square module Shell() { Screw = [3.0,6.75,30]; // ID=thread OD=washer LENGTH ScrewClear = 1.0; // additional washer clearance ScrewSides = 8; ScrewOC = M20 + [0,Screw[ID]/cos(180/ScrewSides),Screw[ID]/cos(180/ScrewSides)]; // use PolyCyl hole dia, ignore .x value difference() { union() { hull() for (i=[-1,1], j=[-1,1], k=[-1,1]) translate([i*(ShellOA.x – 2*ShellRadius)/2, j*(ShellOA.y – 2*ShellRadius)/2, k*(ShellOA.z – 2*ShellRadius)/2]) sphere(r=ShellRadius/cos(180/ShellSides),$fn=ShellSides); // fix low-poly approx radius for (j=[-1,1], k=[-1,1]) // screw bosses, full length translate([0,j*ScrewOC.y/2,k*ScrewOC.z/2]) rotate([0,90,0]) rotate(180/ScrewSides) cylinder(d=Screw[OD] + ScrewClear,h=ShellOA.x,center=true,$fn=ScrewSides); translate([-(ShellOA.x – USB.x – ShellWall.x)/2, // USB plug support strut (M20.y + USB.y)/2 – ShellRadius, -M20.z/2] + USBOffset) hull() for (i=[-1,1], j=[-1,1], k=[-1,1]) translate([i*(USB.x + ShellWall.x – 2*ShellRadius)/2, j*(USB.y – 2*ShellRadius)/2, k*(USB.z – 2*ShellRadius)/2]) rotate(0*180/ShellSides) rotate([90,0,90]) sphere(r=ShellRadius/cos(180/ShellSides),$fn=ShellSides); translate([-M20fm/2,0,-ShellOA.z/2 – Interposer.z + Protrusion/2]) InterposerShape(Embiggen = false); } render(convexity=4) // remove camera shape from interior M20Shape(Knockout = true); for (j=[-1,1], k=[-1,1]) // screw bores translate([-ShellOA.x,j*ScrewOC.y/2,k*ScrewOC.z/2]) rotate([0,90,0]) rotate(180/ScrewSides) PolyCyl(Screw[ID],2*ShellOA.x,ScrewSides); translate([ShellOA.x/2 – ThreadThick + Protrusion/2,0,-5]) // recess for legend cube([EmbossDepth,ShellOA.y – 12,7],center=true); translate([0,(M20.y + 1.5*SDCard.z)/2 + ThreadWidth,-M20.z/2 + SDOffset.z]) resize([M20.x,0,0]) sphere(d=1.5*SDCard.z,$fn=24); } translate([ShellOA.x/2 – DebossHeight,0,-5]) rotate([90,0,90]) linear_extrude(height=DebossHeight,convexity=20) text(text="KE4ZNU",size=5,spacing=1.20,font="Arial:style:Bold",halign="center",valign="center"); // Totally ad-hoc support structures if (false) color(SupportColor) { for (j=[-1,1], k=[0,1]) translate([-ShellOA.x/2 + Screw[LENGTH],j*ShellOA.y/2,k*ShellOA.z]) rotate([0,90,0]) SupportScrew(Dia=Screw[OD] + ScrewClear,Length=ShellOA.x – Screw[LENGTH],Num=ScrewSides); } } // Generate support structure for screw boss module SupportScrew(Dia,Length,Num = 6) { for (a=[0 : 360/Num : 360/2]) rotate(a) translate([0,0,(Length + ThreadThick)/2]) cube([Dia – 2*ThreadWidth,2*ThreadWidth,Length – ThreadThick],center=true); } // Generate interposer block // Origin at center bottom surface for E-Z rotation module InterposerShape(Embiggen = false) { translate([0,0,Interposer.z/2]) if (Embiggen) { minkowski() { cube(Interposer,center=true); cube(HoleWindage,center=true); } } else cube(Interposer + [-Protrusion,0,Protrusion],center=true); // avoid slivers, merge with shell } // Cut shell sections for printing // "Front" = lens end, toward +X direction // origin centered on M20.xyz and ShellOA.xyz module ShellSection(Section="Front") { if (Section == "Front") // include front curve intersection() { Shell(); translate([ShellOA.x – (M20fm + ShellWall.x),0,0]) cube([ShellOA.x,2*ShellOA.y,2*ShellOA.z],center=true); } else if (Section == "Center") // exclude front curve for E-Z printing intersection() { Shell(); translate([-M20fm/2,0,0]) cube([M20.x – M20fm,2*ShellOA.y,2*ShellOA.z],center=true); } else if (Section == "Back") // flush with LCD on rear face intersection() { Shell(); translate([-ShellOA.x + (ShellWall.x),0,0]) cube([ShellOA.x,2*ShellOA.y,2*ShellOA.z],center=true); } } //—– // Clamp // Grips seat frame rail // Uses shell rounding values for tidiness // Adjust MountScrew[LENGTH] to put head more-or-less flush with clamp arch RailOD = 20.0; // slightly elliptical in bent section RailSides = 2*3*4; ClampOA = [60.0,40.0,ClampScrew[LENGTH]]; // set clamp size to avoid weird screw spacing echo(str("Clamp OA: ",ClampOA)); ClampOffset = 0.0; // raise clamp to allow more room for mount ClampTop = ClampOA.z/2 + ClampOffset; InsertCap = 6*ThreadThick; // fill layers atop inserts Kerf = 2.0; module Clamp(Support = false) { RibThick = 2*ThreadWidth; NumRibs = IntegerMultiple(ceil(ClampOA.y / 4.0),2); // space ribs roughly 4 mm apart RibSpace = ClampOA.y / NumRibs; echo(str("Ribs: ",NumRibs," spaced: ",RibSpace)); ClampScrewOC = IntegerMultiple(ClampOA.x – ClampScrew[OD] – 10*ThreadWidth,1.0); echo(str("ClampScrew OC: ",ClampScrewOC)); difference() { hull() for (i=[-1,1], j=[-1,1], k=[-1,1]) translate([i*(ClampOA.x – 2*ShellRadius)/2, j*(ClampOA.y – 2*ShellRadius)/2, k*(ClampOA.z – 2*ShellRadius)/2 + ClampOffset]) sphere(r=ShellRadius/cos(180/ShellSides),$fn=ShellSides); cube([2*ClampOA.x,2*ClampOA.y,Kerf],center=true); // split across middle rotate([90,0,0]) // seat rail cylinder(d=RailOD,h=2*ClampOA.y,$fn=RailSides,center=true); for (i=[-1,1]) // clamp inserts translate([i*ClampScrewOC/2,0,0]) rotate(180/6) PolyCyl(ClampInsert[OD],ClampTop – InsertCap,6); for (i=[-1,1]) // clamp screw clearance translate([i*ClampScrewOC/2,0,-(ClampOA.z/2 – ClampOffset) – InsertCap]) rotate(180/6) PolyCyl(ClampScrew[ID],ClampOA.z,6); translate([0,0,ClampTop + 0.7*Interposer.z]) // mounting bolt hole rotate(LookAngle) translate([0,0,ShellOA.z/2]) { M20Shape(Knockout = true); translate([0,0,-ShellOA.z/2 – Interposer.z]) InterposerShape(Embiggen = true); } translate([ClampOA.x/2 – (EmbossDepth – Protrusion)/2, // recess for LookAngle.z 0, ClampOA.z/4 + ClampOffset]) cube([EmbossDepth,17,8],center=true); translate([0.3*ClampOA.x, // recess for LookAngle.z -(ClampOA.y/2 – (EmbossDepth – Protrusion)/2), ClampOA.z/4 + ClampOffset]) cube([10,EmbossDepth,8],center=true); translate([0,0,-ClampOA.z/2 + (EmbossDepth – Protrusion)/2]) // recess bottom legend cube([35,10,EmbossDepth],center=true); } translate([ClampOA.x/2 – DebossHeight,0,ClampOA.z/4 + ClampOffset]) // LookAngle.z legend rotate([90,0,90]) linear_extrude(height=DebossHeight,convexity=20) text(text=str(LookAngle.z),size=6,spacing=1.20, font="Arial:style:Bold",halign="center",valign="center"); translate([0.3*ClampOA.x,-ClampOA.y/2 + DebossHeight + Protrusion/2,ClampOA.z/4 + ClampOffset]) // LookAngle.y legend rotate([90,0,00]) linear_extrude(height=DebossHeight,convexity=20) text(text=str(LookAngle.y),size=6,spacing=1.20, font="Arial:style:Bold",halign="center",valign="center"); translate([0,0,-ClampOA.z/2]) linear_extrude(height=DebossHeight,convexity=20) mirror([0,1,0]) text(text="KE4ZNU",size=5,spacing=1.20, font="Arial:style:Bold",halign="center",valign="center"); if (Support) { difference() { color(SupportColor) union() { for (j=[-NumRibs/2:NumRibs/2]) translate([0,j*RibSpace,0]) rotate([90,0,0]) cylinder(d=RailOD – 2*ThreadThick,h=RibThick,$fn=2*3*4,center=true); cube([RailOD – 4*ThreadWidth,NumRibs*RibSpace,Kerf + 2*ThreadThick],center=true); } cube([2*ClampOA.x,2*ClampOA.y,Kerf],center=true); // split across middle } } } //—– // Battery // Based on Anker PowerCore, simplified shapes // Includes port & button punchouts Battery = [97.5,80.0,22.5]; // X=length, Y includes rounded edges, Z = Y dia module BatteryShape() { USB = [Projection,38,10]; // clearance around USB output ports USBOffset = [0,25.5,0]; // from -Y edge to center of USB block ChargeBtn = [11.0 + 5.0,10,5.0 + 5.0]; // charge level check button, enlarged Btnc = ChargeBtn.z; // figure button recess into battery curve Btnr = Battery.z/2; Btnm = Btnr – sqrt(pow(Btnr,2) – pow(Btnc,2)/4); ChargeBtnOffset = [17.0,0,0]; // from +X edge to center, centered on Z BatterySides = 2*3*4; hull() for (j=[-1,1]) translate([0,j*(Battery.y – Battery.z)/2,0]) rotate([0,90,0]) cylinder(d=Battery.z,h=Battery.x,$fn=BatterySides,center=true); translate([(Battery.x + USB.x)/2 – Protrusion,-Battery.y/2 + USBOffset.y,0]) cube(USB,center=true); translate([Battery.x/2 – ChargeBtnOffset.x,Battery.y/2 + ChargeBtn.y/2 – 2*Btnm,0]) cube(ChargeBtn,center=true); } //—– // Battery cradle RackWidth = 89.0; // flat width between rack rails CradleWall = [4.0,4.0,3.0]; // wall thickness CradleRadius = 2.0; // corner rounding CradlePad = 0.5; // cushion around battery BatteryBase = CradleWall.z + CradlePad; // actual bottom surface of battery CradleOA = [Battery.x + 2*CradleWall.x, min((Battery.y + 2*CradleWall.y),RackWidth), BatteryBase + Battery.z/3]; echo(str("Cradle OA: ",CradleOA)); module Cradle() { difference() { hull() for (i=[-1,1], j=[-1,1]) { // box with tidy rounded corners translate([i*(CradleOA.x/2 – CradleRadius), j*(CradleOA.y/2 – CradleRadius), 1*(CradleOA.z – CradleRadius)]) sphere(r=CradleRadius,$fn=6); translate([i*(CradleOA.x/2 – CradleRadius), j*(CradleOA.y/2 – CradleRadius), 0*(CradleOA.z/2 – CradleRadius)]) cylinder(r=CradleRadius,h=CradleOA.z/2,$fn=6); } translate([0,0,Battery.z/2 + BatteryBase]) // minus the battery minkowski(convexity=3) { // … slightly embiggened BatteryShape(); cube(2*CradlePad,center=true); } if (false) // reveal insets for debug translate([0,0,-Protrusion]) cube(CradleOA + [0,0,CradleOA.z],center=false); translate([0,0,CradleWall.z – ThreadThick + Protrusion/2]) // recess top legend cube([55,20,EmbossDepth],center=true); translate([0,0,(EmbossDepth – Protrusion)/2]) // recess bottom legend cube([70,15,EmbossDepth],center=true); } translate([0,4.0,CradleWall.z – DebossHeight – Protrusion]) linear_extrude(height=DebossHeight,convexity=20) text(text="PowerCore",size=6,spacing=1.20, font="Arial:style:Bold",halign="center",valign="center"); translate([0,-4.0,CradleWall.z – DebossHeight – Protrusion]) linear_extrude(height=DebossHeight,convexity=20) text(text="13000",size=6,spacing=1.20, font="Arial:style:Bold",halign="center",valign="center"); linear_extrude(height=DebossHeight,convexity=20) mirror([0,1,0]) text(text="KE4ZNU",size=10,spacing=1.20, font="Arial:style:Bold",halign="center",valign="center"); } //—– // Build things // Layouts for design & tweaking if (Layout == "Show") if (Part == "Battery") BatteryShape(); else if (Part == "Buttons") Buttons(); else if (Part == "Interposer") InterposerShape(Embiggen = false); else if (Part == "Shell") Shell(); else if (Part == "M20") M20Shape(Knockout = false); else if (Part == "ShellSections") { translate([ShellOA.x,0,0]) ShellSection(Section="Front"); translate([0,0,0]) ShellSection(Section="Center"); translate([-ShellOA.x,0,0]) ShellSection(Section="Back"); } else if (Part == "Clamp") { Clamp(Support = false); color(FadeColor,FadeAlpha) rotate([90,0,0]) cylinder(d=RailOD,h=2*ClampOA.y,$fn=RailSides,center=true); } else if (Part == "Cradle") { Cradle(); translate([0,0,Battery.z/2 + CradleWall.z]) color(FadeColor,FadeAlpha) BatteryShape(); } // Build layouts for top-level parts if (Layout == "Build") if (Part == "Cradle") Cradle(); else if (Part == "Clamp") { translate([0,0.7*ClampOA.y,0]) difference() { translate([0,0,-Kerf/2]) Clamp(Support = true); translate([0,0,-ClampOA.z]) cube(2*ClampOA,center=true); } translate([0,-0.7*ClampOA.y,-0]) difference() { translate([0,0,-Kerf/2]) rotate([0,180,0]) Clamp(Support = true); translate([0,0,-ClampOA.z]) cube(2*ClampOA,center=true); } } else if (Part == "Shell") { translate([0,-1.2*ShellOA.y,ShellOA.x/2]) rotate([0,90,180]) ShellSection(Section="Front"); translate([0,0,M20.x/2]) rotate([0,-90,0]) ShellSection(Section="Center"); translate([0,1.4*ShellOA.y,ShellOA.x/2]) rotate([0,-90,180]) ShellSection(Section="Back"); } // Ad-hoc arrangement to see how it all goes together if (Layout == "Fit") { rotate(180) { Cradle(); translate([0,0,Battery.z/2 + CradleWall.z]) color(FadeColor,FadeAlpha) BatteryShape(); } translate([0,-100,0]) { Clamp(); color(FadeColor,FadeAlpha) rotate([90,0,0]) cylinder(d=RailOD,h=2*ClampOA.y,$fn=RailSides,center=true); } translate([0,-100,(ClampOA.z + ShellOA.z)/2 + Interposer.z]) translate([0,0,-ShellOA.z/2 + Interposer.z]) rotate(LookAngle) translate([0,0,ShellOA.z/2]) { Shell(); color(FadeColor,FadeAlpha) M20Shape(Knockout = false); } }












