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.

Centred Text in FeatureScript

alan_buddenalan_budden Member Posts: 7 ✭✭
I'm trying to create a simple featurescript thing that will take a numeric value (from a variable, but that's incidental), convert it to a string with a prefix and write it as text centred on the origin of a face of a body.  Most of that is quite straightforward, but I've been completely beaten by the (seemingly simple) task of centring the text.  I can set the height easily enough (so could work out how far to move it vertically), but I have no idea what the width will be (as it depends on the actual text) so I don't know how far to move it horizontally.

This is what I've got so far that works:
FeatureScript 1431;
import(path : "onshape/std/geometry.fs", version : "1431.0");

annotation { "Feature Type Name" : "RatioText" }
export const ratioText = defineFeature(function(context is Context, id is Id, definition is map)
    precondition
    {
        // Number to include in the (e.g.) "1:9" text
        annotation { "Name" : "Ratio" }
        isInteger(definition.ratio, POSITIVE_COUNT_BOUNDS);

        // Height of the text (font size)
        annotation { "Name" : "Text Height" }
        isLength(definition.textHeight, { (millimeter) : [0.5, 6, 20] } as LengthBoundSpec);

        // How deep to extrude the text into the object
        annotation { "Name" : "Depth of Text" }
        isLength(definition.depth, { (millimeter) : [0.01, 0.5, 5] } as LengthBoundSpec);

        // On which face should the text be shown?
        annotation { "Name" : "Face for Text", "Filter" : EntityType.FACE, "MaxNumberOfPicks" : 1 }
        definition.faceForText is Query;
    }
    {
        // Create a sketch on the given plane
        var sketch1 = newSketch(context, id + "sketch1", {
                "sketchPlane" : definition.faceForText
            });

        // Create the text, with the bottom-left corner at 0, 0 and of the correct height.
        // Ideally this would centre the text, but for now we'll put it there and then figure
        // out how to work out the text width and then move it by
        // (-textWidth/2.0, -definition.textHeight/2.0) afterwards
        var text = skText(sketch1, "textId", {
                "text" : "1:" ~ toString(definition.ratio),
                "fontName" : "NoboSans-Regular.ttf",
                "firstCorner" : vector(0, 0) * millimeter,
                "secondCorner" : vector(100 * millimeter, definition.textHeight)
            });

        // Now need to figure out how to move text such that it's centred in X & Y on 0, 0
        // not set with the bottom left on 0, 0.  First problem is I have no idea what the
        // width of the text is...
        
        
        skSolve(sketch1);
        
    });
I've spent ages playing around with evBox3D and suchlike but I either get lots of errors (if trying to use it on the text object) or the results don't seem to make any sense (if trying to use it on the resulting sketch).

Can anyone suggest what I'm doing wrong?  This feels like it should be a fairly simple thing to do.

Comments

  • kevin_o_toole_1kevin_o_toole_1 Onshape Employees, Developers, HDM Posts: 565
    I agree, evBox3d followed by an opTransform is what you will need.

    Things to keep in mind:
    1) Sketch geometry is not created until skSolve() is called, so you'll need to do the evBox3d(... qCreatedBy(id + "sketch1", EntityType.BODY)) after skSolve()
    2) evBox3d will also need a coordinate system input to make sure it's measuring the text bounds along the right axes. To pull this data out of your current code I'd recommend some changes small changes:
    // Create a sketch on the given plane
    var sketchPlane = evPlane(context, { "plane" : definition.faceForText });
    var sketch1 = newSketchOnPlane(context, id + "sketch1", {
            "sketchPlane" : sketchPlane
    });
    var cSys = cSys(sketchPlane);
    
    ...
    3) That cSys then becomes useful – you'll need it as an input to your evBox3d so you can figure out the extents of your text in a particular direction
    4) The final opTransform will also need to depend on your cSys. i.e. opTransform(... "transform" : transform( -cSys.xAxis * (bounds[0].maxCorner - bounds[0].minCorner) / 2)) should be the right direction and magnitude of a x-centering transform

    Finally, your current feature decides the x-axis of the sketch (i.e. the rotation) arbitrarily in newSketchOnPlane (and my changes decide it arbitrarily in evPlane()). It could easily be 90º off from what the user expects. If this is an issue you can give more control by adding an x-axis input, or (my preference), changing the input query to BodyType.MATE_CONNECTOR to let the user input the cSys directly. This way the user can click on the selected mate connector and change the rotation (or anything else) if they desire. With that adjustment, evPlane would need to be replaced by evMateConnector (which gives a cSys), and cSys = cSys(sketchPlane) would need to be replaced by sketchPlane = plane(cSys).

    Let us know if you have more questions!
  • alan_buddenalan_budden Member Posts: 7 ✭✭
    I agree, evBox3d followed by an opTransform is what you will need.

    Things to keep in mind:
    1) Sketch geometry is not created until skSolve() is called, so you'll need to do the evBox3d(... qCreatedBy(id + "sketch1", EntityType.BODY)) after skSolve()
    2) evBox3d will also need a coordinate system input to make sure it's measuring the text bounds along the right axes. To pull this data out of your current code I'd recommend some changes small changes:
    // Create a sketch on the given plane
    var sketchPlane = evPlane(context, { "plane" : definition.faceForText });
    var sketch1 = newSketchOnPlane(context, id + "sketch1", {
            "sketchPlane" : sketchPlane
    });
    var cSys = cSys(sketchPlane);
    
    ...
    3) That cSys then becomes useful – you'll need it as an input to your evBox3d so you can figure out the extents of your text in a particular direction
    4) The final opTransform will also need to depend on your cSys. i.e. opTransform(... "transform" : transform( -cSys.xAxis * (bounds[0].maxCorner - bounds[0].minCorner) / 2)) should be the right direction and magnitude of a x-centering transform

    Finally, your current feature decides the x-axis of the sketch (i.e. the rotation) arbitrarily in newSketchOnPlane (and my changes decide it arbitrarily in evPlane()). It could easily be 90º off from what the user expects. If this is an issue you can give more control by adding an x-axis input, or (my preference), changing the input query to BodyType.MATE_CONNECTOR to let the user input the cSys directly. This way the user can click on the selected mate connector and change the rotation (or anything else) if they desire. With that adjustment, evPlane would need to be replaced by evMateConnector (which gives a cSys), and cSys = cSys(sketchPlane) would need to be replaced by sketchPlane = plane(cSys).

    Let us know if you have more questions!
    That's extremely helpful thank you.  I take your point about the mate connector - that definitely sounds sensible for a generic function. For now I'm going to try to keep it simple as I'm only doing this for a specific model (in which the text will always be on the same face) - the only reasons I'm doing it with featurescript are:

    1. For practice
    2. To allow me to use the value of a variable in the text (which doesn't seem to be possible with the built-in text tools).
    With your help, it's very nearly working now. The only thing I can't figure out is why the extrude isn't working.  I've used opExtrude and extrude before with no real issues, but I'm drawing a blank here.  There are no error messages - it just says "Regeneration complete", but the sketch is still visible and hasn't been "cut" into the object.  Can you see what I'm missing?

    FeatureScript 1431;
    import(path : "onshape/std/geometry.fs", version : "1431.0");
    
    annotation { "Feature Type Name" : "RatioText" }
    export const ratioText = defineFeature(function(context is Context, id is Id, definition is map)
        precondition
        {
            // Number to include in the (e.g.) "1:9" text
            annotation { "Name" : "Ratio" }
            isInteger(definition.ratio, POSITIVE_COUNT_BOUNDS);
    
            // Height of the text (font size)
            annotation { "Name" : "Text Height" }
            isLength(definition.textHeight, { (millimeter) : [0.5, 6, 20] } as LengthBoundSpec);
    
            // How deep to extrude the text into the object
            annotation { "Name" : "Depth of Text" }
            isLength(definition.depth, { (millimeter) : [0.01, 0.5, 5] } as LengthBoundSpec);
    
            // On which face should the text be shown?
            annotation { "Name" : "Face for Text", "Filter" : EntityType.FACE, "MaxNumberOfPicks" : 1 }
            definition.faceForText is Query;
    
            annotation { "Name" : "Centre on Face" }
            definition.centreOnFace is boolean;
    
        }
        {
            // Create a sketch on the given plane
            var sketchPlane = evPlane(context, {
                    "face" : definition.faceForText
                });
            var sketch1 = newSketchOnPlane(context, id + "sketch1", {
                    "sketchPlane" : sketchPlane
                });
            var cSys = coordSystem(sketchPlane);
            // Create the text, with the bottom-left corner at 0, 0 and of the correct height.
            // Ideally this would centre the text, but for now we'll put it there and then figure
            // out how to work out the text width and then move it by
            // (-textWidth/2.0, -definition.textHeight/2.0) afterwards
    
            var firstCorner = vector(0, 0) * millimeter;
            var secondCorner = vector(100 * millimeter, definition.textHeight);
    
            if ( ! definition.centreOnFace)
            {
                var fc = fromWorld(cSys) * vector(0 * millimeter, 0 * millimeter, 0 * millimeter);
                var sc = fromWorld(cSys) * vector(100 * millimeter, definition.textHeight, 0*millimeter);
                firstCorner = vector(fc[0], fc[1]);
                secondCorner = vector(sc[0], sc[1]);
            }
    
            skText(sketch1, "textId", {
                        "text" : "1:" ~ toString(definition.ratio),
                        "fontName" : "NoboSans-Regular.ttf",
                        "firstCorner" : firstCorner,
                        "secondCorner" : secondCorner
                    });
    
            skSolve(sketch1);
    
            // Now need to figure out how to move text such that it's centred in X & Y on 0, 0
            // not set with the bottom left on 0, 0.  First problem is I have no idea what the
            // width of the text is...
    
            var bbox = evBox3d(context, {
                    "topology" : qCreatedBy(id + "sketch1", EntityType.BODY),
                    "cSys" : cSys,
                    "tight" : true
                });
    
            opTransform(context, id + "transform1", {
                        "bodies" : qCreatedBy(id + "sketch1", EntityType.BODY),
                        "transform" : transform(vector(
                                    -(bbox.maxCorner[0] - bbox.minCorner[0]) / 2.0,
                                    -(bbox.maxCorner[1] - bbox.minCorner[1]) / 2.0,
                                    0 * millimeter))
                    }); // Commenting out this transform doesn't make it work...
    
            var sketchQuery = qCreatedBy(id + "sketch1");  // I've tried replacing this with id + "transform1", but that just produces an error
            extrude(context, id + "extrude1", {
                        "entities" : qSketchRegion(id + "sketch1"),
                        "direction" : evOwnerSketchPlane(context, { "entity" : sketchQuery }).normal,
                        "endBound" : BoundingType.BLIND,
                        "depth" : definition.depth,
                        "operationType" : NewBodyOperationType.REMOVE,
                        "defaultScope" : true
                    });
        });
    

    Many thanks again
  • kevin_o_toole_1kevin_o_toole_1 Onshape Employees, Developers, HDM Posts: 565
    edited January 2021
    @alan_budden
    The code seems reasonable at first glance. Is it possible the extrude remove is simply pointing the wrong way? That would be fixed with "oppositeDirection" : true in the extrude. Note that "direction" is not a real parameter of extrude, so it's being ignored. It is a parameter of opExtrude, the more flexible underlying operation that extrude is based on, but that's not the function you're calling.

    If that's not it, sharing a link to a public document would make things easiest to debug.
  • alan_buddenalan_budden Member Posts: 7 ✭✭
    @alan_budden
    The code seems reasonable at first glance. Is it possible the extrude remove is simply pointing the wrong way? That would be fixed with "oppositeDirection" : true in the extrude. Note that "direction" is not a real parameter of extrude, so it's being ignored. It is a parameter of opExtrude, the more flexible underlying operation that extrude is based on, but that's not the function you're calling.

    If that's not it, sharing a link to a public document would make things easiest to debug.
    @kevin_o_toole_1

    Oh dear, that's embarrassing!  Changing the direction sorted it out.  Thank you!

    Final version in case other people come hunting for centred text and want to see it for some reason...

    FeatureScript 1431;
    import(path : "onshape/std/geometry.fs", version : "1431.0");
    
    annotation { "Feature Type Name" : "RatioText" }
    export const ratioText = defineFeature(function(context is Context, id is Id, definition is map)
        precondition
        {
            // Number to include in the (e.g.) "1:9" text
            annotation { "Name" : "Ratio" }
            isInteger(definition.ratio, POSITIVE_COUNT_BOUNDS);
    
            // Height of the text (font size)
            annotation { "Name" : "Text Height" }
            isLength(definition.textHeight, { (millimeter) : [0.5, 6, 20] } as LengthBoundSpec);
    
            // How deep to extrude the text into the object
            annotation { "Name" : "Depth of Text" }
            isLength(definition.depth, { (millimeter) : [0.01, 0.5, 5] } as LengthBoundSpec);
    
            // On which face should the text be shown?
            annotation { "Name" : "Face for Text", "Filter" : EntityType.FACE, "MaxNumberOfPicks" : 1 }
            definition.faceForText is Query;
    
            annotation { "Name" : "Centre on Face" }
            definition.centreOnFace is boolean;
    
        }
        {
            // Create a sketch on the given plane
            var sketchPlane = evPlane(context, {
                    "face" : definition.faceForText
                });
            var sketch1 = newSketchOnPlane(context, id + "sketch1", {
                    "sketchPlane" : sketchPlane
                });
            var cSys = coordSystem(sketchPlane);
            // Create the text, with the bottom-left corner at 0, 0 and of the correct height.
            // Ideally this would centre the text, but for now we'll put it there and then figure
            // out how to work out the text width and then move it by
            // (-textWidth/2.0, -definition.textHeight/2.0) afterwards
    
            var firstCorner = vector(0, 0) * millimeter;
            var secondCorner = vector(100 * millimeter, definition.textHeight);
    
            if ( ! definition.centreOnFace)
            {
                var fc = fromWorld(cSys) * vector(0 * millimeter, 0 * millimeter, 0 * millimeter);
                var sc = fromWorld(cSys) * vector(100 * millimeter, definition.textHeight, 0*millimeter);
                firstCorner = vector(fc[0], fc[1]);
                secondCorner = vector(sc[0], sc[1]);
            }
    
            skText(sketch1, "textId", {
                        "text" : "1:" ~ toString(definition.ratio),
                        "fontName" : "NoboSans-Regular.ttf",
                        "firstCorner" : firstCorner,
                        "secondCorner" : secondCorner
                    });
    
            skSolve(sketch1);
    
            // Now need to figure out how to move text such that it's centred in X & Y on 0, 0
            // not set with the bottom left on 0, 0.  First problem is I have no idea what the
            // width of the text is...
    
            var bbox = evBox3d(context, {
                    "topology" : qCreatedBy(id + "sketch1", EntityType.BODY),
                    "cSys" : cSys,
                    "tight" : true
                });
    
            opTransform(context, id + "transform1", {
                        "bodies" : qCreatedBy(id + "sketch1", EntityType.BODY),
                        "transform" : transform(vector(
                                    -(bbox.maxCorner[0] - bbox.minCorner[0]) / 2.0,
                                    -(bbox.maxCorner[1] - bbox.minCorner[1]) / 2.0,
                                    0 * millimeter))
                    });
    
            var sketchQuery = qCreatedBy(id + "sketch1");
            extrude(context, id + "extrude1", {
                        "entities" : qSketchRegion(id + "sketch1"),
                        "endBound" : BoundingType.BLIND,
                        "depth" : definition.depth,
                        "oppositeDirection": true,
                        "operationType" : NewBodyOperationType.REMOVE,
                        "defaultScope" : true
                    });
                    
            opDeleteBodies(context, id + "deleteSketch", { "entities": qCreatedBy(id + "sketch1") });
        });
    


  • MBartlett21MBartlett21 Member, OS Professional, Developers Posts: 2,034 EDU
    @alan_budden

    I find that generally using `extrude` rather than `opExtrude` is also a good way to go to add sheet metal compatibility, since `opBoolean` can't cut sheet metal, and you also get the cutting in `extrude` easier, like you have done.
    mb - draftsman - also FS author: View FeatureScripts
    IR for AS/NZS 1100
  • Frank_AruteFrank_Arute Member, csevp Posts: 3 PRO
    Not to dig this post up from the grave, but thank you all for the helpful posts in this thread and @alan_budden for posting the final version. This saved me hours today.
Sign In or Register to comment.