Giter VIP home page Giter VIP logo

modeling-app's People

Contributors

adamchalmers avatar dependabot[bot] avatar franknoirot avatar greg-kcio avatar gserena01 avatar irev-dev avatar iterion avatar jessfraz avatar jgomez720 avatar lf94 avatar mlfarrell avatar mollyboydtaylor avatar notalfredo avatar paultag avatar pierremtb avatar rametta avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

modeling-app's Issues

Constraint demo must haves

Improvements that need to be made to the constraint system before the demo

Other ideas (might want to add to must-have list above)

  • midpoint selection (and constraints)
  • mirror features

Fix indentation for wrapped array and object expressions, within pipe bodies

indentation currently looks like:

const part001 = startSketchAt([0, 0])
  |> line({ to: [1, 3.82], tag: 'seg01' }, %) 
  |> angledLineToX([
    -angleToMatchLengthX('seg01', myVar, %),
    myVar
  ], %) 
  |> angledLineToY([
    -angleToMatchLengthY('seg01', myVar, %),
    myVar
  ], %) 
  |> angledLine([45, segLen('seg01', %)], %) 
  |> angledLine([45, segLen('seg01', %)], %) 
  |> angledLine([myAng, segLen('seg01', %)], %) 
  |> angledLineToX([
    angleToMatchLengthX('seg01', myVar2, %),
    myVar2
  ], %) 

When it would be clearer to have a little extra indentation when in a pipebody to make it clear when the next pipe begins like eg:

const part001 = startSketchAt([0, 0])
  |> line({ to: [1, 3.82], tag: 'seg01' }, %) 
  |> angledLineToX([
      -angleToMatchLengthX('seg01', myVar, %),
      myVar
    ], %) 
  |> angledLineToY([
      -angleToMatchLengthY('seg01', myVar, %),
      myVar
    ], %) 
  |> angledLine([45, segLen('seg01', %)], %) 
  |> angledLine([45, segLen('seg01', %)], %) 
  |> angledLine([myAng, segLen('seg01', %)], %) 
  |> angledLineToX([
      angleToMatchLengthX('seg01', myVar2, %),
      myVar2
    ], %) 
  |> angledLine([315, segLen('seg01', %)], %) 

Same rules for wrapped object expressions has also been implemented

const part001 = startSketchAt([-0.01, -0.08])
  |> line({ to: [0.62, 4.15], tag: 'seg01' }, %)
  |> line([2.77, -1.24], %)
  |> angledLineThatIntersects({
      angle: 201,
      offset: -1.35,
      intersectTag: 'seg01'
    }, %)
  |> line([-0.42, -1.72], %)
show(part001)

see also https://kittycadworkspace.slack.com/archives/C04KFV6NKL0/p1678160744675179

Replace values with variable helper

Screenshare.-.2023-04-01.9_59_05.AM.mp4
Screenshare.-.2023-04-01.10_01_02.AM.mp4
transcript

0:01
Hey, so I've added I've added this button here, convert, convert the variable.

0:05
But before I kind of explain what that does, I am going to give you a little bit of context on something else.

0:12
So I'm gonna use equal angle as an example.

0:16
Currently, when you when you constrain two line to be equal angle, it does that by it does that by referencing the first line.

0:27
So I've selected two lines here and it's gonna make this one the same as this one by references.

0:32
So I got eagle angle and you can see this the second line was just a line like as in the function call was just called line which had takes an X Y.

0:41
So it's converted to an angle angled line.

0:44
And that way it's able to call this other function which references the first line.

0:47
So this here is seg two.

0:49
So it references that and basically grabs the same angle.

0:53
So I'll just undo that I can do the exact same thing for.

0:57
So if I select different lines, so if I select this one and like the one before it, I can still make them equal angle.

1:03
But now the difference, there's no difference here.

1:06
But just of note is that when it, when it references a line, a previous line, it doesn't matter how that line is defined.

1:13
So because this is the, the previous line here wasn't an angled line.

1:18
It, there's no he uses X Y instead of my A and so this is much more robust for for referencing a a previous line.

1:28
But if we were to go back to my first example, and I want to constrain these two lines because this line here already has an angle like in a variable.

1:40
It, there is a possibility where we could constrain this line to be the same angle simply by using the same variables.

1:46
So that's just a tiny bit of context.

1:48
And now I'm gonna talk about something completely different.

1:50
So I've added I'll get out of sketch mode.

1:53
Actually, I've added this button here, convert the variable.

1:56
So it, it's going to tell it's going to be enabled when you can actually change a value out for a variable.

2:02
So the example here, I'm going to select on this a literal here, 2.68 and I can convert that to a variable.

2:07
So it's gonna prompt me for a variable name.

2:10
And then if I, if I add that variable, then it's going to swap out that 2.68 here.

2:15
And then assign that to a variable up above.

2:17
And so it only works in like specific cases, you have to have your cursor in a in some sort of value.

2:24
And then it's gonna be a bit picky about which values it actually allow.

2:27
So as an example, it works in this literal value, but it'll also work here, which is technically a binary expression.

2:35
And so is this like this is still enabled.

2:38
But if I, if I try and use it on this call expression, it's gonna be disabled.

2:42
It does actually work on should work on call expressions.

2:45
I don't have an example here, but it's not the call expression.

2:47
That's a problem.

2:48
It's this pipe substitution.

2:50
So it's, it understands that you know, it's, it's within a pipe and therefore there's no way to create this variable up above and still reference something from within the pipe because it's like contextual to this line.

3:02
So it's disabled here.

3:05
Funnily enough, it actually let it's gonna let me change that that, that string.

3:09
So yeah, that'll work too.

3:12
But anyway, it's, but it also doesn't work for straight identifiers.

3:16
So this here my A I can't do it.

3:18
And that's really like more of a, like a, a business logic rather than not able to do it kind of use case because this could get swapped out for, you know, my V 01.

3:28
And then up here have no equals new or sorry, nova equals.

3:33
But it's kind of pointless.

3:34
Like what would be the point of doing that?

3:35
So it's, it's disabled, at least for now.

3:37
Now, I will just jump back into edit sketch mode and that'll help us talk about what we were talking about before.

3:43
So one thing to remember about this constraint system is that it, it treats literals as unconstrained.

3:49
If something's unconstrained, if something's a literal, then it, it's able to change.

3:52
So the example here is this has a literal in the Y value which means at this point, I should be able to drag up and down.

4:00
But if I try and drag it left or right, it's not gonna let me because it's constrained right.

4:03
That's kind of the logic.

4:04
And so therefore, the color coding here is also using that logic.

4:08
So this is green because it's fully constrained.

4:10
It's got two non literals in it in its use, but this one before has one literal.

4:15
So it's, it's it's part constrained.

4:17
So it's this red color.

4:18
So in order to make it fully constrained, I need to like basically make this AAA binary expression a core expression something other than a literal.

4:26
And an identifier also will do the trick.

4:28
So that's a bit of an interesting use case of even just converting to a variable will technically make this fully constrained.

4:34
It's now this green color code it's fully constrained.

4:36
So bring it back to the first point that I initially made in that you know, if I wanted to constrain this line to be the same angle as a previous line.

4:46
Well, if I wanted to make the same as this line, that already has an explicit angle called out, there is a possibility that we could use that variable straight away and that's the point I want to get to, but this convert a variable,

0:01
Sorry, slice is a five minute limit.

0:03
I probably should keep these under five minutes.

0:05
Anyway.

0:06
All, all I was just gonna say was that, yeah, this this converter variable helper is just a bit of a stepping stone for that.

0:12
And while I can add it, it's a bit of a nice to have like I wouldn't add it if, if it was the only thing we were getting out of it, but it also helps with stuff in the future.

0:20
Now, just quickly, If you look at the pull request, there's quite a lot of like refactoring work in particular around this get node path from source range.

0:29
So this function is responsible for, for giving a source range, which is kind of primarily what you're doing with when the cursor is in the editor.

0:37
Getting like where that is inside the A S T is quite important.

0:40
And this function, I guess I had a bug really, it was more like it was stubbed out and I hadn't like, hadn't completed it.

0:47
But then it's pretty heavily relied on like as you can imagine getting finding where something is A S T from the source range is used a lot.

0:55
And I had assumed the stubbed out functionality in a lot of the code.

0:59
So there was a, there was a lot of refactoring that needed to be done to get this to work.

1:04
Yes, that's just as a bit of extra context.

Fix equal length sign bug for yLines

If you have a yLine(-5, %) i.e. it's values is negative -5 than when constraining it to the length of another line, it should take the sign into account and transform it to yLine(-segLen('someSeg', %), %), currently it always does it positively i.e. yLine(segLen('someSeg', %), %) which is jaring for users to see the line they are constraining to flip direction.

svg / symbols to be created

@jgomez720 is lead on what we need here and whoever becomes our 'task/ui' designer/coder can work on doing these right - basically we want to also create a symbols database that internally we can use and through us people can access as well


@hanbollar These are the pictures that Rich sent me of the ASME Y14.5 standard of ALL the symbol dimension/proportion requirements. We would have to make the SVG files (or equivalent) ourselves. He even mentioned that if we had all the symbols correctly dimensioned, we would be the first CAD software to ever do so.

image001
image002
image006
image007

Originally posted by @jgomez720 in https://github.com/KittyCAD/PetStore/issues/127#issuecomment-1430286643

Port Adam's sockets and streaming code to the app

See: https://github.com/KittyCAD/api-deux/issues/538#issuecomment-1450310725

Code pasted for reference.

JS WebSocket & RTCPeerConnection

You can just copy and paste this into the console on somewhere like https:dev.kittycad.io/product.

const pc = new RTCPeerConnection();
const url = 'wss://dev.api.kittycad.io/ws/channel';
const socket = new WebSocket(url);

// Connection opened
socket.addEventListener('open', (event) => {
  console.log("Connected to websocket, waiting for ICE servers");
});

socket.addEventListener('close', (event) => {
  console.log("websocket connection closed");
});

socket.addEventListener('error', (event) => {
  console.log("websocket connection error");
});

// Listen for messages
socket.addEventListener('message', (event) => {
  //console.log('Message from server ', event.data);
  if (event.data instanceof Blob) {
    reader = new FileReader();

    reader.onload = () => {
      //console.log("Result: " + reader.result);
    };

    reader.readAsText(event.data);
  } else {
    const message = JSON.parse(event.data);
    if (message.type === "SDPAnswer") {
      pc.setRemoteDescription(new RTCSessionDescription(message.answer))
    } else if (message.type === "IceServerInfo") {
      console.log("received IceServerInfo")
      pc.setConfiguration({
        iceServers: message.ice_servers
      });
      pc.ontrack = function(event) {
        var el = document.createElement(event.track.kind)
        el.srcObject = event.streams[0]
        el.autoplay = true
        el.controls = true

        document.getElementById('__next').appendChild(el)
      }
      pc.oniceconnectionstatechange = e => console.log(pc.iceConnectionState)
      pc.onicecandidate = event => {
        if (event.candidate === null) {
          console.log("sent SDPOffer")
          socket.send(JSON.stringify({
            type: "SDPOffer",
            offer: pc.localDescription,
          }))
        }
      }

      // Offer to receive 1 video track
      pc.addTransceiver('video', {
        'direction': 'sendrecv'
      })
      pc.createOffer().then(d => pc.setLocalDescription(d)).catch(console.log);
    }
  }
});

Add intersect constraint UI

Add UI interactions for helper added in #60, #59

Screenshare.-.2023-03-19.6_31_25.PM.mp4
transcript

0:00
Hey, so new constraint just dropped, I've called it intersect, but it's basically there to help with parallel lines.

0:09
Yeah, I'll show you what I mean.

0:11
So if I just get a few lines going, what it does is if I'm interested in when this line here and this line here intersect or when this line intersects with this one, I can hit this intersect button and it's got to get some extra information from me.

0:30
That's primarily it's asking for the perpendicular distance.

0:34
So if I just leave it as is and hard coded, the line hasn't actually changed.

0:38
But now it's variables are one, it's referencing the first line.

0:43
But then these other two are these other two numbers that define the line in terms of how it intersects the other one.

0:50
So it's, it's a, it's angled.

0:51
So it's just the angle of the line and the offset is it's perpendicular distance away from this line.

0:56
So if I, if I put this to zero for, then it should sit right on the line.

1:02
But otherwise it's, it's going to give it the perpendicular distance.

1:05
And the perpendicular distance is very useful because it means that if we were interested in making parallel lines, like there were another line after this, that's what I meant to do.

1:16
You can see if I, if I hard code or not hard code, the opposite of that, if I had a very well for that offset.

1:23
Now, when I drag and it's always going to be constrained to being like running parallel that line or like sitting just that one value off.

1:31
So like I'm saying, it helps with parallel lines.

1:33
So I'll show you what that looks like.

1:36
So we just start again and let's say we have two lines here, this one and this one where those angles are very important and we want to, we want to come back to those and there's a whole bunch of other crap that goes on and then we have two lines that we want to follow parallel.

1:54
So if I finish that there and maybe this line is like has a specific angle so we can 130 looks good to me.

2:04
We can, we already could make these lines parallel by hitting a equal angle, right?

2:11
So this is just gonna snap this one to the same angle and we do the same here, equal angle, but probably were interested in the distance between the lines.

2:23
And in this case, maybe we want these lines to run parallel even after the sort of equal distance, even after this little kink here.

2:30
So what we can do is we can define that distance with that parallel, like that particular distance by hitting intersect.

2:37
And let's say again, we want it to be my vase, we can hit that in.

2:43
And if we do the same with this one and this one, then it's gonna, it's gonna end up being those two lines will be equal distance.

2:53
So yeah, those are looking, looking pretty good.

2:55
And if I change this to two, you can see like it's still parallel but just, you know, it's getting, getting a lot wider and it's still robust, you know, changes.

3:05
Like if this, if one of these underlying values change its, it'll update and the happy.

3:12
Yeah, I think that's pretty much it.

3:16
Yeah, cool, thanks.

Use shift for multi cusor/selections instead of capslock

Related to #31, it was implemented so that holding down capslock and selecting multiple features adds multiple cursors to the editor. See below:

Screen.Recording.2023-02-16.at.9.15.37.pm.mp4

Capslock was used because shift conflicts with the existing functionality of the codemirror6 editor. The docs here do mention the keymap https://codemirror.net/docs/ref/, and that seems to line up with ChatGPT had to say about it (take with a grain of salt ofcourse, but might be a place to start)

ChatGPT 👇


You can turn on and off the default behavior of the shift key by updating the keymap of the EditorView on the fly. To do this, you can create two separate keymaps: one with the "select" command enabled for the shift key, and one with it disabled. Then, you can switch between these keymaps at runtime depending on whether you want the default behavior to be enabled or not.

Here's an example of how to implement this approach:

import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { keymap } from "@codemirror/view";

// Create a keymap with the "select" command enabled
const selectKeymap = keymap.of([
  {
    key: "Shift",
    run: () => false // Allow shift key to select text
  }
]);

// Create a keymap with the "select" command disabled
const disableSelectKeymap = keymap.of([
  {
    key: "Shift",
    run: () => true // Disable shift key's default behavior
  }
]);

// Create the editor with the selectKeymap as the default keymap
const editor = new EditorView({
  state: EditorState.create({
    doc: "Hello, world!",
    extensions: [selectKeymap]
  }),
  parent: document.body
});

// Turn off default behavior by switching to disableSelectKeymap
function turnOffSelectBehavior() {
  editor.dispatch({
    reconfigure: {
      view: editor.state.update({
        extensions: [disableSelectKeymap]
      })
    }
  });
}

// Turn on default behavior by switching back to selectKeymap
function turnOnSelectBehavior() {
  editor.dispatch({
    reconfigure: {
      view: editor.state.update({
        extensions: [selectKeymap]
      })
    }
  });
}

In this example, we first create two separate keymaps: selectKeymap, which has the default behavior enabled, and disableSelectKeymap, which has it disabled. Then we create the editor with selectKeymap as the default keymap.

To turn off the default behavior, we call turnOffSelectBehavior(), which uses the dispatch method to reconfigure the editor's keymap to use disableSelectKeymap.

To turn the default behavior back on, we call turnOnSelectBehavior(), which again uses the dispatch method to reconfigure the editor's keymap to use selectKeymap.

Note that calling dispatch with the reconfigure option updates the editor's configuration on the fly without changing the current state.

Cursors should stay after a code-mod

After applying a constraint the cursor goes to 0, which is a little jarring.

image

Screenshare.-.2023-04-14.5_07_56.AM.mp4
transcript

0:00
So I've made a bit of an improvement to the way curses work, particularly after a modification.

0:09
So as an example, if I select these two lines here and I want to make them parallel.

0:14
So I've noticed that the curses are on these, like when I selected them, let's just put the curses in the right place.

0:20
If I make them parallel, the cursor jumps back to one like zero, which is a little jarring and it means that you can't change things together.

0:28
So the fix is here.

0:32
If I add the sketch and into the same thing like this line and this line, then when I go parallel, they the cursor stay in the same, well, it's not the same position as the same character index, but they stay on those two lines.

0:47
And because they're now selected, it has been a parallel that I can, you know, straight away, hit another constraint and so and forth so forth.

0:55
I mean, being able to chain constraint together is definitely useful.

0:59
But I think like just more, I guess, philosophically, just like keeping things where they use a lot less of them is a is a good idea.

1:07
So the way this works, it just as a quick explanation.

1:09
So you've got your current A S D of like the last sort of execution, last mode and it has accurate source ranges.

1:17
And so the selection of lines is actually just where the cursor is.

1:20
So that's what those.

1:21
So when I say this is accurate source ranges, like the, the range reach node inside the A S T is accurate.

1:27
And then we have our own source ranges which are like where the user has their cursor.

1:31
And those, those ranges is actually is what's used for, like how we know what to modify.

1:37
So then when we go ahead and modify the A S T, it no longer has accurate source ranges because we're just kind of manipulating it like a big nested object.

1:46
We're just manipulating that directly.

1:48
So when we do so anything we add or delete, it's gonna mess with the source ranges.

1:53
So it's not accurate anymore.

1:56
So in order to keep track of where those cursor are, we instead like keep track of the path to node of like what's just been modified.

2:05
So we'll hold on to that, then the modified S T gets recast to code and then that will be create the new A S T with accurate source ranges.

2:14
And then we can use this path to know to dig inside the A and get new source ranges out and then we'll use those to reset the cursor.

2:22
So they're, where they need to be.

2:24
And before that just kind of, it was kind of, the whole thing was just getting so it would go back to zero.

2:28
So, yeah, that's a fix.

Creating new sketch calling sketch fns with dumby default values

screencap.mp4
transcript

0:02
So when, when starting a sketch, after selecting a plane, it put some dummy code in here.

0:10
And it's definitely not the best thing because if you select a tooltip, then you either have to like come and fix this back up to where you want it or delete these lines.

0:21
And so it is really a polished item but trying to make that process a bit better.

0:28
What I've done is when you started again, once you select the plane, it still puts in a default, like start sketch and line, but explicitly called it with this kind of like dummy value of like string, default or literal default.

0:41
And that allows like further modification, the kind of query the S T for like an argument of this, which it kind of expects to replace.

0:50
So if I select line as a tooltip again and I click a few places, the first click, it's gonna, it's going to move the starting place and replace it with these lines.

0:59
And then when I add another line, it's before I add the new line is going to check to see if there's a line with that same default value and then replace it as well.

1:08
And then any clicks after that, it goes back to normal behavior and is adding line.

1:13
So I think that's just a bit better in terms of still putting something there.

1:16
So it looks, you know what's happening where the how to keep continue on.

1:21
But it's also not something you have to then like annoyingly changed later because it just kind of automatically overrides.

Constraint approaches, the book.

Part1: Reminder of what we're doing with our UI

We're creating a CAD UI that treats code as the source of truth. In any software where a user is producing an artifact (that is video editing, ux-design or CAD software as apposed to project-management, or email UI as an example) as the user interacts with the software they are building up more and more data. Normally this data is stored in some proprietary and obscure datastructure. We're choosing to instead use code itself as our data store for the following three reasons:

  1. Works with our code-first philosophy
  2. It's as "respectful" as we can be to our users by giving them a 1:1 mental model of how the software works
  3. Is much better at capturing intent of the user
  4. Enables engineers to leverage CI style pipelines, another goal of ours, because what they get out of their work is code that can execute in the UI or a pipeline.

The way this works is that the code executes and produces artifacts for the 3d scene. When users click around and modify things in the 3d scene, they are not modifying the scene directly, they are modifying the code that executes again to produce an updated scene. There is always a strong link between the code and the 3d scene, achieved by attaching source-range metadata to the 3d artifacts so that when you hover over a line of code, you can see the corresponding 3d artifact and vice versa.

Part2: Why 2d sketch and extrude is so important

We're aiming for 2d sketch and extrude workflow, simply because:

  1. It's industry standard, and very intuitive to current MEs.
  2. CAD design is centred around manufactured products, and this means that designs are very exact, everything is dimension. A 2d to 3d workflow suits this very well for the majority of functional parts. I.e. we get a high engineering-time ROI out of this relatively simple workflow, we can extend with more exotic features after the fact.

Part3: How this would work with 2d solver approach.

The current norm in CAD software for sketches is to roughly draw your sketch, this gives it some rough initial values and rough initial shape, and from there the user adds a series of constraints. Constraints are relationships between segments of the sketch that can be expressed mathematically. A 2d sketch solver can then tell the user if they have enough constraints to uniquely determine the shape of the sketch, and solve the sketch if so. The inner workings of the solver are never exposed to the user, Even though CAD-users are very competent in reasoning about geometry, they are completely abstracted away from this. It's also worth noting that the initial values of the segments in the sketch are completely disposed of by the time the sketch is fully constrained, They serve only as a starting point, this has to be considered when thinking about the API, and code-gen aspect for our UI. Let's run some thought experiments, let's say we're sketching the following parallelogram

image

Imagine the user clicks around in a loop to create the shape roughly, they came close to the dimensions they need, but not quite. A hypothetical API for how this is expressed as code is to add a number of segments with rough values, they are defined as polar vecs here, but the exact way of defining isn't important. I've also used a piping syntax, but they could just as easily be in an array, again not really important when thinking through this hypothetical API.

image

After the sketch has been roughly defined it's time to add constraints, with this 2d solver approach these constraints will not be inherent to the segments themselves, instead they will be attached later as overrides for the original segments, from a code-gen perspective though it's important for us to remove redundant values when the constraints are added as having stale and therefore possible confusing code is undesirable. Let's start with defining some "equal length" constraints

image

Here the user would have selected the 'A' and 'C' segments and clicked the 'equal' constraint, and so the code now conveys that, i.e. segments 'A' and 'C' are equal, but we also want to remove the length values from when those segments were originally defined, so now they have become polarVec({ angle: 30.02 }, 'A', %) and polarVec({ angle: 209.02 }, 'C', %). We still need the length of the lines, but since it's not clear which length line should take preference instead a nominalLength has instead been added to the constraint itself, this is a little messy as once we add some other length constraint we'll probably want to remove this, making it another awkward temp value. Let's add the next equal length constraint for 'B' and 'D' segments.

image

Very similar to the last, we'll move one with a different type of constraint, equal length.

image

In the above, we've added two constraints to speed things along. They both are effectively length constraints (one is yLength just so the constraint is in values from the original dimensions). When adding these constraints the modify-ast-helpers needs to be aware of the temporary values that need to be removed. In this case the "nominalLength" values from the first two constraints.

Let's add the final constraints

image

Here we've added angle constraints for both 'A' and 'B'. Segments 'C' and 'D' don't need to be constrained by angle as after all of the other equal-length constraints have been applied they will be the correct angles. Notice that the temp angles that were used in the original line declarations have now been removed because they are not needed anymore. This means that those lines are nothing more than line labels, ABC & D, with the constraints using those labels to actually define the shape it creates, I've also changed the function call name to just vec as polarVec has no meaning anymore.

I do not like this API. It's hard to read due the disconnect between where the segments are defined and then how they are constrained. Some of the tooling I've added to connect code with scene-artifacts doesn't work well with this API, for example:

  • If one hovers over a segment in the scene what lines of code should it highlight? both the lines where the label is given as well as any constraints that use the label the line is given?
  • If the user puts their cursor in the editor within vec('A', %), then we should highlight that segment in the 3d scene, but should also highlight other parts in the code, i.e. the constraints that mention 'A'? it's not clear to me if that makes things better or more confusing.

One other concern I need to ask is "is that sketch actually fully constrained yet", it's a little hard to answer, let me explain:
When adding sketch constraints in normal CAD, some nuances are left out, as dimensions are always absolute, i.e. you wouldn't give something a length of -5 (well yes you can, but that's to flip it's direct, but after you've done so, when you come back to edit again it will be 5 again), for angles this takes the effect of 30° meaning the same as 210° (30 + 180), or that line 30 degrees from the y-axis, this could be CW or CCW from the axis. And if we keep the same rubric then really the sketch we've constrained might have as many as 8 solutions

image

I've layered so many sketches on top of each other it's hard to see what's going on but you get the point. I imagine that CAD packages must keep information about the directions of these dimensions that are never exposed to the user. In our case since the code will need to produce the same 3d artifact, we'll need to be a bit more explicit about these things. This might mean adding an extra parameter on the constraints to specify direction or using the sign of the value to indicate the direction.

My concern with adding extra information to denote direction is it smells, it feels like an awkward level of abstraction, the user need not concern themselves with how the lines get constrained/solved, because the solver takes care of it, and yet the values that end up the code don't match 100% with what they entered. (i.e. they specify 5, but appears as -5 in the code or similar).

One other very tentative concern I have with this approach is that I fear it's not compatible with branching logic. Branching logic is going be a very powerful feature for our users as it will allow them to make their models very robust to even dramatic changes in input parameters because they determine how the model will adjust with those changing params. if statements will allow them to add breakpoints as such to adjust the model. This will likely take the form of removing or adding sketch segments depending, but because the lines and their sketches are defined separately and because constraints involve multiple segments, and the final result needs to be a sketch that's not under or over-constrained and doesn't reference segments that don't exist anymore. Therefore branching logic sounds tricky with a 2d-solver approach to say the least.

Part4: How this would work with constraints embedded in the line definitions.

What has already been implemented (but not finished) is a constraint system that is much more code-centric. Instead of constraints defining how segments relate to each other, each segment is defined fully in the same line (line of code) in which it's created, referencing previous segments in order to tie certain values together i.e. line({length: somePreviousline}) to make things equal length or line({xVal: somePreviousLine + 10}) to make the segment 10 units apart on the x-axis. Segments can also be defined using a variety of parameter types which makes it easy define the segments with the dimensions that seem most intuitive, for example:

  • line(...) takes xy coordinates that are relative from the last line
  • lineTo(...) takes xy coordinates absolute to the sketch plane
  • angledLine(...) takes polar coords angle, length
  • angledLineToY(...) takes a combination, it has a particular angle, and will be as long as it needs to be to get to a specific y value
  • so and so forth

This implementation also uses a variation of magic-number concept to determine if segments are constrained. In programming magic-numbers are hard-coded numbers that have no explanation from the surrounding code and therefore there's a high risk that the purpose of the number will be forgotten with time, making it hard to maintain. For example let's say you find the following line while trying to fix a bug const fluxCapacitance = COOL_CONSTANT + 156.43, what is 156.43? it's very specific so it probably has a good reason to be there but a programmer has no idea from reading the code. I can be improved by simply naming it.

const radianceFactor = 156.43
const fluxCapacitance = COOL_CONSTANT + radianceFactor

Now that constant has heaps more meaning. Likewise in the UI, when first adding the sketch segments, initially they have a lot of hard-coded numbers, or "Literals" to be specific of how the language AST refers to them. The UI treats the literals as unconstrained values, and will happily modify them further, either when the user clicks-&-drags the head of a segment, or when adding "constraints". However when adding constraints the "literals" are transformed into CallExpressions, Identifiers or Binary expressions (that is function calls, referencing a variable, or math equations 1 + 4). Once this is done that value is considered constrained, but also now has more meaning, the approach can be thought of as "You should reduce your magic numbers, and constrain your sketch", as a way to both embed meaning into and properly define your sketch.

With this style of constraint, there is also a mental model and best practices for applying constraints.

Mental model is that:

  • Each segment has a head and a tail
  • Each segment starts from the previous segment
  • Each segment has access to the previous segments and can reference them (i.e. backward references), however, forward references become problematic and should be avoided.

Therefore

The best practice for defining constraints is to either:

  • Fully constrain each segment in the same order as they appear in the sketch referencing segments pior-to/already-constrained.
  • Alternatively constraint the angle of each segment in the order they appear in the sketch, then define the length of each segment in the order they appear in the sketch, again referencing segments pior-to/already-constrained.

Also:

  • Don't try and define a segment's length as well as it's endpoint

Not strict, but following these rules will ensure things flow well.

image

Let's walk through the same parallelogram example but with this alternate constraints approach. Since this has already been implemented we can use screenshots from the UI. Starting off with the initial draft shape, lots of literals in the sketch at this point.

image

Let's start by constraining the top horizontal segment to be horizontal, The segment is selected by clicking on it, but what this actually does is put the cursor on the relevant line-of-code, that way the ast-modification-helpers know what part of the code needs to be modified. We can constrain it to be horizontal by clicking the 'horz' button after the segment has been selected.

image

Notice now that line 3 in the editor has transformed to an xLine function call, this only takes a single x-value as it's been constrained on the yAxis, further the segment in the scene has turned red, this indicates that the line is partially constrained, its length can changed but it must stay flat on the xAxis.

image

Now let's specify the angle of the first segment, here we're selecting that segment and the y-axis, in order to specify the angle in the same way it is annotated in the original drawing. We're also given UI to prompt us of the angle, originally it told us from the rough sketch that the angle of this line was 26°, I've changed it to 30, notice that it's also indicating that the angle is negative, that's because from the y-axis to the line is in a CW direction, but the user doesn't have to worry about that, it's just there as an indication. We're also going to create a variable for this angle.

image

After applying the constraint, that segment now has also turned red to indicate it's partially constrained, and in the editor, line 3 has been transformed from a line to an angledLine function call, as this better accommodates defining the angle of the line, notice the angle is defined as _90 - myAng that's because the angles are defined form the xAxis, and so by selecting the yAxis we've added 90° to align to the yAxis, than reducing by myAng/30 to get the angle of 60° from the xAxis.

image

Let's define the angle of the last segment (close doesn't count as it just connects the last to the first) by making it parallel to myAng, we can select those two sloped angles and hit the parallel button.

The result of this is that on line 6 of the editor, a tag has been given to the original angledLine so that it can be referenced. then on line 9, the line has been changed to angledLine similar to last time, but the angle is defined as segAng('seg01', %) + 180 that's because the interface is very explicit about these values, and the second sloped segment is actually 180° different from the first when taking the tail and head of the segment into consideration. The parallel constraint button will snap the line to the nearest 180° to make them parallel. The segment has also changed colour to indicate it's partially constrained.

image

Let us now turn our attention to defining the length of the segments to fully constrain them. Starting with the first sloped segment, we'll select it and the xAxis to define its height to 5, to match the drawing, we'll create a variable here too.

image

On line 4 of the editor, the angledLine has been transformed to angledLineToY as this better accommodates how we're defining the line, and on line 6 the to property has been assigned the height variable, also the segment has also now turned green to indicate that the segment is fully constrained, that is the function call uses all non-literal values.

image

Next we can set the length of the top segment, which can be done by selecting it and using the 'setLength' button. On line 5 of the editor the xLine did not need to change, but the literal value of 5.04 has been swapped out for our variable of the dimension width. The segment has also turned green to indicate that it's fully constrained.

image

Lastly let's set the length of the last segment by selecting the first sloped line and the second and hit the 'equalLength' button. On line 13 of the editor, the length of the line now references the first slopped segment, and the segment has also turned green to indicate it's fully constrained.

image

We're finished with this sketch, as a bonus, by putting our major dimensions into variables, it's now easy to modify the sketch in regards to those parametric values, for example:

image

Since I can actually do the demo we just went through, here's a clip of it from start to end.

My.Movie.1.mp4

It's worth reflecting on how readable the code is that's been generated, the first segment is

angledLineToY({
  angle: _90 - myAng,
  to: height,
  tag: 'seg01'
}, %)

This reads well as a segment that's defined by it's angle and it's height. Then:

xLine(length, %)

Reads as a horizontal segment that has the length of the variable used

angledLine([
  segAng('seg01', %) + 180,
  segLen('seg01', %)
], %)

This segment reads as a segment that has the same angle as a previous segment except it's going in the exact opposite direct, and it also has the same length as that previous segment.

close(%)

Closing off the loop.

It becomes even more readable when we take into account that we can hover over the segments to show us the code that we're concerned with.

The fact that the code is readable is no small thing. If we're trying to give our users a really clear mental model for how the software works, then it's very important that the code produced is intuitive.

What are some of the problems with this approach?

There are a handful, but they can be summarised by "It doesn't replicate the constraint workflow ME are used to exactly". I think this can largely be mitigated by communicating the mental model of the sketches and the best practice for how to use the constraints.

Can't do forward references. In most CAD-packages, you can add constraints in any order you like, but because this method works on backward references only you can get yourself stuck. Take the following situation where the second slope segment has been constrained with an angle first.

image

Here a user might select both of the slopped segments and expect to be able to apply a "parallel" constraint, but with the current setup that can't be done as the second segment needs to reference the first, a few comments on this situation:

  • The easy way out of this situation is to simply select the first segment and the yAxis and use a "setAngle" constraint to set it to the same angle, and now these are effectively constrained the same, obviously that doesn't help the users who is simply confused as to why the "parallel" constraint can't be set
  • We could definitely add some UI elements that indicate why this isn't possible and suggests what to do about it.
  • Lastly we can probably actually just solve this case, in that the second segment is using an angledLine function call, meaning we can simply grab the angle from it, using that value again in the segment we're trying to constrain. Because the second segment is using angledLine and we want to set another segment to the same angle, that makes this forward-referencing case relatively simple, and so it doesn't generalise to more complicated forward-referencing cases.

Let's look at a more complicated example, I'd describe this problem as "not being able to constrain a line more than once" but it also involves forward referencing. Let's say we're trying to sketch the following (some dimensions are missing here, but the important ones to demo the problem are there.)

image

Below I've constrained all of the segment angles, but none of the lengths

image

Now let's say I think it makes sense to constrain the far-right vertical segment to being 0.5 long. Great, that works.

image

After which I might want to select that same vertical line and set where it's head ends by selecting it and the xAxis.

image

But when I do so, non-of the constraint buttons at all are available 👆. That's because that vertical segment is fully constrained, it has a set length, where its tip end is instead controlled by the last segment that has a vertical component. See the clip below, it happens to be the slopped segment.

Screen.Recording.2023-04-10.at.3.28.21.pm.mp4

In order to get the result we're after we'll need to select that sloped segment and the xAxis and set it to 2-0.5 that is the total height we're after 2 minus the thickness of the arm thing 0.5

image

⬇️

image

I really don't see this as an insurmountable problem for ME's once they have the correct mental model, it's pretty easy to reason about. But it might be possible to have to enable forward referencing that help in a situation like this.

const myAng = 45
const armThick = 0.5
const part001 = startSketchAt([0, 0])
  |> xLine(2.61, %)
  |> tempAngledLine({angle: -myAng, tag: 'myLine'}, %)
  |> xLine(3, %)
  |> yLine({length: -armThick, tag: 'otherLine'}, %)
  |> constrainPreviousLine({refernceTag: 'myLine', toY: 2 - segEndY('otherLine', %)}, %)
  |> xLine(-8.08, %)
  |> close(%)
show(part001)

Very hypothetical, but we might be able to generate that code from clicks, including selecting the yLine segment after it's fully constrained and set where its endpoint is.

Part5: Am I straw-manning the 2d solver approach?

I sure hope not, an API or code friendly approach to 2d-solvers has been something I've been thinking about and had on the back burner for years (for the morbidly curious there's also this and this. And my move away from 2d-solver approach was born out of the fact that I never figured out an elegant and expressive API, and the conclusion that they are a good solution for the black-box nature of normal CAD GUI, but not a good solution for the more explicit paradigm that we're pursuing.

Another encouraging fact that makes me think that it's not just my failures to come up with an expressive API, is that since then CadQuery has released a 2d-constraint solver in their library and it's pretty much the same, and just as messy, segments added first with names, then a serious of constraints added after, and it's just as hard to read. The following example is from their docs and it's as simple as they could make it, only two segments, and yet the code is verbose and hard to follow. No shade here, I think they're simply bumping into the same problem.

image

Having said all of that, we have lots of smart people here, if you have a good idea for an expressive constraint API, let's hear it!

Part6: Summary

When it comes to the UI, I by far have the most context on where things are currently at and alternate approaches, so the aim here was mostly to try and share as much of that context as possible so we can have a productive conversation. On the issue of 2d-solver vs not constraints, it's already probably pretty clear that I don't think teaching users our sketching mental model and some best practices is too onerous and the benefit of getting much more readable and deterministic code makes it worth it.

Hopefully, I've given enough context for us to figure it out together.

Part7 Appendix: Is there going to be other contentious APIs going forward?

Our aim with the UI is to meet MEs where they're at, and right out the gate you're proposing something that departs somewhat from that.

I actually don't think there's risk of this happening again, sketches have a very unique place in CAD software, and the problem with solvers I'd mostly put down allowing arbitrary references (backwards and forwards). I don't think we'll have this problem again.

Lengths and angles should be set with |abs| values

Screenshare.-.2023-04-02.4_48_24.PM.mp4
Transcript

0:00
Good improvement here to adding dimensions or constraints that were suggested by Josh.

0:04
And that's effectively, that users are mostly are really only concerned with the absolute value of dimensions when they're adding them.

0:10
However, that I don't care whether it's positive or negative, but positive and negative numbers are actually very important to the U I because it normally denotes a direction.

0:17
So for example, this here is a negative number that's because like from its tail to the head, it's going down.

0:25
So it would be negative.

0:26
But if you're, if you're so it would be positive if I drag it up here right now.

0:30
It's now 0.8.

0:33
And, and the part of the reason why users aren't concerned is especially this sketch, you could be looking at it where it was rotated 108°. So up would be down and you're just trying to slap in a number.

0:44
So previously, so if I go set length here, so it's, it's minus 0.69.

0:51
So when you, when, when this U I prompts you for a value, it used to give you the like what it currently is, right?

0:58
And now it does.

0:59
So, but it gives you the absolute value while still donating what it, what it originally was.

1:03
So if I go ahead and let I want to change this to arm, then it's actually negative thick in terms of we want it to go down, but the user doesn't have to concern themselves with that.

1:12
However, if you, if they do want to, you know, you can still, you can toggle this and then it will make it positive or the same thing will happen if you type in.

1:23
like if I were to put in negative arm, it would like give it, it would just make it positive, right?

1:28
So, yeah, I kind of the way I'm demoing this is like, I've got all of my broad big dimensions that I'm trying to constrain to all in positive numbers.

1:37
But because the sketch, I roughly dragged everything to the right place, then I should just be able to whack in the absolute number and the, the the, the rest of the sketch will figure it out.

1:47
So for example, if I'm trying to set the parallel distance between to parallel lines, I want them to be arm thick.

1:52
It's actually a negative number, but I don't need to worry about that because it's already sort of guessed what it should be.

1:56
If I wanna get this down to the ax access, I can set the horizontal distance to zero.

2:02
I do need like a like a horizontal constraint, but for now I'm just changing things to zero.

2:08
That is fine.

2:10
Now I want this to be have a certain vertical distance.

2:13
It's gonna be the total height minus the arm thick against negative number because it's going down or something's happened here.

2:23
Arm thick.

2:23
Thank you very much and then this needs to be have a zero horizontal distance.

2:30
Thank you very much.

2:32
This needs to be same as this actually.

2:36
So I'll just go equal length.

2:40
OK.

2:41
Here's an interesting case where I actually need this to be like it should kind of be over here and that will give it a negative offset, but I'll just change it here in case like if I kind of knew what was going on, I could select this and this and I could see that this positive would need to be negative.

2:55
So I'm gonna make this arm thick but just flip it quickly and then I'll get it in the right place.

3:00
But otherwise I could have dragged it over beforehand and it would have guessed where it needs to be.

3:04
This needs to be set to negative 0.7, but it's already, it's already got the negative there for me.

3:11
And finally, I'm just gonna make this zero to kind of do that final constraint and we should be good.

3:18
Yeah, I hope that, hope that all makes sense.

3:21
nice little improvement.

Add "remove constraining values" UI

Screenshare.-.2023-03-22.2_47_05.AM.mp4
Transcipt

0:00
It should be a pretty quick demo.

0:02
So with the ability to add constraints at some point, it's going to be convenient to be able to remove them or annoying, not to be able to remove them easily.

0:11
So Reuben constraints just basically comes down to removing any variable uses or any kind of like anything that's not a literal in a in one of these line function calls.

0:22
So as an example, like let's say we have this line and we can train it to be horizontal, I should show what actually happened there just to be clear.

0:34
So this is just like a line call with two literal values and if I go horizontal and it becomes an X line, so it's now constrained to only be horizontal because there is no Y component whatsoever.

0:46
Now, if I change my mind, I want to drag this somewhere else, it's not constrained.

0:50
I can just hit this removed, constraining value and change it back to a line with hard coded values.

0:55
And so I can drag it now and maybe I in fact wanted it to be vertical and that can be done.

1:02
So that's it in a nutshell.

1:05
And I guess like the extreme example is this, so this is a fully constrained sketch by the green.

1:11
There's like variables used in like every part of these lines or variables or equations, which is how it interprets it.

1:18
This is a constraint.

1:22
So the extreme example is if I go through and just select every single one of these lines, yeah.

1:38
And then when we hit remove constraining values, you should see them all collapse into just simple, like calls.

1:44
But the actual actual sketch doesn't change.

1:47
It's just that now I can come and edit these when, before you know, these, I could not like it wouldn't let me drag any of these anyway.

1:58
Cool.

set angle between two selected lines

Screenshare.-.2023-04-06.11_17_41.AM.mp4
Transcript

0:00
Something that I had actually forgot to share is angles between lines.

0:04
So previously, you could select a line and set the angle and that was sort of ab absolutely relative to the X axis.

0:11
But you could also, then I added the ability to select the different axis and define it that way.

0:17
But now there's the ability to do it between two lines.

0:21
So if I if I set that to, I'll set it to 90.

0:26
So it's easy to spot.

0:30
My, So yeah, that looks like 90.

0:35
If I change like the, the angle of the previous line, we should still change it to 45, it should be nice and obvious.

0:41
Yeah.

0:44
But one thing in my, you know, campaign against magic of note here is if we actually have a look at this line, you'll see that my A is here and add at the end.

0:53
But what it's done is it's referenced the first angle.

0:56
So this line here, Then it's added 180 and then, and then finally negative my a.

1:02
So just to break that down, if we were to take this angle here.

1:08
So if we start with the X axis, then this angle is it's like gonna be something over 200 because the axis has to loop all the way down to here because like it's, it's in this direction, it's not just a small angle here.

1:23
So something over 200.

1:24
So plus 1 80 is gonna flip it up so that it is pointing in this direction And then the angle down to this line is, is the 90° we're after, but it's negative 90 because this is a clockwise direction.

1:39
So yeah, it, it, it, it's given it in, in terms of its smallest angle.

1:44
but it's just added the correct math to make that work.

1:48
But as far as the like when the user adding it, they only need to worry about that smallest angle 90 degrees or, you know, Whatever you want me, it changes to seven game.

1:58
Yeah.

Add constrait-like features

Summary

-- edit: I've done a much bigger break down on my spicy take vs more traditional constraint systems here

My spicy take is that 2d constraints are an anti-pattern for a code-code environment, that they only make sense in the black-box nature of existing packages. While we could do a direct conversion of constraint-type features and come up with an api/code approach for them I think they will at best be awkward because they necessitate writing code for lines with no specifics, and then adding constraints after. Which does not read well. I give a bit more of an example in this post (nitty gritty). Further on a more conceptual level 2d constraints systems are mathematical problems, where a number of formulas/relations need to be solved simultaneously. On a philosophical level, this seems way more complex than it needs to be if we have access to variables, math expressions and various helpers to just define where each part of the 2d sketch should be.

Even with the above I probably haven't expressed very well what I intend. I think it will be best to show the workflow, but I'm not 100% sure myself so consider this a spike. Where things are going to be interesting is trying to get the workflow working with code, that's also UI direct-manipulation compatible.

For completeness here's a list of common constraints available in CAD packages: Coincident, Collinear, Concentric, Midpoint, Parallel, Perpendicular, Horizontal, Vertical, Tangent, Equal, Symmetry, Users can also specify dimensions like length, angle which are really just another type of constraint.

Some of these constraints apply to curves (like tangent) and others I suspect might become entirely redundant in a code-cad environment, we'll see, but for now, I think it makes sense to focus on a handful. I'll start with equal, length, parallel and angle and reassess afterwards.

Adding dimensions constraints to single lines

Make horizontal/vertical

Some constraints can be handled directly by helper functions

const part001 = startSketchAt([0, 0])
  |> rx(90, %)
  |> lineTo([1, 1], %)
  |> lineTo([1.4, 2], %) // selected
  |> extrude(4, %)
show(part001)

becomes

const part001 = startSketchAt([0, 0])
  |> rx(90, %)
  |> lineTo([1, 1], %)
  |> yLineTo(2, %) // selected
  |> extrude(4, %)
show(part001)

The same would be true for any constraints to apply to a single line, specifying the angle of the line, or it's height, we can just swap out the line function for one that's better suited.

Update angle of line that already has a variable

|> line([1.4, someVar], %) // selected

Lets say the user wants to specify the angle of this line, but they already have used a variable for the Y value. This seems a little trickier, but we can handle this with a helper function, change it to.

|> angledLineOfYLength([myAngle, someVar],%)

Update length of line that already has a variable

What if instead, they wanted to specify the length (myLength) of the line? that's a little trickier so I think the best approach is this

|> line([legLen(myLength, someVar), someVar], %)

Here legLegth is just calculating the remaining length of the right-angle triangle sqrt(legLength^2 - someVar^2), using a little helper function in place of the x value here I think is appropriate, rather than another unique line function (like lengthAndXLine([myLength, someVar], %)). That's because the LegLength function is always going to return a positive value because of the squaring involved, so IMO the helper function legLen makes things seem less indeterminate and gives the user more control. If they want the line to slope the other way (i.e negative x value) they can instead:

|> line([-legLen(myLength, someVar), someVar], %)

Adding constraints between multiple lines

Make equal length

const part001 = startSketchAt([0, 0])
  |> rx(90, %)
  |> lineTo([1, 1], %)
  |> lineTo([-1.4, 1.73], %) // selected
  |> lineTo([-3.14, -3.04], %)
  |> lineTo([-3.95, -10.15], %) // selected
  |> extrude(4, %)
show(part001)

Option 1)

const length01 = 5
const part001 = startSketchAt([0, 0])
  |> rx(90, %)
  |> lineTo([1, 1], %)
  |> angledLine([5, length01], %) // selected
  |> lineTo([-3.14, -3.04], %)
  |> angledLine([5, length01], %) // selected
  |> extrude(4, %)
show(part001)
Option 2)
const part001 = startSketchAt([0, 0])
  |> rx(90, %)
  |> lineTo([1, 1], %)
  |> lineTo({ to: [-1.4, 1.73], tag: 'seg01'}, %) // selected
  |> lineTo([-3.14, -3.04], %)
  |> angledLine([5, segLen('seg01', %)], %) // selected
  |> extrude(4, %)
show(part001)

I'm leaning towards option 2 as it requires less AST modification, and perhaps more importantly less AST querying. If the first line already was instead |> lineTo([-1.4, someVar], %) // selected We couldn't so easily change it to something like angledLine so that it can be given a length, instead we'd need to query that this was the case and do something else (not sure what though). However with option two, it doesn't matter how the first line is defined, the function segLen can handle whatever it's length is. This will most probably become more important as the user has added more and more "constraints", as there will be less hardcoded/literal values (which are values we can safely modify)

A possible downside to option 2 though is that it's strictly forward-flowing constraints, in that the length of the second line is tied to the first, not equally tied together. There are likely situations where it's desirable to be able to tie backwards (first line length is tied to the length of the second), which I'd like to explore how that can be solved, but in general, because option 2 seems to offer more robust AST modification, it's a clear winner as that's our north star.

Make equal selecting 3 lines

const part001 = startSketchAt([0, 0])
  |> rx(90, %)
  |> lineTo([1, 1], %) // selected
  |> lineTo([-1.4, 1.73], %)
  |> lineTo([-3.14, -3.04], %) // selected
  |> lineTo([-3.95, -10.15], %) // selected
  |> extrude(4, %)
show(part001)

Nothing fundamentally changes in this case

const part001 = startSketchAt([0, 0])
  |> rx(90, %)
  |> lineTo({to: [1, 1], tag: 'seg01'}, %) // selected
  |> lineTo([-1.4, 1.73], %)
  |> angledLine([5, segLen('seg01', %)], %) // selected
  |> angledLine([5, segLen('seg01', %)], %) // selected
  |> extrude(4, %)
show(part001)

Make parallel

  |> lineTo([1, 1], %) // selected
  |> lineTo([-1.4, 1.73], %)
  |> lineTo([-2, -1], %) // selected

becomes

  |> lineTo({to: [1, 1], tag: 'seg01'}, %) // selected
  |> lineTo([-1.4, 1.73], %)
  |> angledLine([segAng('seg01') + 180, 2.2], %) // selected

One interesting wrinkle here is that for any given line there are two angles that are parallel, the angle, or the angle -+180 degrees. This is abstracted away in most packages, but we need to be exacting using code, so in the above, the first line [1,1] would have an angle of 45°, and the second line [-2, -1] has an angle of ~206°, which is closer to 225° (180+45) than it is to 45°, so in order to snap the line to parallel, while moving the line as little as possible segAng('seg01') + 180 is used. Alternatively something like segAng('seg01', true) could be used where the second argument is used to toggle 180°, but that smells a little to me.

I'll try and implement some of the above now. Definitely more that could be speculated on, but I think I'll learn more in implementation at this point.

logging - separate logging from our internal that customers can view and use

right now there isnt really a 'logging' system except either through print statements or passing an error string from an endpoint back to a user

@iterion - this would probably be useful from a user-side debugging perspective, but is it possible for me to add a logger in the engine but have it be accessible to a user on a get request?

thought it'd be useful for what @Irev-Dev 's building alongside any other tool users create that will need the websockets

or is that already handled in our cloud setup (ie users can already see the system prints/error log out there that include the engine errors - not just a returned 'this is the error it failed on)

Add Angle & Length constraint/value, and modal for the details

The details being, when adding angle, if the angle should be defined by a hardcoded value, a variable, or same kind of binary expression/math expression.

Also if the user wants to create a new variable above the sketch or not.

Screenshare.-.2023-03-09.5_03_39.PM.mp4

alignVertically/Horizontally

Screenshare.-.2023-04-06.11_11_18.AM.mp4
Trascript

0:00
So previously, if I wanted to take this line and align it, so that, you know, the end of the fork matched up, I'd have to select this line here and this here, then I could hit set set horizontal distance and just set that to zero.

0:14
And I'd get that effect.

0:15
But now there's a one click button for that line ends vertically and that will step it to that.

0:21
And we got the same thing if you're trying to do it like zero things out to an axis, same thing.

Give `startSketchAt` a 3D artifact

Up till now, the startSketchAt function didn't produce a 3d artifact, now it creates a sphere. Having something in the scene to "select" is important for adding constraints

Screen.Recording.2023-03-16.at.4.13.18.pm.mp4

Add parallel helper (using new helper lib)

Add helper lib and add angledLineThatIntersects fn to lang std

angledLineThatIntersects is an important helper for doing parallel lines in a sketch. It's easy to make two lines parallel, as you just make sure they have the same angle, but it's more difficult to be able to specify the distance between two parallel lines with current helpers. The angledLineThatIntersects helper determines when one line intersects with an existing line, but it also has an offset property that is the perpendicular distance from the line being intersected. Here's an example

image

in the above the thickness var is used three times, twice as the offset of angledLineThatIntersects, in order to ensure the arm has the same width after a few direction changes.

code for good measure: paste into https://untitled-app.kittycad.io/ and use the hover over features to see how this plays together

const thickness = 1
const firstAng = 200
const secondAng = 45
const part001 = startSketchAt([0, 0])
  |> angledLine({ angle: firstAng, length: 8, tag: 'first' }, %)
  |> angledLineToY({ angle: secondAng, to: 5, tag: "second" }, %)
  |> xLine(5, %)
  |> yLine(thickness, %)
  |> angledLineThatIntersects({
  angle: 0,
  intersectTag: 'second',
  offset: thickness
}, %)
  |> angledLineThatIntersects({
  angle: secondAng,
  intersectTag: 'first',
  offset: thickness
}, %)
  |> angledLineToY([firstAng, 0], %)
  
show(part001)

Modelling api // UX proposal/discussion

Edit: looks like we're going to stream the UI I thought this might have a large impact on the proposal here, but because these hyperthical websocket messages are what sent when the code is executing it really doesn't change much besides the final message which was the server responding with a mesh, which isn't needed anymore.

Aside from the messages that are sent when the code are executing, the UI will also have to let the server know when it should be in different UI modes, that will change look of the scene, for example sketch mode might put a grid on the sketch plan, and maybe something happens in the UI when different brushes are selected as Hannah has mentioned.


Proposal for WebSocket interactions between client and server.

Websockets because from some of @iterion's points in this issue. websockets are going to be far easier to get up and running with a persistent connection to GPU, maybe we can RESTify it later?

I had an idea of splitting commands from the client from into mutation and query categories, and that's what I've gone with. The idea is the client can make multiple mutations one after the other without needing a response from the server. The client provides a uuid (I made them human readable for the sake of the example) with each command, that way it can refer to previous steps when mutating or querying later and those Ids will also have an effect on the final export. The queries are made when the client wants some information back on the part/scene so far before making further mutations, the example below is getting the area of a sketch to determine how much to extrude to match a specific volume.

Here the example

  const hypotheticalWebsocketMessages = [
    /* --- CLIENT sends --- */
    // sends a sketch, made up of a nummber of segments (lines, arcs, splines etc)
    // than asks what the area of the face is, and waits for a response before sending more mutations
    {
      mode: 'mutation',
      name: 'createClosedSketch',
      uuid: '2d-uuid-1',
      segments: [
        {
          type: 'line',
          start: [0, 0],
          end: [1, 0],
          uuid: '...',
        },
        {
          type: 'arc',
          start: [1, 0],
          end: [1, 1],
          center: [0.5, 0.5],
          isCW: true,
          uuid: '...',
        },
        {
          type: 'line',
          start: [1, 1],
          end: [0, 0],
          uuid: 'interested-in-face-created-from-extrusion',
        }
      ]
    },
    {
      mode: 'query',
      name: 'getFaceArea',
      faceId: '2d-uuid-1',
      uuid: 'query-uuid-1'
    },
  
  
    /* --- SERVER sends --- */
    // replies with area
    {
      responseTo: 'query-uuid-1',
      type: 'area',
      value: '2'
    },
  
    /* --- CLIENT sends --- */
    // client has the area needed to continue, fires off a few more mutations
    // extrudes the existing sketch, creates a new sktech
    // and asks where a specific face is of the first extrusion is using the uuid of that sketch segment
    {
      mode: 'mutation',
      name: 'extrude',
      sketchId: '2d-uuid-1',
      uuid: 'extrude-uuid-1',
      distance: '3' // aiming for volume of 6 units cubed
    },
    {
      mode: 'mutation',
      nome: 'createClosedSketch',
      uuid: '2d-uuid-2',
      segments: [
        {
          type: 'line',
          start: [0, 0],
          end: [1, 0],
          uuid: 'want-to-fillet-this-edge-later'
        },
        / * more similar to the above * /
      ]
    },
    {
      mode: 'query',
      name: 'getTransformOfExtrudeFace',
      sketchId: '2d-uuid-1', // the first sketch
      segmentId: 'interested-in-face-created-from-extrusion',
      uuid: 'query-uuid-2'
    },
  
    /* --- SERVER sends --- */
    // replies with transform of the face
    {
      responseTo: 'query-uuid-2',
      type: 'transform',
      position: [0, 0, 3],
      quaternion: [0, 0, 0, 1],
    },
  
    /* --- CLIENT sends --- */
    // finishes up the part before finally asking for an export of the UI scene
    //
    // - transforms the second sketch to the position of one of the first extrusion faces, then extrudes it
    // - unions the two extrusions, and fillets the edge of the second extrusion, though I don't know if we need a step like this 🤷
    // - fillets a specific using uuids of previous mutations
    // - asks for a scene export
    {
      mode: 'mutation',
      name: 'transform',
      sketchId: '2d-uuid-2',
      transform: {
        position: [0, 0, 3],
        quaternion: [0, 0, 0, 1],
      }
    },
    {
      mode: 'mutation',
      name: 'extrude',
      sketchId: '2d-uuid-2',
      uuid: 'extrude-uuid-2',
      distance: '2',
    },
    {
      mode: 'mutation',
      name: 'union3D',
      parts: [
        'extrude-uuid-1',
        'extrude-uuid-2',
      ],
      uuid: 'union-uuid-1'
    },
    {
      mode: 'mutation',
      name: 'fillet-extrusion-top-edge',
      sketchId: '2d-uuid-2',
      segmentId: 'want-to-fillet-this-edge-later',
      uuid: 'fillet-uuid-1',
      radius: '0.1',
    },
    {
      // this tell the server that everything executed correctly and the stream UI can now update
      mode: 'query',
      name: 'uiSceneExport',
      uuid: 'query-uuid-3'
    },
  ]

That is very 2d/flat-land centric, is easiest for me to think that way, but since we have some mesh primitives, here's something for that, I still think we need access to individual faces etc in order to do more operations on them, indexes is probably fine so long as they're consistent. Here's an example:

const hypotheticalMessagesWith3dPrimitives = [
  /* --- CLIENT sends --- */
  {
    mode: 'mutation',
    name: 'createDocohedron',
    uuid: '3d-uuid-1',
    radius: '1',
  },
  {
    mode: 'mutation',
    name: 'filletEdgePrimitiveFaceIndices',
    primitiveId: '3d-uuid-1',
    faceA: '5', // specific face indices for a and b
    faceB: '7',
    uuid: 'fillet-uuid-1',
  },
  {
    mode: 'query',
    name: 'uiSceneExport',
    uuid: 'query-uuid-1'
  },
]

colour head of lines (the sphere) with the same constraint colours

Changing the shape of the head of lines from a sphere to an arrow to denote the direction of the line. also making sure the color of the head matches the constraint color of the line to make it clearer how it fits togethr.

left is old, right is new
image

The direction of the line is an important part of the mental model of how these constraints work, so communicating this clearly is important (even though this 3d scene will be discarded when it's connected to the real engine)

Select axis for horz/vert distances

Screenshare.-.2023-04-05.2_56_34.PM.mp4
Transcript

0:00
A should be a quick one.

0:02
So previously, when you wanted to set the horizontal vertical distance, you'd have to select two parts of the sketch.

0:07
So for example, if I select this starting point and this line here, I can now set the vertical distance.

0:13
So let's say I want that to be two of this total half height, I can do so to get set.

0:19
But if you just want to set something in like an absolute value or in relation to one of the axis, you couldn't do that.

0:25
So now you can, so by selecting the line and the axis there is like for example, y absolute and I can set that to maybe this minus this and happy days it works.

0:37
Now it's a little bit Janky, let me just undo that.

0:40
I don't intend it to long term, be a different button like this really should be the same as like set vertical distance, but it's just kind of easier to throw it in a new button and get the functionality there and we can combine them quite easily soon.

Add multi cursor support

Because "selecting" a feature is done by putting the user's cursor on that part of the script, in order to allow selecting multiple features at one, multi-cursor is needed.

Clean up "prompt user for info" modals

There's two of them now, with lots of duplicated code that can be cleaned up.

The setHorizontal/VerticalDistance modal
image

And the setAngle/seLength modals
image

Are different (most notably because the mostHorzVert modal needs to reference a previous line), but should be able to share a lot of code.

--currentq

Bot details
  • Adding --currentq or --yyyyqx (i.e. --2035q3) to your issue body when creating it will add the issue to the correct quarter-project (if it exists).
  • Bot makes sure that all issues are either in KittyCode or another project (adding it to KittyCode while it's already in other projects will remove all of those projects, or adding it to another project while it's in KittyCode will remove KittyCode).
  • Bot comments any changes to projects on issues for tracability.

Add equalAngle constraint

Screenshare.-.2023-03-17.9_06_21.PM.mp4
Transcript

0:00
Hey, so something I've been working on is an equal angle constraint.

0:05
So these two lines here, if I just select them both, you know what I'm talking about?

0:09
If you, if you, if they use one of those at the same angle, they can hit this after they select them equal angle button and it will constrain the first one to the same angle as the second as the second to the same angle as the first.

0:22
And it does that by referencing the first segment and making the angle the same.

0:27
However, one little edge case I wanted to cover is is Really respecting the user's intention.

0:36
So if I undo that change and I drag this down here, these angles, these lines now still look like they have a similar angle, but in reality, they're out of their out by 180°. And it's because the lines, everything here is very explicit in the lines here are not just like the simple angle of like the smallest angle.

0:55
It's, it's really like it's it's direction in terms of having like a head and a tail.

0:59
And so in this case, if if they hit equal angle, we kind of respect that they probably meant like them to be parallel, but not necessarily, maybe I should change the constraint to parallel.

1:10
But what actually happens to make that work in this case is it references that same angle again?

1:16
But then it adds 100 and 80 because I guess it more or less snapped to the nearest 180 degrees because it's assuming that's what the user intended if somehow they didn't, of course, they can just come along here and Get rid of that.

1:27
But by default, it will snap to the nearest 180.

Adding in non-code tokens back into recasted code

Currently because whitespace or anything that's not needed for execution is not stored in the AST, it's hard to respect things like user formatting when recasting.

I think having a by-default-opinioned formatter is a good thing, but where this becomes problematic is when users wants to simply leave a blank space between some lines for a bit of breathing room, a code paragraph if you will, but maybe more importantly comments have not been implemented for the same reason, there wasn't a way with the current setup to insert them back in.

In some ways the most straightforward way to do this is to put whitespace and comments into the AST. Even though they are not crucial for execution, code-gen/recasting needs to be a first-class citizen in this lang so that's probably the long-term solution. However I'm trying to draw inspiration from other languages, and since it's not the norm to put comments et-al into the AST I haven't done so.

Because whitespace is tokenised already if not transformed into the AST, there is somewhat of a map of these things without going back to source code, so atm I'm experimenting with using this to insert extra linebreaks and comments back in between statements. I think this is a good compromise for the time being for what is a nice to have feature atm.

Because it's only going to respect non-code parts in between statements this will mean that you can't format objects or function params how you like (but I think this is good to have an opinioned fmt out of the box) and comments like myFunctionCall('a', /* inline comment */ b) will not work either.

--currentq

Bot details
  • Adding --currentq or --yyyyqx (i.e. --2035q3) to your issue body when creating it will add the issue to the correct quarter-project (if it exists).
  • Bot makes sure that all issues are either in KittyCode or another project (adding it to KittyCode while it's already in other projects will remove all of those projects, or adding it to another project while it's in KittyCode will remove KittyCode).
  • Bot comments any changes to projects on issues for tracability.

Grid axis should be able to be selected for angles

Screenshare.-.2023-04-03.8_28_42.PM.mp4
Transcript

0:00
So I've recently added the ability to select axis in sketches.

0:04
So that's like these guys here.

0:07
Now, these are going to be very useful for things like the distance and select vertical distance.

0:13
But for now, it's just for angles.

0:15
And the basic use case that I've got going is it allows you to select an axis and therefore define an angle in terms of the smallest angle.

0:24
So this very basic example here, if I, by default, if I hit set angle, it's always based off the X axis and it's always like always from this like the line going out to the right going counterclockwise or up to 100 and 80 and then you get start getting negative numbers.

0:47
So if I drag this out here, you're gonna get quite a large number of sonic approaching 100 and 80 degrees.

0:52
But that's kind of unintuitive.

0:53
People do don't usually think of angles in that way.

0:56
So selecting the line and an AIS allows us to find it in the small term, the small.

1:02
So that's 12°. It's actually negative 12.

1:05
But that's simply because if you go from the axis to the line, it's in a negative direction.

1:10
But I can do the same thing, but you can see it's, it's it's not hiding any magic behind it.

1:15
It's not like referencing some access, it's just adding a quick and dirty 180 degrees plus 12.

1:23
So if I do a similar thing for the Y axis, this here by default, using the old method is going to be something around 100 100 and 15, maybe degrees.

1:34
But if I want it just, in terms of the small angle coming off the Y axis, I can hit this, this time, it will be positive because it's going in the 10 wise direction I can hit that and it's just giving me sort of 19 degrees between these two.

1:48
Yes.

Add constraint colour indications

Screenshare.-.2023-03-19.9_08_51.PM.mp4
Transcript

0:01
Hey, so one thing that I've always thought was very important since starting, this is a connection between the code and the three D scene part of the UI and not just like it executes and produces it, but in a very sort of perceived way, it very much fits into the treating the code as a source of truth mentality.

0:22
And the main way that's been achieved so far in terms of like the tangible feel is that when you select any part of the code, it highlights the equivalent line in the three things and then that goes the other way as well.

0:34
When you highlight over line, it will highlight that in the code.

0:38
And if you, if you click, you'll also like add your cursor to the correct place.

0:42
And that's great.

0:42
But I've been trying to think about how that extends into some of the constraint type stuff that I've been working on.

0:50
So at the moment, you can see there's always, it's all yellow lines with the blue for the line that where my cursor is, right.

0:57
But if I go into the edit sketch kind of mode, then the lines change the lines the colors look horrible.

1:03
These aren't like the final color and I'll leave that up to Frank most likely, but they do mean something.

1:09
So the original yellow is still still there has been like what would be considered like a free line is not constrained.

1:15
All the magenta, very sort of brownie color is partially constrained and green is fully constrained.

1:21
And the way that where that actually affects how you interact with the the sketches that these green lines are fully constrained.

1:29
So if I try and drag any of these, they're not gonna, they're not gonna go anywhere.

1:33
The yellow ones with the free ones I can drag anywhere and they should, you know, match exactly where my, where my cursor ends up and then the partially constrained ones means that they'll, they'll move somewhat, but they're not going to follow me all the way they'll be constrained in some manner.

1:50
So they'll try and try and try and move to where my cursor is but not quite and so ideally.

1:56
So I think this is a really great communication tool in terms of, you know, what's constrained.

2:00
If you're trying to fully constrain your sketch, say you can see just from a quick glance what needs work ideally, that would flow over to the editor as well.

2:10
So probably not super soon, but it is a to do that when you're in sketch mode, we can highlight these lines as different colors as well.

2:20
Yeah.

Figuring out MVP

Things to implement:

Original description below

What language is the intpreter written in?

Continue with TS for quicker iteration now (what Kurt knows best), or rust as better long term preformant option?
My gut feel is optimizing for speed of development to get it in front of people sooner means keeping it in TS for now. If it takes off, is getting lots of use and we want to refactor for performance, that's a good problem to have. We'll have lots of tests for the intepreter so doing a 1:1 port that we're confident in is do-able.

[edit]: rust migration is somewhat enable see: #27

What are we looking to achieve this Q?

Hannah wrote "ast interaction thing", I think it's pretty reasonable to focus getting execution to tie in making api calls to our engine. Should also illuminate the problem to "GPU sessions", though probably need to still do some UI work, as in the lang needs to stay connected to the UI.
Would be good to have something a little more concrete to aim towards.

UI tech?

The demo was built with as a react app, I think in 2023 we'd want this thing to run in a browser, but maybe the main path we guide people towards is a desktop app (https://tauri.app/?) since that's what mechies are used to. Though maybe it would be best start with a web app since we're only letting a small group of beta testers in, and that will be easy to update.

What's is the 3d viewer going to be? stick with a three.js scene? That means we need mesh artifacts, so probably not a long term option, though I'm not sure what the long term option would be? like a live render stream from the cloud?

What we are NOT doing for the mvp

  • modules that both
    • multiple file stuff (importing from code from seperate files)
    • or full blown dependencies and a crates.io style hub for them
  • IDE niceties, syntax highlighting, auto complete etc (if its low effort sure, rest is the tech we're trying to prove and should get priority)

What language features will we add

Generally it will lean towards the functional paradigm as removing state and mutability is going to make codegen easier. Here's a non-complete list of things I think should be in the language

  • variable declartions with the common stuff: numbers, string, arrays, hashmaps
  • function calls (at the very least with functions we provide)
  • Very much want to enable branching logic (if statements) and function declaration, but want to confirum how they work with code-gen/ast-modification
  • If the above is all good I've want to looping functionality, but probably in the form of map and other similar functions instead of for(...) syntax
['a','b','c']
    |> map(item => log(item), %)
  • I would love to have types with inference in the language as well, but it's probably not a huge priority

Use shift instead of capslock for multiple line selection

Hard to describe in words, so the video might be best, since I can click around and explain, but here's an attempt at text.

Holding shift while selecting multiple lines is most intuitive for multiple-select, however because shift has existing functionality in the editor, capslock was used as a compromise.

Turns out the issue stems for a previous fix, when the user selects a line naturally they are not focused on the editor anymore, and by default code-mirror stops showing the cursor when the editor is not in focus. So when we update their cursor in the editor (because that's how selections work in the UI) from a click on a 3d line, we update the cursor position in the editor, but also focus the editor manually so that the cursor shows up, and now that the editor is focused, if the user is holding shift when we update the cursor position again, it selects a whole bunch of code.

So the fix was to stop focusing on the editor, and overriding some of the code-mirror styles so that the cursor is still visible. Note that it does stop blinking, but I think that's okay, maybe even desirable.

Looks like this.

Screen.Recording.2023-03-15.at.6.38.37.pm.mp4

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.