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.

How does identity work?

juliaschatzjuliaschatz Member Posts: 5 EDU
I've been using Featurescript for a while but don't really understand how identity is determined.
Specifically, one featurescript I have extrudes each region of a sketch separately, even if they're joined. This will occasionally break if someone adds another region to the sketch used to define it. How can I make this script robust to changes in the sketch?


Another script I have generates a roller chain, and has options for Simplified and Realistic modes which in turn each call a different script. When the mode is changed, the new body has a different identity than the old one, which breaks anything that referenced it. How can I make both of these sub-features produce a body with the same identity?

Chain Path (line 743 for the start of the main feature).


Comments

  • kevin_o_toole_1kevin_o_toole_1 Onshape Employees, Developers, HDM Posts: 493
    edited July 23
    The primary thing that Onshape will use to identify a selected entity is the "id" passed into the operation that creates it. You can read a more detailed explanation in a past comment here.

    To sum it up briefly: When you select an entity, Onshape generates a query for that entity. That query might look like "the face generated by the operation with id id + "extrude1", and was a result of extruding the edge from a sketch with id id + "sketch1" with sketch entity id "line7" ". If any of those ids change when you modify upstream features, that query will break, and the result is a feature running with a query for nothing (or, worse, a query for the wrong thing). Ids are not the only disambiguations used, but they are the most common and the ones that most rely on custom features behaving in a stable way.

    In the case of your "Extrude Individual" feature, you are losing selections for some changes because you create an operation with the id id + ("extrudeRev" ~ safeId). Your "safeId"s, based on entity transient ids, may look more stable than a simple index, but in fact those transient ids can change when upstream operations are added and removed, so they are not stable in the face of most upstream changes.

    Luckly, FS has a built-in solution for this problem via the methods unstableIdComponent and setExternalDisambiguation.

    unstableIdComponent(string) creates an id which will not be used to disambiguate any downstream features.
    setExternalDisambiguation(context, id, entity) will ensure that the identity of "entity" will be used to disambiguate the operation with that id instead.

    To tie it all together, here's a convenient forEachEntity(...) method for handling all this, explained via some very verbose documentation:
    /**
     * Iterate through all entities provided by a query, calling the provided function once for each geometric entity
     * resolved by the provided `query`.
     *
     * `forEachEntity` behaves much like the code:
     * ```
     * const evaluated = evaluateQuery(context, query);
     * for (var i = 0; i < size(evaluated); i += 1)
     * {
     *     operationToPerform(id + i, evaluated[i]);
     * }
     * ```
     * However, `forEachEntity` has one additional benefit: The `entId` this function provides to `operationToPerform` is tied to
     * the entity itself, not its index `i`. This means that downstream features made in the Part Studio will hold up better across
     * upstream changes.
     *
     * For example, imagine the following scenario: A user inserts a custom feature which places a slot on every selected line. That
     * feature calls `forEachEntity(context, lineQuery ...)`. The user then makes a sketch downstream which uses geometry from e.g. the
     * third slot. Finally, the user decides some slots are unnecessary and deletes some of the lines. Since the feature used
     * `forEachEntity`, the user's downstream sketch will still reference the same slots. If the feature instead used the code above,
     * the user's sketch would break or jump around, since a different slot would suddenly become "slot 3".
     *
     * Aside from that difference, the two are interchangeable.
     *
     * Like any expression function, be warned that the provided `operationToPerform` can read but can NOT modify the values of
     * variables outside the function. It can, however, modify values inside a [box](https://cad.onshape.com/FsDoc/variables.html#box).
     * @example ```
     * const allParts = qAllModifiableSolidBodies();
     * const threshold = 0.01 * inch^3;
     * var deletedSizes is box = new box([]); // box containing an empty array
     * forEachEntity(context, id + "deleteSmallParts", allParts, function(entity is Query, id is Id)
     * {
     *     const size = evVolume(context, {
     *             "entities" : entity
     *     });
     *     if (size < threshold)
     *     {
     *         opDeleteBodies(context, id + "deleteBodies1", {
     *                 "entities" : entity
     *         });
     *         deletedSizes[] = append(deletedSizes[], size);
     *     }
     * });
     * println(deletedSizes[]);
     * ```
     *
     * @param id: @autocomplete `id + "operation"`
     * @param operationToPerform: @eg ```function(entity is Query, id is Id)
     * {
     *     // perform operations with the entity
     * }
     * ```
     */
    export function forEachEntity(context is Context, id is Id, query is Query, operationToPerform is function)
    {
        const evaluated = evaluateQuery(context, query);
        const querySize = size(evaluated);
        for (var i = 0; i < querySize; i += 1)
        {
            const innerId = id + unstableIdComponent(i);
            const entity = evaluated[i];
            setExternalDisambiguation(context, innerId, entity);
            operationToPerform(entity, innerId);
        }
    }
    It can be used via the pattern explained in the documentation:
    forEachEntity(context, id + "doThings", definition.faces, function(entity is Query, id is Id)
    {
        opExtrude(context, id + "extrudeThing", { // operation which can be referenced stably in future features!
                "entities" : entity,
                ...
        });
    });

    Now that it's written, hopefully this function is coming soon to an std near you! In the meantime feel free to import it from this public document I tested it in, or copy-paste it into yours, or simply mimic the pattern inside that function of unstableIdComponent and setExternalDisambiguation inside existing features.

    Btw, nice features! I saw them used by many teams in the submissions we saw in the Robots to the Rescue competition. They are definitely simplifying some very useful work!
  • juliaschatzjuliaschatz Member Posts: 5 EDU
    Thanks for the great writeup and function! I think I've successfully applied it to the Extrude Individual featurescript, but am still struggling with the chain path identity.

    I've made a new document here to mess around with testing without breaking anything. When calling the other features, I give it the same ID as was passed in to the main feature. According to my understanding this should make anything that references this feature see the Simplified part and Realistic part and understand they're the same thing because they have the same ID, although they have different component operations.

    Is there a way to use unstable IDs and external disambiguation with this? I tried various configurations of where the unstable ID goes and what to give to external disambiguation, but didn't have any luck.
  • kevin_o_toole_1kevin_o_toole_1 Onshape Employees, Developers, HDM Posts: 493
    edited July 24
    There's a more generic solution in our pipeline in the future. But for your specific case, it's enough to use two facts about body identity:

    1) In a boolean union e.g. opBoolean(... qUnion( [ body1, body2] )), the first resolved body (i.e. body1) maintains its identity.
    2) Bodies from operations based on sketches like opExtrude(... qSketchRegion(id + "sketch1")) maintain their identity if the sketch id is the same and at least one sketch id in the loop of edges around the region is there.

    In your case, the first body of the boolean in your unsimplified code has an identity that's a variation of "The body extruded from id + "sketch1" touching either arc1, arc2, line1 or line2". The first body of the boolean in your simplified code has an identity that's a variation of "The body extruded from id + "sketch2" touching either arc1, arc2, line1 or line2".

    So, changing those sketch ids to be consistent should resolve this issue.
Sign In or Register to comment.