KittyCAD / kcl-experiments

KittyCAD Language
9 stars 0 forks source link

KCL gear example #6

Open adamchalmers opened 1 year ago

adamchalmers commented 1 year ago

We're gonna need examples of how to practically use KCL. I think a gear is a good first step. It's a nontrivial 3D solid, and it's also very common in real CAD. Here's what I've got so far.

@jgomez720 says the way real engineers draw gears is:

  1. Create a cylinder with a cylinder missing in its center
  2. Draw a gap (which, when removed, forms a tooth) on the top of the cylinder
  3. Extrude it down, subtracting it from the cylinder

It'll be something like this (remember, type annotations will be optional, they're included here for clarity)

// Create cylinder with a cylinder missing in center

cylinder = (height: Distance, radius: Distance) =>
    circle(radius) 
    |> extrude(height)

gearWithoutTeeth = (height: Distance) =>
    let
        radius = Distance::cm(30)
    in subtract(
        cylinder(height, radius/10), // outer cylinder
        cylinder(height, radius)     // inner cylinder to subtract
    )

// Draw the negative space which defines the gear tooth.

toothToRemove2D = ... // TODO: define the tooth via lines/path segments

// Extrude the removal tooth into 3D

toothToRemove = (height: Distance) =>
    extrude(toothToRemove2D, height)

// Subtract that tooth from the main gear part via circular pattern

teethToRemove = (teeth: Number, height: Distance) =>
    circularPattern(toothToRemove, teeth, gearWithoutTeeth(height))

// Voila: a parameterized gear function.

gear = (teeth: Number, height: Distance) =>
    subtract(gearWithoutTeeth(height), teethToRemove)

// Which lets you make various individual gears.
gear1 = gear(40, Distance::cm(3))
gear2 = gear(33, Distance::cm(20))

Just walking through this example with Josh was really helpful for bridging the gap between my software and his hardware background.

Open questions:

adamchalmers commented 1 year ago

@Irev-Dev would love your feedback especially on open question 2.

I guess we'd tag the top face of the cylinder, and then build a tooth-shaped path, and then put the path on the tagged face.

Irev-Dev commented 1 year ago

For circularPattern I think what you have with teeth being an int makes sense, because usually, you'll want to repeat the pattern a certain amount of times, and not be thinking about it like "a tooth every 12°", especially since defining the angle might mean the teeth don't match up at the end of the full revolution. This goes for other use-cases too. I think there should be an optional angular distance param that defaults to 360° so that users can specify they want this hole to repeat 5 times but only for 45°.

I think we'll draw the toothToRemove2D by selecting the top face, and then using our sketching API as you alluded to. How we're able to select the face with something worked into you're example code does seem a little tricky. I think tagging yes, but doesn't sit very well with gearWithoutTeeth abstraction, because that gear hasn't been created at the time that toothToRemove2D is being worked on.

Considering that and a few other thoughts on the API this is making me think of, I might try my hand at some psuedo code too. 🔜™️

adamchalmers commented 1 year ago

I think gearWithoutTeeth totally exists simultaneously with toothToRemove2D!

Agree about the angular distance param.

jgomez720 commented 1 year ago

Another thing to think about in the circularPattern is which axis you are revolving around. In traditional CAD software, once you created the "donut" (cylinder missing within another cylinder), the software would now give you the center of the cylinder as an axis to click on, but we would need some way for the user to define that. If it's one of the origin axis (X, Y, Z), I'd assume it's easier, but when it's off-origin, we need a way for the user to define

lf94 commented 1 year ago

I wrote up the same thing in JS which uses node-libfive bindings (an actual working example) to add to the conversation:

const { circle, cm, nothing } = require("../index");

const cylinder = ({ height, radius }) => circle(radius).extrude(height);

const donut = ({ height, outerRadius, innerRadius }) =>
  cylinder({ height, radius: outerRadius })
    .difference(cylinder({ height, radius: innerRadius }));

const tooth2d = () => nothing(); // Define the tooth in whatever way

const tooth3d = ({ height }) => tooth2d.extrude(height);

const teeth = ({ amount, height, radius }) =>
  tooth3d({ height }).distribute.radial({ amount, radius });

const gear = ({ amount, height, radius }) =>
  donut({ height, outerRadius: radius, innerRadius: radius/10 })
    .difference(teeth({ amount, height, radius }));

const gear1 = gear({ amount: 40, height: 3*cm, radius: 30*cm });
const gear2 = gear({ amount: 33, height: 20*cm, radius: 30*cm });

In FREP engines you can also do things like infinite extrude, so the height on the tooth3d is unnecessary.

Quite honestly all I can think about improving this is having native measurement types. Objects, spread, rest, and destructuring really make manipulating variables easy-peasy. The declarative API keeps context where it matters, reducing the need to retype a lot and read naturally.

Irev-Dev commented 1 year ago

So here's my attempt at a gear example, It's a bit vague and handy wavey. What it does, and what I'm emphasising is code-gen from UI interactions. One tricky thing about this lang's constraints/goal which is pretty unique is that it needs to treat code-gen as a first-class-citizen. And when we think about API/function calls etc we can't just think about the final state, but also how the code progressed to get to that point. I'm not wedded to any of the syntax below, though I do think the pipe operator is a god send for not having to generate more variable names since they are hard in code-gen.

  1. Without further ado, the user clicks the start sketch button and selects one of the default planes

    image
  2. App goes into sketch mode (perpendicular to the sketch plane ortho camera)

    image
  3. User selects the circle tooltip, then the axis to set the circle center, then where they want the edge of the circle to be

    image
  4. Does the same thing for the inner circle and exits the sketch

    image
  5. Sketch is selected so that it can be extruded

    image
  6. Bit of an explanation I added startSketch separate from addloop because I still like the idea of inner loops implicitly being subtracted from sketches (when extruded) unless the user specifies otherwise, I think this is a sane default that makes UI interactions -> code-gen much easier, but this is just example code, I'm not set on it absolutely.

    image
  7. Starts a sketch on the top of another extrusion, why it started a new variable/pipe-expression is a little arbitrary, an alternate API might be

    const part01 = startSketch()
    |> addLoop(%)
    |> circle(center: [0,0], radius: 50, %)
    |> addLoop(%)
    |> circle(center: [0,0], radius: 10, %)
    |> extrude(20, tag: extrude01, %)
    |> startSketch(%)
    |> transformToExtrudeTop(ref: 'extrude01', %)
    image
  8. Projects geometry from another solid onto the sketch plane

    image
  9. Adds more reference geometry for the rootRadius, sets the center of the circle by clicking an existing circle then sets where they want the edge to be.

    image
  10. Adds an arc that follows the rootRadius circle and starts aligned with the yaxis, both the axis and the reference geometry would have highlighted when the user hovered and then clicked. I've hand wave over this bit with /* ... on seg02 aligned with positive y axis*/ 😑 The end point of the arc is set with the 3rd click

    image
  11. Involute curve tooltip is selected and it goes up to the other reference circle, hand waving the other involute inputs, can probably just be sane defaults that the user can change if they like

    image
  12. Another arc is added

    image
  13. One of the points is selected

    image
  14. And another with shift for multi-select, with multi-cursor

    image
  15. Fillets are added to both. The filletBetweenPrevTwo seems a little problematic. The second instance would have to know to skip previous fillets, but it's not worth worrying about for the purposes of this example

    image
  16. Loop so far is mirrored along the yaxis, which comes from selecting the yaxis and that's what the 90 is there for.

    image
  17. Loop is closed, I realised later that I've actually cut through the top of the circle, which would cause issues later with the extrude-cut, but I'm being lazy and it doesn't deminish from the example much.

    image
  18. Sketch is exited

    image
  19. The previous sketch is selected so that it can extrude-cut

    image
  20. Extrude cut is actually done

    image
  21. Cut operation is then selected

    image
  22. So that the radial pattern operation can be done, we could probably get away with sane defaults that the user can then edit, but maybe we through up a modal when the button is clicked and prompt them for more details

    image
  23. Fin

    image
  24. But wait there's more, the user wanted to refactor the code a little to get rid of some of the default var names, first of the rootRadius can be move into a var that describes what it is. The user drags that radius up above the pipe expression

    image
  25. Get's a default name, we know it's a radius because that's where it was being used, but the user can update that as they are prompted when they mouse up

    image
  26. One second thought the user now wants to abstract this and see if they can make gears of different shapes and sizes, they select it all and git the function button

    image
  27. The user wants the rootRadius to be part of the function params, so they drag that var into the function definition

    image
  28. Some with the outerRadius (though they've stopped renaming at this point)

    image
  29. And the extrude height

    image
  30. Lastly they create a gear twice as big

    image

After which they did their job as a good developer and gave better names to a bunch of things before putting up a pull request to their team.

Irev-Dev commented 1 year ago

I forgot to do selecting an axis or similar for the radialPattern sorry @jgomez720.

jgomez720 commented 1 year ago

I was able to follow along with no problem. I like it.