vcl: introduce ViewSubPixelToLogicInt() wrappers to CoordinateMapper

This commit completes the inverse sub-pixel pipeline by adding the
ViewSubPixelToLogicIntX/Y() master wrappers and their corresponding
LogicUnits pipeline stages.

Unit testing revealed that the order of operations in SubPixelToLogic
is mathematically sensitive: the floating-point View distance must
be rounded to an integer (llround) BEFORE any integer offsets
(mnMapOfs or mnOutOffLogic) are subtracted. Shifting the rounding
boundary by applying offsets in floating-point space leads to
off-by-one errors for fractional coordinates near zero-crossing points.

Key changes:
- Added ViewSubPixelToLogicUnitsIntX/Y(): Pipeline stages that round
  scaled distances to tools::Long before stripping the mapping offset.
- Added ViewSubPixelToLogicIntX/Y(): Master wrappers that provide the
  full "Round-then-Shift" journey from View sub-pixels to Absolute
  Logic integers.
- Refactored OutputDevice::SubPixelToLogic() in map.cxx to use these
  new wrappers, successfully encapsulating the manual offset
  arithmetic while maintaining exact legacy rounding behavior.

Change-Id: Icecd9a2f5f75cb5353e6c7d9431be22c0889e968
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/203671
Reviewed-by: Dan Williams <dcbw@libreoffice.org>
Tested-by: Jenkins
This commit is contained in:
Christopher Sherlock
2026-09-16 19:00:46 +02:00
committed by Dan Williams
parent 82c68a6f63
commit 5970e72c67
3 changed files with 36 additions and 2 deletions
+6
View File
@@ -167,6 +167,8 @@ public:
// View <-> LogicUnits (Sub-pixel)
double ViewSubPixelToLogicUnitsX(double fX) const;
double ViewSubPixelToLogicUnitsY(double fY) const;
tools::Long ViewSubPixelToLogicUnitsIntX(double fX) const;
tools::Long ViewSubPixelToLogicUnitsIntY(double fY) const;
double LogicUnitsToViewSubPixelX(double fX) const;
double LogicUnitsToViewSubPixelY(double fY) const;
@@ -203,6 +205,10 @@ public:
// To resolve the return-type conflict, these now return double
double ViewSubPixelToLogicX(double fX) const;
double ViewSubPixelToLogicY(double fY) const;
// View (Sub-pixel) -> Absolute Logic (Integer)
// Note: This rounds the distance before stripping offsets to satisfy legacy test parity.
tools::Long ViewSubPixelToLogicIntX(double fX) const;
tools::Long ViewSubPixelToLogicIntY(double fY) const;
double LogicToViewSubPixelX(double fX) const;
double LogicToViewSubPixelY(double fY) const;
+28
View File
@@ -260,6 +260,18 @@ double CoordinateMapper::ViewSubPixelToLogicUnitsY(double fY) const
return ViewToLogicDistanceDoubleY(fY, maMapRes.mfMapScY) - maMapRes.mnMapOfsY;
}
tools::Long CoordinateMapper::ViewSubPixelToLogicUnitsIntX(double fX) const
{
// Uses the version that rounds the distance BEFORE the offset shift
return ViewSubPixelToLogicDistanceX(fX) - maMapRes.mnMapOfsX;
}
tools::Long CoordinateMapper::ViewSubPixelToLogicUnitsIntY(double fY) const
{
// Uses the version that rounds the distance BEFORE the offset shift
return ViewSubPixelToLogicDistanceY(fY) - maMapRes.mnMapOfsY;
}
double CoordinateMapper::LogicUnitsToViewSubPixelX(double fX) const
{
return LogicToViewDistanceSubPixelX(std::llround(fX + maMapRes.mnMapOfsX), maMapRes.mfMapScX);
@@ -582,6 +594,22 @@ double CoordinateMapper::ViewSubPixelToLogicY(double fY) const
return fLogicUnits - static_cast<double>(mnOutOffLogicY);
}
tools::Long CoordinateMapper::ViewSubPixelToLogicIntX(double fX) const
{
// Move from View to Logic Units (Rounds distance, then strips mnMapOfs)
const tools::Long nLogicUnits = ViewSubPixelToLogicUnitsIntX(fX);
return nLogicUnits - mnOutOffLogicX;
}
tools::Long CoordinateMapper::ViewSubPixelToLogicIntY(double fY) const
{
// Move from View to Logic Units (Rounds distance, then strips mnMapOfs)
const tools::Long nLogicUnits = ViewSubPixelToLogicUnitsIntY(fY);
return nLogicUnits - mnOutOffLogicY;
}
// Absolute Logic -> View (Forward: Add Logical -> Add Mapping/Scale)
double CoordinateMapper::LogicToViewSubPixelX(double fX) const
{
+2 -2
View File
@@ -696,8 +696,8 @@ Point OutputDevice::SubPixelToLogic(const basegfx::B2DPoint& rDevicePt) const
return Point(rDevicePt.getX(), rDevicePt.getY());
}
return Point(mpMapper->ViewSubPixelToLogicDistanceX(rDevicePt.getX()) - mpMapper->GetMappingXOffset() - mpMapper->GetLogicalXOffset(),
mpMapper->ViewSubPixelToLogicDistanceY(rDevicePt.getY()) - mpMapper->GetMappingYOffset() - mpMapper->GetLogicalYOffset());
return Point(mpMapper->ViewSubPixelToLogicIntX(rDevicePt.getX()),
mpMapper->ViewSubPixelToLogicIntY(rDevicePt.getY()));
}
Size OutputDevice::PixelToLogic( const Size& rDeviceSize ) const