├── src ├── test │ ├── resources │ │ ├── unit_cube_binary.stl │ │ ├── unit_cube.obj │ │ ├── unit_cube_ascii.stl │ │ └── unit_cube.amf │ └── scala │ │ └── scadla │ │ ├── backends │ │ ├── ObjParserTest.scala │ │ ├── AmfParserTest.scala │ │ ├── StlParserTest.scala │ │ └── OpenSCADTest.scala │ │ ├── SolidTest.scala │ │ └── utils │ │ └── PackageTest.scala └── main │ └── scala │ └── scadla │ ├── utils │ ├── Bigger.scala │ ├── Tube.scala │ ├── gear │ │ ├── Twist.scala │ │ ├── HerringboneGear.scala │ │ ├── HelicalGear.scala │ │ ├── Involute.scala │ │ ├── Rack.scala │ │ ├── Gear.scala │ │ └── InvoluteGear.scala │ ├── Trig.scala │ ├── extrusion │ │ ├── L.scala │ │ ├── T.scala │ │ ├── U.scala │ │ ├── C.scala │ │ ├── Z.scala │ │ ├── H.scala │ │ └── _2020.scala │ ├── CenteredCube.scala │ ├── thread │ │ ├── UTS.scala │ │ ├── ISO.scala │ │ ├── Washer.scala │ │ └── Nut.scala │ ├── Smaller.scala │ ├── RoundedCube.scala │ ├── PieSlice.scala │ ├── Trapezoid.scala │ ├── box │ │ ├── Interval.scala │ │ ├── InBox.scala │ │ ├── Box.scala │ │ └── BoundingBox.scala │ ├── SpherePortion.scala │ ├── Hexagon.scala │ └── package.scala │ ├── backends │ ├── almond │ │ ├── Config.scala │ │ └── Viewer.scala │ ├── Viewer.scala │ ├── MeshLab.scala │ ├── obj │ │ ├── Printer.scala │ │ └── Parser.scala │ ├── RendererAux.scala │ ├── Renderer.scala │ ├── amf │ │ ├── Printer.scala │ │ └── Parser.scala │ ├── ply │ │ └── Printer.scala │ ├── ParallelRenderer.scala │ ├── x3d │ │ └── Printer.scala │ ├── stl │ │ ├── Printer.scala │ │ └── Parser.scala │ └── JCSG.scala │ ├── EverythingIsIn.scala │ ├── examples │ ├── ExtrusionDemo.scala │ ├── cnc │ │ ├── BitsHolder.scala │ │ ├── Common.scala │ │ ├── Pulley.scala │ │ ├── Chuck.scala │ │ ├── Collet.scala │ │ ├── ActuatorFastener.scala │ │ ├── Main.scala │ │ ├── Joints.scala │ │ ├── Motors.scala │ │ ├── Spindle.scala │ │ ├── Platform.scala │ │ ├── LinearActuator.scala │ │ └── Gimbal.scala │ ├── WhiteboardMarkerHolder.scala │ ├── MetricThreadDemo.scala │ ├── BeltMold.scala │ ├── GearBearing.scala │ ├── reach3D │ │ ├── ReachLegs.scala │ │ └── SpoolHolder.scala │ └── ComponentStorageBox.scala │ ├── assembly │ ├── Part.scala │ ├── Frame.scala │ ├── Joints.scala │ └── Assembly.scala │ ├── InlineOps.scala │ ├── Solid.scala │ └── Primitives.scala ├── .gitignore ├── THIRDPARTY-LICENSES.txt └── doc └── sample.ipynb /src/test/resources/unit_cube_binary.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzufferey/scadla/HEAD/src/test/resources/unit_cube_binary.stl -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # model and G-code 5 | *.stl 6 | *.gco 7 | 8 | # sbt specific 9 | .cache/ 10 | .history/ 11 | .lib/ 12 | .bsp/ 13 | dist/* 14 | target/ 15 | lib_managed/ 16 | src_managed/ 17 | project/boot/ 18 | project/plugins/project/ 19 | project/build.properties 20 | 21 | # others 22 | clean.sh 23 | doc/.ipynb_checkpoints/ 24 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/Bigger.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils 2 | 3 | import scadla._ 4 | import squants.space.Length 5 | 6 | object Bigger { 7 | 8 | def apply(obj: Solid, s: Length) = Minkowski(obj, CenteredCube(s,s,s)) 9 | 10 | } 11 | 12 | object BiggerS { 13 | 14 | def apply(obj: Solid, s: Length) = Minkowski(obj, Sphere(s)) 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/scala/scadla/backends/ObjParserTest.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends 2 | 3 | import scadla._ 4 | import org.scalatest.funsuite.AnyFunSuite 5 | 6 | class ObjParserTest extends AnyFunSuite { 7 | 8 | import ParserTest._ 9 | 10 | test("unit cube") { 11 | val cube = obj.Parser(path + "unit_cube.obj") 12 | assert(cube.faces.size == 12) 13 | checkCube(cube) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/scala/scadla/SolidTest.scala: -------------------------------------------------------------------------------- 1 | package scadla 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | import EverythingIsIn.millimeters 5 | 6 | class SolidTest extends AnyFunSuite { 7 | 8 | test("trace test") { 9 | val c = Cube(1,1,1) 10 | //for (t <- c.trace) { 11 | // Console.println(t) 12 | //} 13 | assert(c.trace.head.getFileName == "SolidTest.scala") 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/almond/Config.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends.almond 2 | 3 | import com.github.dzufferey.x3DomViewer.X3D 4 | import scalatags.Text.all._ 5 | import squants.space.{Millimeters, LengthUnit} 6 | 7 | class Config extends com.github.dzufferey.x3DomViewer.Config { 8 | 9 | var shapeAppearance = X3D.appearance(X3D.material(X3D.diffuseColor := "0.7 0.7 0.7")) 10 | val unit: LengthUnit = Millimeters 11 | } 12 | -------------------------------------------------------------------------------- /src/test/scala/scadla/backends/AmfParserTest.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends 2 | 3 | import scadla._ 4 | import org.scalatest.funsuite.AnyFunSuite 5 | 6 | class AmfParserTest extends AnyFunSuite { 7 | 8 | import ParserTest._ 9 | 10 | test("unit cube") { 11 | val cube = amf.Parser(path + "unit_cube.amf") 12 | assert(cube.faces.size == 12) 13 | checkCube(cube) 14 | //amf.Printer.store(cube, "test.amf") 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/Viewer.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends 2 | 3 | import scadla._ 4 | import dzufferey.utils.SysCmd 5 | import java.io._ 6 | 7 | trait Viewer { 8 | 9 | def apply(obj: Polyhedron): Unit 10 | 11 | } 12 | 13 | object Viewer { 14 | 15 | def default: Viewer = { 16 | if (MeshLab.isPresent) MeshLab 17 | else if (x3d.X3DomViewer.isPresent) x3d.X3DomViewer 18 | else sys.error("No viewer available") 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/Tube.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils 2 | 3 | import scadla._ 4 | import squants.space.Length 5 | import scala.language.postfixOps 6 | import squants.space.LengthConversions._ 7 | 8 | object Tube { 9 | 10 | def apply(outerRadius: Length, innerRadius: Length, height: Length) = { 11 | Difference( 12 | Cylinder(outerRadius, height), 13 | Translate(0 mm, 0 mm, -1 mm, Cylinder(innerRadius, height + (2 mm))) 14 | ) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/scadla/EverythingIsIn.scala: -------------------------------------------------------------------------------- 1 | package scadla 2 | 3 | import scala.language.implicitConversions 4 | 5 | object EverythingIsIn { 6 | implicit def millimeters[A](n: A)(implicit num: Numeric[A]) = squants.space.Millimeters(n) 7 | implicit def inches[A](n: A)(implicit num: Numeric[A]) = squants.space.Inches(n) 8 | implicit def radians[A](n: A)(implicit num: Numeric[A]) = squants.space.Radians(n) 9 | implicit def degrees[A](n: A)(implicit num: Numeric[A]) = squants.space.Degrees(n) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/ExtrusionDemo.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scadla.utils.extrusion._ 6 | import scadla.EverythingIsIn.millimeters 7 | 8 | object ExtrusionDemo { 9 | 10 | def main(args: Array[String]): Unit = { 11 | val r = scadla.backends.Renderer.default 12 | val objects = List( 13 | _2020(100), 14 | C(20,20,10,4)(100), 15 | H(20,20,4)(100), 16 | L(20,20,4)(100), 17 | T(20,20,4)(100), 18 | U(20,20,4)(100), 19 | Z(20,20,4)(100) 20 | ) 21 | val moved = objects.zipWithIndex.map{ case(o, i) => o.moveY(25 * i) } 22 | r.view(Union(moved: _*)) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/gear/Twist.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.gear 2 | 3 | import squants.space.Angle 4 | import squants.space.Length 5 | import squants.space.Degrees 6 | import squants.space.Radians 7 | import squants.space.Millimeters 8 | 9 | /** 10 | * Represents an amount of twist [helix] per certain [increment] along a path. 11 | */ 12 | case class Twist(angle: Angle, increment: Length) { 13 | def * (d: Double) = Twist(angle * d, increment) 14 | def / (d: Double) = Twist(angle / d, increment) 15 | def unary_- = Twist(-angle, increment) 16 | } 17 | 18 | object Twist { 19 | def apply(pitch: Length): Twist = Twist(Degrees(360), pitch) 20 | def radiansPerMm(value: Double): Twist = Twist(Radians(value), Millimeters(1)) 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/Trig.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils 2 | 3 | import squants.space.{Angle, Radians, Length, Millimeters} 4 | 5 | object Trig { 6 | 7 | def π = Radians(math.Pi) 8 | def Pi = Radians(math.Pi) 9 | 10 | def cos(a: Angle) = math.cos(a.toRadians) 11 | 12 | def sin(a: Angle) = math.sin(a.toRadians) 13 | 14 | def tan(a: Angle) = math.tan(a.toRadians) 15 | 16 | def acos(d: Double) = Radians(math.acos(d)) 17 | 18 | def asin(d: Double) = Radians(math.asin(d)) 19 | 20 | def atan(d: Double) = Radians(math.atan(d)) 21 | 22 | def atan2(y: Length, x: Length) = Radians(math.atan2(y.toMillimeters, x.toMillimeters)) 23 | 24 | def hypot(x: Length, y: Length) = Millimeters(math.hypot(y.toMillimeters, x.toMillimeters)) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/scala/scadla/backends/StlParserTest.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends 2 | 3 | import scadla._ 4 | import org.scalatest.funsuite.AnyFunSuite 5 | import squants.space.Millimeters 6 | 7 | object ParserTest { 8 | def checkCube(p: Polyhedron) = { 9 | p.faces.forall( t => 10 | List(t.p1, t.p2, t.p3).forall(p => 11 | List(p.x, p.y, p.z).forall( v => v == Millimeters(0) || v == Millimeters(1)))) 12 | } 13 | 14 | val path = "src/test/resources/" 15 | } 16 | 17 | class StlParserTest extends AnyFunSuite { 18 | 19 | import ParserTest._ 20 | 21 | test("ascii stl") { 22 | checkCube(stl.Parser(path + "unit_cube_ascii.stl")) 23 | } 24 | 25 | test("binary stl") { 26 | checkCube(stl.Parser(path + "unit_cube_binary.stl")) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/extrusion/L.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.extrusion 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scadla.utils._ 6 | import squants.space.Length 7 | 8 | object L { 9 | 10 | /* 11 | * ─ ┌─┐ 12 | * │ │ 13 | * a │ │ 14 | * │ └───┐ ─ 15 | * ─ └─────┘ ─ d 16 | * | b | 17 | */ 18 | def apply(a: Length, b: Length, d: Length)(length: Length): Solid = { 19 | apply(a, b, d, d)(length) 20 | } 21 | 22 | /* 23 | * |t| 24 | * ─ ┌─┐ 25 | * │ │ 26 | * a │ │ 27 | * │ └───┐ ─ 28 | * ─ └─────┘ ─ d 29 | * | b | 30 | */ 31 | def apply(a: Length, b: Length, d: Length, t: Length)(length: Length): Solid = { 32 | val c1 = Cube(t, a, length) 33 | val c2 = Cube(b, d, length) 34 | c1 + c2 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/almond/Viewer.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends.almond 2 | 3 | import scadla._ 4 | import com.github.dzufferey.x3DomViewer.X3D._ 5 | import scalatags.Text.all._ 6 | 7 | object Viewer { // does not extend the normal Viewer class as it needs to return a value 8 | 9 | var conf = new Config 10 | 11 | def apply(obj: Polyhedron) = { 12 | val (points, faces) = obj.indexed 13 | val indices = faces.map{ case (a,b,c) => s"$a $b $c -1" }.mkString(" ") 14 | val coord = points.map( p => s"${p.x.to(conf.unit)} ${p.y.to(conf.unit)} ${p.z.to(conf.unit)}" ).mkString(" ") 15 | val ifaces = indexedFaceSet(coordIndex := indices.toString, coordinate( point := coord) ) 16 | val content = shape(conf.shapeAppearance, ifaces) 17 | com.github.dzufferey.x3DomViewer.Viewer.display(content, conf) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/BitsHolder.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import math._ 4 | import scadla._ 5 | import utils._ 6 | import InlineOps._ 7 | import scadla.EverythingIsIn.{millimeters, radians} 8 | 9 | object BitsHolder { 10 | 11 | def apply(cols: Int, rows: Int, shankDiammeter: Double, separation: Double, height: Double) = { 12 | val rounding = min(height, (separation-shankDiammeter)/2) 13 | val x = cols * separation 14 | val y = rows * separation 15 | val z = height 16 | val base = RoundedCube(x,y,z+rounding, rounding).moveZ(-rounding) * Cube(x, y, z) 17 | val holes = for (i <- 0 until cols; j <- 0 until rows) yield { 18 | val mx = separation/2 + i * separation 19 | val my = separation/2 + j * separation 20 | Cylinder(shankDiammeter/2, height).move(mx, my, 1) 21 | } 22 | base -- holes 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/resources/unit_cube.obj: -------------------------------------------------------------------------------- 1 | # Blender v2.74 (sub 0) OBJ File: '' 2 | # www.blender.org 3 | mtllib untitled.mtl 4 | o Cube 5 | v 1.000000 0.000000 0.000000 6 | v 1.000000 0.000000 1.000000 7 | v 0.000000 0.000000 1.000000 8 | v 0.000000 0.000000 0.000000 9 | v 1.000000 1.000000 0.000000 10 | v 1.000000 1.000000 1.000000 11 | v 0.000000 1.000000 1.000000 12 | v 0.000000 1.000000 0.000000 13 | vn 0.000000 -1.000000 0.000000 14 | vn 0.000000 1.000000 0.000000 15 | vn 1.000000 0.000000 0.000000 16 | vn -0.000000 0.000000 1.000000 17 | vn -1.000000 -0.000000 -0.000000 18 | vn 0.000000 0.000000 -1.000000 19 | usemtl Material 20 | s off 21 | f 2//1 3//1 4//1 22 | f 8//2 7//2 6//2 23 | f 5//3 6//3 2//3 24 | f 6//4 7//4 3//4 25 | f 3//5 7//5 8//5 26 | f 1//6 4//6 8//6 27 | f 1//1 2//1 4//1 28 | f 5//2 8//2 6//2 29 | f 1//3 5//3 2//3 30 | f 2//4 6//4 3//4 31 | f 4//5 3//5 8//5 32 | f 5//6 1//6 8//6 33 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/MeshLab.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends 2 | 3 | import scadla._ 4 | import dzufferey.utils.SysCmd 5 | import java.io._ 6 | 7 | object MeshLab extends Viewer { 8 | 9 | def apply(stl: String, options: Iterable[String] = Nil): SysCmd.ExecResult = { 10 | SysCmd( Array("meshlab", stl) ++ options ) 11 | } 12 | 13 | def apply(file: File): SysCmd.ExecResult = apply(file.getPath) 14 | 15 | def apply(obj: Polyhedron): Unit = { 16 | val tmpFile = java.io.File.createTempFile("scadlaModel", ".stl") 17 | stl.Printer.storeBinary(obj, tmpFile.getPath) 18 | apply(tmpFile) 19 | tmpFile.delete 20 | } 21 | 22 | lazy val isPresent = { 23 | val isWindows = java.lang.System.getProperty("os.name").toLowerCase().contains("windows") 24 | val cmd = if (isWindows) "where" else "which" 25 | SysCmd(Array(cmd, "meshlab"))._1 == 0 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/CenteredCube.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils 2 | 3 | import scadla._ 4 | import squants.space.Length 5 | import scala.language.postfixOps 6 | import squants.space.LengthConversions._ 7 | 8 | object CenteredCube { 9 | 10 | def apply(x: Length, y: Length, z:Length) = Translate(-x/2, -y/2, -z/2, Cube(x,y,z)) 11 | 12 | def xy(x: Length, y: Length, z:Length) = Translate(-x/2, -y/2, 0 mm, Cube(x,y,z)) 13 | 14 | def xz(x: Length, y: Length, z:Length) = Translate(-x/2, 0 mm, -z/2, Cube(x,y,z)) 15 | 16 | def yz(x: Length, y: Length, z:Length) = Translate(0 mm, -y/2, -z/2, Cube(x,y,z)) 17 | 18 | def x(x: Length, y: Length, z:Length) = Translate(-x/2, 0 mm, 0 mm, Cube(x,y,z)) 19 | 20 | def y(x: Length, y: Length, z:Length) = Translate(0 mm, -y/2, 0 mm, Cube(x,y,z)) 21 | 22 | def z(x: Length, y: Length, z:Length) = Translate(0 mm, 0 mm, -z/2, Cube(x,y,z)) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/thread/UTS.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.thread 2 | 3 | import scala.language.postfixOps 4 | import squants.space.LengthConversions._ 5 | 6 | /* Radius of UTS imperial sizes. _x_y stands of x/y inches.*/ 7 | object UTS { 8 | val _1_8 = (0.125 inches) / 2.0 9 | val _5_32 = (5.0 inches) / 32 / 2.0 10 | val _1_4 = (0.25 inches) / 2.0 11 | val _5_16 = (5.0 inches) / 16 / 2.0 12 | val _3_8 = (3 inches) * 0.125 / 2.0 13 | val _7_16 = (7.0 inches) / 16 / 2.0 14 | val _1_2 = (0.5 inches) / 2.0 15 | val _9_16 = (9.0 inches) / 16 / 2.0 16 | val _5_8 = (5 inches) * 0.125 / 2.0 17 | val _11_16 = (11.0 inches) / 16 / 2.0 18 | val _3_4 = (0.75 inches) / 2.0 19 | val _7_8 = (7 inches) * 0.125 / 2.0 20 | val _15_16 = (15.0 inches) / 16 / 2.0 21 | val _1 = (1 inches) / 2.0 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/thread/ISO.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.thread 2 | 3 | import scala.language.postfixOps 4 | import squants.space.LengthConversions._ 5 | 6 | /* Radius of ISO metric sizes. _x_y stands of x.y mm. */ 7 | object ISO { 8 | val M1 = (1 mm) / 2.0 9 | val M1_2 = (1.2 mm) / 2.0 10 | val M1_6 = (1.6 mm) / 2.0 11 | val M2 = (2 mm) / 2.0 12 | val M2_5 = (2.5 mm) / 2.0 13 | val M3 = (3 mm) / 2.0 14 | val M4 = (4 mm) / 2.0 15 | val M5 = (5 mm) / 2.0 16 | val M6 = (6 mm) / 2.0 17 | val M8 = (8 mm) / 2.0 18 | val M10 = (10 mm) / 2.0 19 | val M12 = (12 mm) / 2.0 20 | val M16 = (16 mm) / 2.0 21 | val M20 = (20 mm) / 2.0 22 | val M24 = (24 mm) / 2.0 23 | val M30 = (30 mm) / 2.0 24 | val M36 = (36 mm) / 2.0 25 | val M42 = (42 mm) / 2.0 26 | val M48 = (48 mm) / 2.0 27 | val M56 = (56 mm) / 2.0 28 | val M64 = (64 mm) / 2.0 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/Smaller.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils 2 | 3 | import scadla._ 4 | import squants.space.Millimeters 5 | import squants.space.Length 6 | 7 | //try to emulate a Minkowski difference 8 | //works only for objects that are not too concave 9 | 10 | object Smaller { 11 | 12 | def apply(obj: Solid, s: Length) = { 13 | val epsilon = Millimeters(1e-6) 14 | val larger = Minkowski(obj, CenteredCube(epsilon, epsilon, epsilon)) 15 | val shell = Difference(larger, obj) 16 | val biggerShell = Minkowski(shell, CenteredCube(s, s, s)) 17 | Difference(obj, biggerShell) 18 | } 19 | 20 | } 21 | 22 | object SmallerS { 23 | 24 | def apply(obj: Solid, s: Length) = { 25 | val epsilon = Millimeters(1e-6) 26 | val larger = Minkowski(obj, Sphere(epsilon)) 27 | val shell = Difference(larger, obj) 28 | val biggerShell = Minkowski(shell, Sphere(s)) 29 | Difference(obj, biggerShell) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/Common.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import scadla._ 4 | import utils._ 5 | import InlineOps._ 6 | import thread._ 7 | import scala.language.postfixOps // for mm notation 8 | import squants.space.LengthConversions._ // for mm notation 9 | 10 | object Common { 11 | 12 | val tightTolerance = 0.10 mm 13 | val tolerance = 0.15 mm 14 | val looseTolerance = 0.22 mm 15 | 16 | val supportGap = 0.2 mm 17 | 18 | val nut = new NutPlaceHolder(looseTolerance) 19 | val bearing = { 20 | val t = looseTolerance 21 | Cylinder((11 mm) + t, (7 mm) + t).moveZ(-t/2) 22 | } 23 | val threading = new MetricThread(tolerance) 24 | 25 | // wood screws that can be used to hold multiples palstic parts together 26 | val woodScrewRadius = ISO.M3 - (0.5 mm) // for 3mm wood screws 27 | val woodScrewHeadHeight = 2 mm 28 | val woodScrewHeadRadius = 3 mm 29 | val woodScrewLength = 18 mm 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/RoundedCube.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils 2 | 3 | import scadla._ 4 | import squants.space.Length 5 | import scala.language.postfixOps 6 | import squants.space.LengthConversions._ 7 | 8 | object RoundedCube { 9 | 10 | def apply(x: Length, y: Length, z: Length, r: Length) = { 11 | if (r.value > 0) { 12 | val d = 2*r 13 | assert(d < x && d < y && d < z, "RoundedCube, radius should be less than x/2, y/2, z/2.") 14 | val c = Translate(r, r, r, Cube(x - d, y - d, z - d)) 15 | Minkowski(c, Sphere(r)) 16 | } else { 17 | Cube(x,y,z) 18 | } 19 | } 20 | 21 | } 22 | 23 | 24 | object RoundedCubeH { 25 | 26 | def apply(x: Length, y: Length, z: Length, r: Length) = { 27 | if (r.value > 0) { 28 | val h = z/2 29 | val d = 2*r 30 | assert(d < x && d < y, "roundedCube, radius should be less than x/2, y/2.") 31 | val c = Translate(r, r, 0 mm, Cube(x - d, y - d, h)) 32 | Minkowski(c, Cylinder(r, h)) 33 | } else { 34 | Cube(x,y,z) 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/obj/Printer.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends.obj 2 | 3 | import scadla._ 4 | import dzufferey.utils._ 5 | import dzufferey.utils.LogLevel._ 6 | import java.io._ 7 | import squants.space.{Length, Millimeters, LengthUnit} 8 | 9 | //TODO make parametric in terms of unit 10 | 11 | object Printer extends Printer(Millimeters) { 12 | } 13 | 14 | class Printer(unit: LengthUnit = Millimeters) { 15 | 16 | def store(obj: Polyhedron, fileName: String) = { 17 | val writer = new BufferedWriter(new FileWriter(fileName)) 18 | try { 19 | val (points, faces) = obj.indexed 20 | writer.write("g ScadlaObject") 21 | writer.newLine 22 | points.foreach{ p => 23 | writer.write("v " + p.x.to(unit) + " " + p.y.to(unit) + " " + p.z.to(unit)) 24 | writer.newLine 25 | } 26 | writer.newLine 27 | faces.foreach{ case (a,b,c) => 28 | writer.write("f " + a + " " + b + " " + c) 29 | writer.newLine 30 | } 31 | writer.newLine 32 | } finally writer.close 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/RendererAux.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends 2 | 3 | import scadla._ 4 | import squants.space.{Length, Millimeters, LengthUnit} 5 | 6 | abstract class RendererAux[A](unit: LengthUnit = Millimeters) extends Renderer(unit) { 7 | 8 | def shape(s: Shape): A 9 | def operation(o: Operation, args: Seq[A]): A 10 | def transform(t: Transform, arg: A): A 11 | 12 | def render(s: Solid): A = s match { 13 | case s: Shape => shape(s) 14 | case o: Operation => operation(o, o.children.map(render)) 15 | case t: Transform => transform(t, render(t.child)) 16 | } 17 | 18 | def toMesh(aux: A): Polyhedron 19 | 20 | def apply(s: Solid): Polyhedron = { 21 | val a = render(s) 22 | toMesh(a) 23 | } 24 | 25 | } 26 | 27 | class RendererAuxAdapter(r: Renderer) extends RendererAux[Polyhedron] { 28 | 29 | def shape(s: Shape): Polyhedron = r(s) 30 | def operation(o: Operation, args: Seq[Polyhedron]) = r(o.setChildren(args)) 31 | def transform(t: Transform, arg: Polyhedron) = r(t.setChild(arg)) 32 | 33 | def toMesh(aux: Polyhedron): Polyhedron = aux 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/scadla/assembly/Part.scala: -------------------------------------------------------------------------------- 1 | package scadla.assembly 2 | 3 | import scadla._ 4 | import scadla.backends.Renderer 5 | 6 | //final for pickling 7 | 8 | //TODO immutable 9 | final class Part(val name: String, val model: Solid, val printableModel: Option[Solid] = None) { 10 | 11 | var description: String = "" 12 | 13 | var vitamin = false 14 | 15 | //TODO should be protected but serialization ... 16 | var poly: Polyhedron = null 17 | var polyPrint: Polyhedron = null 18 | 19 | def preRender(r: Renderer): Unit = { 20 | if (poly == null) { 21 | poly = r(model) 22 | polyPrint = printableModel.map(r(_)).getOrElse(poly) 23 | } 24 | } 25 | 26 | def mesh = { 27 | if (poly != null) { 28 | poly 29 | } else { 30 | sys.error("Part not yet rendered.") 31 | } 32 | } 33 | 34 | def printable = { 35 | if (polyPrint != null) { 36 | polyPrint 37 | } else { 38 | sys.error("Part not yet rendered.") 39 | } 40 | } 41 | 42 | override def equals(any: Any) = { 43 | if (any.isInstanceOf[Part]) { 44 | val p = any.asInstanceOf[Part] 45 | name == p.name && model == p.model 46 | } else false 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/extrusion/T.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.extrusion 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scadla.utils._ 6 | import squants.space.Length 7 | 8 | object T { 9 | 10 | /* 11 | * |d| 12 | * ─ ┌─┐ 13 | * │ │ 14 | * h │ │ 15 | * ┌──┘ └──┐ 16 | * ─ └───────┘ 17 | * | b | 18 | */ 19 | def apply(b: Length, h: Length, d: Length)(length: Length): Solid = { 20 | apply(b, h, d, d)(length) 21 | } 22 | 23 | /* 24 | * |t| 25 | * ─ ┌─┐ 26 | * │ │ 27 | * h │ │ 28 | * ┌──┘ └──┐ ─ 29 | * ─ └───────┘ ─ d 30 | * | b | 31 | */ 32 | def apply(b: Length, h: Length, d: Length, t: Length)(length: Length): Solid = { 33 | val m = (b-t)/2 34 | apply(m, m, h, d, t)(length) 35 | } 36 | 37 | /* 38 | * |t| 39 | * ─ ┌─┐ 40 | * │ │ 41 | * h │ │ 42 | * ┌──┘ └──┐ ─ 43 | * ─ └───────┘ ─ d 44 | * |b1|t|b2| 45 | */ 46 | def apply(b1: Length, b2: Length, h: Length, d: Length, t: Length)(length: Length): Solid = { 47 | val c1 = Cube(t, h, length) 48 | val c2 = Cube(b1+t+b2, d, length) 49 | c1.moveX(b1) + c2 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/Pulley.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import math._ 4 | import scadla._ 5 | import utils._ 6 | import InlineOps._ 7 | import Common._ 8 | import scadla.EverythingIsIn.{millimeters, radians} 9 | 10 | object Pulley { 11 | 12 | /** A pulley for a 5mm D shaft (stepper motor shaft) 13 | * @param radiusO outer radius 14 | * @param radiusI inner radius (groove) 15 | * @param n number of grooves 16 | * @param h0 height of the outer discs 17 | * @param h1 height of the transition cones 18 | * @param h2 height of the inner discs 19 | */ 20 | def apply(radiusO: Double, radiusI: Double, n: Int, h0: Double, h1: Double, h2: Double) = { 21 | val outerDisc = Cylinder(radiusO, h0) 22 | val innerDisc = Cylinder(radiusI, h2) 23 | val cone1 = Cylinder(radiusO, radiusI, h1) 24 | val cone2 = Cylinder(radiusI, radiusO, h1) 25 | val h = h0+h1+h2+h1 26 | val groove = Union( 27 | outerDisc, 28 | cone1.moveZ(h0), 29 | innerDisc.moveZ(h0+h1), 30 | cone2.moveZ(h0+h1+h2), 31 | outerDisc.moveZ(h) 32 | ) 33 | val grooves = (0 until n).map( i => groove.moveZ(i * h)) 34 | Union(grooves: _*) - Bigger(Nema17.axis(n * h + h0), looseTolerance) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/extrusion/U.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.extrusion 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scadla.utils._ 6 | import squants.space.Length 7 | 8 | object U { 9 | 10 | /* 11 | * ─ ┌─┐ ┌─┐ 12 | * │ │ │ │ 13 | * h │ │ │ │ 14 | * │ └───┘ │ ─ 15 | * ─ └───────┘ ─ d 16 | * | b | 17 | */ 18 | def apply(b: Length, h: Length, d: Length)(length: Length): Solid = { 19 | apply(b, h, d, d)(length) 20 | } 21 | 22 | /* 23 | * |t| 24 | * ─ ┌─┐ ┌─┐ 25 | * │ │ │ │ 26 | * h │ │ │ │ 27 | * │ └───┘ │ ─ 28 | * ─ └───────┘ ─ d 29 | * | b | 30 | */ 31 | def apply(b: Length, h: Length, d: Length, t: Length)(length: Length): Solid = { 32 | apply(b, h, d, t, t)(length) 33 | } 34 | 35 | /* 36 | * t1 t2 37 | * | | | | 38 | * ─ ┌─┐ ┌─┐ 39 | * │ │ │ │ 40 | * h │ │ │ │ 41 | * │ └───┘ │ ─ 42 | * ─ └───────┘ ─ d 43 | * | b | 44 | */ 45 | def apply(b: Length, h: Length, d: Length, t1: Length, t2: Length)(length: Length): Solid = { 46 | val c1 = Cube( b, d, length) 47 | val c2 = Cube( t1, h, length) 48 | val c3 = Cube( t2, h, length) 49 | c1 + c2 + c3.moveX(b-t2) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /THIRDPARTY-LICENSES.txt: -------------------------------------------------------------------------------- 1 | Some of the code in `src/main/scala/scadla/backends/x3d/X3DomViewer.scala` 2 | has been copied from [X3DOM Component Editor](https://github.com/x3dom/component-editor) 3 | which is distibuted under MIT license: 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test/scala/scadla/backends/OpenSCADTest.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends 2 | 3 | import scadla._ 4 | import org.scalatest.funsuite.AnyFunSuite 5 | import scadla.EverythingIsIn.{millimeters, radians} 6 | 7 | 8 | class OpenSCADTest extends AnyFunSuite { 9 | 10 | test("rendering a cube") { 11 | if (OpenSCAD.isPresent) { 12 | val obj = OpenSCAD(Cube(1, 1, 1)) 13 | assert(obj.faces.size == 12) 14 | } else { 15 | // skip 16 | } 17 | } 18 | 19 | //test("rendering 1a") { 20 | // val obj = OpenSCADnightly(Sphere(1.0)) 21 | // x3d.X3DomViewer(obj) 22 | //} 23 | 24 | //test("rendering 1b") { 25 | // OpenSCAD.view(Sphere(1.0)) 26 | //} 27 | 28 | //test("rendering 2a") { 29 | // val obj = Intersection( 30 | // Union( 31 | // Cube(1,1,1), 32 | // Translate( -0.5, -0.5, 0, Cube(1,1,1)) 33 | // ), 34 | // Sphere(1.5) 35 | // ) 36 | // val mesh = OpenSCADnightly(obj) 37 | // x3d.X3DomViewer(mesh) 38 | //} 39 | 40 | //test("rendering 2b") { 41 | // val c = Cube(1,1,1) 42 | // val s = Sphere(1.5) 43 | // val u = Union(c, Translate( -0.5, -0.5, 0, c)) 44 | // val obj = Intersection(u, s) 45 | // OpenSCAD.view(obj) 46 | //} 47 | 48 | //test("rendering 2c") { 49 | // import InlineOps._ 50 | // val c = Cube(1,1,1) 51 | // val s = Sphere(1.5) 52 | // val obj = (c + c.move( -0.5, -0.5, 0)) * s 53 | // OpenSCAD.view(obj) 54 | //} 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/Renderer.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends 2 | 3 | import scadla._ 4 | import java.io._ 5 | import squants.space.{Millimeters, Length, LengthUnit} 6 | 7 | abstract class Renderer(unit: LengthUnit = Millimeters) { 8 | 9 | protected def length2Double(l: Length): Double = l to unit 10 | 11 | def apply(s: Solid): Polyhedron 12 | 13 | /** Check if all the Operations are supported. 14 | * Not all backends support all the operations. 15 | * By default all the transforms and the CSG operations should be supported 16 | */ 17 | def isSupported(s: Solid): Boolean = s match { 18 | case Cube(_,_,_) | Sphere(_) | Cylinder(_, _, _) | FromFile(_, _) | Empty | Polyhedron(_) => true 19 | case t: Transform => isSupported(t.child) 20 | case u @ Union(_) => u.children.forall(isSupported) 21 | case i @ Intersection(_) => i.children.forall(isSupported) 22 | case d @ Difference(_,_) => d.children.forall(isSupported) 23 | case _ => false 24 | } 25 | 26 | def toSTL(s: Solid, fileName: String): Unit = { 27 | val p = apply(s) 28 | stl.Printer.storeBinary(p, fileName) 29 | //stl.Printer.storeText(p, fileName) 30 | } 31 | 32 | def view(s: Solid) = Viewer.default(apply(s)) 33 | 34 | } 35 | 36 | object Renderer { 37 | 38 | def default: Renderer = { 39 | if (OpenSCADnightly.isPresent) OpenSCADnightly 40 | else if (OpenSCAD.isPresent) OpenSCAD 41 | else JCSG 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/scadla/assembly/Frame.scala: -------------------------------------------------------------------------------- 1 | package scadla.assembly 2 | 3 | import scadla._ 4 | import squants.space.Millimeters 5 | 6 | 7 | 8 | case class Frame(translation: Vector, orientation: Quaternion) { 9 | 10 | def compose(f: Frame): Frame = { 11 | val t = translation + orientation.rotate(f.translation) 12 | val o = orientation * f.orientation //TODO is that the right order 13 | Frame(t, o) 14 | } 15 | 16 | def inverse: Frame = { 17 | val o = orientation.inverse 18 | val t = o.rotate(translation) 19 | Frame(t, o) //TODO does that make sense ? 20 | } 21 | 22 | def toRefence(s: Solid): Solid = { 23 | Rotate(orientation, Translate(translation, s)) 24 | } 25 | 26 | def fromReference(s: Solid): Solid = { 27 | Translate(translation * -1, Rotate(orientation.inverse, s)) 28 | } 29 | 30 | def directTo(p: Point): Point = ((p.toVector + translation).rotateBy(orientation)).toPoint 31 | 32 | def directTo(p: Polyhedron): Polyhedron = { 33 | Polyhedron(p.faces.map{ case Face(p1, p2, p3) => 34 | Face(directTo(p1), directTo(p2), directTo(p3)) 35 | }) 36 | } 37 | 38 | def directFrom(p: Polyhedron): Polyhedron = inverse.directTo(p) 39 | 40 | } 41 | 42 | object Frame { 43 | def apply(t: Vector): Frame = Frame(t, Quaternion(1,0,0,0, t.unit)) 44 | def apply(q: Quaternion): Frame = Frame(Vector(0,0,0, q.unit), q) 45 | def apply(): Frame = Frame(Vector(0,0,0,Millimeters), Quaternion(1,0,0,0,Millimeters)) 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/PieSlice.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scadla.utils.Trig._ 6 | import squants.space.{Length, Angle} 7 | import scala.language.postfixOps 8 | import squants.space.LengthConversions._ 9 | 10 | object PieSlice { 11 | 12 | def apply(outerRadius: Length, innerRadius: Length, angle: Angle, height: Length) = { 13 | val o1 = outerRadius + (1 mm) 14 | val h1 = height + (1 mm) 15 | val blocking_half = Cube(2* o1, o1, h1).move(-o1, -o1, -0.5 mm) 16 | val blocking_quarter = Cube(o1, o1, h1).move(0 mm, 0 mm, -0.5 mm) 17 | if (angle.value <= 0) { 18 | Empty 19 | } else { 20 | val block = 21 | if (angle <= Pi/2) { 22 | Union( 23 | blocking_half, 24 | blocking_quarter.move(-o1, -0.5 mm, 0 mm), 25 | blocking_quarter.rotateZ(angle) 26 | ) 27 | } else if (angle <= Pi) { 28 | Union( 29 | blocking_half, 30 | blocking_quarter.rotateZ(angle) 31 | ) 32 | } else if (angle <= 3*Pi/2) { 33 | Union( 34 | blocking_quarter.moveY(-o1), 35 | blocking_quarter.rotateZ(angle) 36 | ) 37 | } else if (angle <= 2*Pi) { 38 | Intersection( 39 | blocking_quarter.moveY(-o1), 40 | blocking_quarter.rotateZ(angle) 41 | ) 42 | } else { 43 | Empty 44 | } 45 | val t = Tube(outerRadius, innerRadius, height) 46 | Difference(t, block) 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/extrusion/C.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.extrusion 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scadla.utils._ 6 | import squants.space.Length 7 | import scala.language.postfixOps 8 | import squants.space.LengthConversions._ 9 | 10 | object C { 11 | 12 | /* 13 | * | c | 14 | * ─ ┌──┐ ┌──┐ 15 | * │ ┌┘ └┐ │ 16 | * b │ │ │ │ 17 | * │ └─────┘ │ ─ 18 | * ─ └─────────┘ ─ d 19 | * | a | 20 | */ 21 | def apply(a: Length, b: Length, c: Length, d: Length)(length: Length): Solid = { 22 | apply(a, b, c, d, d)(length) 23 | } 24 | 25 | /* 26 | * |t|| c | 27 | * ─ ┌──┐ ┌──┐ 28 | * │ ┌┘ └┐ │ 29 | * b │ │ │ │ 30 | * │ └─────┘ │ ─ 31 | * ─ └─────────┘ ─ d 32 | * | a | 33 | */ 34 | def apply(a: Length, b: Length, c: Length, d: Length, t: Length)(length: Length): Solid = { 35 | apply(a, b, c, d, d, t, t)(length) 36 | } 37 | 38 | /* 39 | * t1 t2 40 | * | || c || | 41 | * ─ ┌──┐ ┌──┐ ─ 42 | * │ ┌┘ └┐ │ ─ d2 43 | * b │ │ │ │ 44 | * │ └─────┘ │ ─ 45 | * ─ └─────────┘ ─ d1 46 | * | a | 47 | */ 48 | def apply(a: Length, b: Length, c: Length, d1: Length, d2: Length, t1: Length, t2: Length)(length: Length): Solid = { 49 | val c1 = Cube( a, d1, length) 50 | val c2 = Cube(t1, b, length) 51 | val c3 = Cube(t2, b, length) 52 | val c4 = Cube( (a-c)/2, d2, length) 53 | c1 + c2 + c3.moveX(a-t2) + c4.moveY(b-d2) + c4.move(a/2+c/2, b-d2, 0 mm) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/amf/Printer.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends.amf 2 | 3 | import scadla._ 4 | import dzufferey.utils._ 5 | import dzufferey.utils.LogLevel._ 6 | import scala.xml._ 7 | import squants.space.{LengthUnit, Millimeters, Microns, Meters, Inches} 8 | 9 | object Printer extends Printer(Millimeters) { 10 | } 11 | 12 | class Printer(unit: LengthUnit = Millimeters) { 13 | 14 | val targetUnit: String = unit match { 15 | case Millimeters => "millimeter" 16 | case Meters => "meter" 17 | case Microns => "micrometer" 18 | case Inches => "inch" 19 | case other => Logger.logAndThrow("amf.Printer", Error, "unsupported unit: " + other) 20 | } 21 | 22 | def store(obj: Polyhedron, fileName: String) = { 23 | val (points, faces) = obj.indexed 24 | val pointNodes = 25 | new Group(points.map{ p => 26 | {p.x.to(unit)}{p.y.to(unit)}{p.z.to(unit)} 27 | }) 28 | val faceNodes = 29 | new Group(faces.map{ case (a,b,c) => 30 | {a}{b}{c} 31 | }.toSeq) 32 | val node = 33 | 34 | Scadla 35 | 36 | 37 | 38 | { pointNodes } 39 | 40 | 41 | { faceNodes } 42 | 43 | 44 | 45 | 46 | XML.save(fileName, node, "UTF-8", true) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/extrusion/Z.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.extrusion 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scadla.utils._ 6 | import squants.space.Length 7 | 8 | object Z { 9 | 10 | /* 11 | * ─ ┌────┐ 12 | * └──┐ │ 13 | * h │ │ 14 | * │ └──┐ ─ 15 | * ─ └────┘ ─ d 16 | * | b | 17 | */ 18 | def apply(b: Length, h: Length, d: Length)(length: Length): Solid = { 19 | val m = (b+d) / 2 20 | apply(m, m, h, d, d)(length) 21 | } 22 | 23 | /* 24 | * | b1 | 25 | * ─ ┌────┐ 26 | * └──┐ │ 27 | * h │ │ 28 | * │ └──┐ ─ 29 | * ─ └────┘ ─ d 30 | * | b2 | 31 | */ 32 | def apply(b1: Length, b2: Length, h: Length, d: Length)(length: Length): Solid = { 33 | apply(b1, b2, h, d, d)(length) 34 | } 35 | 36 | /* 37 | * |t| 38 | * | b1 | 39 | * ─ ┌────┐ 40 | * └──┐ │ 41 | * h │ │ 42 | * │ └──┐ ─ 43 | * ─ └────┘ ─ d 44 | * | b2 | 45 | */ 46 | def apply(b1: Length, b2: Length, h: Length, d: Length, t: Length)(length: Length): Solid = { 47 | apply(b1, b2, h, d, d, t)(length) 48 | } 49 | 50 | /* 51 | * |t| 52 | * | b1 | 53 | * ─ ─ ┌────┐ 54 | * d1 ─ └──┐ │ 55 | * h │ │ 56 | * │ └──┐ ─ 57 | * ─ └────┘ ─ d2 58 | * | b2 | 59 | */ 60 | def apply(b1: Length, b2: Length, h: Length, d1: Length, d2: Length, t: Length)(length: Length): Solid = { 61 | val c1 = Cube(b1, d1, length) 62 | val c2 = Cube( t, h, length) 63 | val c3 = Cube(b2, d2, length) 64 | c1.moveY(h-d1) + c2.moveX(b1-t) + c3.moveX(b1-t) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/ply/Printer.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends.ply 2 | 3 | import scadla._ 4 | import dzufferey.utils._ 5 | import dzufferey.utils.LogLevel._ 6 | import java.io._ 7 | import java.nio.{ByteBuffer,ByteOrder} 8 | import java.nio.channels.FileChannel 9 | import squants.space.{Length, Millimeters, LengthUnit} 10 | 11 | object Printer extends Printer(Millimeters) { 12 | } 13 | 14 | class Printer(unit: LengthUnit = Millimeters) { 15 | 16 | def printHeader(writer: BufferedWriter, nbrVertex: Int, nbrFace: Int): Unit = { 17 | writer.write("ply"); writer.newLine 18 | writer.write("format ascii 1.0"); writer.newLine 19 | writer.write("element vertex " + nbrVertex); writer.newLine 20 | writer.write("property float x"); writer.newLine 21 | writer.write("property float y"); writer.newLine 22 | writer.write("property float z"); writer.newLine 23 | writer.write("element face " + nbrFace); writer.newLine 24 | writer.write("property list uchar uint vertex_indices"); writer.newLine 25 | writer.write("end_header"); writer.newLine 26 | } 27 | 28 | def store(obj: Polyhedron, fileName: String) = { 29 | val (points, faces) = obj.indexed 30 | val writer = new BufferedWriter(new FileWriter(fileName)) 31 | try { 32 | printHeader(writer, points.length, faces.size) 33 | points.foreach{ p => 34 | val x = p.x.to(unit) 35 | val y = p.y.to(unit) 36 | val z = p.z.to(unit) 37 | writer.write(s"$x $y $z") 38 | writer.newLine 39 | } 40 | faces.foreach{ case (a,b,c) => 41 | writer.write("3 " + a + " " + b + " " + c) 42 | writer.newLine 43 | } 44 | } finally writer.close 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/WhiteboardMarkerHolder.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples 2 | 3 | import scadla._ 4 | import utils._ 5 | import Trig._ 6 | import InlineOps._ 7 | import squants.space.{Length, Angle} 8 | import scala.language.postfixOps 9 | import squants.space.LengthConversions._ 10 | 11 | object WhiteboardMarkerHolder { 12 | 13 | val penRadius = 10 mm 14 | 15 | val numberPen = 4 16 | 17 | val thickness = 3 mm 18 | 19 | val magnetRadius = 12 mm 20 | val magnetHeight = 5 mm 21 | 22 | val tolerance = 0.2 mm 23 | 24 | protected def hook = { 25 | PieSlice(penRadius + thickness, penRadius, 2*Pi/3, 20 mm) 26 | } 27 | 28 | def top = { 29 | val mrt = magnetRadius + thickness 30 | val beam1 = Cube( 90 mm, 10 mm, thickness).move(-45 mm, -mrt, thickness) 31 | val beam2 = Cube( 50 mm, (10 mm) + thickness, 2*thickness).move(-25 mm, -mrt, 0 mm) 32 | val center = Hull( 33 | Cylinder(mrt, 2*thickness), 34 | Cube(2*mrt, 1 mm, 2*thickness).move(-mrt, -mrt, 0 mm) 35 | ) 36 | val magnet = Cylinder(magnetRadius + tolerance, magnetHeight + tolerance) 37 | beam1 + beam2 + center - magnet 38 | } 39 | 40 | def side = { 41 | val penSpace = 2.5 * penRadius 42 | val base = Cube(penSpace * numberPen + (10 mm), thickness, 20 mm) 43 | val hooks = (1 to numberPen).map( i => hook.rotateZ(-Pi/3).move(i * penSpace, 11.5 mm, 0 mm) ) 44 | val lip = Cube(thickness, 2 * thickness, 20 mm) 45 | base ++ hooks + lip + lip.moveX((10 mm) + thickness + tolerance) 46 | } 47 | 48 | def main(args: Array[String]): Unit = { 49 | val r = backends.Renderer.default 50 | r.toSTL(top.rotateX(Pi), "wbh_top.stl") // 1x 51 | r.toSTL(side, "wbh_side.stl") // 2x 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/Trapezoid.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils 2 | 3 | import scadla._ 4 | import squants.space.{Length, Angle, Millimeters, Radians} 5 | 6 | object Trapezoid { 7 | 8 | def apply(x: (Length, Length, Angle) /*top, bottom, skew*/, 9 | y: (Length, Length, Angle) /*top, bottom, skew*/, 10 | z: Length): Polyhedron = { 11 | val (xTop, xBottom, xSkew) = x 12 | val xSkewOffest = z * math.tan(xSkew.toRadians) 13 | val dx = (xBottom-xTop)/2 14 | val (yTop, yBottom, ySkew) = y 15 | val ySkewOffest = z * math.tan(ySkew.toRadians) 16 | val dy = (yBottom-yTop)/2 17 | val O = Millimeters(0) 18 | val xTop0 = dx + xSkewOffest 19 | val xTop1 = xBottom - dx + xSkewOffest 20 | val yTop0 = dy + ySkewOffest 21 | val yTop1 = yBottom - dy + ySkewOffest 22 | val pts = Seq( 23 | Point(O, O, O), 24 | Point(xBottom, O, O), 25 | Point(xBottom, yBottom, O), 26 | Point(O, yBottom, O), 27 | Point(xTop0, yTop0, z), 28 | Point(xTop1, yTop0, z), 29 | Point(xTop1, yTop1, z), 30 | Point(xTop0, yTop1, z) 31 | ) 32 | def face(a: Int, b: Int, c: Int) = Face(pts(a), pts(b), pts(c)) 33 | val faces = Seq( 34 | face(0,3,1), 35 | face(1,3,2), 36 | face(4,5,7), 37 | face(5,6,7), 38 | face(0,1,4), 39 | face(1,5,4), 40 | face(1,2,5), 41 | face(2,6,5), 42 | face(2,3,6), 43 | face(3,7,6), 44 | face(3,0,7), 45 | face(0,4,7) 46 | ) 47 | Polyhedron(faces) 48 | } 49 | 50 | def apply(xTop: Length, xBottom: Length, y: Length, z: Length, skew: Angle = Radians(0.0)): Polyhedron = { 51 | val _x = (xTop, xBottom, skew) 52 | val _y = (y, y, Radians(0.0)) 53 | apply(_x, _y, z) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/amf/Parser.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends.amf 2 | 3 | import scadla._ 4 | import scala.util.parsing.combinator._ 5 | import dzufferey.utils._ 6 | import dzufferey.utils.LogLevel._ 7 | import scala.xml._ 8 | import squants.space.{LengthUnit, Millimeters, Microns, Meters, Inches} 9 | 10 | object Parser { 11 | 12 | def apply(fileName: String): Polyhedron = { 13 | val amf = XML.loadFile(fileName) 14 | val unit: LengthUnit = amf \@ "unit" match { 15 | case "millimeter" | "" | null => Millimeters 16 | case "meter" => Meters 17 | case "micrometer" => Microns 18 | case "inch" => Inches 19 | case other => Logger.logAndThrow("amf.Parser", Error, "unkown unit: " + other) 20 | } 21 | def parseVertex(v: Node): Point = { 22 | val coord = v \ "coordinates" 23 | val x = (coord \ "x").text.toDouble 24 | val y = (coord \ "y").text.toDouble 25 | val z = (coord \ "z").text.toDouble 26 | Point(unit(x),unit(y),unit(z)) 27 | } 28 | def parseFace(triangle: Node): (Int, Int, Int) = { 29 | val a = (triangle \ "v1").text.toInt 30 | val b = (triangle \ "v2").text.toInt 31 | val c = (triangle \ "v3").text.toInt 32 | (a, b, c) 33 | } 34 | val meshes = amf \ "object" \ "mesh" 35 | if (meshes.size == 0) { 36 | Logger.logAndThrow("amf.Parser", Error, "no mesh found") 37 | } 38 | if (meshes.size > 1) { 39 | Logger("amf.Parser", Warning, "more than one mesh. taking only the first.") 40 | } 41 | val mesh = meshes.head 42 | val vertex = (mesh \ "vertices" \ "vertex").map(parseVertex) 43 | val faces = (mesh \ "volume" \ "triangle").map( t => 44 | parseFace(t) match { 45 | case (a,b,c) => Face(vertex(a), vertex(b), vertex(c)) 46 | }) 47 | Polyhedron(faces) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/Chuck.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import scadla._ 4 | import utils._ 5 | import InlineOps._ 6 | import thread._ 7 | import Common._ 8 | import scadla.EverythingIsIn.{millimeters, radians} 9 | import squants.space.Length 10 | import scala.language.postfixOps 11 | 12 | object Chuck { 13 | 14 | protected def doubleHex(radius: Length, height: Length) = { 15 | val h = Hexagon(radius, height) 16 | Union(h, h.rotateZ(30°)) 17 | } 18 | 19 | //TODO some more parameters 20 | //assumes M8 middle 21 | def innerThread( outerRadius: Length, 22 | innerHole: Length, 23 | chuckHeight: Length, 24 | colletLength: Length, 25 | mNumber: Length ) = { 26 | val shaft = ISO.M8 + tightTolerance //TODO add more/less tolerance ??? 27 | val splitWasher = 2 28 | val nutHeight = nut.height(shaft) 29 | val body = Union( 30 | Cylinder(outerRadius, chuckHeight - 5).moveZ(5), 31 | doubleHex(Hexagon.minRadius(outerRadius).toMillimeters, 5) 32 | ) 33 | val toRemove = List( 34 | threading.screwThreadIsoInner(mNumber, colletLength), 35 | Cylinder(shaft, chuckHeight), 36 | nut(shaft).moveZ(colletLength + splitWasher + nutHeight), 37 | Cylinder( innerHole, colletLength + splitWasher + nutHeight), 38 | Cylinder( innerHole+1, innerHole, colletLength) 39 | ) 40 | body -- toRemove 41 | } 42 | 43 | //TODO some more parameters 44 | def wrench(outerRadius: Length) = { 45 | val wall = 5 46 | val hex = doubleHex(Hexagon.minRadius(outerRadius).toMillimeters + looseTolerance, 5) 47 | val head = Cylinder(outerRadius + wall, 5) 48 | val handle = RoundedCubeH(outerRadius*6, outerRadius*2, 5, 3) 49 | head + handle.moveY(-outerRadius) - hex 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/resources/unit_cube_ascii.stl: -------------------------------------------------------------------------------- 1 | solid OpenSCAD_Model 2 | facet normal -1 0 0 3 | outer loop 4 | vertex 0 0 1 5 | vertex 0 1 1 6 | vertex 0 0 0 7 | endloop 8 | endfacet 9 | facet normal -1 0 0 10 | outer loop 11 | vertex 0 0 0 12 | vertex 0 1 1 13 | vertex 0 1 0 14 | endloop 15 | endfacet 16 | facet normal 0 0 1 17 | outer loop 18 | vertex 0 0 1 19 | vertex 1 0 1 20 | vertex 1 1 1 21 | endloop 22 | endfacet 23 | facet normal 0 0 1 24 | outer loop 25 | vertex 0 1 1 26 | vertex 0 0 1 27 | vertex 1 1 1 28 | endloop 29 | endfacet 30 | facet normal 0 -1 0 31 | outer loop 32 | vertex 0 0 0 33 | vertex 1 0 0 34 | vertex 1 0 1 35 | endloop 36 | endfacet 37 | facet normal 0 -1 0 38 | outer loop 39 | vertex 0 0 1 40 | vertex 0 0 0 41 | vertex 1 0 1 42 | endloop 43 | endfacet 44 | facet normal 0 0 -1 45 | outer loop 46 | vertex 0 1 0 47 | vertex 1 1 0 48 | vertex 0 0 0 49 | endloop 50 | endfacet 51 | facet normal 0 0 -1 52 | outer loop 53 | vertex 0 0 0 54 | vertex 1 1 0 55 | vertex 1 0 0 56 | endloop 57 | endfacet 58 | facet normal 0 1 0 59 | outer loop 60 | vertex 0 1 1 61 | vertex 1 1 1 62 | vertex 0 1 0 63 | endloop 64 | endfacet 65 | facet normal 0 1 0 66 | outer loop 67 | vertex 0 1 0 68 | vertex 1 1 1 69 | vertex 1 1 0 70 | endloop 71 | endfacet 72 | facet normal 1 0 0 73 | outer loop 74 | vertex 1 0 0 75 | vertex 1 1 0 76 | vertex 1 1 1 77 | endloop 78 | endfacet 79 | facet normal 1 0 0 80 | outer loop 81 | vertex 1 0 1 82 | vertex 1 0 0 83 | vertex 1 1 1 84 | endloop 85 | endfacet 86 | endsolid OpenSCAD_Model 87 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/gear/HerringboneGear.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.gear 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scala.math._ 6 | import squants.space.{Length, Angle, Radians} 7 | 8 | object HerringboneGear { 9 | 10 | /** Create an herringbone gear (from two helical gears of opposite rotations) 11 | * @param pitch the effective radius of the gear 12 | * @param nbrTeeth the number of tooth in the gear 13 | * @param pressureAngle the angle between meshing gears at the pitch radius (0 mean "square" tooths, π/2 no tooths) 14 | * @param addenum how much to add to the pitch to get the outer radius of the gear 15 | * @param dedenum how much to remove to the pitch to get the root radius of the gear 16 | * @param height the height of the gear 17 | * @param helixAngle how much twisting (in rad / mm) 18 | * @param backlash add some space (manufacturing tolerance) 19 | * @param skew generate a gear with an asymmetric profile by skewing the tooths 20 | */ 21 | def apply( pitch: Length, 22 | nbrTeeth: Int, 23 | pressureAngle: Double, 24 | addenum: Length, 25 | dedenum: Length, 26 | height: Length, 27 | twist: Twist, 28 | backlash: Length, 29 | skew: Angle = Radians(0.0)) = { 30 | val stepped = InvoluteGear.stepped(pitch, nbrTeeth, pressureAngle, addenum, dedenum, height, backlash, skew) 31 | def turnPoint(p: Point): Point = { 32 | val z = p.z 33 | val a = if (z < height/2) twist.angle * (z / twist.increment) 34 | else twist.angle * ((height - z) / twist.increment) 35 | val x = p.x * a.cos - p.y * a.sin 36 | val y = p.x * a.sin + p.y * a.cos 37 | Point(x, y, z) 38 | } 39 | Polyhedron(stepped.faces.map( f => Face(turnPoint(f.p1), turnPoint(f.p2), turnPoint(f.p3)) )) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/MetricThreadDemo.scala: -------------------------------------------------------------------------------- 1 | package scadla.example 2 | 3 | import scadla._ 4 | import scadla.utils._ 5 | import InlineOps._ 6 | import scadla.utils.thread._ 7 | import scala.language.postfixOps 8 | import squants.space.LengthConversions._ 9 | 10 | object MetricThreadDemo { 11 | 12 | //TODO some screws by name 13 | 14 | def renderingOption = List("$fn=30;") 15 | def renderer = new backends.OpenSCAD(renderingOption) 16 | 17 | def demo = { 18 | 19 | val m = new MetricThread() 20 | 21 | val set = Map( 22 | 1-> (1 mm), 23 | 2-> (2 mm), 24 | 3-> (3 mm), 25 | 4-> (4 mm), 26 | 5-> (1 mm), 27 | 6-> (8 mm), 28 | 7-> (6 mm), 29 | 8-> (5 mm) 30 | ) 31 | 32 | def boltAndNut(x: Int, y: Int) = { 33 | val i = 4*x + y 34 | val di = set(i) 35 | val bolt = m.hexScrewIso( di, di*2, 2, di*0.6, 0 mm, di/1.7) 36 | val nut = m.hexNutIso(di, di/1.2) 37 | val pair = bolt + nut.moveZ( di/2 + di*0.6 + 6*m.getIsoPitch(di)) 38 | pair.translate(x*12 mm, (y-1)*12 + x*(y-2)*8 mm, 0 mm) 39 | } 40 | 41 | val all = for (x <- 0 to 1; y <- 1 to 4) yield boltAndNut(x, y) 42 | Union(all:_*) 43 | } 44 | 45 | def test = { 46 | val m = new MetricThread() 47 | //m.hexHead(4, 8) 48 | m.hexScrewIso( 8 mm, 12.5 mm, 2, 7.5 mm, 0 mm, 3 mm) 49 | //m.hexNutIso( 8, 4) 50 | //m.screwThreadIsoOuter( 8, 10, 2) 51 | //m.screwThreadIsoInner( 8, 6) 52 | } 53 | 54 | def main(args: Array[String]): Unit = { 55 | //val obj = test 56 | val obj = demo 57 | //render and display the wheel 58 | //backends.OpenSCAD.saveFile("test.scad", obj, renderingOption) 59 | Console.println("rendering set of metric ISO bolts and nuts. This may take a while ...") 60 | //new backends.ParallelRenderer(renderer).toSTL(obj, "metric_threads.stl") 61 | renderer.view(obj) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/gear/HelicalGear.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.gear 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scala.math._ 6 | import squants.space.{Angle, Radians, Length} 7 | 8 | object HelicalGear { 9 | 10 | /** Create an helical gear by twisting an spur gear. 11 | * the following tranform is applied: 12 | * x′ = x * cos(helixAngle * z) - y * sin(helixAngle * z) 13 | * y′ = x * sin(helixAngle * z) + y * cos(helixAngle * z) 14 | * z′ = z 15 | * @param pitch the effective radius of the gear 16 | * @param nbrTeeth the number of tooth in the gear 17 | * @param pressureAngle the angle between meshing gears at the pitch radius (0 mean "square" tooths, π/2 no tooths) 18 | * @param addenum how much to add to the pitch to get the outer radius of the gear 19 | * @param dedenum how much to remove to the pitch to get the root radius of the gear 20 | * @param height the height of the gear 21 | * @param twist how much twisting 22 | * @param backlash add some space (manufacturing tolerance) 23 | * @param skew generate a gear with an asymmetric profile by skewing the tooths 24 | */ 25 | def apply( pitch: Length, 26 | nbrTeeth: Int, 27 | pressureAngle: Double, 28 | addenum: Length, 29 | dedenum: Length, 30 | height: Length, 31 | twist: Twist, 32 | backlash: Length, 33 | skew: Angle = Radians(0.0)) = { 34 | val stepped = InvoluteGear.stepped(pitch, nbrTeeth, pressureAngle, addenum, dedenum, height, backlash, skew) 35 | def turnPoint(p: Point): Point = { 36 | val z = p.z 37 | val a = twist.angle * (z / twist.increment) 38 | val x = p.x * a.cos - p.y * a.sin 39 | val y = p.x * a.sin + p.y * a.cos 40 | Point(x, y, z) 41 | } 42 | Polyhedron(stepped.faces.map( f => Face(turnPoint(f.p1), turnPoint(f.p2), turnPoint(f.p3)) )) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/box/Interval.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.box 2 | 3 | import scadla._ 4 | import squants.space.Length 5 | import squants.space.Millimeters 6 | 7 | case class Interval(min: Length, max: Length) { 8 | 9 | //TODO consider replacing Interval with QuantityRange, PR missing operators into squants 10 | //TODO better handling of degenerate case where min==max 11 | 12 | def isEmpty = min > max 13 | 14 | def contains(x: Length) = x >= min && x <= max 15 | 16 | def size = (max - min).max(min.unit(0)) 17 | 18 | def contains(i: Interval) = 19 | i.isEmpty || (i.min >= min && i.max <= max) 20 | 21 | def overlaps(i: Interval) = 22 | isEmpty || i.isEmpty || 23 | (min <= i.max && i.min <= max) 24 | 25 | def center = (max - min) / 2 26 | 27 | def move(x: Length) = 28 | if (isEmpty) this 29 | else Interval(x + min, x + max) 30 | 31 | def scale(x: Double) = 32 | if (isEmpty) this 33 | else Interval(x * min, x * max) 34 | 35 | def intersection(b: Interval) = Interval(min.max(b.min), max.min(b.max)) 36 | 37 | def add(b: Interval) = 38 | if (isEmpty || b.isEmpty) Interval.empty 39 | else Interval(min + b.min, max + b.max) 40 | 41 | def hull(b: Interval) = 42 | if (isEmpty) b 43 | else if (b.isEmpty) this 44 | else Interval(min.min(b.min), max.max(b.max)) 45 | 46 | // hull(this \ b) 47 | def remove(b: Interval) = 48 | if (b.isEmpty || b.max <= min || b.min >= max) { 49 | this 50 | } else if (b contains this) { 51 | Interval.empty 52 | } else if (b.max >= max) { 53 | Interval(min, b.min) 54 | } else if (b.min <= min) { 55 | Interval(b.max, max) 56 | } else { 57 | sys.error("bug in remove " + this + ", " + b) 58 | } 59 | 60 | def unionUnderApprox(b: Interval) = 61 | if (overlaps(b)) hull(b) else Interval.empty 62 | 63 | } 64 | 65 | object Interval { 66 | val empty = Interval(Millimeters(1), Millimeters(-1)) 67 | val unit = Interval(Millimeters(0), Millimeters(1)) 68 | } 69 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/SpherePortion.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import math._ 6 | import squants.space.Length 7 | import squants.space.Millimeters 8 | import squants.space.Angle 9 | import squants.space.Radians 10 | 11 | /** same idea as PieSlice with with a sphere */ 12 | object SpherePortion { 13 | 14 | //TODO check https://en.wikipedia.org/wiki/Spherical_coordinate_system for the conventions 15 | 16 | def apply(outerRadius: Length, innerRadius: Length, 17 | inclinationStart: Angle, inclinationEnd: Angle, 18 | azimut: Angle) = { 19 | val i = if (innerRadius.value > 0) Sphere(innerRadius) else Empty 20 | val o1 = outerRadius + Millimeters(1) 21 | val carved = Difference( 22 | Sphere(outerRadius), 23 | pointyThing(o1, inclinationStart), 24 | Mirror(0,0,1, pointyThing(o1, Radians(Pi) - inclinationEnd)), 25 | i 26 | ) 27 | val sliced = Intersection( 28 | carved, 29 | PieSlice(o1, Millimeters(0), azimut, 2*o1).moveZ(-o1) 30 | ) 31 | sliced 32 | } 33 | 34 | def elevation(outerRadius: Length, innerRadius: Length, 35 | elevationStart: Angle, elevationEnd: Angle, 36 | azimut: Angle) = { 37 | apply(outerRadius, innerRadius, Radians(Pi/2) - elevationStart, Radians(Pi/2) - elevationEnd, azimut) 38 | } 39 | 40 | private def pointyThing(radius: Length, inclination: Angle) = { 41 | val c = Cylinder(radius, 2*radius) 42 | val t = inclination.tan 43 | val h = radius / t 44 | if (inclination <= Radians(0)) Empty 45 | else if (inclination <= Radians(Pi/4)) Cylinder(Millimeters(0), radius * t, radius) 46 | else if (inclination < Radians(Pi/2)) Cylinder(Millimeters(0), radius, h) + c.moveZ(h) 47 | else if (inclination == Radians(Pi/2)) c 48 | else if (inclination <= Radians(3*Pi/4)) c.moveZ(h) - Cylinder(radius, Millimeters(0), -h).moveZ(h) 49 | else if (inclination < Radians(Pi)) c.moveZ(-radius) - Cylinder(-h, Millimeters(0), radius).moveZ(-radius) 50 | else c.moveZ(-radius) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/test/scala/scadla/utils/PackageTest.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils 2 | 3 | import scadla._ 4 | import org.scalatest.funsuite.AnyFunSuite 5 | import scadla.EverythingIsIn.{millimeters, radians} 6 | 7 | class PackageTest extends AnyFunSuite { 8 | 9 | def incr(map: Map[Solid, Int], s: Solid) = { 10 | val mult = map.getOrElse(s, 0) + 1 11 | map + (s -> mult) 12 | } 13 | 14 | test("fold 1") { 15 | val c = Cube(1,1,1) 16 | val tc = Translate( -0.5, -0.5, 0, Cube(1,1,1)) 17 | val u = Union(Cube(1,1,1), 18 | Translate( -0.5, -0.5, 0, Cube(1,1,1))) 19 | val s = Sphere(1.5) 20 | val i = Intersection( 21 | Union( 22 | Cube(1,1,1), 23 | Translate( -0.5, -0.5, 0, Cube(1,1,1)) 24 | ), 25 | Sphere(1.5) 26 | ) 27 | val map = fold(incr, Map[Solid, Int](), i) 28 | assert(map(i) == 1) 29 | assert(map(s) == 1) 30 | assert(map(u) == 1) 31 | assert(map(tc) == 1) 32 | assert(map(c) == 2) 33 | } 34 | 35 | test("fold 2") { 36 | val top=Difference( 37 | Difference( 38 | Difference( 39 | Minkowski(Translate(3.25,3.25,0.0,Cube(23.5,23.5,13.0)), Cylinder(3.25,3.25,13.0)), 40 | Translate(15.0,15.0,0.0,Cylinder(10.0,10.0,35.0)) 41 | ), 42 | Translate(0.0,0.0,16.0,Translate(3.25,3.25,0.0,Cylinder(1.38,1.5,10.0))), 43 | Translate(0.0,0.0,16.0,Translate(26.75,3.25,0.0,Cylinder(1.38,1.5,10.0))), 44 | Translate(0.0,0.0,16.0,Translate(3.25,26.75,0.0,Cylinder(1.38,1.5,10.0))), 45 | Translate(0.0,0.0,16.0,Translate(26.75,26.75,0.0,Cylinder(1.38,1.5,10.0))) 46 | ), 47 | Translate(15.0,15.0,26.0,Rotate(1.5707963267948966,0.0,0.0,Scale(0.425,1.0,1.0,Translate(0.0,0.0,-25.0,Cylinder(20.0,20.0,50.0))))), 48 | Translate(15.0,15.0,26.0,Rotate(0.0,1.5707963267948966,0.0,Scale(1.0,0.425,1.0,Translate(0.0,0.0,-25.0,Cylinder(20.0,20.0,50.0))))) 49 | ) 50 | val map = fold(incr, Map[Solid, Int](), top) 51 | assert(map(Cube(23.5,23.5,13.0)) == 1) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/Collet.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import math._ 4 | import scadla._ 5 | import utils._ 6 | import InlineOps._ 7 | import thread._ 8 | import Common._ 9 | import scadla.EverythingIsIn.{millimeters, radians} 10 | import squants.space.Length 11 | 12 | object Collet { 13 | 14 | def slits(outer: Length, height: Length, nbrSlits: Int, slitWidth: Length, wall: Length) = { 15 | assert(nbrSlits % 2 == 0, "number of slits must be even") 16 | val slit = Cube(slitWidth, outer, height - wall).moveX(-slitWidth/2) 17 | for(i <- 0 until nbrSlits) yield slit.rotateZ(i * 2 * Pi / nbrSlits).moveZ((i % 2) * wall) 18 | } 19 | 20 | //example: Collet(6, 7, 3, 20, 6, 1, 2) 21 | def apply(outer1: Length, outer2: Length, inner: Length, height: Length, 22 | nbrSlits: Int, slitWidth: Length, wall: Length) = { 23 | val base = Cylinder(outer1, outer2, height) - Cylinder(inner, height) 24 | base -- slits(outer1 max outer2, height, nbrSlits, slitWidth, wall) 25 | } 26 | 27 | def threaded(outer1: Length, outer2: Length, inner: Length, height: Length, 28 | nbrSlits: Int, slitWidth: Length, wall: Length, 29 | mNumber: Length, screwRadius: Length) = { 30 | val innerC = Cylinder(inner+tolerance, height) 31 | val base = Cylinder(outer1, outer2, height) 32 | val slts = slits(mNumber, height, nbrSlits, slitWidth, wall) 33 | val thread = new MetricThread(tolerance).screwThreadIsoOuter(mNumber, height, 2) 34 | val wrenchHoles = for(i <- 0 until nbrSlits) yield 35 | Cylinder(screwRadius+tolerance, height).moveX((outer2+inner)*2/3).rotateZ( (i*2+1) * Pi / nbrSlits) 36 | thread + base -- slts - innerC -- wrenchHoles 37 | } 38 | 39 | //TODO some more parameters 40 | def wrench(outer: Length, inner: Length, nbrSlits: Int, screwRadius: Length) = { 41 | val base = RoundedCubeH(60, 20, 5, 3).move(-30, -10, 0) 42 | val t = screwRadius //Thread.ISO.M2 43 | val screw = Cylinder(t+tolerance, 5) 44 | val hole = Hull(screw.moveX(inner + t), screw.moveX(outer - t)) 45 | base - Cylinder(3, 5) -- (0 until nbrSlits).map( i => hole.rotateZ((2*i+1) * Pi / nbrSlits) ) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/extrusion/H.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.extrusion 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scadla.utils._ 6 | import squants.space.Length 7 | import scala.language.postfixOps 8 | import squants.space.LengthConversions._ 9 | 10 | //actually: more like a double T 11 | object H { 12 | 13 | /* 14 | * ─ ┌───────┐ 15 | * └──┐ ┌──┘ 16 | * h │ │ 17 | * ┌──┘ └──┐ ─ 18 | * ─ └───────┘ ─ d 19 | * | b | 20 | */ 21 | def apply(b: Length, h: Length, d: Length)(length: Length): Solid = { 22 | apply(b, h, d, d)(length) 23 | } 24 | 25 | 26 | /* 27 | * |t| 28 | * ─ ┌───────┐ 29 | * └──┐ ┌──┘ 30 | * h │ │ 31 | * ┌──┘ └──┐ ─ 32 | * ─ └───────┘ ─ d 33 | * | b | 34 | */ 35 | def apply(b: Length, h: Length, d: Length, t: Length)(length: Length): Solid = { 36 | apply(b, h, d, d, t)(length) 37 | } 38 | 39 | /* 40 | * |t| 41 | * ─ ┌───────┐ ─ 42 | * └──┐ ┌──┘ ─ d1 43 | * h │ │ 44 | * ┌──┘ └──┐ ─ 45 | * ─ └───────┘ ─ d2 46 | * | b | 47 | */ 48 | def apply(b: Length, h: Length, d1: Length, d2: Length, t: Length)(length: Length): Solid = { 49 | apply(b, b, h, d1, d2, t)(length) 50 | } 51 | 52 | /* 53 | * |t| 54 | * | b1 | 55 | * ─ ┌───────┐ ─ 56 | * └──┐ ┌──┘ ─ d1 57 | * h │ │ 58 | * ┌──┘ └──┐ ─ 59 | * ─ └───────┘ ─ d2 60 | * | b2 | 61 | */ 62 | def apply(b1: Length, b2: Length, h: Length, d1: Length, d2: Length, t: Length)(length: Length): Solid = { 63 | val m1 = (b1-t) / 2 64 | val m2 = (b2-t) / 2 65 | apply(m1, m1, m2, m2, h, d1, d2, t)(length) 66 | } 67 | 68 | /* 69 | * |b1|t|b2| 70 | * ─ ┌───────┐ ─ 71 | * └──┐ ┌──┘ ─ d1 72 | * h │ │ 73 | * ┌──┘ └──┐ ─ 74 | * ─ └───────┘ ─ d2 75 | * |b3|t|b4| 76 | */ 77 | def apply(b1: Length, b2: Length, b3: Length, b4: Length, h: Length, d1: Length, d2: Length, t: Length)(length: Length): Solid = { 78 | val c1 = Cube( b1+t+b2, d1, length) 79 | val c2 = Cube( b3+t+b4, d2, length) 80 | val c3 = Cube( t, h, length) 81 | c1.move(b3-b1, h-d1, 0 mm) + c2 + c3.moveX(b3) 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/scala/scadla/InlineOps.scala: -------------------------------------------------------------------------------- 1 | package scadla 2 | 3 | import squants.space.Length 4 | import scala.language.postfixOps 5 | import squants.space.Millimeters 6 | import squants.space.Angle 7 | import squants.space.Degrees 8 | 9 | object InlineOps { 10 | 11 | implicit final class AngleConversions[A](n: A)(implicit num: Numeric[A]) { 12 | def ° = Degrees(n) 13 | } 14 | 15 | implicit final class Ops(private val lhs: Solid) extends AnyVal { 16 | import squants.space.LengthConversions._ 17 | 18 | def translate(x: Length, y: Length, z: Length) = Translate(x, y, z, lhs) 19 | def move(x: Length, y: Length, z: Length) = Translate(x, y, z, lhs) 20 | def move(v: Vector) = Translate(v, lhs) 21 | def move(p: Point) = Translate(p.toVector, lhs) 22 | def moveX(x: Length) = Translate(x, 0 mm, 0 mm, lhs) 23 | def moveY(y: Length) = Translate(0 mm, y, 0 mm, lhs) 24 | def moveZ(z: Length) = Translate(0 mm, 0 mm, z, lhs) 25 | 26 | def rotate(x: Angle, y: Angle, z: Angle) = Rotate(x, y, z, lhs) 27 | def rotate(q: Quaternion) = Rotate(q, lhs) 28 | def rotateX(x: Angle) = Rotate(x, 0°, 0°, lhs) 29 | def rotateY(y: Angle) = Rotate(0°, y, 0°, lhs) 30 | def rotateZ(z: Angle) = Rotate(0°, 0°, z, lhs) 31 | 32 | def scale(x: Double, y: Double, z: Double) = Scale(x, y, z, lhs) 33 | def scaleX(x: Double) = Scale(x, 1, 1, lhs) 34 | def scaleY(y: Double) = Scale(1, y, 1, lhs) 35 | def scaleZ(z: Double) = Scale(1, 1, z, lhs) 36 | 37 | def mirror(x: Double, y: Double, z: Double) = Mirror(x, y, z, lhs) 38 | def multiply(m: Matrix) = Multiply(m, lhs) 39 | 40 | def +(rhs: Solid) = Union(lhs, rhs) 41 | def ++(rhs: Iterable[Solid]) = Union((lhs :: rhs.toList): _*) 42 | def union(rhs: Solid) = Union(lhs, rhs) 43 | 44 | def *(rhs: Solid) = Intersection(lhs, rhs) 45 | def **(rhs: Iterable[Solid]) = Intersection((lhs :: rhs.toList): _*) 46 | def intersection(rhs: Solid) = Intersection(lhs, rhs) 47 | 48 | def -(rhs: Solid) = Difference(lhs, rhs) 49 | def --(rhs: Iterable[Solid]) = Difference(lhs, rhs.toList: _*) 50 | def difference(rhs: Solid) = Difference(lhs, rhs) 51 | 52 | def hull(rhs: Solid) = Hull(lhs, rhs) 53 | def minkowski(rhs: Solid) = Minkowski(lhs, rhs) 54 | 55 | def toPolyhedron = backends.Renderer.default(lhs) //TODO a way of being lazy 56 | 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/thread/Washer.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.thread 2 | 3 | import scadla._ 4 | import scadla.utils._ 5 | import squants.space.Length 6 | import scala.language.postfixOps 7 | import squants.space.LengthConversions._ 8 | 9 | object Washer { 10 | 11 | def apply(innerDiameter: Length, outerDiameter: Length, thickness: Length) = { 12 | Tube(outerDiameter/2, innerDiameter/2, thickness) 13 | } 14 | 15 | def metric(innerDiameter: Double, outerDiameter: Double, thickness: Double) = { 16 | apply(innerDiameter mm, outerDiameter mm, thickness mm) 17 | } 18 | 19 | def imperial(innerDiameter: Double, outerDiameter: Double, thickness: Double) = { 20 | apply(innerDiameter inches, outerDiameter inches, thickness inches) 21 | } 22 | 23 | //metric versions (ISO) 24 | //val M1 = did not find 25 | //val M1_2 = did not find 26 | val M1_6 = metric( 1.7, 4, 0.3) 27 | val M2 = metric( 2.2, 5, 0.3) 28 | val M2_5 = metric( 2.7, 6, 0.5) 29 | val M3 = metric( 3.2, 7, 0.5) 30 | val M4 = metric( 4.3, 9, 0.8) 31 | val M5 = metric( 5.3, 10, 1 ) 32 | val M6 = metric( 6.4, 12, 1.6) 33 | val M8 = metric( 8.4, 16, 1.6) 34 | val M10 = metric( 10.5, 20, 2 ) 35 | val M12 = metric( 13 , 24, 2.5) 36 | val M16 = metric( 17 , 30, 3 ) 37 | val M20 = metric( 21 , 37, 3 ) 38 | val M24 = metric( 25 , 44, 4 ) 39 | val M30 = metric( 31 , 56, 4 ) 40 | val M36 = metric( 37 , 66, 5 ) 41 | val M42 = metric( 43 , 78, 7 ) 42 | val M48 = metric( 50 , 92, 8 ) 43 | val M56 = metric( 58 , 105, 9 ) 44 | //val M64 = did not find 45 | 46 | //imperial versions (ANSI type B, regular) 47 | val _1_8 = imperial(0.1410, 0.4060, 0.0400) 48 | val _5_32 = imperial(0.1880, 0.5000, 0.0400) 49 | val _1_4 = imperial(0.2810, 0.7340, 0.0630) 50 | val _5_16 = imperial(0.3440, 0.8750, 0.0630) 51 | val _3_8 = imperial(0.4060, 1.0000, 0.0630) 52 | val _7_16 = imperial(0.4690, 1.1250, 0.0630) 53 | val _1_2 = imperial(0.5310, 1.2500, 0.1000) 54 | val _9_16 = imperial(0.5940, 1.4690, 0.1000) 55 | val _5_8 = imperial(0.6560, 1.7500, 0.1000) 56 | //val _11_16 = did not find 57 | val _3_4 = imperial(0.8120, 2.0000, 0.1000) 58 | val _7_8 = imperial(0.9380, 2.2500, 0.1600) 59 | //val _15_16 = did not find 60 | val _1 = imperial(1.0620, 2.5000, 0.1600) 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/ParallelRenderer.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends 2 | 3 | import scadla._ 4 | import java.util.concurrent.ConcurrentHashMap 5 | import java.util.concurrent.ForkJoinTask 6 | import squants.space.{Length, Millimeters, LengthUnit} 7 | 8 | /** not quite stable yet 9 | * A backend that decompose renders independent parts of a Solid in parallel. 10 | * (problem in parsing and feeding complex objects to openscad) 11 | * @param renderer the (serial) renderer to use for the simpler tasks 12 | */ 13 | class ParallelRendererAux[A >: Null](renderer: RendererAux[A], unit: LengthUnit = Millimeters) extends RendererAux[ForkJoinTask[A]](unit) { 14 | 15 | //TODO optional preprocessing to make reduction tree or flatten 16 | 17 | protected val taskMap = new ConcurrentHashMap[Solid, ForkJoinTask[A]] 18 | 19 | override def isSupported(s: Solid) = renderer.isSupported(s) 20 | 21 | def clear() = taskMap.clear() 22 | 23 | def shape(s: Shape) = new ForkJoinTask[A]{ 24 | protected var res: A = null 25 | protected def exec = { 26 | res = renderer.shape(s) 27 | true 28 | } 29 | protected def setRawResult(p: A): Unit = { res = p } 30 | def getRawResult = res 31 | } 32 | 33 | def operation(o: Operation, args: Seq[ForkJoinTask[A]]) = new ForkJoinTask[A] { 34 | protected var res: A = null 35 | protected def exec = { 36 | res = renderer.operation(o, args.map(_.join)) 37 | true 38 | } 39 | protected def setRawResult(p: A): Unit = { res = p } 40 | def getRawResult = res 41 | } 42 | 43 | def transform(t: Transform, arg: ForkJoinTask[A]) = new ForkJoinTask[A] { 44 | protected var res: A = null 45 | protected def exec = { 46 | res = renderer.transform(t, arg.join) 47 | true 48 | } 49 | protected def setRawResult(p: A): Unit = { res = p } 50 | def getRawResult = res 51 | } 52 | 53 | def toMesh(t: ForkJoinTask[A]) = renderer.toMesh(t.join) 54 | 55 | override def render(s: Solid): ForkJoinTask[A] = { 56 | var task = taskMap.get(s) 57 | if (task == null) { 58 | task = super.render(s) 59 | val t2 = taskMap.putIfAbsent(s, task) 60 | if (t2 != null) { 61 | task = t2 62 | } else { 63 | task.fork 64 | } 65 | } 66 | task 67 | } 68 | 69 | } 70 | 71 | class ParallelRenderer(renderer: Renderer) extends ParallelRendererAux[Polyhedron](new RendererAuxAdapter(renderer)) 72 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/Hexagon.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils 2 | 3 | import scadla._ 4 | import InlineOps._ 5 | import scala.math._ 6 | import squants.space.Length 7 | import squants.space.Radians 8 | 9 | object Hexagon { 10 | 11 | def maxRadius(minRadius: Length) = minRadius / math.sin(math.Pi/3) 12 | 13 | def minRadius(maxRadius: Length) = maxRadius * math.sin(math.Pi/3) 14 | 15 | /* Extrude vertically an hexagon (centered at 0,0 with z from 0 to height) 16 | * @param minRadius the radius of the circle inscribed in the hexagon 17 | * @param height 18 | */ 19 | def apply(minRadius: Length, _height: Length) = { 20 | val unit = minRadius.unit 21 | val height = _height.in(unit) 22 | if (minRadius.value <= 0.0 || height.value <= 0.0) { 23 | Empty 24 | } else { 25 | import scala.math._ 26 | val rd0 = minRadius/sin(Pi/3) 27 | 28 | val pts = for (i <- 0 until 6; j <- 0 to 1) yield 29 | Point(rd0 * cos(i * Pi/3), rd0 * sin(i * Pi/3), height * j) //linter:ignore ZeroDivideBy 30 | def face(a: Int, b: Int, c: Int) = Face(pts(a % 12), pts(b % 12), pts(c % 12)) 31 | 32 | val side1 = for (i <- 0 until 6) yield face( 2*i, 2*i+2, 2*i+3) //linter:ignore ZeroDivideBy 33 | val side2 = for (i <- 0 until 6) yield face(2*i+1, 2*i, 2*i+3) //linter:ignore ZeroDivideBy 34 | val bottom = Array( 35 | face(0, 4, 2), 36 | face(4, 8, 6), 37 | face(8, 0, 10), 38 | face(0, 8, 4) 39 | ) 40 | val top = Array( 41 | face(1, 3, 5), 42 | face(5, 7, 9), 43 | face(9, 11, 1), 44 | face(1, 5, 9) 45 | ) 46 | val faces = side1 ++ side2 ++ bottom ++ top 47 | Polyhedron(faces) 48 | } 49 | } 50 | 51 | /* Extrude vertically a semi-regular hexagon (centered at 0,0 with z from 0 to height) 52 | * @param radius1 the radius of the circle inscribed in the hexagon even faces 53 | * @param radius2 the radius of the circle inscribed in the hexagon odd faces 54 | * @param height 55 | */ 56 | def semiRegular(radius1: Length, radius2: Length, height: Length) = { 57 | val r = maxRadius(radius1) max maxRadius(radius2) 58 | val base = Cylinder(r,height) 59 | val chop = Cube(r, 2*r, height).moveY(-r) 60 | val neg1 = for(i <- 0 until 6 if i % 2 == 0) yield chop.moveX(radius1).rotateZ(Radians(i * Pi / 3)) 61 | val neg2 = for(i <- 0 until 6 if i % 2 == 1) yield chop.moveX(radius2).rotateZ(Radians(i * Pi / 3)) 62 | base -- neg1 -- neg2 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/ActuatorFastener.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import math._ 4 | import scadla._ 5 | import utils._ 6 | import InlineOps._ 7 | import thread._ 8 | import Common._ 9 | import scadla.EverythingIsIn.{millimeters, radians} 10 | 11 | // dX, dZ are given from the center of the extrusion 12 | class ActuatorFasterner(dX: Double, dZ: Double) { 13 | 14 | assert(dX >= 20 || dZ >= 20, "too close") 15 | assert(dX >= 0 && dZ >= 0, "backward") 16 | 17 | var bearingPosition = 0.5 // [0, 1] 18 | var thickness = 6.0 19 | var bearingRetainer = 2.0 20 | var supportLength = 40.0 21 | var guideDepth = 2 22 | var thread = ISO.M3 23 | var beamConnectorRounding = 2 24 | var nbrScrewsPerSide = 2 25 | 26 | def beamConnector = { 27 | val guide = Trapezoid(4.5, 6.5, supportLength, guideDepth, -0.2) 28 | val faceBlank = Union( 29 | Cube(20-beamConnectorRounding, thickness, supportLength).move(-thickness+beamConnectorRounding, -thickness, 0), 30 | PieSlice(thickness, 0, Pi/2, supportLength).rotateZ(-Pi/2).move(20-thickness,0,0), 31 | Cylinder(beamConnectorRounding, supportLength).move(-thickness+beamConnectorRounding, -thickness+beamConnectorRounding, 0), 32 | guide.rotateX(Pi/2).rotateZ(Pi).move(10+6.5/2, 0, 0) 33 | ) 34 | val screw = Cylinder(thread, thickness + guideDepth + 2).rotateX(Pi/2).move(10, guideDepth +1, 0) 35 | val screwDistance = (supportLength - thickness) / nbrScrewsPerSide 36 | val screws = for (i <- 0 until nbrScrewsPerSide) yield screw.moveZ(thickness + screwDistance * (i + 0.5)) 37 | val face = faceBlank -- screws 38 | face + face.mirror(1,-1,0) 39 | } 40 | 41 | def connector(supportDirection: Boolean) = { 42 | val bc1 = beamConnector.rotateZ(Pi).move(10, 10, 0) 43 | val bc2 = beamConnector.move( dX-10, dZ-10, 0) 44 | val beam0 = Hull( 45 | bc1 * Cube(50, 50, thickness).move(-30, -30, 0), 46 | bc2 * Cube(50, 50, thickness).move( dX-30, dZ-30,0), 47 | Cylinder(11 + thickness, thickness).move( dX*bearingPosition, dZ*bearingPosition, 0) 48 | ) 49 | val beam = Difference( 50 | beam0, 51 | Cylinder(11 + looseTolerance, thickness).move( dX*bearingPosition, dZ*bearingPosition, bearingRetainer), 52 | Cylinder(9, thickness).move( dX*bearingPosition, dZ*bearingPosition, 0), 53 | Cube(40, 40, thickness).move( -30, -30, 0), 54 | Cube(40, 40, thickness).move( dX-10, dZ-10,0) 55 | ) 56 | val support: Solid = beam + bc1 + bc2 57 | if (supportDirection) support 58 | else support.mirror(0,0,1) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/gear/Involute.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.gear 2 | 3 | import scadla._ 4 | import scala.math._ 5 | import squants.space.Length 6 | import squants.space.Millimeters 7 | 8 | object Involute { 9 | 10 | // https://en.wikipedia.org/wiki/Involute 11 | // in cartesian form: 12 | // x = r (cos(a) + a * sin(a)) 13 | // y = r (sin(a) - a * cos(a)) 14 | // in polar form: 15 | // ρ = r * sqrt(1 + a^2) 16 | // φ = a - arctan(a) 17 | 18 | def x(radius: Length, phase: Double, theta: Double): Length = { 19 | radius * (cos(theta) + (theta - phase) * sin(theta)) 20 | } 21 | 22 | def y(radius: Length, phase: Double, theta: Double): Length = { 23 | radius * (sin(theta) - (theta - phase) * cos(theta)) 24 | } 25 | 26 | def x(radius: Length, theta: Double): Length = x(radius, 0, theta) 27 | def y(radius: Length, theta: Double): Length = y(radius, 0, theta) 28 | 29 | //def x(theta: Double): Double = x(1, theta) 30 | //def y(theta: Double): Double = y(1, theta) 31 | 32 | 33 | 34 | def apply(radius: Length, phase: Double, start: Double, end: Double, height: Length, stepSize: Double): Polyhedron = { 35 | assert(radius.value > 0) 36 | assert(end > start) 37 | assert(height.value > 0) 38 | assert(stepSize > 0) 39 | 40 | val steps = ceil((end - start) / stepSize).toInt 41 | val actualStepSize = (end - start) / steps 42 | 43 | val points = Array.ofDim[Point](2 * (2 + steps)) 44 | points(0) = Point(Millimeters(0),Millimeters(0),Millimeters(0)) 45 | points(1) = Point(Millimeters(0),Millimeters(0),height) 46 | 47 | for (i <- (0 to steps)) { 48 | val theta = start + i * actualStepSize 49 | val x1 = x(radius, phase, theta) 50 | val y1 = y(radius, phase, theta) 51 | points(2*(i+1) ) = Point(x1, y1, Millimeters(0)) 52 | points(2*(i+1) + 1) = Point(x1, y1, height) 53 | } 54 | 55 | def pt(i: Int) = points(i % points.size) 56 | def face(a: Int, b: Int, c: Int) = Face(pt(a), pt(b), pt(c)) 57 | 58 | val topBot = (1 to steps).flatMap( i => { 59 | val j = 2*i 60 | val top = face(1 , j+1, j+3) 61 | val bot = face(0 , j+2, j ) 62 | Seq(top, bot) 63 | }) 64 | val ext = (0 to steps + 1).flatMap( i => { 65 | val j = 2*i 66 | val ex1 = face(j , j+2, j+1) 67 | val ex2 = face(j+2, j+3, j+1) 68 | Seq(ex1, ex2) 69 | }) 70 | val faces = topBot ++ ext 71 | Polyhedron(faces) 72 | } 73 | 74 | def apply(radius: Length, start: Double, end: Double, height: Length): Polyhedron = { 75 | apply(radius, 0, start, end, height, 0.1) 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/Main.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scala.math._ 6 | 7 | object Main{ 8 | 9 | val r = backends.Renderer.default 10 | //val r = backends.OpenSCAD 11 | //val r = backends.JCSG 12 | //val r = new backends.ParallelRenderer(backends.JCSG) 13 | 14 | def main(args: Array[String]): Unit = { 15 | 16 | //Spindle.objects.par.foreach{ case (name, obj) => 17 | // r.toSTL(obj, "spdl-" + name + ".stl") 18 | //} 19 | 20 | //new Joint2DOF().parts.zipWithIndex.par.foreach{ case (p, i) => 21 | // r.toSTL(p, "j2dof-" + i + ".stl") 22 | //} 23 | //r.view(new Joint2DOF().cross) 24 | //r.view(new Joint2DOF().bottom(20)) 25 | //r.view(new Joint2DOF().top(30, 16, 1.35)) 26 | 27 | //r.view(Platform.with608Bearings()) 28 | //r.toSTL(Platform.with608Bearings(), "platform.stl") 29 | //r.toSTL(Platform.verticalBushing2Rod(), "p2r.stl") 30 | //r.toSTL(Platform.verticalBushing2Platform(), "p2p.stl") 31 | 32 | //Frame.cableTensioner.zipWithIndex.foreach{ case (s,i) => r.toSTL(s, "cableTensioner_" + i + ".stl") } 33 | //r.view(Frame.cableAttachPoint(0)) 34 | //Frame.cableAttachPoint.zipWithIndex.foreach{ case (s,i) => r.toSTL(s, "cableAttach_" + i + ".stl") } 35 | 36 | //r.toSTL(LinearActuatorBlock(true), "la-gimbal.stl") 37 | 38 | //r.toSTL(scadla.examples.extrusion._2020(50), "extrusion.stl") 39 | //r.toSTL(scadla.examples.extrusion._2020.pad(10,1.5,Common.tolerance), "pad.stl") 40 | //val af = new ActuatorFasterner(120 * sin(Pi/4), 120 * cos(Pi/4)) 41 | //r.toSTL(af.connector(true), "connector1.stl") 42 | //r.toSTL(af.connector(false), "connector2.stl") 43 | 44 | //for ( (n,s) <- LinearActuator.parts(true).par) { 45 | // r.toSTL(s(), "la-" + n + ".stl") 46 | //} 47 | 48 | //r.view(LinearActuator.assembled()) 49 | //LinearActuator.gimbal.parts(true).zipWithIndex.par.foreach{ case (s, i) => r.toSTL(s, "gimbal_" + i + ".stl") } 50 | 51 | //r.view(Frame.assembled) 52 | //r.toSTL(Frame.connector1WithSupport, "connectorS1.stl") 53 | //r.toSTL(Frame.connector2WithSupport, "connectorS2.stl") 54 | //r.toSTL(Frame.connector3, "connector3.stl") 55 | //r.toSTL(Frame.actuatorConnector1, "actuatorConnector1.stl") 56 | //r.toSTL(Frame.actuatorConnector2, "actuatorConnector2.stl") 57 | //r.toSTL(Frame.hBeam, "hBeam.stl") 58 | //r.toSTL(Frame.vBeam, "vBeam.stl") 59 | //r.toSTL(Frame.tBeam, "tBeam.stl") 60 | //r.toSTL(Frame.foot(true), "foot.stl") 61 | //r.toSTL(Frame.hBeamJig1, "jig1.stl") 62 | //r.toSTL(Frame.hBeamJig2, "jig2.stl") 63 | 64 | //r.view(Pulley(6, 4, 2, 1, 1, 2)) 65 | 66 | println("work in progress") 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/test/resources/unit_cube.amf: -------------------------------------------------------------------------------- 1 | 2 | 3 | OpenSCAD 2016.02.24.nightly (git bd5b7ba) 4 | 5 | 6 | 7 | 8 | 0 9 | 0 10 | 1 11 | 12 | 13 | 0 14 | 1 15 | 1 16 | 17 | 18 | 0 19 | 0 20 | 0 21 | 22 | 23 | 0 24 | 1 25 | 0 26 | 27 | 28 | 1 29 | 0 30 | 1 31 | 32 | 33 | 1 34 | 1 35 | 1 36 | 37 | 38 | 1 39 | 0 40 | 0 41 | 42 | 43 | 1 44 | 1 45 | 0 46 | 47 | 48 | 49 | 50 | 0 51 | 1 52 | 2 53 | 54 | 55 | 2 56 | 1 57 | 3 58 | 59 | 60 | 0 61 | 4 62 | 5 63 | 64 | 65 | 1 66 | 0 67 | 5 68 | 69 | 70 | 2 71 | 6 72 | 4 73 | 74 | 75 | 0 76 | 2 77 | 4 78 | 79 | 80 | 3 81 | 7 82 | 2 83 | 84 | 85 | 2 86 | 7 87 | 6 88 | 89 | 90 | 1 91 | 5 92 | 3 93 | 94 | 95 | 3 96 | 5 97 | 7 98 | 99 | 100 | 6 101 | 7 102 | 5 103 | 104 | 105 | 4 106 | 6 107 | 5 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/gear/Rack.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.gear 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scadla.utils._ 6 | import scala.math._ 7 | import squants.space.{Length, Radians, Millimeters, Angle} 8 | 9 | object Rack { 10 | 11 | //for carving so the backlash makes the tooth larger 12 | def tooth( toothWidth: Length, 13 | pressureAngle: Double, 14 | addenum: Length, 15 | dedenum: Length, 16 | height: Length, 17 | backlash: Length, 18 | skew: Angle ) = { 19 | assert(pressureAngle < Pi / 2 && pressureAngle >= 0, "pressureAngle must be between in [0;π/2)") 20 | val base = toothWidth + 2 * addenum * tan(pressureAngle) + backlash 21 | val tip = toothWidth - 2 * dedenum * tan(pressureAngle) + backlash 22 | assert(tip.value > 0, "tip of the profile is negative ("+tip+"), try decreasing the pressureAngle, or addenum/dedenum.") 23 | val tHeight = addenum + dedenum + backlash 24 | Trapezoid(tip, base, height, tHeight, skew).rotateX(Radians(Pi/2)).move(-base/2 + addenum*tan(skew.toRadians), addenum, Millimeters(0)).rotateZ(Radians(-Pi/2)) 25 | } 26 | 27 | /** Create an involute spur gear. 28 | * @param toothWidth the width of a tooth 29 | * @param nbrTeeth the number of tooth in the gear 30 | * @param pressureAngle the angle between meshing gears at the pitch radius (0 mean "square" tooths, π/2 no tooths) 31 | * @param addenum how much to add to the pitch to get the outer radius of the gear 32 | * @param dedenum how much to remove to the pitch to get the root radius of the gear 33 | * @param height the height of the gear 34 | * @param backlash add some space (manufacturing tolerance) 35 | * @param skew generate a gear with an asymmetric profile by skewing the tooths 36 | */ 37 | def apply( toothWidth: Length, 38 | nbrTeeth: Int, 39 | pressureAngle: Double, 40 | addenum: Length, 41 | dedenum: Length, 42 | height: Length, 43 | backlash: Length, 44 | skew: Angle = Radians(0.0)) = { 45 | 46 | assert(addenum.value > 0, "addenum must be greater than 0") 47 | assert(dedenum.value > 0, "dedenum must be greater than 0") 48 | assert(nbrTeeth > 0, "number of tooths must be greater than 0") 49 | assert(toothWidth.value > 0, "toothWidth must be greater than 0") 50 | 51 | val rackTooth = tooth(toothWidth, pressureAngle, addenum, dedenum, height, -backlash, skew) 52 | 53 | val space = 2*toothWidth 54 | val teeth = for (i <- 0 until nbrTeeth) yield rackTooth.moveY(i * space) 55 | val base = Cube(Gear.baseThickness, nbrTeeth * space, height).move(dedenum, -space/2, Millimeters(0)) 56 | base ++ teeth 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/box/InBox.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.box 2 | 3 | import scadla._ 4 | import scala.math._ 5 | import dzufferey.utils.Logger 6 | import dzufferey.utils.LogLevel._ 7 | import squants.space.Millimeters 8 | 9 | object InBox { 10 | 11 | private val O = Millimeters(0) 12 | def apply(s: Solid): Box = s match { 13 | case Cube(w, d, h) => 14 | Box(O, O, O, w, d, h) 15 | case Empty => 16 | Box.empty 17 | case Sphere(r) => 18 | val s = r * sqrt(2)/2 19 | Box(-s, -s, -s, s, s, s) 20 | case Cylinder(radiusBot, radiusTop, height) => 21 | val r = radiusBot min radiusTop 22 | val s = r * sqrt(2)/2 23 | Box(-s, -s, O, s, s, height) 24 | case Polyhedron(faces) => 25 | Logger("InBox", Warning, "TODO InBox(Polyhedron)") 26 | Box.empty 27 | case f @ FromFile(_,_) => 28 | apply(f.load) 29 | 30 | case Translate(x, y, z, s2) => 31 | apply(s2).move(x,y,z) 32 | case Scale(x, y, z, s2) => 33 | apply(s2).scale(x,y,z) 34 | case Rotate(x, y, z, s2) => 35 | multiply(apply(s2), Matrix.rotation(x,y,z)) 36 | case Mirror(x, y, z, s2) => 37 | multiply(apply(s2), Matrix.mirror(x,y,z)) 38 | case Multiply(m, s2) => 39 | multiply(apply(s2), m) 40 | 41 | case Union(lst @ _*) => 42 | lst.foldLeft(Box.empty)( (b,s) => b.unionUnderApprox(apply(s)) ) 43 | case Intersection(lst @ _*) => 44 | if (lst.isEmpty) Box.empty 45 | else lst.map(apply).reduce( _ intersection _ ) 46 | case Difference(s2, lst @ _*) => 47 | val pos = apply(s2) 48 | val neg = BoundingBox(Union(lst:_*)) 49 | remove(pos, neg) 50 | case Minkowski(lst @ _*) => 51 | val init = Box(O,O,O,O,O,O) 52 | if (lst.isEmpty) Box.empty 53 | else lst.foldLeft(init)( (b,s) => b.add(apply(s)) ) 54 | case Hull(lst @ _*) => 55 | Logger("InBox", Warning, "TODO improve InBox(Hull)") 56 | apply(Union(lst:_*)) 57 | } 58 | 59 | protected def multiply(b: Box, m: Matrix) = { 60 | Logger("InBox", Warning, "TODO InBox.multiply") 61 | Box.empty 62 | } 63 | 64 | protected def remove(a: Interval, b: Interval): Interval = 65 | if (a.isEmpty || b.isEmpty || a.max <= b.min || a.min >= b.max) a 66 | else if (b contains a) Interval.empty 67 | else if (b.min <= a.min) Interval(b.max, a.max) 68 | else if (b.max >= a.max) Interval(a.min, b.min) 69 | else { 70 | assert(b.min > a.min && b.max < a.max) 71 | val upper = a.max - b.max 72 | val lower = b.min - a.min 73 | if (lower > upper) Interval(a.min, b.min) 74 | else Interval(b.max, a.max) 75 | } 76 | 77 | protected def remove(a: Box, b: Box): Box = 78 | if (a.isEmpty || b.isEmpty) a 79 | else Box(remove(a.x, b.x), 80 | remove(a.y, b.y), 81 | remove(a.z, b.z)) 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/thread/Nut.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.thread 2 | 3 | import scadla._ 4 | import scadla.utils._ 5 | import math._ 6 | import InlineOps._ 7 | import squants.space.Length 8 | import scala.language.postfixOps 9 | import squants.space.LengthConversions._ 10 | 11 | object Nut { 12 | 13 | private lazy val thread = new MetricThread() 14 | 15 | def apply(radius: Length) = { 16 | thread.hexNutIso(radius*2, 1.6 * radius) 17 | } 18 | 19 | def minOuterRadius(innerRadius: Length) = 1.6 * innerRadius 20 | def maxOuterRadius(innerRadius: Length) = Hexagon.maxRadius(minOuterRadius(innerRadius)) 21 | def height(innerRadius: Length) = 1.6 * innerRadius 22 | 23 | //metric versions (ISO) 24 | def M1 = apply( ISO.M1 ) 25 | def M1_2 = apply( ISO.M1_2 ) 26 | def M1_6 = apply( ISO.M1_6 ) 27 | def M2 = apply( ISO.M2 ) 28 | def M2_5 = apply( ISO.M2_5 ) 29 | def M3 = apply( ISO.M3 ) 30 | def M4 = apply( ISO.M4 ) 31 | def M5 = apply( ISO.M5 ) 32 | def M6 = apply( ISO.M6 ) 33 | def M8 = apply( ISO.M8 ) 34 | def M10 = apply( ISO.M10 ) 35 | def M12 = apply( ISO.M12 ) 36 | def M16 = apply( ISO.M16 ) 37 | def M20 = apply( ISO.M20 ) 38 | def M24 = apply( ISO.M24 ) 39 | def M30 = apply( ISO.M30 ) 40 | def M36 = apply( ISO.M36 ) 41 | def M42 = apply( ISO.M42 ) 42 | def M48 = apply( ISO.M48 ) 43 | def M56 = apply( ISO.M56 ) 44 | def M64 = apply( ISO.M64 ) 45 | 46 | } 47 | 48 | /** simple Hexagon as placeholder for nuts (rendering is much faster) */ 49 | class NutPlaceHolder(tolerance: Length = 0.1 mm) { 50 | 51 | protected val factor = 1.6 52 | 53 | def apply(radius: Length) = { 54 | Hexagon(radius * factor + tolerance, factor * radius + tolerance) 55 | } 56 | 57 | def minOuterRadius(innerRadius: Length) = factor * innerRadius + tolerance 58 | def maxOuterRadius(innerRadius: Length) = Hexagon.maxRadius(minOuterRadius(innerRadius)).toMillimeters 59 | def height(innerRadius: Length) = factor * innerRadius + tolerance 60 | 61 | def M1 = apply( ISO.M1 ) 62 | def M1_2 = apply( ISO.M1_2 ) 63 | def M1_6 = apply( ISO.M1_6 ) 64 | def M2 = apply( ISO.M2 ) 65 | def M2_5 = apply( ISO.M2_5 ) 66 | def M3 = apply( ISO.M3 ) 67 | def M4 = apply( ISO.M4 ) 68 | def M5 = apply( ISO.M5 ) 69 | def M6 = apply( ISO.M6 ) 70 | def M8 = apply( ISO.M8 ) 71 | def M10 = apply( ISO.M10 ) 72 | def M12 = apply( ISO.M12 ) 73 | def M16 = apply( ISO.M16 ) 74 | def M20 = apply( ISO.M20 ) 75 | def M24 = apply( ISO.M24 ) 76 | def M30 = apply( ISO.M30 ) 77 | def M36 = apply( ISO.M36 ) 78 | def M42 = apply( ISO.M42 ) 79 | def M48 = apply( ISO.M48 ) 80 | def M56 = apply( ISO.M56 ) 81 | def M64 = apply( ISO.M64 ) 82 | 83 | } 84 | 85 | class StructuralNutPlaceHolder(tolerance: Length = 0.1 mm) extends NutPlaceHolder(tolerance) { 86 | 87 | override protected val factor = 1.8 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/x3d/Printer.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends.x3d 2 | 3 | import scadla._ 4 | import java.io._ 5 | import java.util.Date 6 | import squants.space.{Length, Millimeters, LengthUnit} 7 | 8 | //TODO make parametric in terms of unit 9 | 10 | object Printer extends Printer(Millimeters) { 11 | } 12 | 13 | class Printer(unit: LengthUnit = Millimeters) { 14 | 15 | def write(obj: Polyhedron, writer: BufferedWriter, 16 | onlyShape: Boolean = false, //or add scene 17 | withHeader: Boolean = true, //full xml document 18 | name: String = "scadla object"): Unit = { 19 | assert(!onlyShape || !withHeader, "only shape cannot print header") 20 | def writeLine(s: String): Unit = { 21 | writer.write(s) 22 | writer.newLine 23 | } 24 | val (points, faces) = obj.indexed 25 | if (withHeader) { 26 | writeLine("") 27 | writeLine("") 28 | writeLine("") 29 | writeLine(" ") 30 | writeLine(" ") 31 | writeLine(" ") 32 | writeLine(" ") 33 | } else if (!onlyShape) { 34 | writeLine("") 35 | writeLine(" ") 36 | writeLine(" ") 37 | } else { 38 | writeLine(" ") 39 | } 40 | writeLine(" ") 41 | writeLine(" ") 42 | writeLine(" ") 43 | writer.write(" ") 53 | writer.write(" ") 63 | writeLine(" ") 64 | writeLine(" ") 65 | if (!onlyShape) { 66 | writeLine(" ") 67 | writeLine("") 68 | } 69 | } 70 | 71 | def store(obj: Polyhedron, fileName: String) = { 72 | val writer = new BufferedWriter(new FileWriter(fileName)) 73 | try write(obj, writer) 74 | finally writer.close 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/extrusion/_2020.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.extrusion 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scadla.utils._ 6 | import scadla.utils.thread._ 7 | import scadla.utils.Trig._ 8 | import scadla.EverythingIsIn.{millimeters, radians} 9 | import squants.space.Length 10 | import scala.language.postfixOps // for mm notation 11 | import squants.space.LengthConversions._ // for mm notation 12 | 13 | //place holder for 20x20mm aluminium extrusions 14 | object _2020 { 15 | 16 | val width = 20 mm 17 | 18 | protected def centerHole(length: Length) = Cylinder(2.1, length+2).moveZ(-1) 19 | 20 | def apply(length: Length) = { 21 | val base = RoundedCubeH(20, 20, length, 1.5).move(-10, -10, 0) 22 | val shell = Difference( 23 | base, 24 | CenteredCube.xy(16, 16, length + 1).moveZ(-1), 25 | CenteredCube.xy(22, 6.5, length + 1).moveZ(-1), 26 | CenteredCube.xy(6.5, 22, length + 1).moveZ(-1) 27 | ) 28 | val withInner = Union( 29 | shell, 30 | CenteredCube.xy(8, 8, length), //center 31 | CenteredCube.xy(3, 3, length).move(-7.5, -7.5, 0), //corner 32 | CenteredCube.xy(3, 3, length).move(-7.5, 7.5, 0), //corner 33 | CenteredCube.xy(3, 3, length).move( 7.5, -7.5, 0), //corner 34 | CenteredCube.xy(3, 3, length).move( 7.5, 7.5, 0), //corner 35 | CenteredCube.xy(1.75, 25, length).rotateZ( Pi/4), //cross 36 | CenteredCube.xy(1.75, 25, length).rotateZ(-Pi/4) //cross 37 | ) 38 | withInner - centerHole(length) 39 | } 40 | 41 | def placeHolder(length: Length) = CenteredCube.xy(20, 20, length) 42 | 43 | def connector(plateThicknesss: Length, 44 | knobHeight: Length, 45 | tolerance: Length) = { 46 | val base = CenteredCube.xy(20, 20, plateThicknesss + knobHeight).moveZ(-plateThicknesss) 47 | val negative = Bigger(apply(knobHeight), tolerance).moveZ(tolerance/2) 48 | val hole = Cylinder(ISO.M5, knobHeight + plateThicknesss + 1).moveZ(-plateThicknesss-1) 49 | Difference( 50 | base, 51 | negative, 52 | hole, 53 | CenteredCube.xy(4,4,knobHeight+1).move( 10, 10, 0), 54 | CenteredCube.xy(4,4,knobHeight+1).move( 10,-10, 0), 55 | CenteredCube.xy(4,4,knobHeight+1).move(-10, 10, 0), 56 | CenteredCube.xy(4,4,knobHeight+1).move(-10,-10, 0) 57 | ) 58 | } 59 | 60 | //TODO better name 61 | def pad(length: Length, threadRadius: Length, tolerance: Length) = { 62 | val bot = 14 mm 63 | val top = 8 mm 64 | val height = 3 mm 65 | val d = 2 mm 66 | val base = Trapezoid(top, bot, length, height) 67 | val chamfered = base * Cube(bot - d, length, height).moveX(d/2) 68 | Difference( 69 | chamfered, 70 | Cylinder(threadRadius-tolerance, height).move(bot/2, length/2, 0), 71 | new NutPlaceHolder().apply(threadRadius).move(bot/2, length/2, height-1.6*threadRadius+1) 72 | ) 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/box/Box.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.box 2 | 3 | import scadla._ 4 | import squants.space.Length 5 | 6 | case class Box(x: Interval, y: Interval, z: Interval) { 7 | 8 | def isEmpty = x.isEmpty || y.isEmpty || y.isEmpty 9 | 10 | def contains(p: Point) = 11 | x.contains(p.x) && 12 | y.contains(p.y) && 13 | z.contains(p.z) 14 | 15 | def contains(b: Box) = 16 | x.contains(b.x) && 17 | y.contains(b.y) && 18 | z.contains(b.z) 19 | 20 | def overlaps(b: Box) = 21 | x.overlaps(b.x) && 22 | y.overlaps(b.y) && 23 | z.overlaps(b.z) 24 | 25 | def center = Point(x.center, y.center, z.center) 26 | 27 | def corners = Seq( 28 | Point(x.min, y.min, z.min), 29 | Point(x.min, y.min, z.max), 30 | Point(x.min, y.max, z.min), 31 | Point(x.min, y.max, z.max), 32 | Point(x.max, y.min, z.min), 33 | Point(x.max, y.min, z.max), 34 | Point(x.max, y.max, z.min), 35 | Point(x.max, y.max, z.max) 36 | ) 37 | 38 | def toPolyhedron = { 39 | val c = corners 40 | Polyhedron(Seq( 41 | Face(c(0), c(1), c(4)), //0,1,4,5 42 | Face(c(1), c(5), c(4)), 43 | Face(c(2), c(6), c(3)), //2,3,6,7 44 | Face(c(3), c(6), c(7)), 45 | Face(c(0), c(2), c(1)), //0,1,2,3 46 | Face(c(1), c(2), c(3)), 47 | Face(c(4), c(5), c(6)), //4,5,6,7 48 | Face(c(5), c(7), c(6)), 49 | Face(c(0), c(4), c(6)), //0,2,4,6 50 | Face(c(0), c(6), c(2)), 51 | Face(c(1), c(3), c(7)), //1,3,5,7 52 | Face(c(1), c(5), c(5)) 53 | )) 54 | } 55 | 56 | def move(x: Length, y: Length, z: Length) = 57 | if (isEmpty) Box.empty else { 58 | Box(this.x.move(x), this.y.move(y), this.z.move(z)) 59 | } 60 | 61 | def scale(x: Double, y: Double, z: Double) = 62 | if (isEmpty) Box.empty else { 63 | Box(this.x.scale(x), this.y.scale(y), this.z.scale(z)) 64 | } 65 | 66 | def intersection(b: Box) = 67 | if (isEmpty || b.isEmpty) Box.empty else { 68 | Box(x.intersection(b.x), 69 | y.intersection(b.y), 70 | z.intersection(b.z)) 71 | } 72 | 73 | def add(b: Box) = 74 | if (isEmpty || b.isEmpty) Box.empty else { 75 | Box(x.add(b.x), 76 | y.add(b.y), 77 | z.add(b.z)) 78 | } 79 | 80 | def hull(b: Box) = 81 | if (isEmpty) b 82 | else if (b.isEmpty) this 83 | else Box(x.hull(b.x), 84 | y.hull(b.y), 85 | z.hull(b.z)) 86 | 87 | def unionUnderApprox(b: Box) = 88 | if (overlaps(b)) hull(b) else Box.empty 89 | 90 | } 91 | 92 | object Box { 93 | val empty = Box(Interval.empty, Interval.empty, Interval.empty) 94 | val unit = Box(Interval.unit, Interval.unit, Interval.unit) 95 | 96 | def apply(xMin: Length, yMin: Length, zMin: Length, 97 | xMax: Length, yMax: Length, zMax: Length): Box = 98 | Box(Interval(xMin, xMax), 99 | Interval(yMin, yMax), 100 | Interval(zMin, zMax)) 101 | } 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/main/scala/scadla/assembly/Joints.scala: -------------------------------------------------------------------------------- 1 | package scadla.assembly 2 | 3 | import scadla._ 4 | import squants.space.LengthUnit 5 | import squants.motion.AngularVelocity 6 | import squants.time.Time 7 | import squants.motion.Velocity 8 | import squants.space.Length 9 | import squants.space.Millimeters 10 | import squants.time.Seconds 11 | import squants.motion.MetersPerSecond 12 | import squants.motion.RadiansPerSecond 13 | 14 | case class Joint(direction: Vector, 15 | linearSpeed: Velocity, 16 | angularSpeed: AngularVelocity) { 17 | 18 | //TODO need to know the bounding box of the two objects connected by this joint to scale appropriately 19 | 20 | //TODO override that for putting bounds on the rotation, e.g., hinges 21 | def effectiveTime(time: Time) = time 22 | 23 | def expandAt(expansion: Length, time: Time): Frame = { 24 | val effectiveT = effectiveTime(time) 25 | val r = Quaternion.mkRotation(angularSpeed * effectiveT, direction) 26 | val l = direction * (linearSpeed * effectiveT + expansion).in(direction.unit).value 27 | Frame(l, r) 28 | } 29 | 30 | def expandAt(expansion: Length, time: Time, s: Solid): Solid = { 31 | val effectiveT = effectiveTime(time) 32 | val r = Quaternion.mkRotation(angularSpeed * effectiveT, direction) 33 | val l = direction * (linearSpeed * effectiveT + expansion).in(direction.unit).value 34 | Translate(l, Rotate(r, s)) 35 | } 36 | 37 | def at(t: Time): Frame = expandAt(Millimeters(0), t) 38 | 39 | def at(t: Time, s: Solid): Solid = expandAt(Millimeters(0), t, s) 40 | 41 | def expand(t: Length): Frame = expandAt(t, Seconds(0)) 42 | 43 | def expand(t: Length, s: Solid): Solid = expandAt(t, Seconds(0), s) 44 | 45 | } 46 | 47 | object Joint { 48 | 49 | def fixed(direction: Vector): Joint = new Joint(direction, MetersPerSecond(0), RadiansPerSecond(0)) 50 | def fixed(x:Double, y: Double, z: Double, unit: LengthUnit): Joint = fixed(Vector(x,y,z, unit)) 51 | 52 | def revolute(direction: Vector): Joint = new Joint(direction, MetersPerSecond(0), RadiansPerSecond(1)) 53 | def revolute(direction: Vector, angularSpeed: AngularVelocity): Joint = new Joint(direction, MetersPerSecond(0), angularSpeed) 54 | def revolute(x:Double, y: Double, z: Double, unit: LengthUnit, angularSpeed: AngularVelocity = RadiansPerSecond(1)): Joint = revolute(Vector(x,y,z,unit), angularSpeed) 55 | 56 | def prismatic(direction: Vector): Joint = new Joint(direction, MetersPerSecond(1), RadiansPerSecond(0)) 57 | def prismatic(direction: Vector, linearSpeed: Velocity): Joint = new Joint(direction, linearSpeed, RadiansPerSecond(0)) 58 | def prismatic(x:Double, y: Double, z: Double, unit: LengthUnit, linearSpeed: Velocity = MetersPerSecond(0.001)): Joint = prismatic(Vector(x,y,z,unit), linearSpeed) 59 | 60 | def screw(direction: Vector, linearSpeed: Velocity, angularSpeed: AngularVelocity): Joint = new Joint(direction, linearSpeed, angularSpeed) 61 | def screw(x:Double, y: Double, z: Double, unit: LengthUnit, linearSpeed: Velocity = MetersPerSecond(0.001), angularSpeed: AngularVelocity = RadiansPerSecond(1)): Joint = screw(Vector(x,y,z,unit), linearSpeed, angularSpeed) 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/obj/Parser.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends.obj 2 | 3 | import scadla._ 4 | import scala.util.parsing.combinator._ 5 | import dzufferey.utils._ 6 | import dzufferey.utils.LogLevel._ 7 | import java.io._ 8 | import squants.space.{Length, Millimeters, LengthUnit} 9 | 10 | // https://en.wikipedia.org/wiki/Wavefront_.obj_file 11 | // We assume that the faces are oriented and only triangles 12 | 13 | object Parser extends Parser(Millimeters) { 14 | } 15 | 16 | class Parser(unit: LengthUnit = Millimeters) extends JavaTokenParsers { 17 | 18 | sealed abstract class ObjCmd 19 | case object Skip extends ObjCmd 20 | case class VertexDef(p: Point) extends ObjCmd 21 | case class FaceDef(a: Int, b: Int, c: Int) extends ObjCmd 22 | case class ObjectDef(name: String) extends ObjCmd 23 | case class GroupDef(name: String) extends ObjCmd 24 | 25 | 26 | def nonWhite: Parser[String] = """[^\s]+""".r 27 | 28 | def comment: Parser[String] = """#[^\n]*""".r 29 | 30 | def parseVertex: Parser[Point] = 31 | "v" ~> repN(3, floatingPointNumber) ~ opt(floatingPointNumber) ^^ { 32 | case List(a, b,c) ~ None => Point(Millimeters(a.toDouble), Millimeters(b.toDouble), Millimeters(c.toDouble)) 33 | case List(a, b,c) ~ Some(w) => Point(Millimeters(a.toDouble / w.toDouble), Millimeters(b.toDouble / w.toDouble), Millimeters(c.toDouble / w.toDouble)) 34 | } 35 | 36 | 37 | 38 | def vertexIdx: Parser[Int] = 39 | wholeNumber ~ opt( "/" ~> opt(wholeNumber)) ~ opt("/" ~> wholeNumber) ^^ { 40 | case idx ~ _ ~ _ => idx.toInt 41 | } 42 | 43 | def parseFace: Parser[FaceDef] = 44 | "f" ~> repN(3, vertexIdx) ^^ { 45 | case List(a, b,c) => 46 | assert(a >= 0 && b >= 0 && c >= 0, "obj parser only supports absolute indices") 47 | FaceDef(a, b, c) 48 | } 49 | 50 | def parseCmd: Parser[ObjCmd] = ( 51 | parseVertex ^^ ( v => VertexDef(v) ) 52 | | parseFace 53 | | "g" ~> ident ^^ ( id => GroupDef(id) ) 54 | | "o" ~> ident ^^ ( id => ObjectDef(id) ) 55 | | "vt" ~> repN(2, floatingPointNumber) ~ opt(floatingPointNumber) ^^^ Skip 56 | | "vn" ~> repN(3, floatingPointNumber) ^^^ Skip 57 | | "mtllib" ~> nonWhite ^^^ Skip 58 | | "usemtl" ~> ident ^^^ Skip 59 | | "s" ~> (wholeNumber | ident) ^^^ Skip 60 | | comment ^^^ Skip 61 | ) 62 | 63 | def parseCmds: Parser[List[ObjCmd]] = rep(parseCmd) 64 | 65 | def apply(reader: java.io.Reader): Polyhedron = { 66 | val result = parseAll(parseCmds, reader) 67 | if (result.successful) { 68 | val commands = result.get 69 | assert(commands.count{ case ObjectDef(_) | GroupDef(_) => true; case _ => false} == 1, "expected 1 group/object") 70 | val points: Array[Point] = commands.collect{ case VertexDef(v) => v }.toArray 71 | val faces = commands.collect{ case FaceDef(a,b,c) => Face(points(a-1), points(b-1), points(c-1)) } 72 | Polyhedron(faces) 73 | } else { 74 | Logger.logAndThrow("obj.Parser", dzufferey.utils.LogLevel.Error, "parsing error: " + result.toString) 75 | } 76 | } 77 | 78 | def apply(fileName: String): Polyhedron = { 79 | val reader = new BufferedReader(new FileReader(fileName)) 80 | apply(reader) 81 | } 82 | 83 | } 84 | 85 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/box/BoundingBox.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.box 2 | 3 | import scadla._ 4 | import scala.math._ 5 | import squants.space.Length 6 | import squants.space.Millimeters 7 | 8 | object BoundingBox { 9 | 10 | def apply(points: Iterable[Point]): Box = { 11 | if (points.isEmpty) { 12 | Box.empty 13 | } else { 14 | val init = (Millimeters(Double.MaxValue), Millimeters(Double.MinValue)) 15 | def updt(p: Point, proj: Point => Length, acc: (Length,Length)) = { 16 | ( acc._1 min proj(p), acc._2 max proj(p) ) 17 | } 18 | val ((xMin, xMax), (yMin, yMax), (zMin, zMax)) = 19 | points.foldLeft((init,init,init))( (acc,p) => 20 | ( updt(p, p => p.x, acc._1), 21 | updt(p, p => p.y, acc._2), 22 | updt(p, p => p.z, acc._3) ) ) 23 | Box(xMin, yMin, zMin, xMax, yMax, zMax) 24 | } 25 | } 26 | 27 | private val O = Millimeters(0) 28 | def apply(s: Solid): Box = s match { 29 | case Cube(w, d, h) => 30 | Box(O, O, O, w, d, h) 31 | case Empty => 32 | Box.empty 33 | case Sphere(r) => 34 | Box(-r, -r, -r, r, r, r) 35 | case Cylinder(radiusBot, radiusTop, height) => 36 | val r = radiusBot max radiusTop 37 | Box(-r, -r, O, r, r, height) 38 | case Polyhedron(faces) => 39 | apply(faces.flatMap( f => Seq(f.p1, f.p2, f.p3) )) 40 | case f @ FromFile(_,_) => 41 | apply(f.load) 42 | 43 | case Translate(x, y, z, s2) => 44 | apply(s2).move(x,y,z) 45 | case Scale(x, y, z, s2) => 46 | apply(s2).scale(x,y,z) 47 | case Rotate(x, y, z, s2) => 48 | multiply(apply(s2), Matrix.rotation(x,y,z)) 49 | case Mirror(x, y, z, s2) => 50 | multiply(apply(s2), Matrix.mirror(x,y,z)) 51 | case Multiply(m, s2) => 52 | multiply(apply(s2), m) 53 | 54 | case Union(lst @ _*) => 55 | lst.foldLeft(Box.empty)( (b,s) => b.hull(apply(s)) ) 56 | case Intersection(lst @ _*) => 57 | if (lst.isEmpty) Box.empty 58 | else lst.map(apply).reduce( _ intersection _ ) 59 | case Difference(s2, lst @ _*) => 60 | val pos = apply(s2) 61 | val neg = InBox(Union(lst:_*)) 62 | remove(pos, neg) 63 | case Minkowski(lst @ _*) => 64 | val init = Box(O,O,O,O,O,O) 65 | if (lst.isEmpty) Box.empty 66 | else lst.foldLeft(init)( (b,s) => b.add(apply(s)) ) 67 | case Hull(lst @ _*) => 68 | apply(Union(lst:_*)) 69 | } 70 | 71 | protected def multiply(b: Box, m: Matrix) = { 72 | val newCorners = b.corners.map(m * _) 73 | apply(newCorners) 74 | } 75 | 76 | protected def remove(a: Box, b: Box) = { 77 | if (a.isEmpty || b.isEmpty) a 78 | else { 79 | val overlapX = a.x overlaps b.x 80 | val overlapY = a.y overlaps b.y 81 | val overlapZ = a.z overlaps b.z 82 | val coverX = b.x contains a.x 83 | val coverY = b.y contains a.y 84 | val coverZ = b.z contains a.z 85 | if (coverX && coverY && overlapZ) { 86 | Box(a.x, a.y, a.z.remove(b.z)) 87 | } else if (coverX && overlapY && coverZ) { 88 | Box(a.x, a.y.remove(b.y), a.z) 89 | } else if (overlapX && coverY && coverZ) { 90 | Box(a.x.remove(b.x), a.y, a.z) 91 | } else { 92 | a 93 | } 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/stl/Printer.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends.stl 2 | 3 | import scadla._ 4 | import dzufferey.utils._ 5 | import dzufferey.utils.LogLevel._ 6 | import java.io._ 7 | import java.nio.{ByteBuffer,ByteOrder} 8 | import java.nio.channels.FileChannel 9 | import squants.space.{LengthUnit, Millimeters} 10 | 11 | object Printer extends Printer(Millimeters) { 12 | } 13 | 14 | class Printer(unit: LengthUnit = Millimeters) { 15 | 16 | def writeASCII(obj: Polyhedron, writer: BufferedWriter, name: String = ""): Unit = { 17 | writer.write("solid " + name); writer.newLine 18 | obj.faces.foreach{ case f @ Face(p1, p2, p3) => 19 | val n = f.normal 20 | writer.write("facet normal ") 21 | writer.write(s"${n.x} ${n.y} ${n.z}") 22 | writer.newLine 23 | writer.write("\touter loop"); writer.newLine 24 | writer.write("\t\tvertex ") 25 | writer.write(s"${p1.x} ${p1.y} ${p1.z}") 26 | writer.newLine 27 | writer.write("\t\tvertex ") 28 | writer.write(s"${p2.x} ${p2.y} ${p2.z}") 29 | writer.newLine 30 | writer.write("\t\tvertex ") 31 | writer.write(s"${p3.x} ${p3.y} ${p3.z}") 32 | writer.newLine 33 | writer.write("\tendloop"); writer.newLine 34 | writer.write("endfacet"); writer.newLine 35 | } 36 | writer.write("endsolid " + name); writer.newLine 37 | } 38 | 39 | def writeBinary(obj: Polyhedron, out: ByteBuffer): Unit = { 40 | if (out.order() != ByteOrder.LITTLE_ENDIAN) { 41 | out.order(ByteOrder.LITTLE_ENDIAN) 42 | } 43 | def outputPoint(p: Point): Unit = { 44 | out.putFloat(p.x.to(unit).toFloat) 45 | out.putFloat(p.y.to(unit).toFloat) 46 | out.putFloat(p.z.to(unit).toFloat) 47 | } 48 | val header = Array.fill[Byte](80)(' '.toByte) 49 | "Generated with Scadla".getBytes.copyToArray(header) 50 | out.put(header) 51 | out.putInt(obj.faces.size) 52 | obj.faces.foreach{ case f @ Face(p1, p2, p3) => 53 | val n = f.normal 54 | out.putFloat(n.x.to(unit).toFloat) 55 | out.putFloat(n.y.to(unit).toFloat) 56 | out.putFloat(n.z.to(unit).toFloat) 57 | outputPoint(p1) 58 | outputPoint(p2) 59 | outputPoint(p3) 60 | out.putShort(0) 61 | } 62 | } 63 | 64 | def storeText(obj: Polyhedron, fileName: String) = { 65 | val writer = new BufferedWriter(new FileWriter(fileName)) 66 | try writeASCII(obj, writer) 67 | finally writer.close 68 | } 69 | 70 | /* 71 | def storeBinary(obj: Polyhedron, fileName: String) = { 72 | val stream = new FileOutputStream(fileName) 73 | val chan = stream.getChannel 74 | val size = 84 + 50 * obj.faces.size 75 | assert(size.toLong == 84L + 50L * obj.faces.size, "checking for overflow") 76 | val buffer = ByteBuffer.allocate(size) 77 | writeBinary(obj, buffer) 78 | buffer.flip 79 | chan.write(buffer) 80 | chan.close 81 | } 82 | */ 83 | 84 | def storeBinary(obj: Polyhedron, fileName: String) = { 85 | val stream = new RandomAccessFile(fileName, "rw") 86 | val chan = stream.getChannel 87 | val size = 84 + 50 * obj.faces.size 88 | assert(size.toLong == 84L + 50L * obj.faces.size, "checking for overflow") 89 | chan.truncate(size) 90 | val buffer = chan.map(FileChannel.MapMode.READ_WRITE, 0, size) 91 | chan.close 92 | writeBinary(obj, buffer) 93 | buffer.force 94 | } 95 | 96 | } 97 | 98 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/BeltMold.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples 2 | 3 | import math._ 4 | import scadla._ 5 | import utils._ 6 | import InlineOps._ 7 | 8 | /** An experiment to make belts with silicone and thread / small ropes. 9 | * @param length 10 | * @param threadDiameter (typically ~1) 11 | * @param threadTurns (typically ~3) 12 | * @param jacket how much silicone around the thread (typically ~0.5) 13 | * @param tolerance some tolerance between parts to account for the printer 14 | * 15 | * How is it supposed to work: 16 | * ⒈ screw the two half of the inner mold 17 | * ⒉ add silicone 18 | * ⒊ add 1st outer mold and let it cure 19 | * ⒋ remove outer mold 20 | * ⒌ spread thin layer of silicon in the cured part 21 | * ⒍ wrap the thread in the silicone groove 22 | * ⒎ fill with silicone 23 | * ⒏ add 2nd outer mold and let it cure 24 | * ⒐ take apart and clean 25 | */ 26 | class BeltMold(length: Double, 27 | threadDiameter: Double, 28 | threadTurns: Int, 29 | jacket: Double, 30 | tolerance: Double) { 31 | 32 | import scadla.EverythingIsIn.{millimeters, radians} 33 | 34 | val innerRadius = length / 2 / Pi 35 | val outerRadius = innerRadius + threadDiameter + 2*jacket 36 | val height = threadDiameter * threadTurns + 2*jacket 37 | val screwRadius = 1.5 38 | 39 | def inner = { 40 | val base = Union( 41 | Cylinder(outerRadius + 1, 1), 42 | Cylinder(outerRadius, 1).moveZ(1), 43 | Cylinder(innerRadius, height/2).moveZ(2) 44 | ) 45 | val peg = Cylinder(2, height + 4) 46 | val pegs = (0 until 3).map( i => peg.moveX(innerRadius - 5).rotateZ(i * 2*Pi/3) ) 47 | val baseWithPegs = base ++ pegs - Bigger(Union(pegs:_*), tolerance).rotateZ(Pi/3) 48 | val screw = Cylinder(screwRadius, height + 2) 49 | val screws = (0 until 6).map( i => screw.moveX(innerRadius - 5).rotateZ(i * 2*Pi/6 + Pi/6) ) 50 | baseWithPegs -- screws 51 | } 52 | 53 | def outerFst = { 54 | Union( 55 | outerSnd, 56 | PieSlice(outerRadius+tolerance, innerRadius + jacket + threadDiameter/2 + tolerance, Pi, height).moveZ(1), 57 | PieSlice(outerRadius+tolerance, innerRadius + jacket + tolerance, Pi, threadTurns*threadDiameter).moveZ(1+jacket) 58 | ) 59 | } 60 | 61 | def outerSnd = { 62 | val h = height + 2 63 | val ring = PieSlice(outerRadius + 2, outerRadius, Pi, h) 64 | val block = Cube(10, 5, h) - Cylinder(screwRadius, 5).rotateX(-Pi/2).move( 7, 0, h/2) 65 | val blockPositionned = block.moveX(outerRadius) 66 | ring + blockPositionned + blockPositionned.mirror(1,0,0) 67 | } 68 | 69 | def spreader = { 70 | val base = outerFst * PieSlice(outerRadius + 2, 0, Pi/6, height+2).rotateZ(Pi/2) 71 | base - Cube(innerRadius, innerRadius, height+2).moveX(-innerRadius) 72 | } 73 | 74 | } 75 | 76 | object BeltMold { 77 | 78 | def sampleBelt = new BeltMold(200, 1, 3, 0.5, 0.2) 79 | 80 | def main(args: Array[String]) = { 81 | val r = backends.Renderer.default 82 | r.toSTL(sampleBelt.inner, "belt-inner.stl") 83 | r.toSTL(sampleBelt.outerFst, "belt-outer1.stl") 84 | r.toSTL(sampleBelt.outerSnd, "belt-outer2.stl") 85 | r.toSTL(sampleBelt.spreader, "belt-spreader.stl") 86 | //r.view(sampleBelt.inner) 87 | //r.view(sampleBelt.outerFst) 88 | //r.view(sampleBelt.outerSnd) 89 | //r.view(sampleBelt.spreader) 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/Joints.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import math._ 4 | import scadla._ 5 | import utils._ 6 | import InlineOps._ 7 | import thread._ 8 | import Common._ 9 | import scadla.EverythingIsIn.{millimeters, radians} 10 | import squants.space.Length 11 | 12 | /** A 2 degree of freedom joint. 13 | * @param bottomNut is the size of bottom thread/nut 14 | */ 15 | class Joint2DOF(bottomNut: Length = ISO.M8) { 16 | 17 | import ISO.{M3,M8} 18 | 19 | //max outer radius of the nut 20 | val radius = nut.maxOuterRadius(bottomNut) 21 | // bottom wall, e.g., bellow the nut 22 | val botWall = 2 23 | // side wall, e.g., around the nut 24 | val sideWall = 3 25 | val outerRadius = radius + sideWall 26 | val minRadius = nut.minOuterRadius(bottomNut) + looseTolerance 27 | val washerThickness = 0.6 28 | 29 | //println("size:" + 2*outerRadius) → 21.28823512814129 30 | 31 | def cross(length: Length, height: Length, screwRadius: Length): Solid = { 32 | val h = Hexagon(height/2, length).moveZ(-length/2) 33 | val c1 = h.rotateX(Pi/2) 34 | val c2 = h.rotateZ(Pi/6).rotateY(Pi/2) 35 | val s = Cylinder(screwRadius, length+2).moveZ(-length/2-1) 36 | val s1 = s.rotateX(Pi/2) 37 | val s2 = s.rotateY(Pi/2) 38 | c1 + c2 - s1 - s2 39 | } 40 | 41 | def cross: Solid = { 42 | cross(2*(minRadius - washerThickness - tightTolerance), 2*sideWall+1, M3-0.5) 43 | } 44 | 45 | protected def carving(top: Length, bottom: Length) = { 46 | val r = outerRadius-M3*2+looseTolerance 47 | val h = outerRadius*2 + 2 48 | Cylinder(r, h).moveZ(-h/2.0).rotateY(Pi/2).moveZ(r).scaleZ((top-bottom)/r) 49 | } 50 | 51 | protected def addScrewThingy(base: Solid, height: Length) = { 52 | val r = M3 * 2 + 2 53 | val l = outerRadius * 2 54 | val l2 = outerRadius 55 | val c0 = Cylinder(r-0.01,l+10).moveZ(-l2-5).rotateY(Pi/2).moveZ(height) 56 | val c1 = Cylinder(r,l-2).moveZ(-l2+1).rotateY(Pi/2).moveZ(height) 57 | val c2 = Cylinder(r+0.01,2*minRadius).moveZ(-minRadius).rotateY(Pi/2).moveZ(height) 58 | val c3 = Cylinder(M3, l+10).moveZ(-l2-5).rotateY(Pi/2).moveZ(height) 59 | val sh = Cylinder(r+looseTolerance, sideWall).rotateY(Pi/2).moveZ(height) 60 | base - c0 + c1 - c2 - c3 - sh.moveX(outerRadius - 1) - sh.moveX(-outerRadius-sideWall + 1) 61 | } 62 | 63 | def bottom(height: Length) = { 64 | val nutTop = botWall + bottomNut * 1.5 65 | val base = Difference( 66 | Cylinder(outerRadius, height), 67 | Hexagon(minRadius, height).moveZ(botWall).rotateZ(Pi/6), 68 | Cylinder(bottomNut + tolerance, height), 69 | carving(height, nutTop).move(0, outerRadius, nutTop - 0.5), 70 | carving(height, nutTop).move(0, -outerRadius, nutTop - 0.5) 71 | ) 72 | addScrewThingy(base, height) 73 | } 74 | 75 | def top(height: Length, baseFull: Length, screwRadius: Length) = { 76 | val size = math.ceil(2*outerRadius) 77 | val s = Cylinder(screwRadius, size+2).moveZ(-size/2-1) 78 | val delta = screwRadius + 0.1 79 | val base = Difference( 80 | RoundedCubeH(size, size, height, 2.5).move(-size/2.0, -size/2.0, 0), 81 | Hexagon(minRadius, height).moveZ(baseFull).rotateZ(Pi/6), 82 | carving(height, baseFull).move(0, outerRadius, baseFull - 0.5), 83 | carving(height, baseFull).move(0, -outerRadius, baseFull - 0.5), 84 | //holes for mounting screws 85 | s.rotateX(Pi/2).moveZ(4-delta), 86 | s.rotateX(Pi/2).moveZ(baseFull-3-delta), 87 | s.rotateY(Pi/2).moveZ(4+delta), 88 | s.rotateY(Pi/2).moveZ(baseFull-3+delta) 89 | ) 90 | addScrewThingy(base, height) 91 | } 92 | 93 | def parts = Seq( 94 | cross, 95 | bottom(20), 96 | top(30, 16, M3-0.15) 97 | ) 98 | 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/stl/Parser.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends.stl 2 | 3 | import scadla._ 4 | import scala.util.parsing.combinator._ 5 | import dzufferey.utils._ 6 | import dzufferey.utils.LogLevel._ 7 | import java.io._ 8 | import squants.space.{Length, Millimeters, LengthUnit, SquareMeters} 9 | 10 | class AsciiParser(unit: LengthUnit = Millimeters) extends JavaTokenParsers { 11 | 12 | def parseVertex: Parser[Point] = 13 | "vertex" ~> repN(3, floatingPointNumber) ^^ { 14 | case List(a, b,c) => Point(unit(a.toDouble), unit(b.toDouble), unit(c.toDouble)) 15 | } 16 | 17 | def parseFacet: Parser[Face] = 18 | ("facet" ~> "normal" ~> repN(3, floatingPointNumber)) ~ ("outer" ~> "loop" ~> repN(3, parseVertex) <~ "endloop" <~ "endfacet") ^^ { 19 | case List(nx, ny, nz) ~ List(a, b, c) => 20 | val n = Vector(nx.toDouble, ny.toDouble, nz.toDouble, unit) 21 | val f = Face(a, b, c) 22 | scadla.backends.stl.Parser.checkNormal(f, n) 23 | } 24 | 25 | def parseSolid: Parser[Polyhedron] = 26 | "solid" ~> opt(ident) ~> rep(parseFacet) <~ "endsolid" <~ opt(ident) ^^ ( lst => Polyhedron(lst) ) 27 | 28 | def apply(reader: java.io.Reader): Polyhedron = { 29 | val result = parseAll(parseSolid, reader) 30 | if (result.successful) { 31 | result.get 32 | } else { 33 | Logger.logAndThrow("AsciiParser", dzufferey.utils.LogLevel.Error, "parsing error: " + result.toString) 34 | } 35 | } 36 | 37 | 38 | def apply(fileName: String): Polyhedron = { 39 | val reader = new BufferedReader(new FileReader(fileName)) 40 | apply(reader) 41 | } 42 | 43 | } 44 | 45 | class BinaryParser(unit: LengthUnit = Millimeters) { 46 | 47 | import java.nio.file.FileSystems 48 | import java.nio.channels.FileChannel 49 | import java.nio.ByteBuffer 50 | 51 | protected def vector(buffer: ByteBuffer) = { 52 | val p1 = buffer.getFloat 53 | val p2 = buffer.getFloat 54 | val p3 = buffer.getFloat 55 | Vector(p1, p2, p3, unit) 56 | } 57 | 58 | protected def point(buffer: ByteBuffer) = { 59 | val p1 = buffer.getFloat 60 | val p2 = buffer.getFloat 61 | val p3 = buffer.getFloat 62 | Point(unit(p1), unit(p2), unit(p3)) 63 | } 64 | 65 | def apply(fileName: String) = { 66 | val path = FileSystems.getDefault.getPath(fileName) 67 | val file = FileChannel.open(path) 68 | val buffer = file.map(FileChannel.MapMode.READ_ONLY, 0, file.size) 69 | buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN) 70 | buffer.position(80) //skip the header 71 | val nbrTriangles = buffer.getInt 72 | val triangles = for (_ <- 0 until nbrTriangles) yield { 73 | val n = vector(buffer) 74 | val p1 = point(buffer) 75 | val p2 = point(buffer) 76 | val p3 = point(buffer) 77 | buffer.position(buffer.position() + 2) //skip attributes 78 | Parser.checkNormal(Face(p1, p2, p3), n) 79 | } 80 | Polyhedron(triangles) 81 | } 82 | 83 | } 84 | 85 | object Parser { 86 | 87 | val txtHeader = "solid" 88 | val bytesHeader = txtHeader.getBytes("US-ASCII") 89 | val headerSize = bytesHeader.size 90 | 91 | def checkNormal(f: Face, n: Vector): Face = { 92 | if (n.dot(f.normal) < SquareMeters(1e-16)) f.flipOrientation 93 | else f 94 | } 95 | 96 | protected def isTxt(fileName: String) = { 97 | val stream = new FileInputStream(fileName) 98 | val b = Array.ofDim[Byte](headerSize) 99 | stream.read(b) 100 | (0 until headerSize).forall( i => b(i) == bytesHeader(i) ) 101 | } 102 | 103 | 104 | def apply(fileName: String, unit: LengthUnit = Millimeters): Polyhedron = { 105 | if (isTxt(fileName)) { 106 | new AsciiParser(unit)(fileName) 107 | } else { 108 | new BinaryParser(unit)(fileName) 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/GearBearing.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples 2 | 3 | import math._ 4 | import scadla._ 5 | import utils._ 6 | import utils.gear._ 7 | import InlineOps._ 8 | import scadla.utils.gear.Twist.radiansPerMm 9 | import scadla.EverythingIsIn.{millimeters, radians} 10 | 11 | //inspired by Emmet's Gear Bearing (http://www.thingiverse.com/thing:53451) 12 | 13 | object GearBearing { 14 | 15 | def apply(outerRadius: Double, 16 | height: Double, 17 | nbrPlanets: Int, 18 | nbrTeethPlanet: Int, 19 | nbrTeethSun: Int, 20 | helixAngleOuter: Twist, 21 | pressureAngle: Double, 22 | centerHexagonMinRadius: Double, 23 | backlash: Double) = { 24 | new GearBearing(outerRadius, height, nbrPlanets, nbrTeethPlanet, nbrTeethSun, 25 | helixAngleOuter, pressureAngle, centerHexagonMinRadius, backlash) 26 | } 27 | 28 | def main(args: Array[String]): Unit = { 29 | //val gears = apply(35, 10, 5, 10, 15, 0.02, toRadians(40), 5, 0.1) 30 | val gears = apply(35, 10, 5, 6, 10, radiansPerMm(0.02), toRadians(60), 5, 0.1) 31 | backends.Renderer.default.view(gears.all) 32 | } 33 | 34 | } 35 | 36 | class GearBearing(val outerRadius: Double, 37 | val height: Double, 38 | val nbrPlanets: Int, 39 | val nbrTeethPlanet: Int, 40 | val nbrTeethSun: Int, 41 | val helixAngleOuter: Twist, 42 | val pressureAngle: Double, 43 | val centerHexagonMinRadius: Double, 44 | val backlash: Double) { 45 | 46 | //play with addenum to allow pressureAngle > 45 degree 47 | protected def addenum(pitch: Double, nbrTeeth: Int) = { 48 | val default = Gear.addenum( pitch, nbrTeeth) 49 | val toothWidth = pitch.abs * 2 * sin(Pi/nbrTeeth/2) 50 | val coeff = min(1.0, toothWidth / 2 / default / tan(pressureAngle)) 51 | //println("coeff " + coeff) 52 | default * coeff 53 | } 54 | 55 | protected def gear(pitch: Double, nbrTeeth: Int, helix: Twist) = { 56 | val add = addenum( pitch, nbrTeeth) 57 | HerringboneGear(pitch, nbrTeeth, pressureAngle, add, add, height, helix, backlash) 58 | } 59 | 60 | //constants 61 | val sunToPlanetRatio = nbrTeethSun.toDouble / nbrTeethPlanet 62 | val planetRadius = outerRadius / (2 + sunToPlanetRatio) 63 | val sunRadius = planetRadius * sunToPlanetRatio 64 | val nbrTeethOuter = 2 * nbrTeethPlanet + nbrTeethSun 65 | val helixAnglePlanet = helixAngleOuter * (outerRadius / planetRadius) 66 | val helixAngleSun = -(helixAngleOuter * (outerRadius / sunRadius)) 67 | 68 | def externalRadius = outerRadius + addenum(outerRadius, nbrTeethOuter) + Gear.baseThickness 69 | 70 | //TODO check for interferences 71 | 72 | def outer = gear(-outerRadius, nbrTeethOuter, helixAngleOuter) 73 | def planet = gear(planetRadius, nbrTeethPlanet, helixAnglePlanet) 74 | def sun = { 75 | val sunCenter = Hexagon(centerHexagonMinRadius + backlash, height).rotateX(Pi).moveZ(10) 76 | gear(sunRadius, nbrTeethSun, helixAngleSun) - sunCenter 77 | } 78 | 79 | protected def positionPlanet(p: Solid) = { 80 | val r = sunRadius+planetRadius 81 | val α = 2 * Pi / nbrPlanets 82 | val β = -α * outerRadius / planetRadius 83 | for (i <- 0 until nbrPlanets) yield p.rotateZ(i*β).moveX(r).rotateZ(i*α) 84 | } 85 | 86 | def all = { 87 | val p = planet 88 | val planets = positionPlanet(p) 89 | outer ++ planets + sun //TODO the sun should also rotate a bit ??? 90 | } 91 | 92 | def planetHelper(baseThickness: Double, tolerance: Double) = { 93 | val add = addenum(outerRadius, nbrTeethOuter) + tolerance 94 | val planet = Cylinder(planetRadius+add, height).moveZ(baseThickness) 95 | val planets = positionPlanet(planet) 96 | Tube(outerRadius - add, sunRadius + add, height) -- planets 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/package.scala: -------------------------------------------------------------------------------- 1 | package scadla 2 | 3 | import scadla.utils.Trig._ 4 | import squants.space.{Length, Angle} 5 | import scala.language.postfixOps 6 | import scadla.InlineOps._ 7 | 8 | package object utils { 9 | 10 | import scadla._ 11 | 12 | def polarCoordinates(x: Length, y: Length) = (Trig.hypot(x, y), Trig.atan2(y,x)) 13 | 14 | def traverse(f: Solid => Unit, s: Solid): Unit = s match { 15 | case t: Transform => traverse(f, t.child); f(t) 16 | case o: Operation => o.children.foreach(traverse(f, _)); f(o) 17 | case other => f(other) 18 | } 19 | 20 | def map(f: Solid => Solid, s: Solid): Solid = s match { 21 | case o: Operation => f(o.setChildren(o.children.map(map(f, _)))) 22 | case t: Transform => f(t.setChild(map(f, t.child))) 23 | case other => f(other) 24 | } 25 | 26 | def fold[A](f: (A, Solid) => A, acc: A, s: Solid): A = s match { 27 | case t: Transform => f(fold(f, acc, t.child), t) 28 | case o: Operation => f(o.children.foldLeft(acc)( (acc, s2) => fold(f, acc, s2) ), s) 29 | case other => f(acc, other) 30 | } 31 | 32 | private val Zero = 0° 33 | def simplify(s: Solid): Solid = { 34 | def rewrite(s: Solid): Solid = s match { 35 | case Cube(width, depth, height) if width.value <= 0.0 || depth.value <= 0.0 || height.value <= 0.0 => Empty 36 | case Sphere(radius) if radius.value <= 0.0 => Empty 37 | case Cylinder(radiusBot, radiusTop, height) if height.value <= 0.0 => Empty 38 | //TODO order the points/faces to get a normal form 39 | case Polyhedron(triangles) if triangles.isEmpty => Empty 40 | 41 | case Translate(x1, y1, z1, Translate(x2, y2, z2, s2)) => Translate(x1+x2, y1+y2, z1+z2, s2) 42 | case Rotate(x1, Zero, Zero, Rotate(x2, Zero, Zero, s2)) => Rotate(x1+x2, Zero, Zero, s2) 43 | case Rotate(Zero, y1, Zero, Rotate(Zero, y2, Zero, s2)) => Rotate(Zero, y1+y2, Zero, s2) 44 | case Rotate(Zero, Zero, z1, Rotate(Zero, Zero, z2, s2)) => Rotate(Zero, Zero, z1+z2, s2) 45 | case Scale(x1, y1, z1, Scale(x2, y2, z2, s2)) => Scale(x1*x2, y1*y2, z1*z2, s2) 46 | 47 | //TODO flatten ops, reorganize according to com,assoc,... 48 | case Union(lst @ _*) => 49 | val lst2 = lst.toSet - Empty 50 | if (lst2.isEmpty) Empty else Union(lst2.toSeq: _*) 51 | case Intersection(lst @ _*) => 52 | val lst2 = lst.toSet 53 | if (lst2.contains(Empty) || lst2.isEmpty) Empty else Intersection(lst2.toSeq: _*) 54 | case Difference(s2, lst @ _*) => 55 | val lst2 = lst.toSet - Empty 56 | if (lst2.isEmpty) s2 57 | else if (lst2 contains s2) Empty 58 | else Difference(s2, lst2.toSeq: _*) 59 | case Minkowski(lst @ _*) => 60 | val lst2 = lst.toSet - Empty 61 | if (lst2.isEmpty) Empty else Minkowski(lst2.toSeq: _*) 62 | case Hull(lst @ _*) => 63 | val lst2 = lst.toSet - Empty 64 | if (lst2.isEmpty) Empty else Hull(lst2.toSeq: _*) 65 | 66 | case s => s 67 | } 68 | 69 | var sOld = s 70 | var sCurr = map(rewrite, s) 71 | while (sOld != sCurr) { 72 | sOld = sCurr 73 | sCurr = map(rewrite, sCurr) 74 | } 75 | sCurr 76 | } 77 | 78 | /** return a value ∈ [min,max] which is a multiple of step (or min, max) */ 79 | def round(value: Double, min: Double, max: Double, step: Double): Double = { 80 | val stepped = math.rint(value / step) * step 81 | math.min(max, math.max(min, stepped)) 82 | } 83 | 84 | /** https://en.wikipedia.org/wiki/Chord_(geometry) */ 85 | def chord(angle: Angle) = 2*sin(angle/2) 86 | 87 | /** https://en.wikipedia.org/wiki/Apothem */ 88 | def apothem(nbrSides: Int, sideLength: Length) = sideLength / (2 * tan(Pi / nbrSides)) 89 | def apothemFromR(nbrSides: Int, maxRadius: Length) = maxRadius * cos(Pi / nbrSides) 90 | 91 | def incribedRadius(nbrSides: Int, sideLength: Length) = sideLength / tan(Pi / nbrSides) / 2 92 | 93 | def circumscribedRadius(nbrSides: Int, sideLength: Length) = sideLength / sin(Pi / nbrSides) / 2 94 | 95 | } 96 | 97 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/Motors.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scadla.utils._ 6 | import scala.math._ 7 | import squants.space.Length 8 | import scala.language.postfixOps 9 | import squants.space.LengthConversions._ 10 | 11 | //TODO move that to the lib 12 | //place holder for NEMA stepper motors 13 | 14 | object Nema14 { 15 | 16 | val size = 35.2 mm 17 | 18 | def apply( length: Length, 19 | screwLength: Length, 20 | axisFlat: Length, 21 | axisLengthFront: Length, 22 | axisLengthBack: Length ): Solid = { 23 | NemaStepper(size, length, 24 | 26 mm, thread.ISO.M3, screwLength, 25 | 11 mm, 2.0 mm, 26 | 2.5 mm, axisFlat, axisLengthFront, axisLengthBack) 27 | } 28 | 29 | def apply( length: Length, 30 | screwLength: Length, 31 | axisFlat: Length ): Solid = { 32 | apply(length, screwLength, axisFlat, 22 mm, 0 mm) 33 | } 34 | 35 | def apply( length: Length, 36 | screwLength: Length): Solid = { 37 | apply(length, screwLength, 0.45 mm, 22 mm, 0 mm) 38 | } 39 | 40 | def apply( length: Length): Solid = { 41 | apply(length, 3 mm, 0.45 mm, 22 mm, 0 mm) 42 | } 43 | 44 | def putOnScrew(s: Solid) = { 45 | NemaStepper.putOnScrew(26 mm, s) 46 | } 47 | 48 | } 49 | 50 | object Nema17 { 51 | 52 | val size = 42.3 mm 53 | 54 | def apply( length: Length, 55 | screwLength: Length, 56 | axisFlat: Length, 57 | axisLengthFront: Length, 58 | axisLengthBack: Length ): Solid = { 59 | NemaStepper(size, length, 60 | 31 mm, thread.ISO.M3, screwLength, 61 | 11 mm, 2.0 mm, 62 | 2.5 mm, axisFlat, axisLengthFront, axisLengthBack) 63 | } 64 | 65 | def apply( length: Length, 66 | screwLength: Length, 67 | axisFlat: Length ): Solid = { 68 | apply(length, screwLength, axisFlat, 25 mm, 0 mm) 69 | } 70 | 71 | def apply( length: Length, 72 | screwLength: Length): Solid = { 73 | apply(length, screwLength, 0.45 mm, 25 mm, 0 mm) 74 | } 75 | 76 | def apply( length: Length): Solid = { 77 | apply(length, 5 mm, 0.45 mm, 22 mm, 0 mm) 78 | } 79 | 80 | def putOnScrew(s: Solid) = { 81 | NemaStepper.putOnScrew(31 mm, s) 82 | } 83 | 84 | def axis(length: Length) = NemaStepper.axis(2.5 mm, length, 0.45 mm) 85 | 86 | } 87 | 88 | object NemaStepper { 89 | 90 | def axis(axisRadius: Length, length: Length, axisFlat: Length) = { 91 | val a = Cylinder(axisRadius, length) 92 | if (axisFlat > (0 mm)) { 93 | a - Cube(2*axisRadius, 2*axisRadius, length).move(axisRadius - axisFlat, -axisRadius, 0 mm) 94 | } else { 95 | a 96 | } 97 | } 98 | 99 | def apply( side: Length, 100 | length: Length, 101 | screwSeparation: Length, 102 | screwSize: Length, 103 | screwLength: Length, 104 | flangeRadius: Length, 105 | flangeDepth: Length, 106 | axisRadius: Length, 107 | axisFlat: Length, 108 | axisLengthFront: Length, 109 | axisLengthBack: Length ) = { 110 | val base = CenteredCube.xy(side, side, length).moveZ(-length) 111 | val withFlange = if (flangeDepth > (0 mm)) base + Cylinder(flangeRadius, flangeDepth) else base 112 | val screw = Cylinder(screwSize, screwLength.abs) 113 | val screws = putOnScrew(screwSeparation, screw) 114 | val withScrews = 115 | if (screwLength > (0 mm)) { 116 | withFlange - screws.moveZ(-screwLength) 117 | } else if (screwLength < (0 mm)) { 118 | withFlange + screws 119 | } else { 120 | withFlange 121 | } 122 | val withFrontAxis = withScrews + axis(axisRadius, axisLengthFront, axisFlat) 123 | val withBackAxis = withFrontAxis + axis(axisRadius, axisLengthBack, axisFlat).moveZ(-length -axisLengthBack) 124 | withBackAxis 125 | } 126 | 127 | def putOnScrew(screwSeparation: Length, s: Solid) = { 128 | Union( 129 | s.move( -screwSeparation/2, -screwSeparation/2, 0 mm), 130 | s.move( -screwSeparation/2, screwSeparation/2, 0 mm), 131 | s.move( screwSeparation/2, -screwSeparation/2, 0 mm), 132 | s.move( screwSeparation/2, screwSeparation/2, 0 mm) 133 | ) 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/gear/Gear.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.gear 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scala.math._ 6 | import squants.space.Millimeters 7 | import squants.space.Length 8 | 9 | //some references read when coding this 10 | // https://en.wikipedia.org/wiki/Involute_gear 11 | // https://en.wikipedia.org/wiki/Gear#Nomenclature 12 | // https://upload.wikimedia.org/wikipedia/commons/2/28/Gear_words.png 13 | // https://en.wikipedia.org/wiki/List_of_gear_nomenclature 14 | // http://lcamtuf.coredump.cx/gcnc/ and 15 | // http://www.hessmer.org/blog/2014/01/01/online-involute-spur-gear-builder/ 16 | 17 | //TODO redesign to avoid the global variables (toothProfileAccuracy, baseThickness) maybe use trait and mixin 18 | 19 | object Gear { 20 | 21 | /** ~accuracy of the tooth profile */ 22 | var toothProfileAccuracy = 30 23 | 24 | /** This determines the number of vertical steps for helical and herringbone gear. 25 | * This should more or less corresponds to the thickness of the layer when printing */ 26 | var zResolution = Millimeters(0.2) 27 | 28 | /** only for rack and internal gears: thickness of the outer */ 29 | var baseThickness = Millimeters(2) 30 | 31 | /** default value for the addenum given the pitch and the number of teeth */ 32 | def addenum(pitch: Length, nbrTeeth: Int) = pitch.abs * 2 / nbrTeeth 33 | 34 | /** suggested value for the pitch given the number of teeth and the addenum */ 35 | def pitch(nbrTeeth: Int, addenum: Length) = addenum / 2 * nbrTeeth 36 | 37 | /** aprroximative value for the number of teeth given the pitch and the addenum */ 38 | def approxNbrTeeth(pitch: Length, addenum: Length) = pitch / addenum * 2 39 | 40 | /** Simplified interface for spur gear (try to guess some parameters). 41 | * To mesh gears of different sizes, the pitch/nbrTeeth ratio must be the same for all the gears. 42 | */ 43 | def spur(pitch: Length, nbrTeeth: Int, height: Length, backlash: Length) = { 44 | val add = addenum( pitch, nbrTeeth) 45 | InvoluteGear(pitch, nbrTeeth, toRadians(25), add, add, height, backlash) 46 | } 47 | 48 | /** Simplified interface for helical gear (try to guess some parameters) 49 | * Typical helix is 0.05 50 | * To mesh gears of different sizes, the pitch/nbrTeeth and pitch/helix ratio must be the same for all the gears. 51 | */ 52 | def helical(pitch: Length, nbrTeeth: Int, height: Length, helix: Twist, backlash: Length) = { 53 | val add = addenum( pitch, nbrTeeth) 54 | HelicalGear(pitch, nbrTeeth, toRadians(25), add, add, height, helix, backlash) 55 | } 56 | 57 | /** simplified interface for herringbone gear (try to guess some parameters) 58 | * To mesh gears of different sizes, the pitch/nbrTeeth and pitch/helix ratio must be the same for all the gears. 59 | */ 60 | def herringbone(pitch: Length, nbrTeeth: Int, height: Length, helix: Twist, backlash: Length) = { 61 | val add = addenum( pitch, nbrTeeth) 62 | HerringboneGear(pitch, nbrTeeth, toRadians(25), add, add, height, helix, backlash) 63 | } 64 | 65 | /** Simplified interface for rack (try to guess some parameters). 66 | * TODO what needs to match for gears to mesh 67 | */ 68 | def rack(toothWidth: Length, nbrTeeth: Int, height: Length, backlash: Length) = { 69 | val add = toothWidth / 2 70 | Rack(toothWidth, nbrTeeth, toRadians(25), add, add, height, backlash) 71 | } 72 | 73 | 74 | /* some examples 75 | def main(args: Array[String]) { 76 | val obj = spur(10, 12, 2, 0.1) 77 | //val obj = spur(20, 30, 5, 0.1) 78 | //val obj = helical(10, 18, 5, 0.05, 0.1) 79 | //val obj = herringbone(10, 18, 5, 0.05, 0.1) 80 | //val obj = InvoluteGear(10, 12, toRadians(25), 1.5, 1.5, 2, 0.1) 81 | //val obj = InvoluteGear(10, 30, toRadians(25), 1, 1, 5, 0) 82 | //val obj = InvoluteGear(10, 12, toRadians(40), 1.5, 1.5, 2, 0.1, toRadians(-15)) 83 | //val obj = InvoluteGear(10, 18, toRadians(30), 1.2, 1.2, 2, 0.1, toRadians(-12)) 84 | //val obj = InvoluteGear(10, 18, toRadians(30), 1.2, 1.2, 2, 0.1, toRadians(12)) 85 | //val obj = HelicalGear(10, 12, toRadians(25), 1.5, 1.5, 10, 0.1, 0.1, 0, 0.4) 86 | //val obj = HerringboneGear(10, 12, toRadians(25), 1.5, 1.5, 10, Pi/20, 0.1, 0, 0.4) 87 | //val obj = spur(-10, 12, 2, 0.1) 88 | //val obj = spur(-20, 30, 2, 0.1) 89 | //val obj = helical(-10, 18, 5, 0.05, 0.1) 90 | //val obj = herringbone(-10, 18, 5, 0.05, 0.1) 91 | //val obj = rack(2, 12, 2, 0.1) 92 | backends.OpenSCAD.view(obj) 93 | } 94 | */ 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/reach3D/ReachLegs.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.reach3D 2 | 3 | import scadla._ 4 | import utils._ 5 | import Trig._ 6 | import InlineOps._ 7 | import squants.space.{Length, Angle} 8 | import scala.language.postfixOps 9 | import squants.space.LengthConversions._ 10 | import scala.collection.parallel.CollectionConverters._ 11 | 12 | /** Some legs for the Reach3D printer */ 13 | object ReachLegs { 14 | 15 | // two long legs attaching on the side of the 2040 extrusion 16 | // one short leg attaching on the end/bottom of the 2020 extrusion 17 | 18 | // base shape 19 | // t-nuts (M4) 20 | // springy thing 21 | 22 | val thickness = 4.0 mm 23 | val padWidth = 12.5 mm 24 | val width = 2 * padWidth 25 | val length = 100.0 mm 26 | 27 | val height = (40 mm) + thickness 28 | val padSpace = 6.0 mm 29 | val meetingPoint = 50.0 mm 30 | val damperThickness = 1.4 mm 31 | 32 | def base1 = { 33 | import scadla.EverythingIsIn.millimeters 34 | val b = Hull( 35 | Cube(width, thickness, height), 36 | Cube(width, length, thickness), 37 | Cylinder(thickness/2, width).scale(1,0.5,1).rotateY(Pi/2).move(0, length, thickness/2) 38 | ) 39 | // ideal dimensions: 40 | // Trapezoid(6, 9, width, 2) 41 | // Trapezoid(6.75, 9, width, 1.5) 42 | // Trapezoid(7.5, 9, width, 1) 43 | val slot = Trapezoid(6.75, 9, width, 1.5).moveX(-4.5).rotateY(-Pi/2).rotateZ(Pi/2).moveX(width) 44 | val added = Union( 45 | b - b.move(thickness, thickness, thickness), 46 | slot.moveZ(thickness + 10), 47 | slot.moveZ(thickness + 30), 48 | PieSlice(thickness, 0, Pi/2, width).rotateY(Pi/2).rotateX(-Pi/2).moveZ(thickness) 49 | ) 50 | val screw = Cylinder(thread.ISO.M4 + 0.1, thickness + 3).rotateX(Pi/2).move(width/2, thickness + 1, 0) 51 | Difference( 52 | added, 53 | screw.moveZ(thickness + 10), 54 | screw.moveZ(thickness + 30) 55 | ) 56 | } 57 | 58 | def vibrationDamper(baseWidth: Length) = { 59 | import scadla.EverythingIsIn.millimeters 60 | val offset = (baseWidth - width) / 2 61 | val c = Cylinder(damperThickness/2, width).rotateY(Pi/2) 62 | val pad = Union( 63 | Cube(width, padWidth + 2 * damperThickness, damperThickness), 64 | c.moveY(damperThickness/2), 65 | c.moveY(damperThickness*3/2 + padWidth) 66 | ) 67 | val leg = Hull( 68 | c.move(offset, 0, damperThickness/2), 69 | Cylinder(damperThickness/2, baseWidth).rotateY(Pi/2).move(0, -meetingPoint, damperThickness*3/2 + padSpace) 70 | ) 71 | (pad.moveX(offset) + leg).moveZ(-damperThickness) 72 | } 73 | 74 | def leg1 = { 75 | base1 + vibrationDamper(width).move(0 mm, length - padWidth - 2*damperThickness, - padSpace) 76 | } 77 | 78 | val beamLength = meetingPoint + padWidth + 2*damperThickness + (20 mm) 79 | 80 | def endcap2040 = { 81 | import scadla.EverythingIsIn.millimeters 82 | //ideal slot: Cube(6,10,2).moveX(-3) + Cube(11, 10, 1.5).move(-5.5,0,2) + Trapezoid(6, 11, 10,2.5).move(-5.5,0,3.5) 83 | val slot = Cube(5.8,10,2.1).moveX(-2.9) + Cube(10.8, 10, 1.3).move(-5.4,0,2.1) + Trapezoid(5.8, 10.8, 10, 2.3).move(-5.4,0,3.4) 84 | Cube(20, thickness, 40) + 85 | slot.move(10, 0, 0) + 86 | slot.move(10, 0, 20) + 87 | slot.rotateY( Pi/2).move( 0, 0, 10) + 88 | slot.rotateY(-Pi/2).move(20, 0, 10) + 89 | slot.rotateY( Pi/2).move( 0, 0, 30) + 90 | slot.rotateY(-Pi/2).move(20, 0, 30) + 91 | slot.rotateY( Pi).move(10, 0, 20) + 92 | slot.rotateY( Pi).move(10, 0, 40) 93 | } 94 | 95 | def base2 = { 96 | import scadla.EverythingIsIn.millimeters 97 | // beam that continues below the extrusion 98 | val beam = Cube(20, beamLength, thickness) + 99 | PieSlice(thickness, 0, Pi/2, 20).rotateY(Pi/2).move(0, beamLength, thickness) + 100 | PieSlice(thickness, 0, Pi/2, 20).rotateY(Pi/2).rotateX(-Pi/2).moveZ(thickness) + 101 | Trapezoid(6.75, 9, beamLength, 1.5).move(-4.5 + 10, 0, thickness) - 102 | Cylinder(thread.ISO.M4 + 0.1, thickness + 3).move(10, 10, 0) 103 | beam + endcap2040.rotateZ(Pi).move(20, beamLength + thickness, thickness) 104 | } 105 | 106 | def leg2 = { 107 | base2 + vibrationDamper(20 mm).move(0 mm, beamLength - padWidth - 2*damperThickness, - padSpace) 108 | } 109 | 110 | def main(args: Array[String]): Unit = { 111 | import scadla.EverythingIsIn.{millimeters, radians} 112 | Seq( 113 | leg1.rotateY(-Pi/2) -> "leg1a.stl", 114 | leg1.mirror(0,0,1).rotateY(-Pi/2) -> "leg1b.stl", 115 | leg2.rotateY(-Pi/2) -> "leg2.stl" 116 | ).par.foreach{ case (obj, name) => 117 | backends.Renderer.default.toSTL(obj, name) 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/main/scala/scadla/assembly/Assembly.scala: -------------------------------------------------------------------------------- 1 | package scadla.assembly 2 | 3 | import scadla._ 4 | import scadla.backends.Renderer 5 | import scala.language.implicitConversions 6 | import squants.space.Length 7 | import squants.time.Time 8 | import squants.time.Seconds 9 | import squants.space.Millimeters 10 | 11 | sealed abstract class Assembly(name: String, children: List[(Frame,Joint,Assembly,Frame)]) { 12 | 13 | protected def checkNotInChildren(as: Set[Assembly]): Boolean = { 14 | val tas = as + this 15 | !as(this) && children.forall(_._3.checkNotInChildren(tas)) 16 | } 17 | 18 | protected def addChild(where: Frame, joint: Joint, child: Assembly, whereChild: Frame): Assembly 19 | 20 | 21 | def +(where: Frame, joint: Joint, child: Assembly, whereChild: Frame): Assembly = { 22 | assert(child.checkNotInChildren(Set(this))) 23 | addChild(where, joint, child, whereChild.inverse) 24 | } 25 | 26 | def +(where: Vector, joint: Joint, child: Assembly, whereChild: Frame): Assembly = 27 | this.+(Frame(where), joint, child, whereChild) 28 | 29 | def +(joint: Joint, child: Assembly, whereChild: Frame): Assembly = 30 | this.+(Frame(), joint, child, whereChild) 31 | 32 | def +(where: Frame, joint: Joint, child: Assembly, whereChild: Vector): Assembly = 33 | this.+(where, joint, child, Frame(whereChild)) 34 | 35 | def +(where: Vector, joint: Joint, child: Assembly, whereChild: Vector): Assembly = 36 | this.+(Frame(where), joint, child, Frame(whereChild)) 37 | 38 | def +(joint: Joint, child: Assembly, whereChild: Vector): Assembly = 39 | this.+(Frame(), joint, child, Frame(whereChild)) 40 | 41 | def +(where: Frame, joint: Joint, child: Assembly): Assembly = 42 | this.+(where, joint, child, Frame()) 43 | 44 | def +(where: Vector, joint: Joint, child: Assembly): Assembly = 45 | this.+(Frame(where), joint, child, Frame()) 46 | 47 | def +(joint: Joint, child: Assembly): Assembly = 48 | this.+(Frame(), joint, child, Frame()) 49 | 50 | def expandAt(expansion: Length, time: Time): Seq[(Frame,Polyhedron)] = { 51 | children.flatMap{ case (f1, j, c, f3) => 52 | val f2 = j.expandAt(expansion, time) 53 | val f = f1.compose(f2).compose(f3) 54 | val children = c.expandAt(expansion, time) 55 | children.map{ case (fc,p) => (f.compose(fc),p) } 56 | } 57 | } 58 | 59 | def at(t: Time): Seq[(Frame,Polyhedron)] = expandAt(Millimeters(0), t) 60 | 61 | def expand(t: Length): Seq[(Frame,Polyhedron)] = expandAt(t, Seconds(0)) 62 | 63 | //TODO immutable 64 | def preRender(r: Renderer): Unit = { 65 | children.foreach( _._3.preRender(r) ) 66 | } 67 | 68 | def parts: Set[Part] = { 69 | children.foldLeft(Set[Part]())( _ ++ _._3.parts ) 70 | } 71 | 72 | def bom: Map[Part,Int] = { 73 | children.foldLeft(Map[Part,Int]())( (acc,c) => { 74 | c._3.bom.foldLeft(acc)( (acc, kv) => { 75 | val (k,v) = kv 76 | acc + (k -> (acc.getOrElse(k,0) + v)) 77 | }) 78 | }) 79 | } 80 | 81 | def plate(x: Double, y: Double, gap: Double = 5): Seq[(Frame,Polyhedron)] = { 82 | //filter out vitamines 83 | val b = bom.filter(!_._1.vitamin) 84 | //gets all the parts to print 85 | val polys = b.flatMap{ case (p,n) => Seq.fill(n)(p.printable) } 86 | //TODO compute bounding box and place within the x-y space 87 | ??? 88 | } 89 | 90 | } 91 | 92 | object Assembly { 93 | 94 | def apply(name: String) = EmptyAssembly(name, Nil) 95 | def apply(part: Part) = SingletonAssembly(part, Nil) 96 | 97 | implicit def part2Assembly(part: Part): Assembly = new SingletonAssembly(part, Nil) 98 | 99 | def quickRender(assembly: Seq[(Frame,Polyhedron)]) = { 100 | val p2 = assembly.map{ case (f,p) => f.directTo(p) } 101 | Polyhedron(p2.flatMap(_.faces)) 102 | } 103 | 104 | } 105 | 106 | case class EmptyAssembly(name: String, children: List[(Frame,Joint,Assembly,Frame)]) extends Assembly(name, children) { 107 | protected def addChild(where: Frame, joint: Joint, child: Assembly, whereChild: Frame): Assembly = { 108 | EmptyAssembly(name, (where, joint, child, whereChild) :: children) 109 | } 110 | } 111 | 112 | case class SingletonAssembly(part: Part, children: List[(Frame,Joint,Assembly,Frame)]) extends Assembly(part.name, children) { 113 | 114 | protected def addChild(where: Frame, joint: Joint, child: Assembly, whereChild: Frame): Assembly = { 115 | SingletonAssembly(part, (where, joint, child, whereChild) :: children) 116 | } 117 | 118 | override def expandAt(e: Length, t: Time): Seq[(Frame,Polyhedron)] = super.expandAt(e, t) :+ (Frame() -> part.mesh) 119 | 120 | override def preRender(r: Renderer): Unit = { 121 | super.preRender(r) 122 | part.preRender(r) 123 | } 124 | 125 | override def parts = super.parts + part 126 | 127 | override def bom = { 128 | val b = super.bom 129 | b.+(part -> (b.getOrElse(part, 0) + 1)) 130 | } 131 | 132 | } 133 | 134 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/reach3D/SpoolHolder.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.reach3D 2 | 3 | import scadla._ 4 | import utils._ 5 | import Trig._ 6 | import InlineOps._ 7 | import thread.{ISO, StructuralNutPlaceHolder} 8 | import squants.space.{Length, Angle} 9 | import scala.language.postfixOps 10 | import squants.space.LengthConversions._ 11 | import scala.collection.parallel.CollectionConverters._ 12 | 13 | object SpoolHolder { 14 | 15 | val t = ISO.M6 16 | 17 | val looseTolerance = 0.2 mm 18 | 19 | val bbRadius = 3.0 mm 20 | val bearingGap = 1.0 mm 21 | val grooveDepth = bbRadius / cos(Pi/4) 22 | 23 | // make sures the BBs fit nicely 24 | def adjustGrooveRadius(radius: Length): Length = { 25 | assert(radius > bbRadius) 26 | // find n such that the circumscribed radius is the closest to the given radius 27 | val sideLength = 2*bbRadius + looseTolerance 28 | val nD = Pi / asin(sideLength / radius / 2) 29 | val n = nD.toInt // rounding to nearest regular polygon 30 | circumscribedRadius(n, sideLength) 31 | } 32 | 33 | def flatGroove(radius: Length, depth: Length, angle: Angle = Pi/2, undercut: Length = 0.5 mm) = { 34 | val width = depth / tan(Pi/2 - angle/2) 35 | val outer = Cylinder(radius + width, radius, depth) 36 | val inner = Cylinder(radius - width, radius, depth) 37 | (outer - inner) * Cylinder(radius + width, depth - undercut) 38 | } 39 | 40 | def radialGroove(radius: Length, depth: Length, angle: Angle = Pi/2, undercut: Length = 0.5 mm) = { 41 | val width = (depth / tan(Pi/2 - angle/2)).abs 42 | val middleRadius = radius - depth 43 | val body = Cylinder(radius, 2*width) 44 | val top = Cylinder(middleRadius, radius, width).moveZ(width) 45 | val bot = Cylinder(radius, middleRadius, width) 46 | val shape = 47 | if (depth >= (0 mm)) body - top - bot - Cylinder(middleRadius + undercut, 2*width) 48 | else (top + bot - body) * Cylinder(middleRadius - undercut, 2*width) 49 | shape.moveZ(-width) 50 | } 51 | 52 | def steppedCone(baseRadius: Length, steps: Int, stepRadiusDec: Length, stepHeight: Length) = { 53 | Union((0 until steps).map( i => Cylinder(baseRadius - i * stepRadiusDec, stepHeight).moveZ(i * stepHeight) ): _*) 54 | } 55 | 56 | def stemShape(radius1: Length, height1: Length, radius2: Length, height2: Length, bevel: Length) = { 57 | val c1 = Cylinder(radius1, height1) 58 | val c2 = Cylinder(radius2, height2).moveZ(height1) 59 | val bev = if (radius1 > radius2) Cylinder(radius2 + bevel, radius2, bevel).moveZ(height1) 60 | else Cylinder(radius1, radius1 + bevel, bevel).moveZ(height1 - bevel) 61 | c1 + c2 + bev - Cylinder(t, height1 + height2) 62 | } 63 | 64 | //////////////////////// 65 | // hard-coded numbers // 66 | //////////////////////// 67 | 68 | // bearings 69 | val radialBearingRadius = adjustGrooveRadius(t + (6 mm) + bearingGap / 2) 70 | val flatBearingRadius = adjustGrooveRadius(25 mm) 71 | val groove1 = flatGroove(flatBearingRadius, grooveDepth - bearingGap / 2) 72 | val groove2a = radialGroove(radialBearingRadius, grooveDepth) 73 | val groove2b = radialGroove(radialBearingRadius, -grooveDepth) 74 | 75 | // cone dimensions 76 | val coneLength = 18 mm 77 | val coneMaxRadius = 45 mm 78 | val coneSteps = 14 79 | val stepHeight = 1.2 mm //1.1171875 80 | val stepRadius = 2 mm 81 | 82 | val radialBearingPos1 = 4.8 mm //3.6 // or 4.8 83 | val radialBearingPos2 = coneLength - radialBearingPos1 84 | 85 | val stem = { 86 | val base = stemShape(30 mm, 5 mm, radialBearingRadius - bearingGap / 2, coneLength + bearingGap, 1 mm) 87 | val grooves = List( 88 | groove1.mirror(0,0,1), 89 | groove2a.moveZ(bearingGap + radialBearingPos1), 90 | groove2a.moveZ(bearingGap + radialBearingPos2) 91 | ) 92 | base -- grooves.map(_.moveZ(5 mm)) - (new StructuralNutPlaceHolder).M6 93 | } 94 | 95 | val cone = { 96 | val screw = (Cylinder(1.25 mm, 16 mm) + Cylinder(3 mm, 8 mm).moveZ(16 mm)).moveZ(1 mm) 97 | val screws = (0 until 3).map( i => { screw.moveX(radialBearingRadius + grooveDepth + (2 mm)).rotateZ(2 * Pi / 3 * i) }) 98 | val toRemove = screws ++ Seq( 99 | Cylinder(radialBearingRadius + bearingGap/2, coneLength), 100 | groove1, 101 | groove2b.moveZ(radialBearingPos1), 102 | groove2b.moveZ(radialBearingPos2) 103 | ) 104 | val baseHeight = coneLength - coneSteps * stepHeight 105 | val base = Cylinder(coneMaxRadius, baseHeight + (0.001 mm)) + steppedCone(coneMaxRadius, coneSteps, stepRadius, stepHeight).moveZ(baseHeight) 106 | base -- toRemove 107 | } 108 | 109 | val conePart1 = cone * Cylinder(coneMaxRadius, radialBearingPos1) 110 | val conePart2 = cone * Cylinder(coneMaxRadius, radialBearingPos2 - radialBearingPos1 - (0.01 mm)).moveZ(radialBearingPos1 + (0.005 mm)) 111 | val conePart3 = cone * Cylinder(coneMaxRadius, radialBearingPos1).moveZ(radialBearingPos2 + (0.005 mm)) 112 | 113 | def main(args: Array[String]): Unit = { 114 | //backends.Renderer.default.view(stem) 115 | Seq( 116 | stem -> "stem.stl", 117 | conePart1 -> "conePart1.stl", 118 | conePart2 -> "conePart2.stl", 119 | conePart3 -> "conePart3.stl" 120 | ).par.foreach{ case (obj, name) => 121 | backends.Renderer.default.toSTL(obj, name) 122 | } 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/main/scala/scadla/backends/JCSG.scala: -------------------------------------------------------------------------------- 1 | package scadla.backends 2 | 3 | import eu.mihosoft.jcsg.{Cube => JCube, Sphere => JSphere, Cylinder => JCylinder, Polyhedron => JPolyhedron, _} 4 | import eu.mihosoft.vvecmath.{Vector3d, Transform, Plane} 5 | import scadla._ 6 | import InlineOps._ 7 | import java.util.ArrayList 8 | import squants.space.{Length, Millimeters, LengthUnit} 9 | 10 | //backend using: https://github.com/miho/JCSG 11 | object JCSG extends JCSG(16, Millimeters) 12 | 13 | class JCSG(numSlices: Int, unit: LengthUnit = Millimeters) extends Renderer(unit) { 14 | 15 | override def isSupported(s: Solid): Boolean = s match { 16 | case s: Shape => super.isSupported(s) 17 | case _: Multiply => false 18 | case t: scadla.Transform => isSupported(t.child) 19 | case u @ Union(_) => u.children.forall(isSupported) 20 | case i @ Intersection(_) => i.children.forall(isSupported) 21 | case d @ Difference(_,_) => d.children.forall(isSupported) 22 | case c @ Hull(_) => c.children.forall(isSupported) 23 | case m @ Minkowski(_) => m.children.forall(isSupported) 24 | case _ => false 25 | } 26 | 27 | protected def empty = new JPolyhedron(Array[Vector3d](), Array[Array[Integer]]()).toCSG 28 | 29 | protected def stupidMinkowski2(a: Polyhedron, b: Polyhedron): Polyhedron = { 30 | // for all face in A: move B to each point and take the hull 31 | val parts1 = a.faces.toSeq.map{ case Face(p1, p2, p3) => 32 | Hull( b.move(p1.x, p1.y, p1.z), 33 | b.move(p2.x, p2.y, p2.z), 34 | b.move(p3.x, p3.y, p3.z)) 35 | } 36 | // and then deal with internal holes (union with A translated by every point in B ?) 37 | val bPoints = b.faces.foldLeft(Set.empty[Point])( (acc, f) => acc + f.p1 + f.p2 + f.p3 ) 38 | val parts2 = bPoints.toSeq.map(a.move(_)) 39 | // then union everything and render 40 | val res = Union( (parts1 ++ parts2) :_*) 41 | apply(res) 42 | } 43 | 44 | protected def stupidMinkowski(objs: Seq[Solid]): CSG = { 45 | val objs2 = objs.map( apply ) 46 | val res = objs2.reduce(stupidMinkowski2) 47 | to(res) 48 | } 49 | 50 | protected def to(s: Solid): CSG = { 51 | import scala.language.implicitConversions 52 | implicit def toDouble(l: Length): Double = length2Double(l) 53 | s match { 54 | case Empty => empty 55 | case Cube(width, depth, height) => new JCube(Vector3d.xyz(width/2, depth/2, height/2), Vector3d.xyz(width, depth, height)).toCSG() 56 | case Sphere(radius) => new JSphere(radius, numSlices, numSlices/2).toCSG() 57 | case Cylinder(radiusBot, radiusTop, height) => new JCylinder(radiusBot, radiusTop, height, numSlices).toCSG() 58 | case Polyhedron(triangles) => 59 | val points = triangles.foldLeft(Set[Point]())( (acc, face) => acc + face.p1 + face.p2 + face.p3 ) 60 | val indexed = points.toSeq.zipWithIndex 61 | val idx: Map[Point, Int] = indexed.toMap 62 | val vs = Array.ofDim[Vector3d](indexed.size) 63 | indexed.foreach{ case (p, i) => vs(i) = Vector3d.xyz(p.x,p.y,p.z) } 64 | val is = Array.ofDim[Array[Integer]](triangles.size) 65 | triangles.zipWithIndex.foreach { case (Face(a,b,c), i) => is(i) = Array(idx(a), idx(b), idx(c)) } 66 | new JPolyhedron(vs, is).toCSG() 67 | case f @ FromFile(path, format) => 68 | format match { 69 | case "stl" => STL.file(java.nio.file.Paths.get(path)) 70 | case _ => to(f.load) 71 | } 72 | case Union(objs @ _*) => if (objs.isEmpty) empty else objs.map(to).reduce( _.union(_) ) 73 | case Intersection(objs @ _*) => if (objs.isEmpty) empty else objs.map(to).reduce( _.intersect(_) ) 74 | case Difference(pos, negs @ _*) => negs.map(to).foldLeft(to(pos))( _.difference(_) ) 75 | case Minkowski(objs @ _*) => stupidMinkowski(objs) 76 | case Hull(objs @ _*) => to(Union(objs:_*)).hull 77 | case Scale(x, y, z, obj) => to(obj).transformed(Transform.unity().scale(x, y, z)) 78 | case Rotate(x, y, z, obj) => to(obj).transformed(Transform.unity().rot(x.toDegrees, y.toDegrees, z.toDegrees)) 79 | case Translate(x, y, z, obj) => to(obj).transformed(Transform.unity().translate(x, y, z)) 80 | case Mirror(x, y, z, obj) => to(obj).transformed(Transform.unity().mirror(Plane.fromPointAndNormal(Vector3d.ZERO, Vector3d.xyz(x,y,z)))) 81 | case Multiply(m, obj) => sys.error("JCSG does not support arbitrary matrix transform") 82 | } 83 | } 84 | 85 | protected def polyToFaces(p: Polygon): List[Face] = { 86 | val vs = p.vertices.toArray(Array.ofDim[Vertex](p.vertices.size)) 87 | val pts = vs.map( v => Point(unit(v.pos.x), unit(v.pos.y), unit(v.pos.z))) 88 | //if more than 3 vertices if needs to be triangulated 89 | //assume that the vertices form a convex loop 90 | (0 until vs.size - 2).toList.map(i => Face(pts(0), pts(i+1), pts(i+2)) ) 91 | } 92 | 93 | protected def from(c: CSG): Polyhedron = { 94 | val polygons = c.getPolygons.iterator 95 | var faces = List[Face]() 96 | while (polygons.hasNext) { 97 | faces = polyToFaces(polygons.next) ::: faces 98 | } 99 | Polyhedron(faces) 100 | } 101 | 102 | def apply(obj: Solid): Polyhedron = obj match { 103 | case p @ Polyhedron(_) => p 104 | case other => from(to(other)) 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/scala/scadla/Solid.scala: -------------------------------------------------------------------------------- 1 | package scadla 2 | 3 | import squants.space.Length 4 | import squants.space.Angle 5 | 6 | abstract class Solid { 7 | 8 | /** Since we render in a second step, let's keep track of the stack trace so we can trace error to the source. 9 | * 10 | * The downside is that it might make this a bit slower and take much more memory... 11 | * 12 | * drop(4) to get rid of: 13 | * java.base/java.lang.Thread.getStackTrace 14 | * scadla.Solid. 15 | * scadla.Shape. 16 | * scadla.???. 17 | */ 18 | val trace = Thread.currentThread().getStackTrace().drop(4).toSeq 19 | 20 | } 21 | 22 | //basic shapes 23 | abstract class Shape extends Solid 24 | 25 | case class Cube(width: Length, depth: Length, height: Length) extends Shape 26 | case class Sphere(radius: Length) extends Shape 27 | case class Cylinder(radiusBot: Length, radiusTop: Length, height: Length) extends Shape 28 | case class FromFile(path: String, format: String = "stl") extends Shape { 29 | def load: Polyhedron = format match { 30 | case "stl" => backends.stl.Parser(path) 31 | case "obj" => backends.obj.Parser(path) 32 | case "amf" => backends.amf.Parser(path) 33 | case other => sys.error("parsing " + other + " not yet supported") 34 | } 35 | } 36 | case object Empty extends Shape 37 | case class Polyhedron(faces: Iterable[Face]) extends Shape { 38 | def indexed = Polyhedron.indexed(this) 39 | } 40 | 41 | //operations 42 | abstract class Operation(val children: Seq[Solid]) extends Solid { 43 | def isCommutative = false 44 | def isLeftAssociative = false 45 | def isRightAssociative = false 46 | def isAssociative = isLeftAssociative && isRightAssociative 47 | def setChildren(c: Seq[Solid]): Operation 48 | } 49 | 50 | case class Union(objs: Solid*) extends Operation(objs) { 51 | override def isCommutative = true 52 | override def isLeftAssociative = true 53 | override def isRightAssociative = true 54 | def setChildren(c: Seq[Solid]) = Union(c: _*) 55 | } 56 | case class Intersection(objs: Solid*) extends Operation(objs) { 57 | override def isCommutative = true 58 | override def isLeftAssociative = true 59 | override def isRightAssociative = true 60 | def setChildren(c: Seq[Solid]) = Intersection(c: _*) 61 | } 62 | case class Difference(pos: Solid, negs: Solid*) extends Operation(pos +: negs) { 63 | override def isCommutative = false 64 | override def isLeftAssociative = true 65 | override def isRightAssociative = false 66 | def setChildren(c: Seq[Solid]) = Difference(c.head, c.tail: _*) 67 | } 68 | case class Minkowski(objs: Solid*) extends Operation(objs) { 69 | override def isCommutative = true 70 | override def isLeftAssociative = true 71 | override def isRightAssociative = true 72 | def setChildren(c: Seq[Solid]) = Minkowski(c: _*) 73 | } 74 | case class Hull(objs: Solid*) extends Operation(objs) { 75 | override def isCommutative = true 76 | override def isLeftAssociative = true 77 | override def isRightAssociative = true 78 | def setChildren(c: Seq[Solid]) = Hull(c: _*) 79 | } 80 | 81 | //transforms 82 | sealed abstract class Transform(val child: Solid) extends Solid { 83 | def matrix: Matrix 84 | def asMultiply = Multiply(matrix, child) 85 | def setChild(c: Solid): Transform = if (c == child) this else Multiply(matrix, c) 86 | } 87 | 88 | case class Scale(x: Double, y: Double, z: Double, obj: Solid) extends Transform(obj) { 89 | def matrix = Matrix.scale(x, y, z) 90 | override def setChild(c: Solid) = if (c == obj) this else Scale(x, y, z, c) 91 | } 92 | case class Rotate(x: Angle, y: Angle, z: Angle, obj: Solid) extends Transform(obj) { 93 | def matrix = Matrix.rotation(x, y, z) 94 | override def setChild(c: Solid) = if (c == obj) this else Rotate(x, y, z, c) 95 | } 96 | case class Translate(x: Length, y: Length, z: Length, obj: Solid) extends Transform(obj) { 97 | def matrix = Matrix.translation(x, y, z) 98 | override def setChild(c: Solid) = if (c == obj) this else Translate(x, y, z, c) 99 | } 100 | case class Mirror(x: Double, y: Double, z: Double, obj: Solid) extends Transform(obj) { 101 | def matrix = Matrix.mirror(x, y, z) 102 | override def setChild(c: Solid) = if (c == obj) this else Mirror(x, y, z, c) 103 | } 104 | case class Multiply(m: Matrix, obj: Solid) extends Transform(obj) { 105 | def matrix = m 106 | } 107 | 108 | //modifiers 109 | 110 | ///////////////////////////// 111 | // additional constructors // 112 | ///////////////////////////// 113 | 114 | object Cylinder { 115 | def apply(radius: Length, height: Length): Cylinder = Cylinder(radius, radius, height) 116 | } 117 | 118 | object Translate { 119 | def apply(v: Vector, s: Solid): Translate = Translate(v.x, v.y, v.z, s) 120 | } 121 | 122 | object Rotate { 123 | def apply(q: Quaternion, s: Solid): Rotate = { 124 | // TODO make sure OpendSCAD use the same sequence of roation 125 | val v = q.toRollPitchYaw 126 | Rotate(v.x, v.y, v.z, s) 127 | } 128 | } 129 | 130 | /////////// 131 | // utils // 132 | /////////// 133 | 134 | object Polyhedron { 135 | 136 | def indexed(faces: Iterable[Face]): (IndexedSeq[Point], Iterable[(Int,Int,Int)]) = { 137 | val points = faces.foldLeft(Set[Point]())( (acc, face) => acc + face.p1 + face.p2 + face.p3 ) 138 | val indexed = points.toIndexedSeq 139 | val idx: Map[Point, Int] = indexed.zipWithIndex.toMap 140 | (indexed, faces.map{ case Face(p1,p2,p3) => (idx(p1),idx(p2),idx(p3)) }) 141 | } 142 | def indexed(p: Polyhedron): (IndexedSeq[Point], Iterable[(Int,Int,Int)]) = indexed(p.faces) 143 | 144 | } 145 | -------------------------------------------------------------------------------- /doc/sample.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "55600447-a1ea-4890-b717-cf415fbe85ec", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import coursierapi._\n", 11 | "interp.repositories() ++= Seq(MavenRepository.of(\"https://jitpack.io\"))" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "47708706-19d0-4e4d-883f-14575a7199ce", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "//1. Local version: clone this repository, run `sbt publishLocal`\n", 22 | "//2. Remote version: nothing to do (but you depend on what versions are available)\n", 23 | "import $ivy.`com.github.dzufferey::scadla:0.1.1`" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "id": "1517558d-4d31-448f-8345-4edd7ba192cc", 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "import scadla._\n", 34 | "import InlineOps._\n", 35 | "import EverythingIsIn.{millimeters, degrees}\n", 36 | "import scadla.backends.OpenSCAD // for rendering (getting a mesh)\n", 37 | "import scadla.backends.almond.Viewer // to show the mesh in jupyter/almond" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "id": "f8fd9997-7f2b-453c-9928-98f83b164fdc", 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "// exmaple from the README\n", 48 | "val c = Cube(1, 1, 1)\n", 49 | "val s = Sphere(1.5)\n", 50 | "val obj = (c + c.move(-0.5, -0.5, 0)) * s\n", 51 | "val mesh = OpenSCAD(obj) // Solid -> Polyhedron\n", 52 | "Viewer(mesh)" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "id": "bd783282-7fa5-4777-94ef-5c002e9f547c", 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "// a more complex example\n", 63 | "import squants.space.{Length, Angle}\n", 64 | "import scala.language.postfixOps\n", 65 | "import squants.space.LengthConversions._\n", 66 | "import scadla.utils.{RoundedCubeH, Trig, Trapezoid}\n", 67 | "\n", 68 | "// dimensions\n", 69 | "val baseWidth = 95 mm\n", 70 | "val baseLength = 150 mm\n", 71 | "val baseDepth = 5 mm\n", 72 | "\n", 73 | "val overallWidth = 200 mm\n", 74 | "val overallLength = baseLength\n", 75 | "val cornerRadius = 10 mm\n", 76 | "val wall = 2 mm\n", 77 | "val wallStraight = 40 mm\n", 78 | "val wallSlopped = 10 mm\n", 79 | "val wallAngle: Angle = 45\n", 80 | "\n", 81 | "val grooveWidth = 3 mm\n", 82 | "val grooveDepth = 2 mm\n", 83 | "val nbrGrooveH = 12\n", 84 | "val nbrGrooveV = 10\n", 85 | "\n", 86 | "def hat(x: Length, y: Length, z: Length, r: Length, a: Angle) = {\n", 87 | " val d = r*2\n", 88 | " val smaller = z * Trig.sin(a)\n", 89 | " Minkowski(\n", 90 | " Trapezoid((x-d-smaller,x-d,0),(y-d-smaller,y-d,0),z-1),\n", 91 | " Cylinder(r,1)\n", 92 | " ).move(r,r,0)\n", 93 | "}\n", 94 | "\n", 95 | "val base = Union(\n", 96 | " Cube(baseWidth, baseLength, baseDepth).move((overallWidth-baseWidth)/2,(overallLength-baseLength)/2,-baseDepth),\n", 97 | " RoundedCubeH(overallWidth, overallLength, wallStraight, cornerRadius),\n", 98 | " hat(overallWidth, overallLength, wallSlopped, cornerRadius, wallAngle).moveZ(wallStraight)\n", 99 | ")\n", 100 | "val sampleGrooveH = Cube(grooveWidth, overallLength - 2*wall - 10, grooveDepth)\n", 101 | "val stepH = (overallWidth - 2*wall - 10) / nbrGrooveH\n", 102 | "val groovesH = for (i <- 1 until nbrGrooveH) yield sampleGrooveH.move(wall + 5 + i * stepH - grooveWidth/2, wall+5, wall)\n", 103 | "val sampleGrooveV = Cube(overallWidth - 2*wall - 10, grooveWidth, grooveDepth)\n", 104 | "val stepV = (overallLength - 2*wall - 10) / nbrGrooveV\n", 105 | "val groovesV = for (i <- 1 until nbrGrooveV) yield sampleGrooveV.move(wall + 5, wall + 5 + i * stepV - grooveWidth/2,wall)\n", 106 | "val shell = Difference(\n", 107 | " base,\n", 108 | " RoundedCubeH(overallWidth - 2*wall, overallLength - 2*wall, wallStraight-wall-grooveDepth, cornerRadius-wall).move(wall,wall,wall+grooveDepth),\n", 109 | " hat(overallWidth - 2*wall, overallLength - 2*wall, wallSlopped, cornerRadius-wall, wallAngle).move(wall,wall,wallStraight),\n", 110 | " Union(groovesH: _*),\n", 111 | " Union(groovesV: _*)\n", 112 | ")\n", 113 | "val mesh = OpenSCAD(shell)\n", 114 | "Viewer(mesh)" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": null, 120 | "id": "0b87f12e-127c-47a3-9bb1-17f082b7fdb1", 121 | "metadata": {}, 122 | "outputs": [], 123 | "source": [ 124 | "// save the object as an STL file\n", 125 | "scadla.backends.stl.Printer.storeBinary(mesh, \"milling_basket.stl\")" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": null, 131 | "id": "174221c4-74f9-42d9-8613-75e972bd3b2f", 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [] 135 | } 136 | ], 137 | "metadata": { 138 | "kernelspec": { 139 | "display_name": "Scala", 140 | "language": "scala", 141 | "name": "scala" 142 | }, 143 | "language_info": { 144 | "codemirror_mode": "text/x-scala", 145 | "file_extension": ".sc", 146 | "mimetype": "text/x-scala", 147 | "name": "scala", 148 | "nbconvert_exporter": "script", 149 | "version": "2.13.4" 150 | } 151 | }, 152 | "nbformat": 4, 153 | "nbformat_minor": 5 154 | } 155 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/Spindle.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import scadla._ 4 | import utils._ 5 | import Trig._ 6 | import utils.gear._ 7 | import InlineOps._ 8 | import thread._ 9 | import Common._ 10 | import scadla.EverythingIsIn.{millimeters, radians} 11 | import squants.space.{Length, Angle, Degrees, Millimeters} 12 | import scala.language.postfixOps 13 | import squants.space.LengthConversions._ 14 | 15 | //parameters: 16 | //- thickness of structure 17 | //- bolt 18 | // - diameter 19 | // - head height 20 | // - non-threaded length 21 | // - threaded length 22 | //- motor 23 | // - base fixation 24 | // - height from base to gear: 37.3 25 | // - rotor diameter 26 | // - additional thing to make the gear hold better to the rotor 27 | //- chuck 28 | // - outer diameter (inner diameter is constrained by the size of the nut/bolt diameter) 29 | // - thread (type, lead, size) 30 | // - collet height 31 | 32 | /* 33 | for a parallax 1050kv outrunner brushless motor 34 | 35 | rotary thing 36 | height: 2x5mm 37 | hole diameter: 5mm 38 | outer diameter: 17mm 39 | 40 | base 41 | height 15mm 42 | space between the screw 23.5 mm 43 | screw diameter 3.5 mm 44 | base of rotary thing is 38 mm above the base 45 | */ 46 | 47 | //TODO as a class with the parameter in the ctor 48 | object Spindle { 49 | 50 | //////////////// 51 | // parameters // 52 | //////////////// 53 | 54 | val motorBoltDistance = (30 + 30) / 2f //depends on the size of the motorBase and boltSupport 55 | 56 | val gearHeight = 10 mm 57 | val chuckNonThreadOverlap = 10 mm 58 | 59 | val topBoltWasher = 2 mm 60 | val bottomBoltWasher = 2 mm 61 | 62 | val boltThreadedLength = 25 mm //23 63 | val boltNonThreadedLength = 96 mm //86 64 | 65 | val boltSupportTop = boltNonThreadedLength - gearHeight - chuckNonThreadOverlap - topBoltWasher - bottomBoltWasher 66 | 67 | val motorBaseToGear = 37.3 mm 68 | val motorBaseHeight = boltSupportTop + topBoltWasher - motorBaseToGear 69 | 70 | val bitsLength = 25 mm 71 | val colletLength = bitsLength - 3 72 | val chuckHeight = colletLength + boltThreadedLength + chuckNonThreadOverlap 73 | val innerHole = 9 mm //17.5 / 2 74 | 75 | /////////// 76 | // gears // 77 | /////////// 78 | 79 | lazy val gear1 = Gear.helical( motorBoltDistance * 2 / 3.0, 32, gearHeight, Twist(-0.03), tolerance) 80 | lazy val gear2 = Gear.helical( motorBoltDistance / 3.0 , 16, gearHeight, Twist(0.06), tolerance) 81 | val motorKnobs = { 82 | val c = Cylinder(3-tolerance, 2).moveZ(gearHeight) 83 | val u = Union(c.moveX(9), c.moveX(-9), c.moveY(9), c.moveY(-9)) 84 | val r = (motorBoltDistance / 3.0) * (1.0 - 2.0 / 16) 85 | u * Cylinder(r, 20) 86 | } 87 | val nutTop = Cylinder(ISO.M8 * 3, 14) - nut.M8.moveZ(gearHeight) 88 | //gears 89 | lazy val gearBolt = gear1 + nutTop - Cylinder(ISO.M8 + tolerance, gearHeight) 90 | lazy val gearMotor = gear2 - Cylinder(ISO.M5 + tolerance, gearHeight) + motorKnobs 91 | 92 | 93 | ///////////////////////////////// 94 | // bolt support and motor base // 95 | ///////////////////////////////// 96 | 97 | //TODO not so square ... 98 | val motorBase = { 99 | 100 | val topZ = 3 101 | val bot = Trapezoid(46, 30, 5, 21).rotateX(-Pi/2).moveZ(5) 102 | val top = Cube(30,30,topZ).moveZ(motorBaseHeight - topZ) 103 | val subTtop = Cube(30,30-6.5,topZ).moveZ(motorBaseHeight - 2 * topZ) 104 | val base = Union( 105 | Hull(bot, subTtop), 106 | top 107 | ) 108 | val nm3 = Bigger(Hull(nut.M3, nut.M3.moveX(5)), 0.4) 109 | val screw_hole = Cylinder(ISO.M3, 10) 110 | val fasteners = Seq( 111 | screw_hole.move( 3.25, 3.25, 0), 112 | screw_hole.move(26.75, 3.25, 0), 113 | screw_hole.move( 3.25,26.75, 0), 114 | screw_hole.move(26.75,26.75, 0), 115 | nm3.move(26.75, 3.25, 4), 116 | nm3.move(26.75,26.75, 4), 117 | nm3.rotateZ(Pi).move( 3.25, 3.25, 4), 118 | nm3.rotateZ(Pi).move( 3.25,26.75, 4) 119 | ).map(_.moveZ(motorBaseHeight - 10)) 120 | 121 | val shaftHole = { 122 | val c = Cylinder( 8, motorBaseHeight).move(15, 15, 0) //hole for the lower part of the motor's shaft 123 | Hull(c, c.moveY(15)) 124 | } 125 | val breathingSpaces = Seq( 126 | Cylinder(20, 50).moveZ(-25).scaleX(0.30).rotateX(Pi/2).move(15,15,motorBaseHeight)//, 127 | //Cylinder(20, 50).moveZ(-25).scaleY(0.30).rotateY(Pi/2).move(15,15,motorBaseHeight) 128 | ) 129 | 130 | //the block on which the motor is screwed 131 | base - shaftHole -- fasteners -- breathingSpaces 132 | } 133 | 134 | val fixCoord = List[(Length, Length, Angle)]( 135 | (31, 4, -Pi/5.2), 136 | (-1, 4, Pi+Pi/5.2), 137 | (34, 30, 0), 138 | (-4, 30, Pi), 139 | (34, 56, Pi/2), 140 | (-4, 56, Pi/2) 141 | ) 142 | 143 | //centered at 0, 0 144 | val boltSupport = { 145 | val base = Hull( 146 | Cylinder(15, boltSupportTop), 147 | Cube(30, 1, motorBaseHeight).move(-15, 14, 0) 148 | ) 149 | val lowerBearing = Hull(Cylinder(10, 7.5), bearing.moveZ(-0.5)) //add a small chamfer 150 | base - lowerBearing - bearing.moveZ(boltSupportTop - 7) - Cylinder(9, boltSupportTop) 151 | } 152 | 153 | val spindle = { 154 | val s = Cylinder(ISO.M3 + tolerance, 5) 155 | val fix = Cylinder(4, 4) + Cube(5, 8, 4).move(-5, -4, 0) - s 156 | Union( 157 | boltSupport.move(15, 15, 0), 158 | motorBase.moveY(30) 159 | ) ++ fixCoord.map{ case (x,y,a) => fix.rotateZ(a).move(x,y,0) } 160 | } 161 | 162 | 163 | /////////// 164 | // chuck // 165 | /////////// 166 | 167 | 168 | val chuck = Chuck.innerThread(13, innerHole+tolerance, chuckHeight, colletLength, 20) 169 | val slits = 4 //6 170 | val collet = Collet.threaded(innerHole+1, innerHole, UTS._1_8, colletLength, 171 | slits, 0.5, 1, 20, ISO.M2) 172 | val colletWrench = Collet.wrench(innerHole, UTS._1_8, slits, ISO.M2) 173 | 174 | def objects = Map( 175 | "gear_bolt" -> gearBolt, 176 | "gear_motor" -> gearMotor, 177 | "bolt_washer_top" -> Tube(6, (4 mm) + 2*tolerance, topBoltWasher), 178 | "bolt_washer_bot" -> Tube(6, (4 mm) + 2*tolerance, bottomBoltWasher), 179 | "spindle_body" -> spindle, 180 | "chuck_wrench" -> Chuck.wrench(13), 181 | "chuck" -> chuck.rotateX(Pi), 182 | "collet_inner" -> collet, 183 | "collet_wrench" -> colletWrench 184 | ) 185 | 186 | } 187 | 188 | -------------------------------------------------------------------------------- /src/main/scala/scadla/utils/gear/InvoluteGear.scala: -------------------------------------------------------------------------------- 1 | package scadla.utils.gear 2 | 3 | import scadla._ 4 | import scadla.InlineOps._ 5 | import scadla.utils._ 6 | import scala.math._ 7 | import squants.space.Length 8 | import squants.space.Millimeters 9 | import squants.space.Angle 10 | import squants.space.Radians 11 | 12 | object InvoluteGear { 13 | 14 | protected def placeOnInvolute(pitch: Length, profile: Solid, angle: Angle) = { 15 | val _x = Involute.x(pitch, 0, angle.toRadians) 16 | val y = Involute.y(pitch, 0, angle.toRadians) 17 | profile.rotateZ(angle).move(_x, y, Millimeters(0)) 18 | } 19 | 20 | protected def makeToothCarvingProfile(pitch: Length, profile: Solid) = { 21 | val samples = Gear.toothProfileAccuracy 22 | assert(samples > 1, "toothProfileAccuracy must be larger than 1") 23 | val range = Radians(2*Pi/3) //TODO vary angle and samples according to pressureAngle 24 | val trajectory = for (i <- 1 until samples) yield { 25 | val a = Radians(0) - range / 2 + i * range / samples 26 | val s = placeOnInvolute(pitch.abs, profile, a) 27 | if (pitch.value > 0) s else s.moveX(2*pitch) 28 | } 29 | val hulled = trajectory.sliding(2).map( l => if (l.size > 1) Hull(l:_*) else l.head ).toSeq 30 | Union(hulled:_*) 31 | } 32 | 33 | /** Create a gear by carving the tooths along an involute curve. 34 | * The method to generate spur gear inspired by 35 | * http://lcamtuf.coredump.cx/gcnc/ and 36 | * http://www.hessmer.org/blog/2014/01/01/online-involute-spur-gear-builder/ 37 | * It is a certain computation cost but has the advantage of properly generating the fillet and undercut. 38 | * @param baseShape the base cylinder/tube for the gear 39 | * @param pitch the effective radius of the gear 40 | * @param nbrTeeth the number of tooth in the gear 41 | * @param rackToothProfile the profile of a tooth on a rack (infinite gear) the profile must be centered ad 0,0. 42 | */ 43 | def carve( baseShape: Solid, 44 | pitch: Length, 45 | nbrTeeth: Int, 46 | rackToothProfile: Solid) = { 47 | val negative = makeToothCarvingProfile(pitch, rackToothProfile) 48 | 49 | val angle = Radians(Pi) / nbrTeeth //between tooths 50 | val negatives = for (i <- 0 until nbrTeeth) yield negative.rotateZ((2 * i) * angle) 51 | 52 | baseShape -- negatives 53 | } 54 | 55 | /** Create an involute spur gear. 56 | * @param pitch the effective radius of the gear 57 | * @param nbrTeeth the number of tooth in the gear 58 | * @param pressureAngle the angle between meshing gears at the pitch radius (0 mean "square" tooths, π/2 no tooths) 59 | * @param addenum how much to add to the pitch to get the outer radius of the gear 60 | * @param dedenum how much to remove to the pitch to get the root radius of the gear 61 | * @param height the height of the gear 62 | * @param backlash add some space (manufacturing tolerance) 63 | * @param skew generate a gear with an asymmetric profile by skewing the tooths 64 | */ 65 | def apply( pitch: Length, 66 | nbrTeeth: Int, 67 | pressureAngle: Double, 68 | addenum: Length, 69 | dedenum: Length, 70 | height: Length, 71 | backlash: Length, 72 | skew: Angle = Radians(0.0)) = { 73 | 74 | assert(addenum.value > 0, "addenum must be greater than 0") 75 | assert(dedenum.value > 0, "dedenum must be greater than 0") 76 | assert(nbrTeeth > 0, "number of tooths must be greater than 0") 77 | assert(pitch != 0.0, "pitch must be different from 0") 78 | 79 | val angle = Pi / nbrTeeth //between tooths 80 | val effectivePitch = pitch.abs 81 | val toothWidth = effectivePitch * 2 * sin(angle/2) //TODO is that right or should we use the cordal value ? 82 | val ad = if (pitch.value >= 0) addenum else dedenum 83 | val de = if (pitch.value >= 0) dedenum else addenum 84 | val rackTooth = Rack.tooth(toothWidth, pressureAngle, ad, de, height, backlash, skew) 85 | 86 | if (pitch == Millimeters(0)) { 87 | val space = 2*toothWidth 88 | val teeth = for (i <- 0 until nbrTeeth) yield rackTooth.moveX(i * space) 89 | val bt = dedenum + Gear.baseThickness 90 | val base = Cube((nbrTeeth+1) * space, bt, height).move(-space/2, -bt, Millimeters(0)) 91 | base ++ teeth 92 | } else { 93 | val base = 94 | if (pitch.value > 0) Cylinder(pitch + addenum, height) 95 | else Tube(effectivePitch + addenum+Gear.baseThickness, effectivePitch - dedenum, height) 96 | carve(base, pitch, nbrTeeth, rackTooth) 97 | } 98 | } 99 | 100 | /** An involute gear with z tiled into many layers. */ 101 | def stepped( pitch: Length, 102 | nbrTeeth: Int, 103 | pressureAngle: Double, 104 | addenum: Length, 105 | dedenum: Length, 106 | height: Length, 107 | backlash: Length, 108 | skew: Angle = Radians(0.0)) = { 109 | val zStep = Gear.zResolution 110 | assert(zStep.value > 0.0, "zResolution must be greater than 0") 111 | val stepCount = ceil(height / zStep).toInt 112 | val stepSize = height / stepCount 113 | def isZ(p: Point, z: Length) = (p.z - z).abs.value <= 1e-10 //TODO better way of dealing with numerical error 114 | val base = apply(pitch, nbrTeeth, pressureAngle, addenum, dedenum, height, backlash, skew) 115 | val (bot, rest) = base.toPolyhedron.faces.partition{ case Face(p1, p2, p3) => isZ(p1, Millimeters(0)) && isZ(p2, Millimeters(0)) && isZ(p3, Millimeters(0)) } 116 | val (top, middle) = rest.partition{ case Face(p1, p2, p3) => isZ(p1, height) && isZ(p2, height) && isZ(p3, height) } 117 | 118 | def mvz(i: Int, z: Length) = { 119 | if ((z - height).abs.value <= 1e-10) { 120 | if (i == stepCount - 1) z 121 | else (i+1) * stepSize 122 | } else { 123 | i * stepSize 124 | } 125 | } 126 | def mvp(i: Int, p: Point) = Point(p.x, p.y, mvz(i, p.z)) 127 | def mv(i: Int, f: Face) = Face(mvp(i, f.p1), mvp(i, f.p2), mvp(i, f.p3)) 128 | 129 | val newMiddle = middle.flatMap( f => for (i <- 0 until stepCount) yield mv(i, f) ) 130 | val faces = bot ++ top ++ newMiddle 131 | //TODO why/when do we need to flip ? 132 | //TODO we need to have a short analysis that compute the normals ... 133 | Polyhedron(faces.map(_.flipOrientation)) 134 | //Polyhedron(faces) 135 | } 136 | 137 | } 138 | 139 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/ComponentStorageBox.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples 2 | 3 | import scadla._ 4 | import utils._ 5 | import Trig._ 6 | import InlineOps._ 7 | import squants.space.{Length, Angle} 8 | import scala.language.postfixOps 9 | import squants.space.LengthConversions._ 10 | import scala.collection.parallel.CollectionConverters._ 11 | 12 | class ComponentStorageBox( 13 | width: Length, 14 | depth: Length, 15 | wallThickness: Length, 16 | numberOfDrawers: Int, 17 | drawerHeight: Length, 18 | drawerWallThickness: Length, 19 | gap: Length, 20 | labelWallThickness: Length 21 | ) { 22 | 23 | val height = numberOfDrawers * (drawerHeight + gap) + 2 * wallThickness 24 | 25 | val w2 = wallThickness * 2 26 | val w2g = w2 + gap 27 | 28 | protected def rail = { 29 | Cube(2*wallThickness, depth, wallThickness) + Cylinder(wallThickness/2, depth).rotateX(-Pi/2).move(2*wallThickness, 0 mm, wallThickness/2) 30 | } 31 | 32 | protected def rails = { 33 | rail + rail.mirror(1,0,0).moveX(width - w2) 34 | } 35 | 36 | protected def spread(s: Solid, direction: Vector, min: Length, max: Length, n: Int) = { 37 | val delta = ((max - min) / n).toMillimeters 38 | Union((0 until n).map( i => s.move(direction * (min.toMillimeters + delta/2 + i * delta))) :_*) 39 | } 40 | 41 | def shelf = { 42 | val outer = Cube(width, depth, height) 43 | val inner = Cube(width - w2g, depth - wallThickness - gap, height - w2g) 44 | val base = outer - inner.move(wallThickness, 0 mm, wallThickness) 45 | val rs = (1 to numberOfDrawers).map( i => rails.move( wallThickness, 0 mm, i * (drawerHeight + gap)) ) 46 | val full = base ++ rs 47 | val hSize = (drawerHeight - w2) / math.sqrt(2) 48 | //holes to reduce the amount of plastic needed 49 | val hole = CenteredCube(hSize, hSize, 4 * wallThickness).rotateZ(Pi/4) 50 | val nbrHolesWidth = math.floor( (width - w2g) / (drawerHeight + w2) ).toInt 51 | val nbrHolesDepth = math.floor( (depth - w2g) / (drawerHeight + w2) ).toInt 52 | val holesBack = spread(hole.rotateX(Pi/2), Vector.x, wallThickness, width - wallThickness, nbrHolesWidth).moveY(depth) 53 | val holesLeft = spread(hole.rotateY(Pi/2), Vector.y, wallThickness, depth - wallThickness, nbrHolesDepth) 54 | val holesRight = holesLeft.moveX(width) 55 | val holesRow = holesBack + holesLeft + holesRight 56 | val holes = spread(holesRow, Vector.z, wallThickness, height-wallThickness, numberOfDrawers) 57 | full - holes.moveZ(-wallThickness/2) 58 | } 59 | 60 | protected def labelHolder(width: Length) = { 61 | val c1 = Cube(width, 2* labelWallThickness, drawerHeight) 62 | val c2 = Cube(width - 2 * labelWallThickness, labelWallThickness, drawerHeight) 63 | val c3 = Cube(width - w2, 2 * labelWallThickness, drawerHeight) 64 | c1 - c2.move(labelWallThickness, 0 mm, labelWallThickness) - c3.move(wallThickness, 0 mm, drawerWallThickness) 65 | } 66 | 67 | def drawer(divisionsX: Int, divisionsY: Int) = { 68 | val dw2g = 2 * drawerWallThickness 69 | val handle = { 70 | val c1 = Cube(wallThickness, drawerWallThickness, drawerHeight) 71 | val c2 = Cylinder(drawerHeight/2,wallThickness) 72 | val h = Hull(c1, c2.rotateY(Pi/2).move(0 mm, drawerHeight/2, drawerHeight/2)) 73 | val radius = drawerHeight/2 - drawerWallThickness 74 | val s = Sphere(radius).scale((drawerWallThickness- (1 mm))/radius/2, 1, 1) 75 | h - s.move(0 mm, drawerHeight/2, drawerHeight/2) - s.move(wallThickness, drawerHeight/2, drawerHeight/2) 76 | } 77 | val dwidth = width - w2g 78 | val ddepth = depth - wallThickness - gap 79 | val c1 = Cube(dwidth, ddepth, drawerHeight) 80 | val c2 = Cube(dwidth - dw2g, ddepth - dw2g, drawerHeight).move(drawerWallThickness, drawerWallThickness, drawerWallThickness) 81 | val lh = labelHolder( (dwidth-wallThickness)/2 + labelWallThickness) 82 | //base with handle and thing to hold paper description 83 | val box = Union(c1 - c2, 84 | handle.mirror(0,1,0).moveX( (dwidth-wallThickness)/2 ), 85 | lh.mirror(0,1,0), 86 | lh.mirror(0,1,0).moveX( (dwidth+wallThickness)/2 -labelWallThickness) 87 | ) 88 | //add inner dividers 89 | val dividersX = (1 until divisionsX).map( i => Cube( drawerWallThickness, ddepth, drawerHeight).moveX( -drawerWallThickness/2 + i * dwidth / divisionsX) ) 90 | val dividersY = (1 until divisionsY).map( i => Cube( dwidth, drawerWallThickness, drawerHeight).moveY( -drawerWallThickness/2 + i * (depth-w2g) / divisionsY) ) 91 | val withDividers = box ++ dividersX ++ dividersY 92 | //add space for the rails 93 | val r0 = (rails + rails.moveZ(-wallThickness)).move(-gap/2, 0 mm, drawerHeight+gap/2) 94 | val r1 = r0 + r0.moveY(-2*labelWallThickness) 95 | val rLeft = r1 * Cube(4*wallThickness, depth + 2*labelWallThickness, drawerHeight + (1 mm)).moveY(-2*labelWallThickness) 96 | val rRight= r1 * Cube(4*wallThickness, depth + 2*labelWallThickness, drawerHeight + (1 mm)).move(dwidth-4*wallThickness, -2*labelWallThickness, 0 mm) 97 | val railing = Bigger(Union(Hull(rLeft), Hull(rRight)), gap) 98 | withDividers - railing 99 | } 100 | 101 | } 102 | 103 | object ComponentStorageBox { 104 | 105 | 106 | def main(args: Array[String]) = { 107 | val box = new ComponentStorageBox( 108 | 120 mm, //width 109 | 120 mm, //depth 110 | 2.4 mm, //wallThickness 111 | 5, //numberOfDrawers 112 | 20 mm, //drawerHeight 113 | 1.2 mm, //drawerWallThickness 114 | 0.8 mm, //gap 115 | 0.4 mm //labelWallThickness 116 | ) 117 | val r = backends.Renderer.default 118 | //r.view(box.shelf) 119 | //r.view(box.drawer(1, 1)) 120 | //r.view(box.drawer(3, 2)) 121 | //r.view(box.shelf + box.drawer(1, 1).move(2.8, 0, 2.8)) 122 | val elts = Seq( 123 | (box.shelf.rotateX(-Pi/2), "shelf.stl"), 124 | (box.drawer(1, 1), "drawer_1x1.stl"), 125 | (box.drawer(2, 1), "drawer_2x1.stl"), 126 | (box.drawer(2, 2), "drawer_2x2.stl"), 127 | (box.drawer(3, 1), "drawer_3x1.stl"), 128 | (box.drawer(3, 2), "drawer_3x2.stl"), 129 | (box.drawer(3, 3), "drawer_3x3.stl"), 130 | (box.drawer(4, 1), "drawer_4x1.stl"), 131 | (box.drawer(4, 2), "drawer_4x2.stl"), 132 | (box.drawer(4, 3), "drawer_4x3.stl"), 133 | (box.drawer(4, 4), "drawer_4x4.stl") 134 | ) 135 | elts.par.foreach{ case (s, f) => r.toSTL(s, f) } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/Platform.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import scadla._ 4 | import utils._ 5 | import Trig._ 6 | import InlineOps._ 7 | import Common._ 8 | import thread.ISO 9 | import squants.space.Length 10 | import scadla.EverythingIsIn.{millimeters, radians} 11 | 12 | //a platform to put hold the spindle 13 | object Platform { 14 | 15 | protected def bearings(space: Length) = { 16 | Union( 17 | bearing.moveZ(-tolerance/2), 18 | bearing.moveZ(7 + space + tolerance/2), 19 | Cylinder(9, space).moveZ(7), 20 | Cylinder(11 + tolerance, 11 + tolerance - space, space).moveZ(7), 21 | Cylinder(11 + tolerance - space, 11 + tolerance, space).moveZ(7) 22 | ) 23 | } 24 | 25 | protected def oldSpindleMount(height: Length, gap: Length) = { 26 | val screw = Cylinder(ISO.M3+tolerance, ISO.M3+tolerance, height) 27 | Cylinder(15+gap, height).move(15,15,0) ++ Spindle.fixCoord.map{ case (x,y,_) => screw.move(x,y,0) } 28 | } 29 | 30 | protected def spindleMount(spindleHoleRadius: Length, height: Length) = { 31 | Union( 32 | Cylinder(spindleHoleRadius + looseTolerance, height), 33 | Cylinder(mountScrews, height).move( spindleHoleRadius, spindleHoleRadius, 0), 34 | Cylinder(mountScrews, height).move( spindleHoleRadius,-spindleHoleRadius, 0), 35 | Cylinder(mountScrews, height).move(-spindleHoleRadius, spindleHoleRadius, 0), 36 | Cylinder(mountScrews, height).move(-spindleHoleRadius,-spindleHoleRadius, 0) 37 | ) 38 | } 39 | 40 | //For each hinge, I need: 41 | // - 2x 608 bearing 42 | // - 2x M8 nut 43 | // - 1x M8 washer 44 | // - 40mm M8 threaded rod 45 | 46 | //For the assembly: 47 | // - M8 rod 48 | // * file away 10mm of the thread on the M8 rod 49 | // * cut a 2mm vertical slot in the M8 rod 50 | // * drill a 3mm hole perpendicular to the slot 51 | // - M6 rod 52 | // * file the side until a 2mm tab is left 53 | // * drill a 3mm hole in the tab 54 | // - M4x10 screw 55 | // * file the thread away to get a M3 rod 56 | // * make a M3 thread on the last few mm 57 | 58 | //space should be ~ zBearingSpace + 2*tolerance 59 | def with608Bearings(radius: Length = 50, 60 | wall: Length = 5, 61 | bearingGap: Length = 10, 62 | height: Length = 10, 63 | space: Length = 1.6, 64 | spindleHoleRadius: Length = 25) = { 65 | val bNeg = bearings(space).moveZ(height/2 - 7 - space/2) 66 | val bHolder = Cylinder(11 + wall, height) 67 | val offset = (wall/2) max (bearingGap/2) 68 | def place(s: Solid) = { 69 | val paired = Union( 70 | s.move( 11 +offset, radius, 0), 71 | s.move(-11 -offset, radius, 0) 72 | ) 73 | for (i <- 0 until 3) yield paired.rotateZ(2 * i * Pi / 3) //linter:ignore ZeroDivideBy 74 | } 75 | val base = Hull(place(bHolder): _*) -- place(bNeg) 76 | base - spindleMount(spindleHoleRadius, height).rotateZ(Pi/4) 77 | } 78 | 79 | //bushing: 80 | // - brass tube of length: height + 2 81 | // - shaft of length: height + 2 + 2 * washer thickness + 2 * 0.8 * radius (for the thread) 82 | // - use smaller washer and enlarge them until it is a tight fit around the shaft 83 | // - M4/5 hole in the center of the shaft for attaching stuff on top 84 | // - sand (& polish) the shaft until it slides into the tube, add some grease 85 | // - on the outside, use threadlocker or solder to hold the nuts in place 86 | // 87 | // with my current setup 88 | // * horizontal bushing 89 | // - brass tube: ∅ 10mm, length 12 mm 90 | // - brass shaft: ∅ 8mm, length 22 mm ≅ 12 + 3.2 + 6.4 91 | // * vertical bushing (simpler) 92 | // - brass tube: ∅ (8 - ε) mm 93 | // - brass shaft: ∅ 8mm 94 | 95 | val bushingRadius = 5 96 | val mountScrews = ISO.M4 97 | 98 | def withBushing(radius: Length = 50, height: Length = 10, 99 | wall: Length = 4, bearingGap: Length = 10, 100 | spindleHoleRadius: Length = 25): Solid = { 101 | val offset = bushingRadius + (wall/2) max (bearingGap/2) 102 | def place(s: Solid) = { 103 | val paired = Union( 104 | s.move( offset, radius, 0), 105 | s.move(-offset, radius, 0) 106 | ) 107 | for (i <- 0 until 3) yield paired.rotateZ(2 * i * Pi / 3) //linter:ignore ZeroDivideBy 108 | } 109 | val bNeg = Cylinder(bushingRadius, height) 110 | val bHolder = Cylinder(bushingRadius + wall, height) 111 | val base = Hull(place(bHolder): _*) -- place(bNeg) 112 | val spindle = spindleMount(spindleHoleRadius, height).rotateZ(Pi/4) 113 | base - spindle 114 | } 115 | 116 | def verticalBushing2Rod(wall: Length = 4, 117 | length: Length = 50, 118 | brassThickness: Length = 8, 119 | slit: Length = 2) = { 120 | val bw = bushingRadius + wall 121 | val n = nut(ISO.M6).rotateY(Pi/2) 122 | val base = Cylinder(bw, brassThickness) + CenteredCube.y(length, 1.5 * bw, brassThickness) 123 | Difference( 124 | base, 125 | Cylinder(bushingRadius + tolerance, brassThickness), 126 | CenteredCube.y(bw + 10, slit, brassThickness), 127 | Hull(n.moveX(bw + 10 + wall), n.move(bw + 10 + wall, 0, brassThickness)), 128 | Cylinder(ISO.M6 + tolerance, length).rotateY(Pi/2).move(bw + 10 + wall, 0, brassThickness/2), 129 | Cylinder(ISO.M3 + tolerance, 2*bw).rotateX(Pi/2).move(bw + 3, bw, brassThickness/2), 130 | nut(ISO.M3).rotateX(Pi/2).move(bw + 3, 0.8*bw, brassThickness/2) 131 | ) 132 | } 133 | 134 | def verticalBushing2Platform(wall: Length = 4, 135 | brassThickness: Length = 8) = { 136 | val washerM6Inner = 3.2 137 | val washerM6Thickness = 1.6 138 | val b2w = brassThickness + wall*2 139 | val bw = bushingRadius + wall 140 | val h = bw + 4 + wall // 4 to allow an M4 screw head + washer XXX check that ... 141 | val base = Hull( 142 | CenteredCube.x(bw*2, b2w, 1), 143 | Cylinder(ISO.M6 * 2, b2w).rotateX(-Pi/2).moveZ(h) 144 | ) 145 | val innerDelta = wall - 0.5 146 | val w = Tube(ISO.M6 * 2+ looseTolerance, washerM6Inner - tolerance, washerM6Thickness) 147 | Difference( 148 | base, 149 | Cylinder(mountScrews + tightTolerance, wall).moveY(b2w/2), 150 | CenteredCube.x(2*bw, b2w - 2*innerDelta, h + bw).move(0, innerDelta, wall), 151 | w.rotateX( Pi/2).move(0, innerDelta + 0.01, h), 152 | w.rotateX(-Pi/2).move(0, b2w - innerDelta - 0.01, h), 153 | Cylinder(mountScrews + tightTolerance, b2w).rotateX(-Pi/2).moveZ(h) 154 | ) 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/LinearActuator.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import scadla._ 4 | import utils._ 5 | import utils.gear._ 6 | import Trig._ 7 | import InlineOps._ 8 | import thread._ 9 | import scadla.examples.GearBearing 10 | import Common._ 11 | import scadla.examples.reach3D.SpoolHolder 12 | import scadla.EverythingIsIn.{millimeters, radians} 13 | import squants.space.{Length, Angle, Degrees, Millimeters} 14 | import scala.language.postfixOps 15 | import squants.space.LengthConversions._ 16 | 17 | 18 | //TODO need to add some springy thing on one nut to reduce the backlash (preload) 19 | 20 | object LinearActuator { 21 | 22 | val rodThread = ISO.M6 23 | val rodLead = 1.0 mm 24 | 25 | val motorYOffset = 21.5 mm 26 | def length = motorYOffset + Nema14.size 27 | def width = Nema14.size 28 | 29 | // motor 30 | val motorSocket = 4.0 mm //how deep the screw goes in 31 | val motor = Nema14(28, 0) 32 | 33 | // the screws holding the motors 34 | val screwHead = 2.0 mm 35 | val screwLength = 12.0 mm 36 | val motorScrew = Union(Cylinder(ISO.M3 + looseTolerance, screwLength), 37 | Cylinder(ISO.M3 * 2.1 + looseTolerance, screwHead)) 38 | 39 | val bbRadius = 3.0 mm // airsoft ∅ is 6mm 40 | val bearingGapBase = 3.5 mm 41 | val bearingGapSupport = 2.5 mm 42 | 43 | val gearHeight = 10 mm 44 | val nbrTeethMotor = 14 45 | val nbrTeethRod = 2 * nbrTeethMotor 46 | 47 | val motorGearRadius = motorYOffset * nbrTeethMotor / (nbrTeethMotor + nbrTeethRod) 48 | val rodGearRadius = motorYOffset * nbrTeethRod / (nbrTeethMotor + nbrTeethRod) 49 | val mHelix = Twist(-0.1) 50 | val rHelix = -mHelix * (motorGearRadius / rodGearRadius) 51 | 52 | val grooveDepthBase = bbRadius / cos(Pi/4) - bearingGapBase / 2 53 | val grooveDepthSupport = bbRadius / cos(Pi/4) - bearingGapSupport / 2 54 | val grooveRadiusBase = SpoolHolder.adjustGrooveRadius(nut.maxOuterRadius(rodThread) + grooveDepthBase + 1) // +1 for the adjust radius 55 | val grooveRadiusSupport = SpoolHolder.adjustGrooveRadius(rodGearRadius - Gear.addenum(rodGearRadius, nbrTeethRod).toMillimeters - grooveDepthSupport) 56 | 57 | val grooveBase = SpoolHolder.flatGroove(grooveRadiusBase, grooveDepthBase) 58 | val grooveSupport = SpoolHolder.flatGroove(grooveRadiusSupport, grooveDepthSupport) 59 | 60 | // to attach to the gimbal 61 | val gimbalWidth = Nema14.size + 4 62 | val gimbalKnob = 7.0 mm 63 | 64 | val plateThickness = screwLength - motorSocket + screwHead 65 | val pillarHeight = gearHeight + bearingGapBase + bearingGapSupport 66 | 67 | def basePlate(knob: Boolean = false, support: Boolean = false) = { 68 | val n14s2 = Nema14.size/2 69 | val gimbalMount = if (knob) { 70 | val thr = ISO.M3 71 | val w2k = gimbalWidth+2*gimbalKnob 72 | val c1 = Cylinder(6, gimbalWidth) 73 | val c2 = Cylinder(4 - tolerance, w2k).moveZ(-gimbalKnob) 74 | val c3 = Cylinder(thr, gimbalWidth+20).moveZ(-10) 75 | val nonOriented = c1 + c2 - c3 76 | val oriented = nonOriented.moveZ(-gimbalWidth/2).rotateY(Pi/2).moveZ(plateThickness/2) 77 | val trimmed = oriented * CenteredCube.xy(w2k, w2k, plateThickness) 78 | if (support) { 79 | val beam = CenteredCube.xy(w2k, 7, plateThickness / 2 - thr).moveZ(plateThickness / 2 + thr) 80 | Union( 81 | trimmed, 82 | beam - Bigger(trimmed, 2*supportGap) 83 | ) 84 | } else { 85 | trimmed 86 | } 87 | } else Empty 88 | val base = Union( 89 | RoundedCubeH(width, length, plateThickness, 3).move(-n14s2, -(motorYOffset + n14s2), 0), 90 | gimbalMount 91 | ) 92 | val motorMount = 93 | Union( 94 | //motor, 95 | Cylinder(8, 1), 96 | Cylinder(8, 11.5, plateThickness - 4).moveZ(1), 97 | Cylinder(11.5, 3).moveZ(plateThickness - 3), 98 | Nema14.putOnScrew(motorScrew) 99 | ) 100 | val rodHole = Cylinder(rodThread + 1, plateThickness) 101 | val pillar = { 102 | val p = Cylinder(3, pillarHeight) 103 | val h = Cylinder(1.25, pillarHeight) // hole for self tapping screw 104 | (p - h).moveZ(-pillarHeight) 105 | } 106 | val supportPillars = { 107 | val s1 = width - 6 108 | NemaStepper.putOnScrew(s1, pillar) 109 | } 110 | Union( 111 | Difference( 112 | base, 113 | motorMount.moveY(-motorYOffset), 114 | rodHole, 115 | grooveBase 116 | ), 117 | supportPillars 118 | ).rotateX(Pi) 119 | } 120 | 121 | val supportHeight = 4 122 | val supportPlate = { 123 | val height = supportHeight 124 | Difference( 125 | RoundedCubeH(width, width, height, 3).move(-width/2,-width/2,0), 126 | NemaStepper.putOnScrew(width - 6, Cylinder(1.25, height)), 127 | Cylinder(rodThread + 1, height), 128 | grooveSupport 129 | ).rotateX(Pi) 130 | } 131 | 132 | def motorGear(support: Boolean) = { 133 | val g = Gear.herringbone(motorGearRadius, nbrTeethMotor, gearHeight, mHelix, tightTolerance) 134 | val done = g - Bigger(motor, 2.2*tolerance).moveZ(-5) //clear the flange 135 | if (support) done.moveZ(0.2) + Cylinder(motorGearRadius + 2, 0.2) 136 | else done 137 | } 138 | 139 | def rodGear(support: Boolean) = { 140 | val n = nut(rodThread) 141 | val nh = rodThread * 1.6 //nut height 142 | val g = Gear.herringbone(rodGearRadius, nbrTeethRod, gearHeight, rHelix, tightTolerance) 143 | val done = Difference( 144 | g, 145 | Cylinder(rodThread + 1, gearHeight), 146 | grooveBase, 147 | grooveSupport.mirror(0,0,1).moveZ(gearHeight), 148 | n.moveZ( gearHeight / 2 + 1), 149 | n.moveZ( gearHeight / 2 - 1 - nh) 150 | ) 151 | if (support) (done + Cylinder(nh+2, 0.2).moveZ(gearHeight / 2 - 1)).moveZ(0.2) + Cylinder(rodGearRadius + 2, 0.2) 152 | else done 153 | } 154 | 155 | val gimbalLength1 = { 156 | val extraLength = 3.0 // space for the wiring 157 | // side of the motor 158 | val ySide1 = Nema14.size / 2.0 + motorYOffset 159 | val zSide1 = plateThickness / 2.0 + 30 //nema height 160 | hypot(ySide1, zSide1) + extraLength 161 | } 162 | 163 | val gimbalLength2 = { 164 | // side of the gear 165 | val ySide2 = Nema14.size / 2.0 166 | val zSide2 = plateThickness / 2.0 + pillarHeight + supportHeight + 2 //screw head 167 | hypot(ySide2, zSide2) 168 | } 169 | 170 | val gimbalLength = gimbalLength1 + gimbalLength2 171 | 172 | val gimbalOffset = (gimbalLength1 - gimbalLength2) / 2.0 173 | 174 | val retainerThickness = 1 mm 175 | 176 | def gimbal = { 177 | Gimbal.inner( 178 | gimbalLength, //length 179 | gimbalWidth - retainerThickness * 2, //width 180 | 30, //height 181 | gimbalOffset, //lengthOffset 182 | 0, //widthOffset 183 | 8, //maxThickness 184 | 5, //minThickness 185 | retainerThickness, //retainerThickness 186 | 2 //knobLength 187 | ) 188 | } 189 | 190 | def parts(support: Boolean) = Map( 191 | "base" -> (() => basePlate(true, support)), 192 | "motor" -> (() => motorGear(support)), 193 | "support" -> (() => supportPlate), 194 | "gear" -> (() => rodGear(support)) 195 | ) 196 | 197 | // ¬centered → axis of the actuator is at 0 198 | // centered → center of mass is at 0 199 | def assembled(withGears: Boolean = true, centered: Boolean = false) = { 200 | val block = Union( 201 | basePlate(true, false), 202 | supportPlate.rotateX(Pi).moveZ(pillarHeight), 203 | if (withGears) motorGear(false).move(0, motorYOffset, bearingGapBase) else Empty, 204 | if (withGears) rodGear(false).moveZ(bearingGapBase) else Empty, 205 | gimbal.model.rotateZ(-Pi/2).move(0,gimbalOffset,-plateThickness/2) 206 | ).moveZ(plateThickness/2) 207 | if (centered) { 208 | block.moveY(-gimbalOffset) 209 | } else { 210 | block 211 | } 212 | } 213 | 214 | 215 | } 216 | -------------------------------------------------------------------------------- /src/main/scala/scadla/Primitives.scala: -------------------------------------------------------------------------------- 1 | package scadla 2 | 3 | import squants.space.Length 4 | import squants.space.Area 5 | import squants.space.Millimeters 6 | import squants.space.LengthUnit 7 | import squants.space.AngleUnit 8 | import squants.space.Radians 9 | import squants.space.Angle 10 | 11 | case class Point(x: Length, y: Length, z: Length) { 12 | private def unit = x.unit 13 | def to(p: Point) = Vector( 14 | (p.x - x).in(unit).value, 15 | (p.y - y).in(unit).value, 16 | (p.z - z).in(unit).value, unit) 17 | def toVector = Vector(x.in(unit).value, y.in(unit).value, z.in(unit).value, unit) 18 | def toQuaternion = Quaternion(0, x.in(unit).value, y.in(unit).value, z.in(unit).value, unit) 19 | def +(v: Vector): Point = Point(x + v.x, y + v.y, z + v.z) 20 | def -(p: Point): Vector = p.to(this) 21 | } 22 | 23 | case class Face(p1: Point, p2: Point, p3: Point) { 24 | def normal = { 25 | val v1 = p1 to p2 26 | val v2 = p1 to p3 27 | val n1 = v1 cross v2 28 | n1.toUnitVector 29 | } 30 | def flipOrientation = Face(p1, p3, p2) 31 | } 32 | 33 | //TODO unit 34 | case class Matrix(m00: Double, m01: Double, m02: Double, m03:Double, 35 | m10: Double, m11: Double, m12: Double, m13:Double, 36 | m20: Double, m21: Double, m22: Double, m23:Double, 37 | m30: Double, m31: Double, m32: Double, m33:Double) { 38 | 39 | private def prod(r: Seq[Double], c: Seq[Double]): Double = { 40 | r(0)*c(0) + r(1)*c(1) + r(2)*c(2) + r(3)*c(3) 41 | } 42 | 43 | def row(i: Int) = i match { 44 | case 0 => Seq(m00, m01, m02, m03) 45 | case 1 => Seq(m10, m11, m12, m13) 46 | case 2 => Seq(m20, m21, m22, m23) 47 | case 3 => Seq(m30, m31, m32, m33) 48 | case _ => sys.error("0 ≤ " + i + " ≤ 3") 49 | } 50 | 51 | def col(i: Int) = i match { 52 | case 0 => Seq(m00, m10, m20, m30) 53 | case 1 => Seq(m01, m11, m21, m31) 54 | case 2 => Seq(m02, m12, m22, m32) 55 | case 3 => Seq(m03, m13, m23, m33) 56 | case _ => sys.error("0 ≤ " + i + " ≤ 3") 57 | } 58 | 59 | def *(m: Matrix): Matrix = { 60 | def p(r: Int, c: Int) = prod(row(r), m.col(c)) 61 | Matrix( 62 | p(0, 0), p(0, 1), p(0, 2), p(0, 3), 63 | p(1, 0), p(1, 1), p(1, 2), p(1, 3), 64 | p(2, 0), p(2, 1), p(2, 2), p(2, 3), 65 | p(3, 0), p(3, 1), p(3, 2), p(3, 3) 66 | ) 67 | } 68 | 69 | def *(p: Point): Point = { 70 | val extended = Seq(p.x.toMillimeters, p.y.toMillimeters, p.z.toMillimeters, 1) 71 | val x = prod(row(0), extended) 72 | val y = prod(row(1), extended) 73 | val z = prod(row(2), extended) 74 | val w = prod(row(3), extended) 75 | Point(Millimeters(x/w), Millimeters(y/w), Millimeters(z/w)) 76 | } 77 | 78 | def *(q: Quaternion): Matrix = this * q.toMatrix 79 | } 80 | 81 | object Matrix { 82 | def unit = Matrix(1, 0, 0, 0, 83 | 0, 1, 0, 0, 84 | 0, 0, 1, 0, 85 | 0, 0, 0, 1) 86 | 87 | def translation(x: Length, y: Length, z: Length) = { 88 | Matrix(1, 0, 0, x.toMillimeters, 89 | 0, 1, 0, y.toMillimeters, 90 | 0, 0, 1, z.toMillimeters, 91 | 0, 0, 0, 1) 92 | } 93 | 94 | def rotation(x: Angle, y: Angle, z: Angle) = { 95 | val qx = Quaternion.mkRotation(x, Vector.x) 96 | val qy = Quaternion.mkRotation(y, Vector.y) 97 | val qz = Quaternion.mkRotation(z, Vector.z) 98 | val q = qx * (qy * qz) 99 | q.toMatrix 100 | } 101 | 102 | def mirror(_x: Double, _y: Double, _z: Double) = { 103 | val v = Vector(_x,_y,_z,Millimeters).toUnitVector 104 | val x = v.x.toMillimeters 105 | val y = v.y.toMillimeters 106 | val z = v.z.toMillimeters 107 | Matrix(1-2*x*x, -2*y*x, -2*z*x, 0, 108 | -2*x*y, 1-2*y*y, -2*z*y, 0, 109 | -2*x*z, -2*y*z, 1-2*z*z, 0, 110 | 0, 0, 0, 1) 111 | } 112 | 113 | def scale(x: Double, y: Double, z: Double) = { 114 | Matrix(x, 0, 0, 0, 115 | 0, y, 0, 0, 116 | 0, 0, z, 0, 117 | 0, 0, 0, 1) 118 | } 119 | 120 | } 121 | 122 | case class Vector(private val _x: Double, private val _y: Double, private val _z: Double, unit: LengthUnit) { 123 | def x = unit(_x) 124 | def y = unit(_y) 125 | def z = unit(_z) 126 | def unary_- : Vector = Vector(-_x, -_y, -_z, unit) 127 | def +(v: Vector): Vector = { 128 | val vu = v.to(unit) 129 | Vector(_x+vu._x, _y+vu._y, _z+vu._z, unit) 130 | } 131 | def -(v: Vector): Vector = { 132 | val vu = v.to(unit) 133 | Vector(_x-vu._x, _y-vu._y, _z-vu._z, unit) 134 | } 135 | def *(c: Double): Vector = Vector(_x*c, _y*c, _z*c, unit) 136 | def /(c: Double): Vector = Vector(_x/c, _y/c, _z/c, unit) 137 | def dot(v: Vector): Area = x*v.x + y*v.y + z*v.z 138 | def cross(v: Vector) = { 139 | val vu = v.to(unit) 140 | Vector(_y*vu._z - _z*vu._y, _z*vu._x - _x*vu._z, _x*vu._y - _y*vu._x, unit) 141 | } 142 | private def _norm: Double = Math.sqrt(_x*_x + _y*_y + _z*_z) 143 | def norm: Length = unit(_norm) 144 | def toUnitVector: Vector = this / _norm 145 | def toQuaternion = Quaternion(0, _x, _y, _z, unit) 146 | def toQuaternion(real: Double) = Quaternion(real, _x, _y, _z, unit) 147 | def toPoint = Point(x, y, z) 148 | def rotateBy(q: Quaternion) = q.rotate(this) 149 | def to(newUnit: LengthUnit) = { 150 | if (newUnit == unit) this 151 | else Vector(x to newUnit, y to newUnit, z to newUnit, newUnit) 152 | } 153 | } 154 | 155 | object Vector { 156 | /** A 1mm vector pointing in the positive X direction */ 157 | def x = new Vector(1, 0, 0, Millimeters) 158 | /** A 1mm vector pointing in the positive Y direction */ 159 | def y = new Vector(0, 1, 0, Millimeters) 160 | /** A 1mm vector pointing in the positive Z direction */ 161 | def z = new Vector(0, 0, 1, Millimeters) 162 | } 163 | 164 | case class Quaternion(a: Double, i: Double, j: Double, k: Double, unit: LengthUnit) { 165 | /** Hammilton product */ 166 | def *(q: Quaternion) = { 167 | val qu = q.to(unit) 168 | Quaternion(a*qu.a - i*qu.i - j*qu.j - k*qu.k, 169 | a*qu.i + i*qu.a + j*qu.k - k*qu.j, 170 | a*qu.j - i*qu.k + j*qu.a + k*qu.i, 171 | a*qu.k + i*qu.j - j*qu.i + k*qu.a, unit) 172 | } 173 | def inverse = Quaternion(a, -i, -j, -k, unit) 174 | private def _norm: Double = math.sqrt(a*a + i*i + j*j + k*k) 175 | def norm: Length = unit(_norm) 176 | def toUnitQuaternion = { 177 | val n = _norm 178 | Quaternion(a/n, i/n, j/n, k/n, unit) 179 | } 180 | def toVector = Vector(i, j, k, unit) 181 | def toPoint = Point(unit(i), unit(j), unit(k)) 182 | def toMatrix = Matrix(1 - 2*(j*j + k*k), 2*(i*j - k*a), 2*(i*k + j*a), 0, 183 | 2*(i*j + k*a), 1 - 2*(i*i + k*k), 2*(j*k - i*a), 0, 184 | 2*(i*k - j*a), 2*(j*k + i*a), 1 - 2*(i*i + j*j), 0, 185 | 0, 0, 0, 1) 186 | /** Get the axis of rotation for an unit quaternion */ 187 | def getDirection: Vector = { 188 | if (i == 0.0 && j == 0.0 && k == 0.0) Vector.x 189 | else toVector.toUnitVector 190 | } 191 | /** Get the angle of rotation for an unit quaternion */ 192 | def getAngle = 2 * math.acos(a) 193 | 194 | // https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles 195 | def toRollPitchYaw = RollPitchYaw( 196 | math.atan2(2 * (i*a + j*k), 1 - 2 * (i*i + j*j)), 197 | math.asin(2 * (a*k - i*k)), 198 | math.atan2(2 * (a*k + i*j), 1 - 2 * (j*j + k*k)), 199 | Radians 200 | ) 201 | 202 | def rotate(v: Vector): Vector = (this * v.toQuaternion * inverse).toVector 203 | def rotate(p: Point): Point = (this * p.toQuaternion * inverse).toPoint 204 | 205 | def to(newUnit: LengthUnit) = { 206 | if (newUnit == unit) this 207 | else Quaternion(unit(a) to newUnit, unit(i) to newUnit, unit(j) to newUnit, unit(k) to newUnit, newUnit) 208 | } 209 | } 210 | 211 | case class RollPitchYaw(_roll: Double, _pitch: Double, _yaw: Double, unit: AngleUnit) { 212 | def roll = unit(_roll) 213 | def x = roll 214 | def pitch = unit(_pitch) 215 | def y = pitch 216 | def yaw = unit(_yaw) 217 | def z = yaw 218 | } 219 | 220 | object Quaternion { 221 | def mkRotation(alpha: Angle, direction: Vector) = { 222 | val a = (alpha / 2).cos 223 | val d = direction.toUnitVector * (alpha / 2).sin 224 | d.toQuaternion(a) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/main/scala/scadla/examples/cnc/Gimbal.scala: -------------------------------------------------------------------------------- 1 | package scadla.examples.cnc 2 | 3 | import scadla._ 4 | import utils._ 5 | import Trig._ 6 | import InlineOps._ 7 | import thread._ 8 | import Common._ 9 | import scadla.EverythingIsIn.{millimeters, radians} 10 | import squants.space.Length 11 | 12 | //The part that holds the linear actuator connected to the frame 13 | 14 | // outer dimensions 15 | class Gimbal( 16 | length: Length, 17 | width: Length, 18 | height: Length, 19 | lengthOffset: Length, 20 | widthOffset: Length, 21 | maxThickness: Length, 22 | minThickness: Length, 23 | retainerThickness: Length, 24 | knobLength: Length) { 25 | 26 | protected def carveBearing(shape: Solid) = { 27 | val b = Cylinder(11 + looseTolerance, maxThickness) 28 | val b1 = Cylinder(9, width) + 29 | b.moveZ(-retainerThickness) + 30 | b.moveZ(width-maxThickness+retainerThickness) 31 | val b2 = b1.moveZ(-width/2).rotateX(Pi/2) 32 | val b3 = b2.moveX(lengthOffset) 33 | shape - b3 34 | } 35 | 36 | protected def addKnob(shape: Solid) = { 37 | val k0 = Union( 38 | Cylinder(4 - tolerance, knobLength + 7), 39 | Cylinder(6, knobLength + tolerance).moveZ(-tolerance) //tolerance guarantees interferences 40 | ) 41 | val k1 = k0 - Cylinder(ISO.M3, knobLength + 7) 42 | shape + 43 | k1.rotateY(-Pi/2).move(-length/2, widthOffset, 0) + 44 | k1.rotateY(Pi/2).move(length/2, widthOffset, 0) 45 | } 46 | 47 | protected def halfCylinder(r1: Length, r2: Length, h: Length) = { 48 | val c1 = if (r1 <= r2) Cylinder(r1, h) else PieSlice(r1, 0, Pi, h) 49 | val c2 = if (r2 <= r1) Cylinder(r2, h) else PieSlice(r2, 0, Pi, h).rotateZ(Pi) 50 | c1 + c2 51 | } 52 | 53 | val innerWidth = width - 2 * maxThickness 54 | 55 | protected def shapeOuter = { 56 | 57 | def distO(center: Length, radius: Length) = { 58 | val l0 = radius - center 59 | val l1 = hypot( l0 - maxThickness + minThickness, height/2) 60 | l1 min l0 61 | } 62 | 63 | def distI(center: Length, radius: Length) = { 64 | val l0 = radius - center 65 | val l1 = hypot( l0 - maxThickness, height/2) 66 | l1 max (l0 - minThickness) 67 | } 68 | 69 | val radiusWidth1 = distO(width/2 + widthOffset, width) 70 | val radiusWidth2 = distO(width/2 - widthOffset, width) 71 | 72 | val radiusLength1 = distI(length/2 + lengthOffset, length) 73 | val radiusLength2 = distI(length/2 - lengthOffset, length) 74 | 75 | val outer = halfCylinder( radiusWidth1, radiusWidth2, length).rotateY(Pi/2) * CenteredCube.yz(length, width, height) 76 | val inner = halfCylinder( radiusLength1, radiusLength2, innerWidth). 77 | moveZ(-innerWidth/2). 78 | rotateZ(-Pi/2). 79 | rotateX(Pi/2). 80 | move( length/2+lengthOffset, widthOffset, 0) 81 | 82 | val x0 = outer - inner 83 | val x1 = x0 - CenteredCube.yz(length-2*maxThickness, innerWidth, height).moveX(maxThickness) 84 | x1.moveX(-length/2) //center the object at (0,0,0) 85 | } 86 | 87 | def model = { 88 | val s0 = shapeOuter 89 | val s1 = carveBearing(s0) 90 | val s2 = addKnob(s1) 91 | s2 92 | } 93 | 94 | //split into multiple parts connected with dovetail (and screws) for easier printing 95 | def parts(support: Boolean = false) = { 96 | val screw = Cylinder(woodScrewHeadRadius, woodScrewHeadHeight+10).moveZ(-10) + Cylinder(woodScrewRadius, woodScrewLength+woodScrewHeadHeight) 97 | val screwOffset = woodScrewHeadRadius max(minThickness/2) 98 | val screwX = length/2 - screwOffset 99 | val screwY = width/2 100 | val screwZ = 0 101 | val m = Difference( 102 | model, 103 | screw.rotateX(-Pi/2).move(-screwX,-screwY, screwZ), 104 | screw.rotateX(-Pi/2).move( screwX,-screwY, screwZ), 105 | screw.rotateX( Pi/2).move(-screwX, screwY, screwZ), 106 | screw.rotateX( Pi/2).move( screwX, screwY, screwZ) 107 | ) 108 | val c = Cube(length,width,height).move(-length/2, -width/2, -height/2) 109 | val part1 = c.moveY(maxThickness - width) 110 | val part2 = c.moveY(width - maxThickness) 111 | val part3 = c.moveX(maxThickness - length) 112 | val part4 = c.moveX(length - maxThickness) 113 | val delta = 2 //XXX fix to make 0 114 | val t = Trapezoid(height/3, height/2, width, maxThickness+delta).move(-height/4, -width/2, -maxThickness-delta) 115 | val doveTail = Difference( 116 | c, 117 | t.rotateY(-Pi/2).move(-width, 0, height/2), 118 | t.rotateY(-Pi/2).move(-width, 0, - height/2), 119 | t.rotateY( Pi/2).move( width, 0, height/2), 120 | t.rotateY( Pi/2).move( width, 0, - height/2) 121 | ) 122 | val dovetail1 = doveTail.moveY(maxThickness - width) 123 | val dovetail2 = doveTail.moveY(width - maxThickness) 124 | val dovetailB1 = Bigger(dovetail1, tightTolerance) 125 | val dovetailB2 = Bigger(dovetail2, tightTolerance) 126 | val ps = Seq( 127 | m * dovetail1, 128 | m * dovetail2, 129 | m * part3 - dovetailB1 - dovetailB2, 130 | m * part4 - dovetailB1 - dovetailB2 131 | ) 132 | if (support) { 133 | def addSupport(s: Solid) = { 134 | val under = Cube(height, innerWidth, maxThickness - minThickness).moveY(-width/2 + (width-innerWidth)/2) 135 | val sb1 = Bigger(s, 2*supportGap) 136 | val sb2 = Minkowski(s, Cylinder(1.5 * supportGap, 0.0625)) 137 | s + (under - sb1 - sb2) 138 | } 139 | Seq( 140 | ps(0).rotateX(-Pi/2), 141 | ps(1).rotateX( Pi/2), 142 | addSupport(ps(2).move( length/2,0, height/2).rotateY( Pi/2).moveZ(maxThickness)), 143 | addSupport(ps(3).move(-length/2,0,-height/2).rotateY(-Pi/2).moveZ(maxThickness)) 144 | ) 145 | } else { 146 | ps 147 | } 148 | } 149 | 150 | def printDimensions: Unit = { 151 | Console.println( 152 | "outer length: " + length + "\n" + 153 | "outer width: " + width + "\n" + 154 | "inner length: " + (length - 2 * maxThickness) + "\n" + 155 | "inner width: " + (width - 2 * maxThickness) + "\n" + 156 | "height: " + height 157 | ) 158 | } 159 | 160 | } 161 | 162 | object Gimbal { 163 | 164 | def outer( 165 | length: Length, 166 | width: Length, 167 | height: Length, 168 | lengthOffset: Length, 169 | widthOffset: Length, 170 | maxThickness: Length, 171 | minThickness: Length, 172 | retainerThickness: Length, 173 | knobLength: Length) = { 174 | new Gimbal(length, width, height, lengthOffset, widthOffset, 175 | maxThickness, minThickness, retainerThickness, knobLength) 176 | } 177 | 178 | def outerDimensions( 179 | length: Length, 180 | width: Length, 181 | height: Length, 182 | lengthOffset: Length, 183 | widthOffset: Length, 184 | maxThickness: Length, 185 | minThickness: Length, 186 | retainerThickness: Length, 187 | knobLength: Length) = { 188 | val g = new Gimbal(length, width, height, lengthOffset, widthOffset, 189 | maxThickness, minThickness, retainerThickness, knobLength) 190 | g.model 191 | } 192 | 193 | def inner( 194 | length: Length, 195 | width: Length, 196 | height: Length, 197 | lengthOffset: Length, 198 | widthOffset: Length, 199 | maxThickness: Length, 200 | minThickness: Length, 201 | retainerThickness: Length, 202 | knobLength: Length) = { 203 | outer( 204 | length + 2 * maxThickness, 205 | width + 2 * maxThickness, 206 | height, 207 | lengthOffset, 208 | widthOffset, 209 | maxThickness, 210 | minThickness, 211 | retainerThickness, 212 | knobLength) 213 | } 214 | 215 | def innerDimensions( 216 | length: Length, 217 | width: Length, 218 | height: Length, 219 | lengthOffset: Length, 220 | widthOffset: Length, 221 | maxThickness: Length, 222 | minThickness: Length, 223 | retainerThickness: Length, 224 | knobLength: Length) = { 225 | outerDimensions( 226 | length + 2 * maxThickness, 227 | width + 2 * maxThickness, 228 | height, 229 | lengthOffset, 230 | widthOffset, 231 | maxThickness, 232 | minThickness, 233 | retainerThickness, 234 | knobLength) 235 | } 236 | 237 | } 238 | --------------------------------------------------------------------------------