Open joncursi opened 4 years ago
@joncursi
use zoom
96px = 1in 37.79px = 1cm
new Ruler(??, {
zoom: 96,
unit: 1,
});
This did not work for me. can you please be specific what i can do to fix. I needed to use this component with inches so I used the following in my html <ngx-guides #guides1 type="horizontal" unit="1"> and zoom in my typescript file
this.guides1.zoom = 96;
@joncursi since you have access to unit
and textFormat
, you can simply use pixels as the measurement in your code, but calculate a metric or imperial value to show the user like textFormat: (px: number) => pixelToUnit(px, measuringUnits)
(where pixelToUnit is just some math you have to do).
The only issue we are running into while doing this is javascript precision. So if eg. you say you want the ruler to show you 0mm, 25mm, 50mm etc. the unit
prop of the ruler could be "200 pixels divided by dpi times one inch in mm": 200 / 72 * 25.4
, then javascript number
precision gets you to 70.55555555555554
, which, since textFormat
returns a number
its unfortunately fairly imprecise (eg. returning 141.11111111111111
instead of 140
ish), leaving the ruler sections at 0, 25, 50, 99, 124 etc., meaning the higher the number, the more unprecise the label.
We're using Decimal instead of number
to help with that, but since unit needs to be a number
and textFormat
returns a number
we have no control over the final imprecision.
@daybrush ideally the calculations in Ruler.tsx use precise Decimal
types of your choice instead of numbers (we like decimal.js since it has no dependencies, it also has a smaller brother big.js that is only 6kb) to yield precise results. Are you willing to take on a dependency for precision support? Also, if understandably you don't have time for this – I could do a PR if that would be ok for you? If so let me know your thoughts regarding the new dependency.
So for others running into the same issue. A pretty hacky, but actually for reasonable values kinda working solution is to round the final value to the desired steps again, note that you likely want a dynamic steps value depending on your zoom, but eg. on the zoom level shown above you'll note that our desired values are all multiples of 5, so we can snap to that using Math.round(value / 5) * 5
. It's hacky since obviously at some large value the imprecision is bigger than the snap threshold, but with the values in our case that would only start at unreasonable viewer pan widths. And since you can specify a max range we can actually ensure people don't run into those values.
@tom2strobl would you like to make your example of the image above available in your repository or codepen?
@daybrush
That wouldn't be good. I'm working with mm and when scrolling or zooming the page, the rulers don't keep the measures synchronized. I saw that @tom2strobl apparently got around this and wanted to see how he did it!
@hitaloramon sorry that notification slipped through the gutter. I can't share the repo and am frankly too lazy to put together a full sample codepen—but it's along the lines of this:
// arbitrary helper function that does more than you need and lacks other helper functions but I just pasted it in here, you'll figure it out
const pixelToUnit = (
pixel: number | Decimal,
unit: MeasuringUnit,
withUnit = true,
returnAsReactNode = true,
rounding = 0,
fractionsForInches = true
) => {
let pixelNumber = pixel as number
if (typeof pixel !== 'number') {
pixelNumber = pixel.toNumber()
}
if (unit === MeasuringUnit.Pixel) {
return withUnit ? `${pixelNumber}${MeasuringUnitUnits[unit]}` : pixelNumber
}
if (unit === MeasuringUnit.Metric) {
let value = pixelToMillimeter(pixel).toNumber()
// due to javascript imprecision and the fact that 3rd party libs don't implement Decimal, although its hacky
// we need to round in steps of 5 so the value eg. on the ruler is correct
if (rounding) {
value = Math.round(value / rounding) * rounding
}
return withUnit ? `${value}${MeasuringUnitUnits[unit]}` : value
}
if (unit === MeasuringUnit.Imperial) {
const value = fractionsForInches
? floatToFractionString(pixelToInch(pixel).toNumber(), returnAsReactNode)
: pixelToInch(pixel).toNumber()
return withUnit ? `${value}${MeasuringUnitUnits[unit]}` : value
}
throw new Error(`Unknown measuring unit: ${unit}`)
}
// Base unit is dependent on the measuring unit
const BaseUnit = MeasuringUnitBase[measuringUnits]
// by leveraging unit steps and the current zoom, we show more detailed steps when zoomed in
const UnitSteps = BaseUnit.dividedBy(2).toNumber()
const unit = Math.round(BaseUnit.toNumber() / zoom / UnitSteps) * UnitSteps
const textFormat = useCallback(
(px: number) => pixelToUnit(px, measuringUnits, true, false, 5) as string,
[measuringUnits]
)
const dragPosFormat = useCallback(
(px: number) => pixelToUnit(px, measuringUnits, true, false, 1) as string,
[measuringUnits]
)
const baseGuidesProps = useMemo(
() => ({
// [...some more vars I dont list here]
lockGuides,
segment: measuringUnits === MeasuringUnit.Imperial ? 8 : 10,
zoom,
unit,
snapThreshold: 5,
textFormat,
dragPosFormat,
}),
[dragPosFormat, lockGuides, measuringUnits, showRuler, textFormat, theme, unit, zoom]
)
// we take 1cm in inches and convert it to pixels with 300 dpi
const snapMargin = new Decimal(new Decimal(1).dividedBy(ONE_INCH_IN_MM.dividedBy(10))).times(defaultDpi).toNumber()
const horizontalSnaps = useMemo(
() => [0 + snapMargin, pageFormatValues[1] - snapMargin].map(Math.round),
[pageFormatValues, snapMargin]
)
const verticalSnaps = useMemo(
() => [0 + snapMargin, pageFormatValues[0] - snapMargin].map(Math.round),
[pageFormatValues, snapMargin]
)
const jsx = (
<Guides
// [...some more props I dont list here]
{...baseGuidesProps}
snaps={horizontalSnaps}
type="horizontal"
/>
<Guides
// [...some more props I dont list here]
{...baseGuidesProps}
snaps={verticalSnaps}
type="vertical"
/>
)
I would like my rulers to use a different set of units than pixels. Some examples would be:
It would be great if there was a prop that allowed me to specify a pixel --> custom unit conversion. (E.g. 1 px = 1/72 inch)