`DisplayDimension` bounding box with API

I’m going to ask this question without much expectation that there is an answer. Can you get an accurate bounding box for the string of a DisplayDimension using the API? I’ve tried all the obvious calls with no luck. DisplayDimension.GetAnnotation().GetPosition() gives me a point in the center of the text but offset downwards by a value that I can’t figure out. Is this y-shift linked to any combination of properties in Document Settings?
Note that if you read the docs, there are 3 separate API calls that claim to return the top-left corner of the string. NONE OF THEM DO: they all return the bottom center - yshift. Without knowing how this yshift is calculated (because it’s different per font size, and not proportional), I can’t get a really accurate bounding box.

From what I remember, this indeed does not exist. Can we request one and sunset the broken APIs?

1 Like

Please free to send to post the code. We appreciate your hardwork!

1 Like

Follow-up: I should have specified that if the dimension is not rectilinear, the returned RectangleF isn’t going to hug the angled borders of the dimension, it will remain rectilinear but shrunk down to the min and max points. This function does indeed find the corners of the angled bounding box (and can draw it as such), but returning a RectangleF doesn’t allow for doing point-containment or overlap checks of it.
If an angled bounding box is desired, this function (or a copy) could return an array of the four computed Vec2 points instead, which could be passed to a separate function to test the point-containment of a rotated rectangle. I’ve written such a routine in C++ before, but it would take some translation and adaptation.

Just confirming my hunch that on a SWX Sheet, you have to give RectangleF different arguments than it asks for. You have to give it the bottom left corner instead of the top left, because RectangleF expects y to grown downward and Sheet is the opposite.

I encountered a similar problem when dealing with this problem. It was related to font height and font width, but the calculated result was not very accurate. Fortunately, the font I used was fixed. I used GetTextInBoxHeightAtIndex and GetTextInBoxWidthAtIndex to offset the value obtained by GetPosition.

1 Like

If the API calls return values are trash, you can get the screen coordinates of the bounding box and translate that to the coordinate system of the sheet. Unfortunately, this requires that SW is visible.

Food for thought.

1 Like

I’ve got a pretty workable solution for this problem. I believed that the y-shift was not proportional because I was trying to factor the line spacing value into things, but it does not seem to be in play, at least for simple dimensions.
It turns out that this y-shift is actually very nearly proportional to the font size, at least for most common fonts.
I’ve now got a bounding box that is nearly pixel-perfect for Arial Narrow and some other common fonts. If anyone is interested (@AmenJlili) I can post code for a full getBoundingBox(DisplayDimension) routine. But here are the key points:

  • pos = GetAnnotation().GetPosition()
  • bottomLeft = DisplayDimension.GetDisplayData().GetTextPositionAtIndex(0)
  • multiply GetAnnotation().GetTextFormat(0).CharHeightInPts by .000064
  • add this product to the y value of pos. This is very nearly the bottom pixel border of the dimension text. From there you add GetTextHeightAtIndex(0) and whatever padding you want for your use case. For most fonts that I tested, the y values of the box were almost perfect across the board.
  • the width is slightly more finicky, but still accurate for lots of fonts and only a couple pixels off for some fonts. The constant value in play here is .0001… except when the font size is 17 or greater, then the value is .000011!
    Here’s what you do: get the x distance between pos and bottomLeft; from this, subtract CharHeightInPts times the appropriate constant. Double this result, and you have basically a pixel-perfect width for Arial Narrow and some other common faces. (As I’m writing this I realize I never tried bold cases.) Scarcely any font faces strayed more than a pixel outside of the width box, though my test set was by no means exhaustive.
    One more asterisk about the width: at least one font face did not switch which constant to use above size 16… it seemed to always use .0001. If this topic ever became especially important to anyone, more comprehensive testing could be done per font, and the font face could be queried through the TextFormat object before determining what values to use to create the box.

The bottom line is that for most common fonts, these three fixed values yield almost perfect results. The results hold when the text is rotated at an angle.
In my own implementation I created a lightweight 2D point class which supports vector arithmetic and easy conversion from rectangular to polar vectors, to represent x-y Sheet coordinates. Then, I calculate the corners of the bounding box as polar vectors from pos plus the angle of rotation, so that when the text is rotated, you never need to recompute your x and y values. You can pass x-y Sheet coordinates to the routine, and even if the dimension is a rotated one, you don’t have to do any extra work to make sure the bounding box rotates with it.

After describing all the quirkiness about trying to nail down the right width, I forgot that I was going to mention a person could probably also get good results with TextRenderer.MeasureText. I haven’t gone down that road yet, because I’ve got to keep moving forward and I think what I’ve got does the job I need.
Here’s the code, slightly altered. You’ll need to paste different parts of it to appropriate destinations to use it. Also, you’ll see at the end that I haven’t gotten as far yet as verifying which way a RectangleF falls, although that’s probably on my docket for tomorrow.
Not all of the GeomFuncs are required for this code, but I didn’t take the time to strip out the ones that aren’t used.



/*
 * Methods GetDDimBoundingBox and SketchLine need to be placed in
 * appropriate class for use case. Classes Vec2 and GeomFuncs 
 * need to be made available
 */


using System;
using static System.Math;
using static GeomFuncs;
    

    /* Return RectangleF so we can immediately use methods for checking
     * to contain a point or intersecting with another rectangle
     */
public static RectangleF GetDDimBoundingBox (DisplayDimension dispDim)
{
	TextFormat tfmt = dispDim.GetAnnotation().GetTextFormat(0);

	DisplayData ddata = dispDim.GetDisplayData();
	double[] annPos = dispDim.GetAnnotation().GetPosition();
	double[] ddatPos = ddata.GetTextPositionAtIndex(0);
	double ang = ToDeg(ddata.GetTextAngleAtIndex(0));
	double hgt = ddata.GetTextHeightAtIndex(0);
	int fontSize = tfmt.CharHeightInPts;

		/* Width adjustment to be subtracted from each side */
	double wadj = fontSize * (fontSize > 16 ? .000011 : .0001);
		/* Air space to be added around text */
	double pad = 0; // .0003 for a slight buffer;
		/* Height shift from GetPosition to string bottom */
	double hshift = fontSize * .000064; 
		/* String width plus any padding */
	double wid = Hyp(new Vec2(ddatPos), new Vec2(annPos)) * 2 - (wadj * 2) + (pad * 2);
		/* Dist from GetPosition to top of bound box incl. any padding */
	double hhh = hshift + hgt + pad;
		/* "  " to bottom " " */
	double hh = hshift - pad;
		/* Half of wid */
	double hwid = wid / 2;

		/* Points representing corners of the final bounding box. All derived as 
		  * GetPosition plus the appropriate polar vector, to allow for rotations
		  */
	Vec2 tl = new Vec2(annPos) +
		new Vec2(ToRect(Hyp(-hwid, hhh), Atan2Dg(hhh, -hwid) + ang));
	Vec2 tr = new Vec2(annPos) +
		new Vec2(ToRect(Hyp(hwid, hhh), Atan2Dg(hhh, hwid) + ang));
	Vec2 bl = new Vec2(annPos) +
		new Vec2(ToRect(Hyp(-hwid, hh), Atan2Dg(hh, -hwid) + ang));
	Vec2 br = new Vec2(annPos) +
		new Vec2(ToRect(Hyp(hwid, hh), Atan2Dg(hh, hwid) + ang));

		/* Uncomment to debug draw the bounding box */
        // In my implementation SketchLine was part of a drawing 
        // utilities class that already had access to App and current Sheet, 
        // and only needed the two points. For this, app and sheet will need to be 
        // obtained and passed
	//SketchLine(App, curSheet, tl, tr);
	//SketchLine(App, curSheet, tr, br);
	//SketchLine(App, curSheet, br, bl);
	//SketchLine(App, curSheet, bl, tl);


		/* Conversions to use RectangleF */
	float minx, maxx, miny, maxy;
	Vec2[] allPts = new Vec2[] { tl, tr, bl, br };
	minx = (float)allPts.Min(v => v.x);
	maxx = (float)allPts.Max(v => v.x);
	miny = (float)allPts.Min(v => v.y);
	maxy = (float)allPts.Max(v => v.y);

	    //test orientation of rect: increasing y goes up or down?
	return new RectangleF(minx, miny, maxx - minx, maxy - miny);
	//return new RectangleF(minx, maxy, maxx - minx, maxy - miny);
}


    
public static SketchSegment SketchLine (SldWorks App, Sheet curSheet, Vec2 start, Vec2 end)
{
    return SketchLine(start.x, start.y, end.x, end.y);
}
public static SketchSegment SketchLine (SldWorks App, Sheet curSheet, double x1, double y1, double x2, double y2)
{
	ModelDoc2 model = (ModelDoc2)App.ActiveDoc;
	SketchManager skMgr = model.SketchManager;
	DrawingDoc drw = (DrawingDoc)model;
    double scale = curSheet.GetProperties2()[3] / curSheet.GetProperties2()[2];
        /* Draw in Sheet space */
	drw.ActivateView("");
        /* Without setting AddToDB, the SketchManager might switch us to View
         * coordinates if we draw too close to an Edge of the View
         */
	bool savedVal = skMgr.AddToDB;
	try {
		skMgr.AddToDB = true;
		return skMgr.CreateLine(x1 * scale, y1 * scale, 0, x2 * scale, y2 * scale, 0);
	}
	finally {
		skMgr.AddToDB = savedVal;
	}
}


    /* Represent a 2D point (such as Sheet coordinates),  
        * with operations for vector arithmetic 
        */
public class Vec2
{
    public double x;
    public double y;

        /* Primary constructor */
    public Vec2 (double x_, double y_)
    {
        x = x_;
        y = y_;
    }

		/* Construct from SWX double array */
	public Vec2 (double[] da)
	{
            /* Assume `da` is [x,y] or [x,y,z] */
		x = da[0];
		y = da[1];
	}

		/* Default constructor */
	public Vec2 () : this(0, 0) { }

        /* Copy constructor */
    public Vec2 (Vec2 v) : this(v.x, v.y) { }

        /* Operator overloads */
    public static Vec2 operator + (Vec2 v1, Vec2 v2)
    {
        return new Vec2(v1.x + v2.x, v1.y + v2.y);
    }

    public static Vec2 operator - (Vec2 v1, Vec2 v2)
    {
        return new Vec2(v1.x - v2.x, v1.y - v2.y);
    }

    public static Vec2 operator * (Vec2 v, double s)
    {
        return new Vec2(v.x * s, v.y * s);
    }

	public static Vec2 operator * (double s, Vec2 v)
	{
		return new Vec2(v.x * s, v.y * s);
	}

	public static Vec2 operator / (Vec2 v, double s)
    {
        if (s == 0)
            return v;   // ignore the operation
        return new Vec2(v.x / s, v.y / s);
    }

    public static bool operator == (Vec2 v1, Vec2 v2)
    {
        return v1.x == v2.x && v1.y == v2.y;
    }

    public static bool operator != (Vec2 v1, Vec2 v2)
    {
        return !(v1 == v2);
    }

        /* Debug printing */
    public override string ToString ()
    {
        return $"Vec2({x}, {y})";
    }
}

	
    /* Collection of math and geometry functions, 
        * relying on Vec2 to represent points
        */
public static class GeomFuncs
{
    public const double degsPerRad = 180 / PI; // ≈57.297

        /* Default error margin for comparing double equality */
    public const double epsilon = .00000001;

        /* Canonicalize degrees: 0 <= deg < 360 */
    public static double Czdg (double deg)
    {
        double ret = deg % 360;
        if (ret < 0)
            ret += 360;
        return ret;
    }

        /* Degree/radian conversions */
    public static double ToDeg (double rad)
    {
        return Czdg(rad * degsPerRad);
    }

    public static double ToRad (double deg)
    {
        return Czdg(deg) / degsPerRad;
    }

        /* Trigonometric functions using degrees */
    public static double CosDg (double ang)
    {
        return Cos(ToRad(ang));
    }

    public static double SinDg (double ang)
    {
        return Sin(ToRad(ang));
    }

    public static double TanDg (double ang)
    {
        return Tan(ToRad(ang));
    }

    public static double Atan2Dg (double x, double y)
    {
        return ToDeg(Atan2(x, y));
    }

        /* Get distance between two points */
    public static double Hyp (Vec2 v1, Vec2 v2)
    {
        double X = v2.x - v1.x;
        var Y = v2.y - v1.y;
        return Abs(Sqrt(X * X + Y * Y));
    }

        /* Calculate hypotenuse of two right-angle sides */
    public static double Hyp (Vec2 v)
    {
        return Abs(Sqrt((v.x * v.x) + (v.y * v.y)));
    }

    public static double Hyp (double x, double y)
    {
        return Abs(Sqrt((x * x) + (y * y)));
    }

        /* Convert polar coordinates to rectangular, expecting degrees */
    public static Vec2 ToRect (Vec2 vec)     // x is magnitude, y is direction in degrees
    {
        Vec2 ret = new Vec2(Cos(ToRad(vec.y)) * vec.x, Sin(ToRad(vec.y)) * vec.x);
        if (Abs(vec.y) == 90 || Abs(vec.y) == 270)
            ret.x = 0;
        else if (Abs(vec.y) == 0 || Abs(vec.y) == 180)
            ret.y = 0;
        if (Abs(vec.x) < epsilon) {
            ret.x = 0;
            ret.y = 0;
        }
        return ret;
    }

    public static Vec2 ToRect (double mag, double direc)
    {

        Vec2 ret = new Vec2(CosDg(direc) * mag, SinDg(direc) * mag);
        if (Abs(direc) == 90 || Abs(direc) == 270)
            ret.x = 0;
        else if (Abs(direc) == 0 || Abs(direc) == 180)
            ret.y = 0;
        if (Abs(mag) < epsilon) {
            ret.x = 0;
            ret.y = 0;
        }
        return ret;
    }

        /* To rectangular coordinates given a vector in radians */
    public static Vec2 ToRectFromRads (double mag, double direc)
    {
        Vec2 ret = new Vec2(Cos(direc) * mag, Sin(direc) * mag);
        if (Abs(mag) < epsilon) {
            ret.x = 0;
            ret.y = 0;
        }
        return ret;
    }

        /* Convert rectangular coordinates to polar, yielding degrees */
    public static Vec2 ToPolar (Vec2 vec)
    {
        double mag = Hyp(vec.x, vec.y);
        double ang = Atan2(vec.y, vec.x);
        double direc = ToDeg(ang);
        if (direc < 0)
            direc += 360;
        return new Vec2(mag, direc);
    }

    public static Vec2 ToPolar (double x, double y)
    {
        double mag = Hyp(x, y);
        double ang = Atan2(y, x);
        double direc = ToDeg(ang);
        if (direc < 0)
            direc += 360;
        return new Vec2(mag, direc);
    }

        /* Convert to polar yielding radians */
    public static Vec2 ToPolarWithRads (Vec2 vec)
    {
        double mag = Hyp(vec.x, vec.y);
        double direc = Atan2(vec.y, vec.x);
        return new Vec2(mag, direc);
    }

    public static Vec2 PVec2 (double mag, double direc)
    {
        return new Vec2(ToRect(mag, direc));
    }

        /* Epsilon-equals: compare doubles for equality with  
            * tolerance for minor representational inaccuracies
            */
    public static bool EpsEquals (double a, double b, double eps = epsilon)
    {
        if (double.IsNaN(a))
            return double.IsNaN(b);
            /* Not distinguishing direction of infinity for sake
                * of collinearity checks: put in separate method?
                */
        if (double.IsInfinity(a))
            return double.IsInfinity(b);
        return Abs(b - a) < eps;
    }
}