ON_Viewport::GetWorldToScreenScale pixel scaling issue in high DPI display

Hi,

I have an issue related to High DPI display and I don’t find a solution in the guide.

I have a getter derived from CRhinoGetPoint in which I dynamically draw a filled circle of a fixed real distance. For that I use CRhinoDisplayPipeline::DrawPoint as there’s no function to draw a filled circle from ON_Circle.
I also draw a non-filled circle of the same radius, with CRhinoDisplayPipeline::DrawCircle.
In this picture I draw the inner part in grey and the outer part in black with the same center (note, this is just an example as actually I have to draw different filled/non-filled circles with different centers but same radius):

The problem is that in High DPI displays there’s a mismatch of radius: the filled circle (whose radius is defined in pixels in the SDK) is way bigger than the non-filled circle. I’ve been reported this by a customer using a 4K monitor; unfortunately I don’t have such a monitor but I know there’s a problem there.

Please take a look on how to fix this, here’s the sample:
cmdSampleGetPointPixelCircle.cpp (2.7 KB)
The getter:

class GetPointPixelCircle : public CRhinoGetPoint
{
public:
	GetPointPixelCircle(double circleRadius);
	void DynamicDraw(CRhinoDisplayPipeline& dp, const ON_3dPoint& pt);
private:
	double m_circleRadius;
	ON_Color m_innerColor, m_outerColor;
	int m_thickness;
};

GetPointPixelCircle::GetPointPixelCircle(double circleRadius)
{
	m_circleRadius = circleRadius;
	m_innerColor = ON_Color::Gray230;
	m_outerColor = ON_Color::Black;
	m_thickness = 10;
}

void GetPointPixelCircle::DynamicDraw(CRhinoDisplayPipeline& dp, const ON_3dPoint& pt)
{
	int circlePixelRadius;
	const CRhinoViewport* vp = dp.GetRhinoVP();
	if (nullptr != vp) 
	{
		double pixelsPerUnit = 0.0;
		if (vp->View().m_vp.GetWorldToScreenScale(vp->View().TargetPoint(), &pixelsPerUnit)) 
		{
			// Draw inner part
			circlePixelRadius = (int)(m_circleRadius * pixelsPerUnit);
			dp.DrawPoint(pt, circlePixelRadius, ERhinoPointStyle::RPS_CIRCLE, m_innerColor);

			// Draw outer part
			const ON_Plane& plane = vp->ConstructionPlane().m_plane;
			ON_Circle circle(plane, pt, m_circleRadius);
			dp.DrawCircle(circle, m_outerColor, m_thickness);
		}
	}

	CRhinoGetPoint::DynamicDraw(dp, pt);
}

I think the issue is about the conversion of the real radius to screen pixel radius through ON_Viewport::GetWorldToScreenScale. I took that part of code from @dale’s SampleMarker.

Thank you,
Pablo

Hi @pagarcia,

See this:

– Dale

Thank you @dale, I see CRhinoDpi::Scale(int x) is what I need. Though I don’t know exactly when Rhino does the scaling and when I have to do it on my own.

For example, when using your sample cmdSampleViewportDecoration:

// Font enumeration callback
int CALLBACK EnumFontFamiliesExCallback(ENUMLOGFONTEX *lpelfe, NEWTEXTMETRICEX *lpntme, int FontType, LPARAM lParam)
{
  UNREFERENCED_PARAMETER(lpntme);
  UNREFERENCED_PARAMETER(FontType);

  if (0 != lpelfe && 0 != lParam)
  {
    ON_wString str(lpelfe->elfFullName);
    ON_ClassArray<ON_wString>* pFontFaces = (ON_ClassArray<ON_wString>*)lParam;
    pFontFaces->Append(str);
  }
  return 1;
}

class CSampleViewportDecorationConduit : public CRhinoDisplayConduit
{
public:
  CSampleViewportDecorationConduit();
  ~CSampleViewportDecorationConduit() = default;

  // CRhinoDisplayConduit override
  bool ExecConduit(CRhinoDisplayPipeline& dp, UINT nChannelID, bool& bTerminate);

private:
  const wchar_t* m_default_font_face;
  ON_ClassArray<ON_wString> m_font_faces;
};

CSampleViewportDecorationConduit::CSampleViewportDecorationConduit()
  : CRhinoDisplayConduit(CSupportChannels::SC_DRAWFOREGROUND)
  , m_default_font_face(L"Arial")
{
  // Find some font faces to draw with
  LOGFONT lf;
  memset(&lf, 0, sizeof(lf));
  wcscpy_s(lf.lfFaceName, LF_FACESIZE, m_default_font_face);
  lf.lfCharSet = ANSI_CHARSET;

  HDC hDC = ::GetDC(0);
  EnumFontFamiliesEx(hDC, &lf, (FONTENUMPROC)EnumFontFamiliesExCallback, (LPARAM)&m_font_faces, 0);
  ReleaseDC(0, hDC);
}

bool CSampleViewportDecorationConduit::ExecConduit(CRhinoDisplayPipeline& dp, UINT nChannelID, bool& bTerminate)
{
  UNREFERENCED_PARAMETER(bTerminate);

  // Get the active view
  CRhinoView* view = RhinoApp().ActiveView();
  if (0 == view)
    return true;

  // Get the viewport that we are currently drawing in
  CRhinoViewport* vp = dp.GetRhinoVP();
  if (nullptr == vp || view->ActiveViewportID() != vp->ViewportId())
    return true;

  if (nChannelID == CSupportChannels::SC_DRAWFOREGROUND)
  {
    // String to draw
    const wchar_t* str = L"Hello Rhino!";
    const ON_Color color(0, 0, 0);

    // Determine rect of text string
    const bool bMiddle = false;
    const int height = 12;
    ON_4iRect rect;
    dp.MeasureString(rect, str, ON_2dPoint(0, 0), bMiddle, height, m_default_font_face);

    // Use the screen port to determine text location
    int vp_left, vp_right, vp_top, vp_bottom;
    vp->VP().GetScreenPort(&vp_left, &vp_right, &vp_bottom, &vp_top);
    int vp_width = vp_right - vp_left;
    int vp_height = vp_bottom - vp_top;

    // Make sure text will fit on string
    const int x_gap = 6;
    const int y_gap = 6;
    if (rect.Width() + (2 * x_gap) < vp_width || rect.Height() + (2 * y_gap) < vp_height)
    {
      // Cook up text location (lower right corner of viewport)
      ON_2dPoint point(vp_right - rect.Width() - x_gap, vp_bottom - y_gap);

      // Draw text
      if (0 == m_font_faces.Count())
        dp.DrawString(str, color, point, bMiddle, height, m_default_font_face);
      else
      {
        for (int i = 0; i < m_font_faces.Count(); i++)
        {
          dp.DrawString(str, color, point, bMiddle, height, m_font_faces[i]);
          point.y -= 20.0;
        }
      }
    }
  }

  return true;
}

Inside ExecConduit:

  1. When calling dp.MeasureString(rect, str, ON_2dPoint(0, 0), bMiddle, height, m_default_font_face); does height have to be scaled before being entered as argument? And is the output parameter rect already scaled or not?

  2. When calling vp->VP().GetScreenPort(&vp_left, &vp_right, &vp_bottom, &vp_top); are output parameters vp_left, vp_right, vp_bottom, vp_top already scaled by the SDK or should I do it instead?

  3. When calling dp.DrawString(str, color, point, bMiddle, height, m_font_faces[i]); should I also scale first the two coordinates of parameter point?

Pablo