Welcome to the Onshape forum! Ask questions and join in the discussions about everything Onshape.

First time visiting? Here are some places to start:
  1. Looking for a certain topic? Check out the categories filter or use Search (upper right).
  2. Need support? Ask a question to our Community Support category.
  3. Please submit support tickets for bugs but you can request improvements in the Product Feedback category.
  4. 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.

Featurescript to create plane from an edge and a direction

gauthier_östervallgauthier_östervall Member Posts: 99 ✭✭
edited June 2020 in FeatureScript
I'm only just learning about Featurescript.

This is (a step towards) what I want: from an edge, create a plane that contains the edge and is vertical. Here is my attempt.

I've tried opPlane, but that requires a point and a normal. I don't have the normal (yet).

I've tried cPlane (seems like a logical choice, this what I want is practically the builtin "Line-angle" plane), but I can't find the documentation for the map it expects (auto-complete in the editor only says "definition").

My questions are:

- which one is the right way? opPlane, cPlane, something else, and why?
- how do I find out what the "definition" for cPlane should look like? I don't even know the name of the keys for that map.
- while trying opPlane, I noticed I couldn't even create a normal, that is a vector perpendicular to edge, and horizontal. I couldn't even extract the vertices at the end of Edge.
- isn't there an easier way to get a plane from an edge and a direction?

Some background: the plane is going to have a sketch with the projection of a face, then I'll do an Extrude between that face and the original face.

Comments

  • NeilCookeNeilCooke Moderator, Onshape Employees Posts: 5,671
    edited June 2020
    In FeatureScript you can take a face at any angle and extrude it by any vector by a distance. So you don’t need a sketch or a plane. 

    If you want to do it your way, use opPlane. You can use qAdjacent to find the ends of a curve and evEdgeTangent (sic) to get the normal. 
    Senior Director, Technical Services, EMEAI
  • gauthier_östervallgauthier_östervall Member Posts: 99 ✭✭
    @NeilCooke That seems appropriate! I also want the extrude to end as a vertical face, rather than parallel to the original face. This is why I wanted to go with the plane, but maybe there's another way to control the end of the extrude?
  • NeilCookeNeilCooke Moderator, Onshape Employees Posts: 5,671
    You’re right. You will need a plane but you can use up to face in the extrude. 
    Senior Director, Technical Services, EMEAI
  • gauthier_östervallgauthier_östervall Member Posts: 99 ✭✭
    edited August 2020
    @NeilCooke I'm back. I start with small steps, just a plane that contains the end of my definition.edge, and a normal that is constant for now. I've got this:

    <div>export const rotateToPlane = defineFeature(function(context is Context, id is Id, definition is map)
     &nbsp; precondition
    </div><div> &nbsp; {
    <span> &nbsp;   annotation { "Name" : "Face to rotate", "Filter" : EntityType.FACE, "MaxNumberOfPicks" : 1 }
    </span>     definition.faceToRotate is Query;
       &nbsp; &nbsp; &nbsp; &nbsp; annotation { "Name" : "Edge to rotate around", "Filter" : EntityType.EDGE, "MaxNumberOfPicks" : 1 }
       &nbsp; &nbsp; &nbsp; &nbsp; definition.edge is Query;
        &nbsp; }
          {
    &nbsp; &nbsp; &nbsp; &nbsp; var myVertex = qAdjacent(definition.edge, AdjacencyType.VERTEX);
    &nbsp; &nbsp; &nbsp; &nbsp; var myPlane = opPlane(context, id + "plane1", {
    &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "plane" : plane(myVertex, vector(0, 0, 1)),
    &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "width" : 6 * inch,
    &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "height" : 6 * inch
    &nbsp; &nbsp; &nbsp; &nbsp; });
    &nbsp; &nbsp; });</div>

    but "plane" seems to want a vector first, not a vertex:

    <table><tbody><tr><td colspan="2">Call plane(Query (map), Vector (array)) does not match plane(Vector, ...)
    
    <table><tbody><tr><td>18:27&nbsp;&nbsp;&nbsp;</td><td>Feature Studio 1&nbsp;(const rotateToPlane)</td></tr></tbody></table>
    <table><tbody><tr><td>55:17&nbsp;&nbsp;&nbsp;</td><td>onshape/std/feature.fs&nbsp;(defineFeature)</td></tr></tbody></table>         Part Studio 1&nbsp;(Rotate face to vertical 1)
             Part Studio 1
    <br></td></tr></tbody></table>
    I suppose that the vertex needs to be formatted as a vector from origin. I don't why this shouldn't be compatible with a vertex?
  • NeilCookeNeilCooke Moderator, Onshape Employees Posts: 5,671
    A vertex is a Vector with units, a direction is a unitless vector. 
    Senior Director, Technical Services, EMEAI
  • gauthier_östervallgauthier_östervall Member Posts: 99 ✭✭
    edited August 2020
    @NeilCooke Now that I've found your webinar, I think that my issue is that I put a query variable into `plane()`, instead of an entity (right?).

    I couldn't watch the webinar to the end. I'll do that ASAP, and hopefully learn to convert a query to an entity.
  • NeilCookeNeilCooke Moderator, Onshape Employees Posts: 5,671
    You can convert the query to a point using evVertexPoint
    Senior Director, Technical Services, EMEAI
  • gauthier_östervallgauthier_östervall Member Posts: 99 ✭✭
    @NeilCooke I'm blocked at computing the normal of the new plane.

    I have the original plane, its normal, and now I want to compute the normal of the new plane by zeroing the z of the old normal.

            var myNormal = evFaceNormalAtEdge(context, {
                    "edge" : definition.edge,
                    "face" : definition.faceToRotate,
                    "parameter" : 0
            });
            debug(context, myNormal);
            var newNormal = vector(myNormal[0], myNormal[1], 0);
            //myNormal[2] = 0; // this didn't work either
            debug(context, newNormal);
    I do see the normal of the original plane, but nothing when I put its z dimension to 0.

    I feel that I am missing something fundamental here...
  • Jake_RosenfeldJake_Rosenfeld Moderator, Onshape Employees, Developers Posts: 1,646
    edited August 2020
    @gauthier_östervall

    Generally zeroing out the z direction is not going to make for a great feature, because then you are operating on global Z direction rather than a direction that has anything to do with your geometry. (Imagine if the face normal of the selected face was a plane whose normal was [0, 0, 1], then zeroing out the z direction is always just a zero vector)

    If you are worried though because your `debug` prints look different, it is because you need to call normalize to make your vector a unit vector:

    var newNormal = normalize(vector(myNormal[0], myNormal[1], 0));
    -- or --
    myNormal[0] = 0;
    myNormal = normalize(myNormal);

    Jake Rosenfeld - Modeling Team
  • gauthier_östervallgauthier_östervall Member Posts: 99 ✭✭
    @Jake_Rosenfeld
    I think I see what you mean, I should probably let the user give the direction they need for that. But zeroing z would at least get me started.

    Normalizing worked, thanks! I'm not sure why the opPlane operation couldn't work with a unnormalized vector, but I can live with that.

    Now that I'm continuing my journey with this feature, I think I should go with a cross product instead. In this case, a cross product of a vector aligned with the user selected edge, and [0, 0, 1] (or user selected direction as you mentioned) would give me a normal to the plane I intend to create.

    To be clear: I want a vertical plane that contains the user defined edge.

    So `edge_vector x plane_second_direction` should do. I'd like to test this at least, but I can't find a way to create a vector, from the user selected edge. I could do a line with `evEdgeTangentLine`, but that doesn't help me since I need a vector to `cross()`.

  • Jake_RosenfeldJake_Rosenfeld Moderator, Onshape Employees, Developers Posts: 1,646
    @gauthier_östervall

    evEdgeTangentLine returns an Line: https://cad.onshape.com/FsDoc/library.html#Line

    So once you have that line, you can ask for `edgeLine.direction` to find the direction of the line:

    const edgeLine = evEdgeTangentLine(...);
    const edgeLineDirection = edgeLine.direction;
    const edgeLineOrigin = edgeLine.origin;

    Jake Rosenfeld - Modeling Team
  • gauthier_östervallgauthier_östervall Member Posts: 99 ✭✭
    edited August 2020
    @Jake_Rosenfeld @NeilCooke

    Victory! I've got a vertical plane containing the user-defined edge. This is my code:

            const edgeVertex = qAdjacent(definition.edge, AdjacencyType.VERTEX, EntityType.VERTEX);
    
            const edgeDir = evEdgeTangentLine(context, {
                        "edge" : definition.edge,
                        "parameter" : 0
                }).direction;
            debug(context, normalize(edgeDir));
            
            const planeNorm = cross(
                edgeDir,
                vector(0, 0, 1) // This should be user-defined
            );
            debug(context, planeNorm);
            
            opPlane(context, id + "plane1", {
                    "plane" : plane(
                        evVertexPoint(context, {
                            "vertex" : edgeVertex
                        }), 
                        planeNorm),
                    "width" : 6 * inch,
                    "height" : 6 * inch
            });


    The debug lines show output in the console, but no red vectors show up, though.
  • Jake_RosenfeldJake_Rosenfeld Moderator, Onshape Employees, Developers Posts: 1,646
    @gauthier_östervall

    I would expect the debug directions to show up at the origin, so maybe if your view if off to the side, you are not seeing them?

    A couple tips that may be useful:

    - width and height parameters of opPlane are optional, you can get rid of them if you wish.
    - It is not common to nest calls like evVertexPoint inside a call to opPlane.  For clarity, we usually do this first, and store it in a const.
    - Your call to evVertexPoint is actually redundant with your call to evEdgeTangentLine.  If you use the .origin of the return of evEdgeTangentLine, it will be the location of one of the vertices of the line, and you could skip calling evVertexPoint altogether for a nice performance gain.
    Jake Rosenfeld - Modeling Team
  • gauthier_östervallgauthier_östervall Member Posts: 99 ✭✭
    edited August 2020
    Such a nice reply @Jake_Rosenfeld! It did come out much simpler, thank you:

            const edgeLine = evEdgeTangentLine(context, {
                        "edge" : definition.edge,
                        "parameter" : 0
                });
                
            const edgeDir = edgeLine.direction;
            const edgeVert = edgeLine.origin;
            
            debug(context, normalize(edgeDir));
            
            const planeNorm = cross(
                edgeDir,
                vector(0, 0, 1) // This should be user-defined
            );
            debug(context, planeNorm);
            
            const projPlane = opPlane(context, id + "plane1", {
                    "plane" : plane(edgeVert, planeNorm)
            });

    Any more simplifications? I'm happy to learn best practice.

    Still, no vector showing as a result of debug, see here: https://cad.onshape.com/documents/23e65351356c7baa6e1c9983/w/bb9b70f6fa40ceca6840ff95/e/bc4704f512dae5f04d10f782
  • gauthier_östervallgauthier_östervall Member Posts: 99 ✭✭
    edited September 2020
    @Jake_Rosenfeld @NeilCooke

    I am nearly there! Extruding a user-defined face, up to an edge, horizontally to a vertical plane.

        precondition
        {
            annotation { "Name" : "Face to rotate", "Filter" : EntityType.FACE, "MaxNumberOfPicks" : 1 }
            definition.faceToRotate is Query;
            annotation { "Name" : "Edge to rotate around", "Filter" : EntityType.EDGE, "MaxNumberOfPicks" : 1 }
            definition.edge is Query;
        }
        {
            const edgeLine = evEdgeTangentLine(context, {
                        "edge" : definition.edge,
                        "parameter" : 0
                });
                
            const edgeDir = edgeLine.direction;
            const edgeVert = edgeLine.origin;
    
            const planeNorm = cross(
                edgeDir,
                vector(0, 0, 1) // This should be user-defined
            );
            
            const projPlane = opPlane(context, id + "plane1", {
                    "plane" : plane(edgeVert, planeNorm)
            });
            
            opExtrude(context, id + "extrude1", {
                    "entities" : definition.faceToRotate,
                    "direction" : -normalize(planeNorm),
                    "endBound" : BoundingType.THROUGH_ALL
            });
            
            opSplitPart(context, id + "splitPart1", {
                    "targets" : qCreatedBy(id + "extrude1", EntityType.BODY),
                    "tool" : qCreatedBy(id + "plane1", EntityType.FACE),
                    "keepType" : SplitOperationKeepType.KEEP_FRONT
            });
            
        });


    Now I just need to join the resulting part, with the part that owned the face. I thought it would be easy, just as the "Add" option of standard Extrude.

    The problem is that I can't get a query to the original part. I'd like to union with the part adjacent to the definition.edge, or definition.faceToRotate, but cannot seem to get qAdjacent to do that. Here is what I have:

            opBoolean(context, id + "boolean1", {
                    "tools" : qUnion([
                        qCreatedBy(id + "splitPart1", EntityType.BODY),
                        qAdjacent(definition.faceToRotate, AdjacencyType.EDGE, EntityType.BODY)]),
                    "operationType" : BooleanOperationType.UNION
            });

    I'd expect this to give me the body that has an edge in common with definition.faceToRotate, but it doesn't:

    qAdjacent(definition.faceToRotate, AdjacencyType.EDGE, EntityType.BODY);

    How do I get the part to merge with, without prompting the user?

    The error I get is this: "Precondition of qAdjacent failed (entityType != EntityType.BODY)". Does this mean that qAdjacent only can take as input the same type as is is required to return as output?

    (the project)
  • EvanReeseEvanReese Member, Mentor Posts: 2,125 ✭✭✭✭✭
    opBoolean is looking for a single query for the bodies. For the body that goes with the user-selected face you probably need to use qOwnerBody. For the new body you made use qCreatedBy. Once you have both of them, you can use qUnion to get them into the single query opBoolean wants.

    Also, what you're doing seems similar to what I did in the Captive Nut feature when using the Embedded mode. It finds all faces that are on one side of a body, and extrudes them up to a plane (see the grey part below). If this is relevant feel free to reference that and ask anything that comes up.
     
    Evan Reese
  • Jake_RosenfeldJake_Rosenfeld Moderator, Onshape Employees, Developers Posts: 1,646
    @gauthier_östervall

    qAdjacent is a topological query used for finding vertices, edges, or faces that are adjacent (in other words, attached to) other vertices, edges, or faces.

    So some examples of using that query might be finding all the edges coming into a vertex, or finding the two faces attached to a specific edge.

    Note that qAdjacent is NOT a geometric query.  It does not have any notion of edges of different bodies being coincident, or any notion of where things exist in 3d space at all.  So it can only find topological connections within a body.

    To find what you are looking for, you will want to do what @Evan_Reese is saying: use qOwnerBody on definition.faceToRotate

    Additionally, it is possible that qCreatedBy(id + "split1") is not the right query for the tool that you want to join.  You may need qCreatedBy(id + "extrude1"), since the body is created by extrude 1, and just edited by the split.

    Finally, it is likely that your custom feature would want to maintain the identity of the input body (it's name, color, any properties, etc).  To do this, you will want to make the existing body the first body in the qUnion array going into opBoolean.

    So all together, your `tools` input should be: qUnion([qOwnerBody(definition.faceToRotate), qCreatedBy(id + "extrude1", EntityType.BODY)])

    Jake Rosenfeld - Modeling Team
  • gauthier_östervallgauthier_östervall Member Posts: 99 ✭✭
    @Jake_Rosenfeld @Evan_Reese

    Yes!

    You were right about using the result of the split, it didn't work. But I don't get why. Is there a distinction between "Created" and "Modified"? I mean the split operation does have an output, why doesn't "CreatedBy" use that on the split?
  • EvanReeseEvanReese Member, Mentor Posts: 2,125 ✭✭✭✭✭
    @Jake_Rosenfeld @Evan_Reese

    Yes!

    You were right about using the result of the split, it didn't work. But I don't get why. Is there a distinction between "Created" and "Modified"? I mean the split operation does have an output, why doesn't "CreatedBy" use that on the split?
    @jak@Jake_Rosenfeld can give you the real answer, but my lay understanding is that every entity has what is essentially a list of data associated with it, which helps Onshape keep track of what is what, and the "created by" is in that list, and can only have one entry. If you want to see what all is in that list, you can just use println(myQuery); and it will look something like this:

    { queryType : UNION , subqueries : [ { disambiguationData : [ { disambiguationType : ORIGINAL_DEPENDENCY , originals : [ { entityType : EDGE , historyType : CREATION , operationId : [ FXw4Mfq1r1b0Xs2_0.wireOp ] , queryType : SKETCH_ENTITY , sketchEntityId : SvyLNHGyWj9W } ] } ] , entityType : BODY , historyType : CREATION , operationId : [ FPRjScDcjS49GJl_0.opExtrude ] , queryType : SWEPT_BODY } ] }

    Jake, am I close?
    Evan Reese
  • Jake_RosenfeldJake_Rosenfeld Moderator, Onshape Employees, Developers Posts: 1,646
    @Evan_Reese @gauthier_östervall

    The println() on a query will only work if that is one of the original queries from the definition that was obtained by just clicking on an entity.  What you are looking at is the generated query based on the history of the entity, which is meant to be robust in the face of model changes.  Imagine you were to extrude a block, and then fillet one of the edges of the block.  Then, you moved the rollback bar to before the fillet, and split the block in half (cutting the selected edge in half).  Then, when you roll forward the fillet, you will see that both of those edges will be filleted.

    The query we generate for that selection tracks the history of that edge.  Essentially saying "the top cap edge created by extruding this specific sketch edge".  Now, when we insert the split into the feature list, we can still resolve that query, because the split does not create two new edges, it splits an existing edge.

    Maybe that example makes it a bit clearer, but it really does break down to just entities are only created once, and things like splits and geometric modifications are not considered creation of a new entity.
    Jake Rosenfeld - Modeling Team
  • EvanReeseEvanReese Member, Mentor Posts: 2,125 ✭✭✭✭✭
    That explanation makes sense to me, Jake, thanks
    Evan Reese
Sign In or Register to comment.