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.

Manipulator offset?

MichaelPascoeMichaelPascoe Member Posts: 597 PRO
edited March 9 in FeatureScript
I'm trying to get the manipulator in my custom feature Section to behave more like the real section view model tool. So far, all of my attempts have failed. Is there a good way to do this? A more specific question: Is there an efficient way to offset a manipulator from its origin?
https://cad.onshape.com/documents/bdffe7bf48f3b4155e85d073/w/4aa864c3858500b61d8570f5/e/433e168b78be95d2d6e248ec

What the manipulator should do:
  • When you drag it across the original split plane, it should stay the same direction.
  • Clicking the manipulator should switch the direction of the manipulator but keep it in the same global position. 


Here are my failed attempts:
  • Have the manipulator attached to a plane that is updated when you drag the manipulator. This will fix the opposite direction problem and the global position problem. (Too much lag)
  • Measure the distance from the original split plane and start the manipulator or offset it that distance when the manipulator is clicked. (When I try this, it will work for the very first click, then when I try to move the manipulator, it realizes that "opposite direction" is no longer the opposite direction. Also, if I add to the offset of the manipulator, it doesn't simply offset the manipulator, it offsets it that much every refresh causing it to no longer be a 1:1 change) 
  • Include a set constant that is included in the manipulator offset and the manipulator export. (Too much lag)

My closest attempt involves offsetting a plane from the original section plane. Keeping it in one direction for "opposite direction" reference. With this method, I can't figure out how to offset the manipulator properly when "opposite direction" is clicked. 

Here is the summed up version of the code:

annotation { "Feature Type Name" : "Section", "Manipulator Change Function" : "sectionManipulatorChange", "Icon" : Icon::BLOB_DATA }
export const myFeature = defineFeature(function(context is Context, id is Id, definition is map)
    precondition
    {
        annotation { "Name" : "Section Plane or Face", "Filter" : EntityType.FACE, "MaxNumberOfPicks" : 1 }
        definition.parallelFace is Query;

        annotation { "Name" : "Section Depth" }
        isLength(definition.depth, { (inch) : [-1e5, 0, 1e5] } as LengthBoundSpec);

        annotation { "Name" : "Opposite direction", "UIHint" : UIHint.OPPOSITE_DIRECTION }
        definition.oppositeDirection is boolean;
    }

{
        var allParts = qAllNonMeshSolidBodies();
        var evalPlane = evPlane(context, {
                "face" : definition.parallelFace
            });

        var direction = definition.oppositeDirection ? 1 : -1;
        var localCsys = planeToCSys(evalPlane);
        var offsetPlaneDepth = -direction * 5 * inch;
        var difference = -(offsetPlaneDepth + direction * definition.depth);
        var offsetOrigin = definition.oppositeDirection? 
                                toWorld(localCsys, vector(0 * inch, 0 * inch, offsetPlaneDepth)) 
                                : 
                                toWorld(localCsys, vector(0 * inch, 0 * inch, offsetPlaneDepth));
        var offsetPlane = plane(offsetOrigin, evalPlane.normal, evalPlane.x);
        var correctedDepth = definition.depth + abs(offsetPlaneDepth);

//______________________________________Manipulator______________________________________
        var extrudeManipulator is Manipulator = linearManipulator({
                    "base" : offsetPlane.origin,
                    "direction" : offsetPlane.normal,
                    "offset" : direction * definition.depth,
                    "primaryParameterId" : "depth"
                });

        addManipulators(context, id, {
                    "myManipulator" : extrudeManipulator
                });
}
export function sectionManipulatorChange(context is Context, definition is map, newManipulators is map) returns map
{
    var newDepth is ValueWithUnits = newManipulators["myManipulator"].offset;
    definition.depth = abs(newDepth);
    definition.oppositeDirection = newDepth > 0;
    return definition;
} 


Best Answer

  • Jake_RosenfeldJake_Rosenfeld Moderator, Onshape Employees, Developers Posts: 1,642
    Accepted Answer
    @MichaelPascoe

    Here you go!

    https://cad.onshape.com/documents/7dccb52fb132ba73b280ad47/w/3e8730a19eeb6090d51d05a9/e/3381a7cbdfa4a89d2041c44d

    const MANIPULATOR_ID = "linear";
    
    annotation { "Feature Type Name" : "Plane manipulator - advanced", "Manipulator Change Function" : "manipulatorChange" }
    export const planeManipulator = defineFeature(function(context is Context, id is Id, definition is map)
        precondition
        {
            annotation { "Name" : "Plane", "Filter" : GeometryType.PLANE, "MaxNumberOfPicks" : 1 }
            definition.rootPlane is Query;
    
            annotation { "Name" : "Offset" }
            isLength(definition.offset, LENGTH_BOUNDS);
    
            annotation { "Name" : "Opposite direction", "UIHint" : UIHint.OPPOSITE_DIRECTION }
            definition.oppositeDirection is boolean;
        }
        {
            const rootPlane = evPlane(context, {
                        "face" : definition.rootPlane
                    });
    
            const manipulatorParameters = getManipulatorParameters(rootPlane, definition.offset, definition.oppositeDirection);
    
            const manipulator = linearManipulator({
                        "base" : manipulatorParameters.base,
                        "direction" : manipulatorParameters.direction,
                        "offset" : manipulatorParameters.offset,
                        "primaryParameterId" : "offset",
                    });
            addManipulators(context, id, { (MANIPULATOR_ID) : manipulator });
    
            const offsetPlane = plane(manipulator.base + (manipulator.offset * manipulator.direction), manipulator.direction);
    
            debug(context, offsetPlane);
        });
    
    // Theoretically you could be more discerning about this by calculating it based off of evBox3d of the selected parts.
    const A_VERY_LARGE_DISTANCE = 250 * meter;
    
    function getManipulatorParameters(rootPlane is Plane, definitionOffset is ValueWithUnits, oppositeDirection is boolean) returns map
    {
        // Manipulator will always face this direction
        const manipulatorDirection = (oppositeDirection ? -1 : 1) * rootPlane.normal;
        // Make sure manipulator does not flip, by placing the base point very far away
        const manipulatorBase = rootPlane.origin + (-manipulatorDirection * A_VERY_LARGE_DISTANCE);
        // Flipping `oppositeDirection` should not affect where on-screen the arrow is, but since we are moving
        // the base point all the way to the other side of the world, the offset of the manipulator (in reference
        // to the base point) has to change.  The simplest way to represent this mathematically is probably:
        // const worldSpaceBasePoint = rootPlane.origin + (definitionOffset * rootPlane.normal);
        // const manipulatorOffset = norm(worldSpaceBasePoint - manipulatorBase)
        // But that is fairly slow compared to just using the numbers directly
        const manipulatorOffset = A_VERY_LARGE_DISTANCE + ((oppositeDirection ? -1 : 1) * definitionOffset);
    
        return {
                "base" : manipulatorBase,
                "direction" : manipulatorDirection,
                "offset" : manipulatorOffset
            };
    }
    
    function getDefinitionOffsetFromManipulator(manipulatorOffset is ValueWithUnits, oppositeDirection is boolean) returns ValueWithUnits
    {
        // Rearrange the equation from above:
        // manipulatorOffset = A_VERY_LARGE_DISTANCE + ((oppositeDirection ? -1 : 1) * definitionOffset);
        // (oppositeDirection ? -1 : 1) * (manipulatorOffset - A_VERY_LARGE_DISTANCE) = definitionOffset;
        return (oppositeDirection ? -1 : 1) * (manipulatorOffset - A_VERY_LARGE_DISTANCE);
    }
    
    export function manipulatorChange(context is Context, definition is map, newManipulators is map)
    {
        const newOffset = newManipulators[MANIPULATOR_ID].offset;
        if (newOffset < 0 * meter)
        {
            // Some trickery here.  If the user clicks on the manipulator, treat that as flipping the oppositeDirection button.
            // this means that if the user ever drags past A_VERY_LARGE_DISTANCE, the manipulator will do something weird
            definition.oppositeDirection = !definition.oppositeDirection;
        }
        else
        {
            // User has dragged.  Update the offset.
            definition.offset = getDefinitionOffsetFromManipulator(newManipulators[MANIPULATOR_ID].offset, definition.oppositeDirection);
        }
        return definition;
    }

    This is definitely a hack, because the linear manipulator is meant to flip all the way across the base point when you click on the manipulator.  What we are doing here is moving the base point very far away, so that the manipulator cannot change the direction it is facing unless you want it to, but then when you click on it (or click the opposite direction button) we have to do something hacky so that it stays in place instead of going all the way across the world.

    Let me know if you have any questions!
    Jake Rosenfeld - Modeling Team

Answers

  • Jake_RosenfeldJake_Rosenfeld Moderator, Onshape Employees, Developers Posts: 1,642
    Hi @MichaelPascoe

    I am having a hard time understanding exactly what you want the behavior to be.

    Here is a simplified implementation of calculating a cutting plane based on a plane selection, and having a linear manipulator to control the offset:
    https://cad.onshape.com/documents/7dccb52fb132ba73b280ad47/w/3e8730a19eeb6090d51d05a9/e/813991c3a26168e92708510c

    const MANIPULATOR_ID = "linear";
    
    annotation { "Feature Type Name" : "Plane manipulator", "Manipulator Change Function" : "manipulatorChange" }
    export const planeManipulator = defineFeature(function(context is Context, id is Id, definition is map)
        precondition
        {
            annotation { "Name" : "Plane", "Filter" : GeometryType.PLANE, "MaxNumberOfPicks" : 1 }
            definition.rootPlane is Query;
    
            annotation { "Name" : "Offset" }
            isLength(definition.offset, LENGTH_BOUNDS);
    
            annotation { "Name" : "Opposite direction", "UIHint" : UIHint.OPPOSITE_DIRECTION }
            definition.oppositeDirection is boolean;
        }
        {
            const rootPlane = evPlane(context, {
                        "face" : definition.rootPlane
                    });
                    
            const manipulator = linearManipulator({
                        "base" : rootPlane.origin,
                        "direction" : rootPlane.normal,
                        "offset" : (definition.oppositeDirection ? -1 : 1) * definition.offset,
                        "primaryParameterId" : "offset"
                    });
            addManipulators(context, id, { (MANIPULATOR_ID) : manipulator });
            
            const offsetPlane = plane(manipulator.base + (manipulator.offset * manipulator.direction), manipulator.direction);
            
            debug(context, offsetPlane);
        });
        
    export function manipulatorChange(context is Context, definition is map, newManipulators is map)
    {
        const manipulator = newManipulators[MANIPULATOR_ID];
        definition.offset = abs(manipulator.offset);
        definition.oppositeDirection = manipulator.offset < 0;
        return definition;
    }


    Is this sufficient for your need (besides doing the actual cutting, which it seems you have already mastered)?  If not, we can build on this together to arrive at a good solution.

    For what its worth, it seems like a lot of the oddity of what you have posted has come from the additional "offsetPlaneDepth" you are imposing, and the fact that you are applying the "definition.oppositeDirection" to it (by way of "direction" variable).  If you want to have some kind of additional base offset, I would multiply it by "evalPlane.normal" (and just generally make sure that it is in terms of "evalPlane" only) so that the base point of the manipulator does not change unless the user changes selection.
    Jake Rosenfeld - Modeling Team
  • MichaelPascoeMichaelPascoe Member Posts: 597 PRO
    edited March 9
    Thanks @Jake_Rosenfeld for the speedy response!

    The example you gave is similar to how the feature originally worked. The problem with this, is when the manipulator is dragged past the selected plane, "opposite direction" is activated. I'm trying to make it so that no matter where the manipulator is dragged, the direction does not change unless the manipulator is clicked to activate "opposite direction". 
  • Jake_RosenfeldJake_Rosenfeld Moderator, Onshape Employees, Developers Posts: 1,642
    @MichaelPascoe

    Interesting, I see.  So really your goal here is to use the flipper to control which side of the section is exposed.  The simplest way is to just decouple these controls, and control which side is removed with a separate control in the Feature Dialog.  But, it is definitely possible to do what you are asking.  I will mock up an example for you.
    Jake Rosenfeld - Modeling Team
  • MichaelPascoeMichaelPascoe Member Posts: 597 PRO
    @Jake_Rosenfeld Thanks! Yes, that is the goal. 
  • Jake_RosenfeldJake_Rosenfeld Moderator, Onshape Employees, Developers Posts: 1,642
    Accepted Answer
    @MichaelPascoe

    Here you go!

    https://cad.onshape.com/documents/7dccb52fb132ba73b280ad47/w/3e8730a19eeb6090d51d05a9/e/3381a7cbdfa4a89d2041c44d

    const MANIPULATOR_ID = "linear";
    
    annotation { "Feature Type Name" : "Plane manipulator - advanced", "Manipulator Change Function" : "manipulatorChange" }
    export const planeManipulator = defineFeature(function(context is Context, id is Id, definition is map)
        precondition
        {
            annotation { "Name" : "Plane", "Filter" : GeometryType.PLANE, "MaxNumberOfPicks" : 1 }
            definition.rootPlane is Query;
    
            annotation { "Name" : "Offset" }
            isLength(definition.offset, LENGTH_BOUNDS);
    
            annotation { "Name" : "Opposite direction", "UIHint" : UIHint.OPPOSITE_DIRECTION }
            definition.oppositeDirection is boolean;
        }
        {
            const rootPlane = evPlane(context, {
                        "face" : definition.rootPlane
                    });
    
            const manipulatorParameters = getManipulatorParameters(rootPlane, definition.offset, definition.oppositeDirection);
    
            const manipulator = linearManipulator({
                        "base" : manipulatorParameters.base,
                        "direction" : manipulatorParameters.direction,
                        "offset" : manipulatorParameters.offset,
                        "primaryParameterId" : "offset",
                    });
            addManipulators(context, id, { (MANIPULATOR_ID) : manipulator });
    
            const offsetPlane = plane(manipulator.base + (manipulator.offset * manipulator.direction), manipulator.direction);
    
            debug(context, offsetPlane);
        });
    
    // Theoretically you could be more discerning about this by calculating it based off of evBox3d of the selected parts.
    const A_VERY_LARGE_DISTANCE = 250 * meter;
    
    function getManipulatorParameters(rootPlane is Plane, definitionOffset is ValueWithUnits, oppositeDirection is boolean) returns map
    {
        // Manipulator will always face this direction
        const manipulatorDirection = (oppositeDirection ? -1 : 1) * rootPlane.normal;
        // Make sure manipulator does not flip, by placing the base point very far away
        const manipulatorBase = rootPlane.origin + (-manipulatorDirection * A_VERY_LARGE_DISTANCE);
        // Flipping `oppositeDirection` should not affect where on-screen the arrow is, but since we are moving
        // the base point all the way to the other side of the world, the offset of the manipulator (in reference
        // to the base point) has to change.  The simplest way to represent this mathematically is probably:
        // const worldSpaceBasePoint = rootPlane.origin + (definitionOffset * rootPlane.normal);
        // const manipulatorOffset = norm(worldSpaceBasePoint - manipulatorBase)
        // But that is fairly slow compared to just using the numbers directly
        const manipulatorOffset = A_VERY_LARGE_DISTANCE + ((oppositeDirection ? -1 : 1) * definitionOffset);
    
        return {
                "base" : manipulatorBase,
                "direction" : manipulatorDirection,
                "offset" : manipulatorOffset
            };
    }
    
    function getDefinitionOffsetFromManipulator(manipulatorOffset is ValueWithUnits, oppositeDirection is boolean) returns ValueWithUnits
    {
        // Rearrange the equation from above:
        // manipulatorOffset = A_VERY_LARGE_DISTANCE + ((oppositeDirection ? -1 : 1) * definitionOffset);
        // (oppositeDirection ? -1 : 1) * (manipulatorOffset - A_VERY_LARGE_DISTANCE) = definitionOffset;
        return (oppositeDirection ? -1 : 1) * (manipulatorOffset - A_VERY_LARGE_DISTANCE);
    }
    
    export function manipulatorChange(context is Context, definition is map, newManipulators is map)
    {
        const newOffset = newManipulators[MANIPULATOR_ID].offset;
        if (newOffset < 0 * meter)
        {
            // Some trickery here.  If the user clicks on the manipulator, treat that as flipping the oppositeDirection button.
            // this means that if the user ever drags past A_VERY_LARGE_DISTANCE, the manipulator will do something weird
            definition.oppositeDirection = !definition.oppositeDirection;
        }
        else
        {
            // User has dragged.  Update the offset.
            definition.offset = getDefinitionOffsetFromManipulator(newManipulators[MANIPULATOR_ID].offset, definition.oppositeDirection);
        }
        return definition;
    }

    This is definitely a hack, because the linear manipulator is meant to flip all the way across the base point when you click on the manipulator.  What we are doing here is moving the base point very far away, so that the manipulator cannot change the direction it is facing unless you want it to, but then when you click on it (or click the opposite direction button) we have to do something hacky so that it stays in place instead of going all the way across the world.

    Let me know if you have any questions!
    Jake Rosenfeld - Modeling Team
  • MichaelPascoeMichaelPascoe Member Posts: 597 PRO
    @Jake_Rosenfeld
    This. Is one of the most helpful answers I have ever received. THANK YOU!

    It's perfect.


  • Evan_ReeseEvan_Reese Member Posts: 1,049 PRO
    @Jake_Rosenfeld
    This. Is one of the most helpful answers I have ever received. THANK YOU!

    It's perfect.
    Jake has a way of doing that
    Evan Reese / Principal and Industrial Designer with Ovyl
    Website: ovyl.io
  • Jake_RosenfeldJake_Rosenfeld Moderator, Onshape Employees, Developers Posts: 1,642
     o:)  o:) You flatter me Evan.  You rock too! 
    Jake Rosenfeld - Modeling Team
Sign In or Register to comment.