├── 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 |
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 |
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 |
--------------------------------------------------------------------------------