Welcome to the Onshape forum! Ask questions and join in the discussions about everything Onshape.
First time visiting? Here are some places to start:- Looking for a certain topic? Check out the categories filter or use Search (upper right).
- Need support? Ask a question to our Community Support category.
- Please submit support tickets for bugs but you can request improvements in the Product Feedback category.
- Be respectful, on topic and if you see a problem, Flag it.
If you would like to contact our Community Manager personally, feel free to send a private message or an email.
Help getting started with FS?
eric_schimelpfenig
Member Posts: 75 EDU
Hey everyone, I was wondering if I could get some help in writing my first FS.
I did the tutorial, and I watched @NeilCooke s webinar which was actually really helpful in getting my head somewhat around the context of programming FS.
For some background on me: I'm not a programmer but I am very well versed in modeling stuff in Onshape. I'd like to learn to automate a lot of the tedious tasks I do in Onshape. A lot of this stuff is furniture/CNC joinery.
@Aaron_Hoover had written this for me: https://cad.onshape.com/documents/3926df9f5f87e416bc920f0b/w/cac04c63becd1cb085f1f257/e/35e2df86eaf9a664427633a8 a while back. It's for these furniture connectors I use a lot. I wanted to tweak the code a little, and I realized that rebuilding it might be a really good opportunity to learn how to make a FS. Just hacking apart that code seems like not the best way to learn..
Here's what I understand:
I can build a UI, so I can ask the user for the relevant inputs (edges, faces, bodies, etc)
https://cad.onshape.com/documents/c8b9ff43446d2e7e88132990/w/407c5bd44194c6da3616a4d2/e/a66b1ccb7069351a04ea59a1
After this I essentially need to take that data and automate the tasks I need to do, which really just amounts to drilling a hole in each panel.
From what I can see I need to somehow use the user selected edge to query a face, and then somehow locate and drill a hole on that face. Then, query a face on the adjacent part and drill a hole there.
Assuming that my plan of attack is right, this is what I was trying (and failing) to do:
Query a face that's attached (or "near"?) that user selected line, and then use a debug command to confirm that I in fact had the right face selected.
I'm just not clear how to query a face like this... Again I lack a basic understanding of this sort of coding... Can anyone here guide me on this first step?
I did the tutorial, and I watched @NeilCooke s webinar which was actually really helpful in getting my head somewhat around the context of programming FS.
For some background on me: I'm not a programmer but I am very well versed in modeling stuff in Onshape. I'd like to learn to automate a lot of the tedious tasks I do in Onshape. A lot of this stuff is furniture/CNC joinery.
@Aaron_Hoover had written this for me: https://cad.onshape.com/documents/3926df9f5f87e416bc920f0b/w/cac04c63becd1cb085f1f257/e/35e2df86eaf9a664427633a8 a while back. It's for these furniture connectors I use a lot. I wanted to tweak the code a little, and I realized that rebuilding it might be a really good opportunity to learn how to make a FS. Just hacking apart that code seems like not the best way to learn..
Here's what I understand:
I can build a UI, so I can ask the user for the relevant inputs (edges, faces, bodies, etc)
https://cad.onshape.com/documents/c8b9ff43446d2e7e88132990/w/407c5bd44194c6da3616a4d2/e/a66b1ccb7069351a04ea59a1
After this I essentially need to take that data and automate the tasks I need to do, which really just amounts to drilling a hole in each panel.
From what I can see I need to somehow use the user selected edge to query a face, and then somehow locate and drill a hole on that face. Then, query a face on the adjacent part and drill a hole there.
Assuming that my plan of attack is right, this is what I was trying (and failing) to do:
Query a face that's attached (or "near"?) that user selected line, and then use a debug command to confirm that I in fact had the right face selected.
I'm just not clear how to query a face like this... Again I lack a basic understanding of this sort of coding... Can anyone here guide me on this first step?
0
Comments
First, let's talk about the qAdjacent query:
https://cad.onshape.com/FsDoc/library.html#qAdjacent-Query-AdjacencyType-EntityType
The concept of adjacency we are using is "topological adjacency", so rather than adjacency meaning a face is physically close to another face in the world, it means that a face is attached to another face by an edge or a vertex. An edge is adjacent to a face if it is one of the edges bounding that face, or if it shares a vertex with an edge bordering that face. So if the faces you are looking to find are on two different parts, they will never be adjacent, because they do not share edges or vertices.
This will be a useful tool, but I don't think its the first step you want to take.
I am going to outline how I would do what you are asking to do, but it seems like you're interested in the programming aspect, so I'm going to leave out the code snippets. Let me know if you would like more tangible examples and I will post those too!
Looking at Aaron's doc, I think there are two distinct cases you will need to be handling:
In the Left Case, the selected edge does not have a corresponding edge on the other body, in the Right Case, there are two overlapping edges, and it is basically random whether you are going to be selecting the one from the blue body or the grey body. So, given the edge selection, and these two possible cases, what is the goal?
- Find the Major Face (I'm sure theres a better term for it, but this is what I'm going to call the face that gets the big hole drilled into it)
- Find the Minor Face (Again, stupid name, but its the face that gets the small hole)
So, where's a good place to start? Lets find all the faces in play at the joint. Since they are not on the same body, we cannot rely on topological connections, so we're going to have to rely on physical location. We can start by finding a point shared by all the faces: the midpoint of the selected edge. Using evEdgeTangentLine with a "parameter" of 0.5 will get us that point:https://cad.onshape.com/FsDoc/library.html#evEdgeTangentLine-Context-map
Now we have a point, and we want to find all the planar faces touching it. The user has not given us any explicit bodies that are supposed to be cut into, so we'll just have to look globally using qEverything. This is somewhat problematic because its possible that a part studio might contain unrelated geometry passing through this joint that will be picked up accidentally, but for the purpose of a custom feature, we can not worry about it and make the assumption that the part studio is formatted nicely for us. So, the query for all the faces touching that point is:
https://cad.onshape.com/FsDoc/library.html#qEverything-EntityType
https://cad.onshape.com/FsDoc/library.html#qGeometry-Query-GeometryType
https://cad.onshape.com/FsDoc/library.html#qGeometry-Query-GeometryType
Now we have all planar faces in the part studio that touch the edge midpoint. If we call `evaluateQuery` on the above query, we will have them each individually in an array. Here we can notice a difference between the Left Case and the Right Case by noticing the Left case will have found three faces and the Right case will have found 4. It's not going to make a difference to the algorithm going forward, but it's good to make sure we are covering all the cases as we go along.
https://cad.onshape.com/FsDoc/library.html#evaluateQuery-Context-Query
Next, to start narrowing down our faces, lets find the two faces that abut each other. We can find the Plane (the mathematical object describing the actual plane the face represents) of each face by calling evPlane on all of the faces.
https://cad.onshape.com/FsDoc/library.html#evPlane-Context-map
Then, look through the faces and find the two that are opposites:
Once you find those two opposing faces, the Minor Face is the larger of the two. qLargest will query for this.
https://cad.onshape.com/FsDoc/library.html#qLargest-Query
Now, this last one is going to take a bit of a leap, but to get the Major face, I would do the following:
smallerOpposedFace is somewhat self explanatory. Its just the opposed face that we didn't pick as the minor face
https://cad.onshape.com/FsDoc/library.html#qSubtraction-Query-Query
candidateMajor faces is all of the faces surrounding the smallerOpposedFace. AdjacencyType.EDGE means we want all neighbor faces that we can reach by hopping over an edge. The excludes neighbor faces that we can reach by hopping over a vertex, but not by hopping over an edge. EntityType.FACE means we are asking for faces.
https://cad.onshape.com/FsDoc/library.html#qAdjacent-Query-AdjacencyType-EntityType
Finally, we intersect two sets. The 3 or 4 faces we found at the beginning, and all of the faces neighboring the smaller opposed face. These sets share one common face, the Major Face.
https://cad.onshape.com/FsDoc/library.html#qIntersection-array
Feel free to ask more questions, and let me know if you want this written as a block of code. Hope it helps!
I read it over a few times and I hacked around with some code and I think I got it ... a little... But I have questions. You definitely got the use cases right, and your terminology works. I had actually thought a better approach would be to ask a bit more from the user, perhaps have them click on the "major" face, "minor" face, and a vertical line... More on that in a moment...
Ok, it took me a few tries but I think I got this part worked out:
https://cad.onshape.com/documents/c8b9ff43446d2e7e88132990/v/1305c7363ae4f65a960fad03/e/a66b1ccb7069351a04ea59a1
I was able to find that line using the debug command (I changed it to .5 later)
You say:
"Now we have a point, and we want to find all the planar faces touching it. The user has not given us any explicit bodies that are supposed to be cut into, so we'll just have to look globally using qEverything. This is somewhat problematic because its possible that a part studio might contain unrelated geometry passing through this joint that will be picked up accidentally, but for the purpose of a custom feature, we can not worry about it and make the assumption that the part studio is formatted nicely for us. So, the query for all the faces touching that point is:"
Referencing my earlier comment to me looking at "everything" seems hard/potentially problematic? Perhaps it makes more sense just to have the user identify the faces for us? I feel like two clicks isn't too much to ask
But, going along with what you said I did try that next command and I guess I got it to work (insofar as it didn't throw an error) but I'm not sure what I did...
https://cad.onshape.com/documents/c8b9ff43446d2e7e88132990/v/66c7be0d1ea942d447bb1bba/e/a66b1ccb7069351a04ea59a1
This is the command I built: qEverything(qContainsPoint(qGeometry(qEverything(EntityType.FACE), GeometryType.PLANE), superedge).VERTEX);
So if I'm reading this right, is this saying "Look at everything, then find stuff that contains a point, then something that's geometry? then everything that's a face, then everything that's a plane (face?) that's touching "superedge" (that's the line that the user selects)
If my vague understanding is correct, should have have returned those two "major" and "minor" faces that are touching that line? Or since you say 4 faces, are you talking about the top and bottom faces too? I'm not totally sure how you're getting to four...
If I'm right on that, it looks like you want to use the area of each face to determine which is the larger, and smaller one? While in this particular model that would work (again if I'm understanding the logic here) but in the "real world" the larger hole doesn't always go on the larger face...
I'm wondering if this would make a lot more sense and be easier to code:
https://cad.onshape.com/documents/c8b9ff43446d2e7e88132990/v/1d0d4c6d23c11754ba1e77ca/e/a66b1ccb7069351a04ea59a1
Ask the user to identify the face for the large hole (turn lock thinger) and the stud side. I'm also asking for the edge too so the holes can start at the center of the edge they select ( at some point I'd like for the user to be able to use sketch points to place these connectors at explicit places, but I'm trying to keep it simple to start with)
Again, I cannot thank you enough for helping me on these first few steps here!
HWM-Water Ltd
It's actually this system: https://www.hafele.com/us/en/product/connector-housing-rafix-tab-20-system/0000001700027b3a00060023/
Once I get it working I'll get it over to the marketing folks to give it some real branding
1.
Referencing my earlier comment to me looking at "everything" seems hard/potentially problematic? Perhaps it makes more sense just to have the user identify the faces for us? I feel like two clicks isn't too much to ask
[...]
I'm wondering if this would make a lot more sense and be easier to code:
https://cad.onshape.com/documents/c8b9ff43446d2e7e88132990/v/1d0d4c6d23c11754ba1e77ca/e/a66b1ccb7069351a04ea59a1
Ask the user to identify the face for the large hole (turn lock thinger) and the stud side. I'm also asking for the edge too so the holes can start at the center of the edge they select ( at some point I'd like for the user to be able to use sketch points to place these connectors at explicit places, but I'm trying to keep it simple to start with)
2.
But, going along with what you said I did try that next command and I guess I got it to work (insofar as it didn't throw an error) but I'm not sure what I did...
https://cad.onshape.com/documents/c8b9ff43446d2e7e88132990/v/66c7be0d1ea942d447bb1bba/e/a66b1ccb7069351a04ea59a1
This is the command I built: qEverything(qContainsPoint(qGeometry(qEverything(EntityType.FACE), GeometryType.PLANE), superedge).VERTEX);
So if I'm reading this right, is this saying "Look at everything, then find stuff that contains a point, then something that's geometry? then everything that's a face, then everything that's a plane (face?) that's touching "superedge" (that's the line that the user selects)
The feature is red, when you highlight it it says "error regenerating" in the tooltip. You can click on the "FeatureScript console" button in the upper right corner (the {!} button) to reveal the featurescript console, which will reveal that the error is that qContainsPoint wants a Vector as its second parameter, but it is receiving a Line instead. The reason your debug is showing up is that it happens before you get to this failure in the code.
Before we get into fixing that, lets discuss queries a bit more. They can be a bit daunting at first, but they really just act as normal function calls. Let me clarify what this query does:
You have to read a query like this from the inside, out. Just like if I had a function call that said:
First, findTheNumber() is going to be run, and return a number. Then addTwoToTheNumber(...) is going to be run, taking the return value of of findTheNumber() as its input, and returning (if it keeps its promise), two more than that number. Finally println(...) will be run to print it out.
In the same way, the queries are going to be run from the inside out, with each filtering down on what we are looking for. To make it clear though, the one-liner above is just shorthand for:
Most querying in FeatureScript is done like this. Finding some "core" or "seed" query that provides too much geometry (usually a qCreatedBy or qEverything), and then filtering down to what you actually want by whittling down on the query in different ways.
So, back to the point at hand, why is your query failing?
is being evaluated and returning a Query. Because of how types work in FeatureScript, a Query is actually just a map. So when you then say you are asking that map whether it has some field called "VERTEX", which is does not, so it returns `undefined`. So really this whole call is now just saying:
In short, your query will be correct if you rephrase it as:
but also feel free to break it out and go one query at a time if that makes it more readable for you. Reading from the inside-out can be annoying once you start nesting a few deep.
To be continued...
This is getting pretty long so I am going to post it and answer the third question in a follow-up.3.
If my vague understanding is correct, should have have returned those two "major" and "minor" faces that are touching that line? Or since you say 4 faces, are you talking about the top and bottom faces too? I'm not totally sure how you're getting to four...
If I'm right on that, it looks like you want to use the area of each face to determine which is the larger, and smaller one? While in this particular model that would work (again if I'm understanding the logic here) but in the "real world" the larger hole doesn't always go on the larger face...
The raw part studio:
In the Left Case, the user selects this edge:
In the Right Case, the user is selecting either one of these edges, at random, because they are completely overlapping each other:
A better name for these cases would be "Interior Case" and "Exterior Case" respectively. You'll notice that in what I've been calling the Left Case, if I select the exterior edge instead of the interior edge, you could just rotate this whole picture around 180 degrees and call it the right case. So from now on I will refer to it as the Interior and Exterior cases instead.
The Interior Case edge-midpoint and the three faces that midpoint touches:
The Exterior Case edge-midpoint and the four faces that midpoint touches:
The algorithm, then, isn't simply to take the smaller or larger of all of those faces, but rather to find the two faces that are opposed to each other:
And see that the larger of those two faces is the "minor face" i.e. the face that wants the small hole drilled into it.
Then from there, there are a number of ways to get the "major face" i.e. the face that wants the big hole drilled into it. But basically, in both cases its just the face from the original 3 or 4 faces (depending on case) which is on the same part as the smaller "opposing face" (i.e. the "opposing face" that was not picked to be the minor face).
Describing this gave me an idea of how to write a simpler query for the major face than in my original post (based on how I described it above), so here's that query:
https://cad.onshape.com/FsDoc/library.html#qOwnerBody-Query
https://cad.onshape.com/FsDoc/library.html#qOwnedByBody-Query-Query
Note that qOwnerBody is not always a filter, there are other interfaces that can just get you all the entities of a specific part, so that it can be used as a "seed" or "core" query, as discussed earlier:
https://cad.onshape.com/FsDoc/library.html#qOwnedByBody-Query-EntityType
Anyway I think that covers all the questions. Please keep em coming!
I was looking at your examples and they seem to read right to left, not left to right as I was reading them.
I'm wondering if I should just hand draw some super basic stuff like a cube, cylinder, etc and just practice using queries on them so I can really get them down as best as I can before I move on to the next thing.
Maybe could use queries with the visual debugging as sort of "target practice" to see what I could get to work?
For example, make the user select a single body (like a cube) and see if I could target a particular face, edge, point, etc?
And I got this:
This makes sense, I basically said "give me everything" and then "only show me the faces" right?
Let's say I wanted to narrow this down to the faces that are just on the cube, since I had the user pick the cube, How do I:
Say "hey, start with what the user picked"
and then from there how do I say "Ok, the user picked this cube so ignore everything but the faces"
Can/should I build that like I did here as a "var" (variable). I only did that because that's the only way I know how to get it to show up in debug...
is the same as:
Storing the return value of the qEverything(...) call into a variable called "test" just lets you use that name as a placeholder for the value you are trying to get at. So, if you don't want to store a variable, you can just use the query directly as the argument to debug(...).
But, it's not wrong to use var. Your only two choices are "var" and "const", they both allow you to store variables, the "const" ones just can't be changed later. I like to use "const" for everything except things that absolutely must be "var".
I'm going to be pedantic, but just as a way to give out more examples. I would think of qEverything(EntityType.FACE) one thing, rather than two: it's more like "give me all the faces". Something that's more "give me everything", then "filter it down so its only faces" would look like:
I wouldn't ever use the code that's above, because qEverything(EntityType.FACE) is exactly the same and simpler, but the point was just that qEverything(EntityType.FACE) is just one query, doing one thing (not two queries or two steps).Say "hey, start with what the user picked"
and then from there how do I say "Ok, the user picked this cube so ignore everything but the faces"
should do the trick!
A side note on something that I see being confusing. You may be asking, why can I call "qEverything(EntityType.FACE)" but also "qEverything()". Is this some kind of weird FeatureScript syntax magic?? Is it two queries in one? Theres actually nothing special going on here. We just provide two different overloads of the 'qEverything' function, one of which takes an EntityType parameter and one which doesn't.
https://cad.onshape.com/FsDoc/library.html#qEverything-EntityType
https://cad.onshape.com/FsDoc/library.html#qEverything
We do this for lots of our queries that fetch geometry (as opposed to filtering down geometry), since it is a lot easier than having to call qEntityFilter right after, every time.
Another example is qOwnedByBody:
https://cad.onshape.com/FsDoc/library.html#qOwnedByBody-Query-EntityType
https://cad.onshape.com/FsDoc/library.html#qOwnedByBody-Query
And another is qCreatedBy:
https://cad.onshape.com/FsDoc/library.html#qCreatedBy-Id-EntityType
https://cad.onshape.com/FsDoc/library.html#qCreatedBy-Id
Why???
Are we going to be able to inspect local variables while the feature is executing?
IR for AS/NZS 1100
Don't get that excited
Owen S.
HWM-Water Ltd
We do have a separate process for keeping that code versioned with the rest of Onshape and deploying it with the rest of Onshape, so it's actually many more steps for me to temporarily get code into a Feature Studio and copy it back out (vs just editing it in a local text editor). But, when it comes to interactive, fast development of FeatureScript, nothing beats a Feature Studio so that's the route I usually go.
We actually mostly write FeatureScript using a Feature Studio as the editor.
That or your favorite text editor of choice...
HWM-Water Ltd
No need to apologize though Most of the developments that come for the Feature Studio are driven by things we need (autocomplete, monitor, profile, etc.) and we all agree we need debugging. Just not badly enough yet to justify the development time.
Still hanging in there? Let us know if you are making good progress or have any questions. Sorry this thread has gone a bit off the rails.
For TypeScript, Java and C++ we have full-fledged IDEs, but other than "watch" I pretty much never use any other features that you mentioned. Go-to-definition, doc-on-hover, autocomplete, project search I use all the time and (not entirely by coincidence) feature studios have those.
In any case, if we had a better way of doing FS development than a feature studio, why would we not release it?
I am! I had a busy day or two and I'm back at this now. More soon!
I was able to get all of the edges:
And even the faces if I wanted to. That wasn't too hard to pull off, but now how do I narrow it down more? Let's say I wanted the top or front face?
This line filters the user selection down to either edges or faces (depending on the enitytype setting I choose)
const userBodyFaces = qOwnedByBody(definition.UserBody, EntityType.EDGE);
Can I filter further in that line of code to get a particular element?
Sorry if these are super basic questions, but at this point I'm so new to coding I'm not even sure where to start looking for the answer?
const userBodyFaces = qOwnedByBody(definition.UserBody, EntityType.EDGE);
I tried this: const userBodyFaces = qOwnedByBody(definition.UserBody, EntityType.EDGE, qCapEntity(id + "definition.UserBody", CapType.END, EntityType.FACE));
But it didn't work, and I'm not sure why... Perhaps it's because there's no "extruding" happening here? The body already exists and I'm just having the user select it...
https://forum.onshape.com/discussion/comment/59757/#Comment_59757
As you said in one of your webinars I watched: If you can learn queries you can do a lot with FS (obviously paraphrasing)
I was able to target all of the faces, and edges separately in this cube, but I want to get more specific to practice.. I'm just not sure what makes the most sense to try out as I really don't have a good sense of what I can do with queries at all...
but that is entirely dependent upon the feature having being created using an extrude or sweep.
@eric_schimelpfenig
Here are a couple things to try:
I tried this: const userBodyFaces = qOwnedByBody(definition.UserBody, EntityType.EDGE, qCapEntity(id + "definition.UserBody", CapType.END, EntityType.FACE));
Generally it is actually a bit hard to do what you are doing. When you set up a Query parameter, you usually already know what you are looking for, and don't need to do much post-processing on it. Queries are a little more powerful when you are creating geometry inside of your own feature. This is because you'll have the Id that creates that geometry, and a lot of Queries are keyed based off of Id.
Try the following:
use fCuboid to make a box
https://cad.onshape.com/FsDoc/library.html#fCuboid-Context-Id-map
Then query using the cap entity query:
https://cad.onshape.com/FsDoc/library.html#qCapEntity-Id-CapType-EntityType