msdf-atlas-gen/msdf-atlas-gen/GridAtlasPacker.cpp

650 lines
25 KiB
C++

#include "GridAtlasPacker.h"
#include <algorithm>
namespace msdf_atlas {
static int floorPOT(int x) {
int y = 1;
while (x >= y && y<<1)
y <<= 1;
return y>>1;
}
static int ceilPOT(int x) {
int y = 1;
while (x > y && y<<1)
y <<= 1;
return y;
}
static bool squareConstraint(DimensionsConstraint constraint) {
switch (constraint) {
case DimensionsConstraint::SQUARE:
case DimensionsConstraint::EVEN_SQUARE:
case DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE:
case DimensionsConstraint::POWER_OF_TWO_SQUARE:
return true;
case DimensionsConstraint::NONE:
case DimensionsConstraint::POWER_OF_TWO_RECTANGLE:
return false;
}
return true;
}
void GridAtlasPacker::lowerToConstraint(int &width, int &height, DimensionsConstraint constraint) {
if (squareConstraint(constraint))
width = height = std::min(width, height);
switch (constraint) {
case DimensionsConstraint::NONE:
case DimensionsConstraint::SQUARE:
break;
case DimensionsConstraint::EVEN_SQUARE:
width &= ~1;
height &= ~1;
break;
case DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE:
width &= ~3;
height &= ~3;
break;
case DimensionsConstraint::POWER_OF_TWO_RECTANGLE:
case DimensionsConstraint::POWER_OF_TWO_SQUARE:
if (width > 0)
width = floorPOT(width);
if (height > 0)
height = floorPOT(height);
break;
}
}
void GridAtlasPacker::raiseToConstraint(int &width, int &height, DimensionsConstraint constraint) {
if (squareConstraint(constraint))
width = height = std::max(width, height);
switch (constraint) {
case DimensionsConstraint::NONE:
case DimensionsConstraint::SQUARE:
break;
case DimensionsConstraint::EVEN_SQUARE:
width += width&1;
height += height&1;
break;
case DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE:
width += -width&3;
height += -height&3;
break;
case DimensionsConstraint::POWER_OF_TWO_RECTANGLE:
case DimensionsConstraint::POWER_OF_TWO_SQUARE:
if (width > 0)
width = ceilPOT(width);
if (height > 0)
height = ceilPOT(height);
break;
}
}
double GridAtlasPacker::dimensionsRating(int width, int height, bool aligned) const {
return ((double) width*width+(double) height*height)*(aligned ? 1-alignedColumnsBias : 1);
}
GridAtlasPacker::GridAtlasPacker() :
columns(-1), rows(-1),
width(-1), height(-1),
cellWidth(-1), cellHeight(-1),
spacing(0),
dimensionsConstraint(DimensionsConstraint::NONE),
cellDimensionsConstraint(DimensionsConstraint::NONE),
hFixed(false), vFixed(false),
scale(-1),
minScale(1),
fixedX(0), fixedY(0),
unitRange(0),
pxRange(0),
miterLimit(0),
pxAlignOriginX(false), pxAlignOriginY(false),
scaleMaximizationTolerance(.001),
alignedColumnsBias(.125),
cutoff(false)
{ }
msdfgen::Shape::Bounds GridAtlasPacker::getMaxBounds(double &maxWidth, double &maxHeight, GlyphGeometry *glyphs, int count, double scale, double outerRange) const {
static const double LARGE_VALUE = 1e240;
msdfgen::Shape::Bounds maxBounds = { +LARGE_VALUE, +LARGE_VALUE, -LARGE_VALUE, -LARGE_VALUE };
for (GlyphGeometry *glyph = glyphs, *end = glyphs+count; glyph < end; ++glyph) {
if (!glyph->isWhitespace()) {
double geometryScale = glyph->getGeometryScale();
double shapeOuterRange = outerRange/geometryScale;
geometryScale *= scale;
const msdfgen::Shape::Bounds &shapeBounds = glyph->getShapeBounds();
double l = shapeBounds.l, b = shapeBounds.b, r = shapeBounds.r, t = shapeBounds.t;
l -= shapeOuterRange, b -= shapeOuterRange;
r += shapeOuterRange, t += shapeOuterRange;
if (miterLimit > 0)
glyph->getShape().boundMiters(l, b, r, t, shapeOuterRange, miterLimit, 1);
l *= geometryScale, b *= geometryScale;
r *= geometryScale, t *= geometryScale;
maxBounds.l = std::min(maxBounds.l, l);
maxBounds.b = std::min(maxBounds.b, b);
maxBounds.r = std::max(maxBounds.r, r);
maxBounds.t = std::max(maxBounds.t, t);
maxWidth = std::max(maxWidth, r-l);
maxHeight = std::max(maxHeight, t-b);
}
}
if (maxBounds.l >= maxBounds.r || maxBounds.b >= maxBounds.t)
maxBounds = msdfgen::Shape::Bounds();
Padding fullPadding = scale*(innerUnitPadding+outerUnitPadding)+innerPxPadding+outerPxPadding;
pad(maxBounds, fullPadding);
maxWidth += fullPadding.l+fullPadding.r;
maxHeight += fullPadding.b+fullPadding.t;
// If origin is pixel-aligned but not fixed, one pixel has to be added to max dimensions to allow for aligning the origin by shifting by < 1 pixel
if (hFixed)
maxWidth = maxBounds.r-maxBounds.l;
else if (pxAlignOriginX)
maxWidth += 1;
if (vFixed)
maxHeight = maxBounds.t-maxBounds.b;
else if (pxAlignOriginY)
maxHeight += 1;
return maxBounds;
}
double GridAtlasPacker::scaleToFit(GlyphGeometry *glyphs, int count, int cellWidth, int cellHeight, msdfgen::Shape::Bounds &maxBounds, double &maxWidth, double &maxHeight) const {
static const int BIG_VALUE = 1<<28;
if (cellWidth <= 0)
cellWidth = BIG_VALUE;
if (cellHeight <= 0)
cellHeight = BIG_VALUE;
--cellWidth, --cellHeight; // Implicit half-pixel padding from each side to make sure that no representable values are beyond outermost pixel centers
cellWidth -= spacing, cellHeight -= spacing;
bool lastResult = false;
#define TRY_FIT(scale) (maxWidth = 0, maxHeight = 0, maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, (scale), -(unitRange.lower+pxRange.lower/(scale))), lastResult = maxWidth <= cellWidth && maxHeight <= cellHeight)
double minScale = 1, maxScale = 1;
if (TRY_FIT(1)) {
while (maxScale < 1e+32 && ((maxScale = 2*minScale), TRY_FIT(maxScale)))
minScale = maxScale;
} else {
while (minScale > 1e-32 && ((minScale = .5*maxScale), !TRY_FIT(minScale)))
maxScale = minScale;
}
if (minScale == maxScale)
return 0;
while (minScale/maxScale < 1-scaleMaximizationTolerance) {
double midScale = .5*(minScale+maxScale);
if (TRY_FIT(midScale))
minScale = midScale;
else
maxScale = midScale;
}
if (!lastResult)
TRY_FIT(minScale);
return minScale;
}
// Can this spaghetti code be simplified?
// Idea: Maybe it could be rewritten into a while (not all properties deduced) cycle, and compute one value in each iteration
int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
if (!count)
return 0;
GridAtlasPacker initial(*this);
int cellCount = 0;
if (columns > 0 && rows > 0)
cellCount = columns*rows;
else {
// Count non-whitespace glyphs only
for (const GlyphGeometry *glyph = glyphs, *end = glyphs+count; glyph < end; ++glyph) {
if (!glyph->isWhitespace())
++cellCount;
}
if (columns > 0)
rows = (cellCount+columns-1)/columns;
else if (rows > 0)
columns = (cellCount+rows-1)/rows;
else if (width > 0 && cellWidth > 0) {
columns = (width+spacing)/cellWidth;
rows = (cellCount+columns-1)/columns;
}
}
if (width < 0 && cellWidth > 0 && columns > 0)
width = columns*cellWidth;
if (height < 0 && cellHeight > 0 && rows > 0)
height = rows*cellHeight;
if (width != initial.width || height != initial.height)
raiseToConstraint(width, height, dimensionsConstraint);
if (cellWidth < 0 && width > 0 && columns > 0)
cellWidth = (width+spacing)/columns;
if (cellHeight < 0 && height > 0 && rows > 0)
cellHeight = (height+spacing)/rows;
if (cellWidth != initial.cellWidth || cellHeight != initial.cellHeight) {
bool positiveCellWidth = cellWidth > 0, positiveCellHeight = cellHeight > 0;
lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
if ((cellWidth == 0 && positiveCellWidth) || (cellHeight == 0 && positiveCellHeight))
return -1;
}
if ((cellWidth > 0 && cellWidth-spacing-1 <= -2*pxRange.lower) || (cellHeight > 0 && cellHeight-spacing-1 <= -2*pxRange.lower)) // cells definitely too small
return -1;
msdfgen::Shape::Bounds maxBounds = { };
double maxWidth = 0, maxHeight = 0;
if (scale <= 0) {
// If both pxRange and miterLimit is non-zero, miter bounds have to be computed for all potential scales
if (pxRange.lower != pxRange.upper && miterLimit > 0) {
if (cellWidth > 0 || cellHeight > 0) {
scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight);
if (scale < minScale) {
scale = minScale;
cutoff = true;
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, scale, -(unitRange.lower+pxRange.lower/scale));
}
}
else if (width > 0 && height > 0) {
double bestAlignedScale = 0;
int bestCols = 0, bestAlignedCols = 0;
for (int q = (int) sqrt(cellCount)+1; q > 0; --q) {
int cols = q;
int rows = (cellCount+cols-1)/cols;
int tWidth = (width+spacing)/cols;
int tHeight = (height+spacing)/rows;
lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint);
if (tWidth > 0 && tHeight > 0) {
double curScale = scaleToFit(glyphs, count, tWidth, tHeight, maxBounds, maxWidth, maxHeight);
if (curScale > scale) {
scale = curScale;
bestCols = cols;
}
if (cols*tWidth == width && curScale > bestAlignedScale) {
bestAlignedScale = curScale;
bestAlignedCols = cols;
}
}
cols = (cellCount+q-1)/q;
rows = (cellCount+cols-1)/cols;
tWidth = (width+spacing)/cols;
tHeight = (height+spacing)/rows;
lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint);
if (tWidth > 0 && tHeight > 0) {
double curScale = scaleToFit(glyphs, count, tWidth, tHeight, maxBounds, maxWidth, maxHeight);
if (curScale > scale) {
scale = curScale;
bestCols = cols;
}
if (cols*tWidth == width && curScale > bestAlignedScale) {
bestAlignedScale = curScale;
bestAlignedCols = cols;
}
}
}
if (!bestCols)
return -1;
// If columns can be aligned with total width at a slight cost to glyph scale, use that number of columns instead
if (bestAlignedScale >= minScale && (alignedColumnsBias+1)*bestAlignedScale >= scale) {
scale = bestAlignedScale;
bestCols = bestAlignedCols;
}
columns = bestCols;
rows = (cellCount+columns-1)/columns;
cellWidth = (width+spacing)/columns;
cellHeight = (height+spacing)/rows;
lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight);
if (scale < minScale)
scale = -1;
}
if (scale <= 0) {
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, minScale, -(unitRange.lower+pxRange.lower/minScale));
cellWidth = (int) ceil(maxWidth)+spacing+1;
cellHeight = (int) ceil(maxHeight)+spacing+1;
raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight);
if (scale < minScale)
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, scale = minScale, -(unitRange.lower+pxRange.lower/minScale));
}
if (initial.rows < 0 && initial.cellHeight < 0) {
int optimalCellWidth = cellWidth, optimalCellHeight = (int) ceil(maxHeight)+spacing+1;
raiseToConstraint(optimalCellWidth, optimalCellHeight, cellDimensionsConstraint);
if (optimalCellHeight < cellHeight && optimalCellWidth <= cellWidth) {
cellWidth = optimalCellWidth;
cellHeight = optimalCellHeight;
}
}
} else {
Padding pxPadding = innerPxPadding+outerPxPadding;
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, 1, -unitRange.lower);
// Undo pxPadding added by getMaxBounds before pixel scale is known
pad(maxBounds, -pxPadding);
maxWidth -= pxPadding.l+pxPadding.r;
maxHeight -= pxPadding.b+pxPadding.t;
int hSlack = 0, vSlack = 0;
if (pxAlignOriginX && !hFixed) {
maxWidth -= 1; // Added by getMaxBounds
hSlack = 1;
}
if (pxAlignOriginY && !vFixed) {
maxHeight -= 1; // Added by getMaxBounds
vSlack = 1;
}
double extraPxWidth = -2*pxRange.lower+pxPadding.l+pxPadding.r;
double extraPxHeight = -2*pxRange.lower+pxPadding.b+pxPadding.t;
double hScale = 0, vScale = 0;
if (cellWidth > 0)
hScale = (cellWidth-hSlack-spacing-extraPxWidth-1)/maxWidth;
if (cellHeight > 0)
vScale = (cellHeight-vSlack-spacing-extraPxHeight-1)/maxHeight;
if (hScale || vScale) {
if (hScale && vScale)
scale = std::min(hScale, vScale);
else
scale = hScale+vScale;
if (scale < minScale) {
scale = minScale;
cutoff = true;
}
}
else if (width > 0 && height > 0) {
double bestAlignedScale = 0;
int bestCols = 0, bestAlignedCols = 0;
// TODO optimize to only test up to sqrt(cellCount) cols and rows like in the above branch (for (int q = (int) sqrt(cellCount)+1; ...)
for (int cols = 1; cols < width; ++cols) {
int rows = (cellCount+cols-1)/cols;
int tWidth = (width+spacing)/cols;
int tHeight = (height+spacing)/rows;
lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint);
if (tWidth > 0 && tHeight > 0) {
hScale = (tWidth-hSlack-spacing-extraPxWidth-1)/maxWidth;
vScale = (tHeight-vSlack-spacing-extraPxHeight-1)/maxHeight;
double curScale = std::min(hScale, vScale);
if (curScale > scale) {
scale = curScale;
bestCols = cols;
}
if (cols*tWidth == width && curScale > bestAlignedScale) {
bestAlignedScale = curScale;
bestAlignedCols = cols;
}
}
}
if (!bestCols)
return -1;
// If columns can be aligned with total width at a slight cost to glyph scale, use that number of columns instead
if (bestAlignedScale >= minScale && (alignedColumnsBias+1)*bestAlignedScale >= scale) {
scale = bestAlignedScale;
bestCols = bestAlignedCols;
}
columns = bestCols;
rows = (cellCount+columns-1)/columns;
cellWidth = (width+spacing)/columns;
cellHeight = (height+spacing)/rows;
lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
if (scale < minScale)
scale = -1;
}
if (scale <= 0) {
cellWidth = (int) ceil(minScale*maxWidth+extraPxWidth)+hSlack+spacing+1;
cellHeight = (int) ceil(minScale*maxHeight+extraPxHeight)+vSlack+spacing+1;
raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
hScale = (cellWidth-hSlack-spacing-extraPxWidth-1)/maxWidth;
vScale = (cellHeight-vSlack-spacing-extraPxHeight-1)/maxHeight;
scale = std::min(hScale, vScale);
}
if (initial.rows < 0 && initial.cellHeight < 0) {
int optimalCellWidth = cellWidth, optimalCellHeight = (int) ceil(scale*maxHeight+extraPxHeight)+vSlack+spacing+1;
raiseToConstraint(optimalCellWidth, optimalCellHeight, cellDimensionsConstraint);
if (optimalCellHeight < cellHeight && optimalCellWidth <= cellWidth) {
cellWidth = optimalCellWidth;
cellHeight = optimalCellHeight;
}
}
maxBounds.l *= scale, maxBounds.b *= scale;
maxBounds.r *= scale, maxBounds.t *= scale;
maxWidth *= scale, maxHeight *= scale;
// Redo addition of pxPadding once scale is known
pad(maxBounds, pxPadding);
maxWidth += pxPadding.l+pxPadding.r;
maxHeight += pxPadding.b+pxPadding.t;
}
} else {
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, scale, -(unitRange.lower+pxRange.lower/scale));
int optimalCellWidth = (int) ceil(maxWidth)+spacing+1;
int optimalCellHeight = (int) ceil(maxHeight)+spacing+1;
if (cellWidth < 0 || cellHeight < 0) {
cellWidth = optimalCellWidth;
cellHeight = optimalCellHeight;
raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
} else if (cellWidth < optimalCellWidth || cellHeight < optimalCellHeight)
cutoff = true;
}
// Compute fixed origin
if (hFixed) {
if (pxAlignOriginX) {
int sl = (int) floor(maxBounds.l-.5);
int sr = (int) ceil(maxBounds.r+.5);
fixedX = (-sl+(cellWidth-spacing-(sr-sl))/2)/scale;
} else
fixedX = (-maxBounds.l+.5*(cellWidth-spacing-maxWidth))/scale;
}
if (vFixed) {
if (pxAlignOriginY) {
int sb = (int) floor(maxBounds.b-.5);
int st = (int) ceil(maxBounds.t+.5);
fixedY = (-sb+(cellHeight-spacing-(st-sb))/2)/scale;
} else
fixedY = (-maxBounds.b+.5*(cellHeight-spacing-maxHeight))/scale;
}
if (width < 0 || height < 0) {
if (columns <= 0) {
double bestRating = -1;
for (int q = (int) sqrt(cellCount)+1; q > 0; --q) {
int cols = q;
int rows = (cellCount+cols-1)/cols;
int curWidth = cols*cellWidth, curHeight = rows*cellHeight;
raiseToConstraint(curWidth, curHeight, dimensionsConstraint);
double rating = dimensionsRating(curWidth, curHeight, cols*cellWidth == curWidth);
if (rating < bestRating || bestRating < 0) {
bestRating = rating;
columns = cols;
}
rows = q;
cols = (cellCount+rows-1)/rows;
curWidth = cols*cellWidth, curHeight = rows*cellHeight;
raiseToConstraint(curWidth, curHeight, dimensionsConstraint);
rating = dimensionsRating(curWidth, curHeight, cols*cellWidth == curWidth);
if (rating < bestRating || bestRating < 0) {
bestRating = rating;
columns = cols;
}
}
rows = (cellCount+columns-1)/columns;
}
width = columns*cellWidth, height = rows*cellHeight;
raiseToConstraint(width, height, dimensionsConstraint);
// raiseToConstraint may have increased dimensions significantly, rerun if cell dimensions can be optimized.
if (dimensionsConstraint != DimensionsConstraint::NONE && initial.cellWidth < 0 && initial.cellHeight < 0) {
cellWidth = initial.cellWidth;
cellHeight = initial.cellHeight;
columns = initial.columns;
rows = initial.rows;
scale = initial.scale;
return pack(glyphs, count);
}
}
if (columns < 0) {
columns = (width+spacing)/cellWidth;
rows = (cellCount+columns-1)/columns;
}
if (rows*cellHeight > height)
rows = height/cellHeight;
GlyphGeometry::GlyphAttributes attribs = { };
attribs.scale = scale;
attribs.range = unitRange+pxRange/scale;
attribs.innerPadding = innerUnitPadding+1/scale*innerPxPadding;
attribs.outerPadding = outerUnitPadding+1/scale*outerPxPadding;
attribs.miterLimit = miterLimit;
attribs.pxAlignOriginX = pxAlignOriginX;
attribs.pxAlignOriginY = pxAlignOriginY;
int col = 0, row = 0;
for (GlyphGeometry *glyph = glyphs, *end = glyphs+count; glyph < end; ++glyph) {
if (!glyph->isWhitespace()) {
glyph->frameBox(attribs, cellWidth-spacing, cellHeight-spacing, hFixed ? &fixedX : nullptr, vFixed ? &fixedY : nullptr);
glyph->placeBox(col*cellWidth, height-(row+1)*cellHeight);
if (++col >= columns) {
if (++row >= rows) {
return end-glyph-1;
}
col = 0;
}
}
}
return 0;
}
void GridAtlasPacker::setFixedOrigin(bool horizontal, bool vertical) {
hFixed = horizontal, vFixed = vertical;
}
void GridAtlasPacker::setCellDimensions(int width, int height) {
cellWidth = width, cellHeight = height;
}
void GridAtlasPacker::unsetCellDimensions() {
cellWidth = -1, cellHeight = -1;
}
void GridAtlasPacker::setCellDimensionsConstraint(DimensionsConstraint dimensionsConstraint) {
cellDimensionsConstraint = dimensionsConstraint;
}
void GridAtlasPacker::setColumns(int columns) {
this->columns = columns;
}
void GridAtlasPacker::setRows(int rows) {
this->rows = rows;
}
void GridAtlasPacker::unsetColumns() {
columns = -1;
}
void GridAtlasPacker::unsetRows() {
rows = -1;
}
void GridAtlasPacker::setDimensions(int width, int height) {
this->width = width, this->height = height;
}
void GridAtlasPacker::unsetDimensions() {
width = -1, height = -1;
}
void GridAtlasPacker::setDimensionsConstraint(DimensionsConstraint dimensionsConstraint) {
this->dimensionsConstraint = dimensionsConstraint;
}
void GridAtlasPacker::setSpacing(int spacing) {
this->spacing = spacing;
}
void GridAtlasPacker::setScale(double scale) {
this->scale = scale;
}
void GridAtlasPacker::setMinimumScale(double minScale) {
this->minScale = minScale;
}
void GridAtlasPacker::setUnitRange(msdfgen::Range unitRange) {
this->unitRange = unitRange;
}
void GridAtlasPacker::setPixelRange(msdfgen::Range pxRange) {
this->pxRange = pxRange;
}
void GridAtlasPacker::setMiterLimit(double miterLimit) {
this->miterLimit = miterLimit;
}
void GridAtlasPacker::setOriginPixelAlignment(bool align) {
pxAlignOriginX = align, pxAlignOriginY = align;
}
void GridAtlasPacker::setOriginPixelAlignment(bool alignX, bool alignY) {
pxAlignOriginX = alignX, pxAlignOriginY = alignY;
}
void GridAtlasPacker::setInnerUnitPadding(const Padding &padding) {
innerUnitPadding = padding;
}
void GridAtlasPacker::setOuterUnitPadding(const Padding &padding) {
outerUnitPadding = padding;
}
void GridAtlasPacker::setInnerPixelPadding(const Padding &padding) {
innerPxPadding = padding;
}
void GridAtlasPacker::setOuterPixelPadding(const Padding &padding) {
outerPxPadding = padding;
}
void GridAtlasPacker::getDimensions(int &width, int &height) const {
width = this->width, height = this->height;
}
void GridAtlasPacker::getCellDimensions(int &width, int &height) const {
width = cellWidth, height = cellHeight;
}
int GridAtlasPacker::getColumns() const {
return columns;
}
int GridAtlasPacker::getRows() const {
return rows;
}
double GridAtlasPacker::getScale() const {
return scale;
}
msdfgen::Range GridAtlasPacker::getPixelRange() const {
return pxRange+scale*unitRange;
}
void GridAtlasPacker::getFixedOrigin(double &x, double &y) {
x = fixedX-.5/scale;
y = fixedY-.5/scale;
}
bool GridAtlasPacker::hasCutoff() const {
return cutoff;
}
}