ducjs
Version:
The duc 2D CAD file format is a cornerstone of our advanced design system, conceived to cater to professionals seeking precision and efficiency in their design work.
662 lines (661 loc) • 31.8 kB
JavaScript
import { UNIT_SYSTEM } from "../flatbuffers/duc";
export const MIN_ZOOM = 1e-32;
export const MAX_ZOOM = 1e32;
export const NEUTRAL_SCOPE = "m";
// Define string-based measurement units
export const metricMeasures = [
'qm', // Quectometer
'rm', // Rontometer
'ym', // Yoctometer
'zm', // Zeptometer
'am', // Attometer
'fm', // Femtometer
'pm', // Picometer
'Å', // Angstrom
'nm', // Nanometer
'µm', // Micrometer
'mm', // Millimeter
'cm', // Centimeter
'dm', // Decimeter
'm', // Meter
'dam', // Decameter
'hm', // Hectometer
'km', // Kilometer
'Mm', // Megameter
'Gm', // Gigameter
'Tm', // Terameter
'Pm', // Petameter
'Em', // Exameter
'Zm', // Zettameter
'Ym', // Yottameter
'Rm', // Ronnameter
'Qm', // Quettameter
];
export const imperialMeasures = [
'th', // Thou/mil
'ln', // Line
'in', // Inches
'h', // Hand
'ft', // Feet
'yd', // Yards
'rd', // Rods
'ch', // Chains
'fur', // Furlongs
'mi', // Miles
'lea', // Leagues
];
// Define metric units - exponents represent powers of 10
export const metricUnits = [
{ prefix: 'qm', unit: 'quecto', full: 'quectometer', exponent: -30 },
{ prefix: 'rm', unit: 'ronto', full: 'rontometer', exponent: -27 },
{ prefix: 'ym', unit: 'yocto', full: 'yoctometer', exponent: -24 },
{ prefix: 'zm', unit: 'zepto', full: 'zeptometer', exponent: -21 },
{ prefix: 'am', unit: 'atto', full: 'attometer', exponent: -18 },
{ prefix: 'fm', unit: 'femto', full: 'femtometer', exponent: -15 },
{ prefix: 'pm', unit: 'pico', full: 'picometer', exponent: -12 },
{ prefix: 'Å', unit: 'angstrom', full: 'angstrom', exponent: -10 },
{ prefix: 'nm', unit: 'nano', full: 'nanometer', exponent: -9 },
{ prefix: 'µm', unit: 'micro', full: 'micrometer', exponent: -6 },
{ prefix: 'mm', unit: 'milli', full: 'millimeter', exponent: -3 },
{ prefix: 'cm', unit: 'centi', full: 'centimeter', exponent: -2 },
{ prefix: 'dm', unit: 'deci', full: 'decimeter', exponent: -1 },
{ prefix: 'm', unit: '', full: 'meter', exponent: 0 },
{ prefix: 'dam', unit: 'deca', full: 'decameter', exponent: 1 },
{ prefix: 'hm', unit: 'hecto', full: 'hectometer', exponent: 2 },
{ prefix: 'km', unit: 'kilo', full: 'kilometer', exponent: 3 },
{ prefix: 'Mm', unit: 'mega', full: 'megameter', exponent: 6 },
{ prefix: 'Gm', unit: 'giga', full: 'gigameter', exponent: 9 },
{ prefix: 'Tm', unit: 'tera', full: 'terameter', exponent: 12 },
{ prefix: 'Pm', unit: 'peta', full: 'petameter', exponent: 15 },
{ prefix: 'Em', unit: 'exa', full: 'exameter', exponent: 18 },
{ prefix: 'Zm', unit: 'zetta', full: 'zettameter', exponent: 21 },
{ prefix: 'Ym', unit: 'yotta', full: 'yottameter', exponent: 24 },
{ prefix: 'Rm', unit: 'ronna', full: 'ronnameter', exponent: 27 },
{ prefix: 'Qm', unit: 'quetta', full: 'quettameter', exponent: 30 }
];
// Define imperial units - exponents are now relative to meter as the base unit
// These values are log10 of their meter equivalents
export const imperialUnits = [
{ prefix: 'th', unit: 'thou', full: 'thou', exponent: -4.595 }, // log10(0.0000254) ≈ -4.595
{ prefix: 'ln', unit: 'line', full: 'line', exponent: -2.674 }, // log10(0.00211667) ≈ -2.674
{ prefix: 'in', unit: 'inch', full: 'inch', exponent: -1.595 }, // log10(0.0254) ≈ -1.595
{ prefix: 'h', unit: 'hand', full: 'hand', exponent: -0.993 }, // log10(0.1016) ≈ -0.993
{ prefix: 'ft', unit: 'foot', full: 'foot', exponent: -0.516 }, // log10(0.3048) ≈ -0.516
{ prefix: 'yd', unit: 'yard', full: 'yard', exponent: -0.039 }, // log10(0.9144) ≈ -0.039
{ prefix: 'rd', unit: 'rod', full: 'rod', exponent: 0.701 }, // log10(5.0292) ≈ 0.701
{ prefix: 'ch', unit: 'chain', full: 'chain', exponent: 1.304 }, // log10(20.1168) ≈ 1.304
{ prefix: 'fur', unit: 'furlong', full: 'furlong', exponent: 2.304 }, // log10(201.168) ≈ 2.304
{ prefix: 'mi', unit: 'mile', full: 'mile', exponent: 3.207 }, // log10(1609.344) ≈ 3.207
{ prefix: 'lea', unit: 'league', full: 'league', exponent: 3.684 } // log10(4828.032) ≈ 3.684
];
// Scale factors for unit conversions - using meter as the base unit
export const ScaleFactors = {
// Metric scales
qm: 1e-30,
rm: 1e-27,
ym: 1e-24,
zm: 1e-21,
am: 1e-18,
fm: 1e-15,
pm: 1e-12,
Å: 1e-10,
nm: 1e-9,
µm: 1e-6,
mm: 1e-3,
cm: 1e-2,
dm: 1e-1,
m: 1,
dam: 1e1,
hm: 1e2,
km: 1e3,
Mm: 1e6,
Gm: 1e9,
Tm: 1e12,
Pm: 1e15,
Em: 1e18,
Zm: 1e21,
Ym: 1e24,
Rm: 1e27,
Qm: 1e30,
// Imperial scales
th: 0.0000254, // 0.001 inch
ln: 0.00211667, // 1/12 inch
in: 0.0254,
h: 0.1016, // 4 inches
ft: 0.3048,
yd: 0.9144,
rd: 5.0292,
ch: 20.1168,
fur: 201.168,
mi: 1609.344,
lea: 4828.032, // 3 miles
};
/**
* Determines if a measure belongs to the metric system.
* @param measure The measure to check.
* @returns `true` if the measure is metric, `false` otherwise.
*/
export function isMetricMeasure(measure) {
return metricMeasures.includes(measure);
}
/**
* Gets the unit system ('metric' or 'imperial') for a given measure.
* @param measure The measure to determine the system for.
* @returns The unit system the measure belongs to.
*/
export function getUnitSystemForMeasure(measure) {
return isMetricMeasure(measure) ? UNIT_SYSTEM.METRIC : UNIT_SYSTEM.IMPERIAL;
}
/**
* Clamps a zoom value between minimum and maximum allowed values
*/
export function clampZoom(zoom) {
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
}
/**
* Determines the zoom direction based on previous and current zoom values
*/
export function getZoomDirectionChange(prevZoom, currentZoom) {
if (currentZoom > prevZoom) {
return 'up';
}
else if (currentZoom < prevZoom) {
return 'down';
}
return 'neutral';
}
/**
* Gets the unit definitions (prefix, name, exponent) for the specified unit system.
* @param unitSystem The unit system ('metric' or 'imperial') to get definitions for.
* @returns An array of unit definitions.
*/
export function getUnitDefinitions(unitSystem) {
return unitSystem === UNIT_SYSTEM.METRIC ? metricUnits : imperialUnits;
}
/**
* Gets the standard exponents associated with the units in a specified system.
* These exponents represent the power of 10 (for metric) or log10 equivalent (for imperial) relative to the base unit (meter).
* @param unitSystem The unit system ('metric' or 'imperial').
* @returns An array of standard exponent numbers.
*/
export function getStandardExponents(unitSystem) {
const definitions = getUnitDefinitions(unitSystem);
return definitions.map(unit => unit.exponent);
}
/**
* Gets the exponent value for a given measure.
* @param measure The measure (e.g., 'mm', 'ft') to get the exponent for.
* @returns The exponent value, defaulting to 0 (base unit) if not found.
*/
export function getExponentForMeasure(measure) {
const unitSystem = getUnitSystemForMeasure(measure);
const definitions = getUnitDefinitions(unitSystem);
const unitDef = definitions.find(def => def.prefix === measure);
return unitDef ? unitDef.exponent : 0; // Default to base unit (m or in) if not found
}
/**
* Gets the measure (unit prefix) that is closest to a given exponent value within a unit system.
* @param exponent The exponent value to find the closest measure for.
* @param unitSystem The unit system ('metric' or 'imperial') to search within.
* @returns The prefix of the closest measure.
*/
export function getMeasureForExponent(exponent, unitSystem) {
const definitions = getUnitDefinitions(unitSystem);
let closestDef = definitions[0];
let minDiff = Math.abs(definitions[0].exponent - exponent);
for (const def of definitions) {
const diff = Math.abs(def.exponent - exponent);
if (diff < minDiff) {
minDiff = diff;
closestDef = def;
}
}
return closestDef.prefix;
}
/**
* Calculates how much influence the main scope should have based on zoom level.
* This function now provides a simple normalized distance metric for UI animations.
*
* Returns a value from 0 to 1 where:
* - 1 means we're exactly at the main scope's exponent
* - 0 means we're at or beyond the threshold from the main scope's exponent
*/
export function getMainScopeInfluence(zoomExponent, mainScopeExponent, scopeExpThreshold) {
// The relationship between zoom exponent and unit exponent is inverse
const invertedZoomExponent = -zoomExponent;
const absDiff = Math.abs(invertedZoomExponent - mainScopeExponent);
// If we're outside the threshold, there's no influence
if (absDiff >= scopeExpThreshold) {
return 0;
}
// Within the threshold, calculate a linear falloff from 1 (at exact match) to 0 (at threshold)
return 1 - (absDiff / scopeExpThreshold);
}
/**
* Calculates the appropriate scope index based on zoom level with an improved gravity well effect
*/
export function calculateScopeIndex(zoom, unitSystem, scopeExpThreshold, mainScopeExponent) {
const zoomClamped = clampZoom(zoom);
// Handle edge case where zoom is non-positive before taking log
if (zoomClamped <= 0) {
// Return index for the largest unit (smallest zoom)
return getStandardExponents(unitSystem).length - 1;
}
// Calculate zoom exponent for the scope progression
const zoomExponent = Math.log10(zoomClamped);
// Calculate the inverted exponent (this determines the natural scope)
const invertedZoomExponent = -zoomExponent;
const standardExponents = getStandardExponents(unitSystem);
// Find main scope index
const mainScopeIndex = standardExponents.findIndex(exp => exp === mainScopeExponent);
// If main scope index not found, fall back to natural scope selection
if (mainScopeIndex === -1) {
return findNaturalScopeIndex(invertedZoomExponent, standardExponents);
}
// Calculate distance from main scope in exponent space
const distFromMainScope = Math.abs(invertedZoomExponent - mainScopeExponent);
const effectiveThreshold = scopeExpThreshold - 0.95; // e.g., an input of 1 becomes an effective 0.05
// CORE BEHAVIOR: If we're within the effective threshold, ALWAYS use the main scope
// This ensures stable behavior with small threshold values
if (distFromMainScope < effectiveThreshold) {
return mainScopeIndex;
}
// Beyond the threshold, use natural scope selection
return findNaturalScopeIndex(invertedZoomExponent, standardExponents);
}
/**
* Helper function to find the natural scope index based solely on zoom exponent
*/
function findNaturalScopeIndex(invertedZoomExponent, standardExponents) {
// Find natural scope index
let baseIndex = -1;
for (let i = 0; i < standardExponents.length - 1; i++) {
if (invertedZoomExponent >= standardExponents[i] && invertedZoomExponent < standardExponents[i + 1]) {
baseIndex = i;
break;
}
}
// Handle edge cases
if (baseIndex === -1) {
if (invertedZoomExponent < standardExponents[0]) {
baseIndex = 0;
}
else {
baseIndex = standardExponents.length - 1;
}
}
return baseIndex;
}
/**
* Calculates the appropriate scope measure (unit prefix) based on the current zoom level,
* considering the main scope and threshold settings.
* @param zoom The current raw zoom level.
* @param scopeExpThreshold The exponent difference threshold for main scope influence.
* @param mainMeasure The designated main measure acting as a gravity well.
* @returns The calculated scope measure (unit prefix).
*/
export function calculateScope(zoom, scopeExpThreshold, mainMeasure) {
const unitSystem = getUnitSystemForMeasure(mainMeasure);
const mainScopeExponent = getExponentForMeasure(mainMeasure);
const scopeIndex = calculateScopeIndex(zoom, unitSystem, scopeExpThreshold, mainScopeExponent);
const unitDefinitions = getUnitDefinitions(unitSystem);
// Ensure we don't exceed array bounds
const safeIndex = Math.min(Math.max(0, scopeIndex), unitDefinitions.length - 1);
return unitDefinitions[safeIndex].prefix;
}
/**
* Gets the full unit definition object for the current scope based on zoom and main scope settings.
* @param zoom The current raw zoom level.
* @param scopeExpThreshold The exponent difference threshold for main scope influence.
* @param mainMeasure The designated main measure.
* @returns The full UnitDefinition object for the current scope.
*/
export function getCurrentUnitDefinition(zoom, scopeExpThreshold, mainMeasure) {
const unitSystem = getUnitSystemForMeasure(mainMeasure);
const mainScopeExponent = getExponentForMeasure(mainMeasure);
const scopeIndex = calculateScopeIndex(zoom, unitSystem, scopeExpThreshold, mainScopeExponent);
const unitDefinitions = getUnitDefinitions(unitSystem);
const safeIndex = Math.min(Math.max(0, scopeIndex), unitDefinitions.length - 1);
return unitDefinitions[safeIndex];
}
/**
* Gets the lower bound of the current scope's *exponent* range.
* This represents the minimum exponent value covered by the current scope.
* @param zoom The current raw zoom level.
* @param scopeExpThreshold The exponent difference threshold for main scope influence.
* @param mainMeasure The designated main measure.
* @returns The lower exponent bound of the current scope.
*/
export function getCurrentScopeExponentLowerBound(zoom, scopeExpThreshold, mainMeasure) {
const unitSystem = getUnitSystemForMeasure(mainMeasure);
const mainScopeExponent = getExponentForMeasure(mainMeasure);
const scopeIndex = calculateScopeIndex(zoom, unitSystem, scopeExpThreshold, mainScopeExponent);
const standardExponents = getStandardExponents(unitSystem);
// Ensure index is valid before accessing
const safeIndex = Math.max(0, Math.min(scopeIndex, standardExponents.length - 1));
return standardExponents[safeIndex];
}
/**
* Gets the upper bound of the current scope's *exponent* range.
* This represents the maximum exponent value covered by the current scope (exclusive).
* @param zoom The current raw zoom level.
* @param scopeExpThreshold The exponent difference threshold for main scope influence.
* @param mainMeasure The designated main measure.
* @returns The upper exponent bound of the current scope.
*/
export function getCurrentScopeExponentUpperBound(zoom, scopeExpThreshold, mainMeasure) {
const unitSystem = getUnitSystemForMeasure(mainMeasure);
const mainScopeExponent = getExponentForMeasure(mainMeasure);
const scopeIndex = calculateScopeIndex(zoom, unitSystem, scopeExpThreshold, mainScopeExponent);
const standardExponents = getStandardExponents(unitSystem);
const safeIndex = Math.max(0, Math.min(scopeIndex, standardExponents.length - 1));
const nextIndex = Math.min(safeIndex + 1, standardExponents.length - 1);
// If already at the last index, estimate the next exponent step
if (nextIndex === safeIndex && standardExponents.length > 1) {
return standardExponents[safeIndex] + (standardExponents[1] - standardExponents[0]);
}
else if (nextIndex === safeIndex) {
return standardExponents[safeIndex]; // Only one exponent defined
}
return standardExponents[nextIndex];
}
/**
* Gets the lower bound of the *zoom* range for the current scope.
* Derived from the scope's upper exponent bound.
* @param zoom The current raw zoom level.
* @param scopeExpThreshold The exponent difference threshold for main scope influence.
* @param mainMeasure The designated main measure.
* @returns The minimum raw zoom value that falls within the current scope.
*/
export function getCurrentScopeLowerZoomBound(zoom, scopeExpThreshold, mainMeasure) {
// Use the exponent bounds to derive zoom bounds based on the new logic
const upperExp = getCurrentScopeExponentUpperBound(zoom, scopeExpThreshold, mainMeasure);
// Scope 'i' applies when lowerExp <= -log10(zoom) < upperExp
// Which means -upperExp < log10(zoom) <= -lowerExp
// Lower zoom bound is 10^(-upperExp)
return Math.pow(10, -upperExp);
}
/**
* Gets the upper bound of the *zoom* range for the current scope.
* Derived from the scope's lower exponent bound.
* @param zoom The current raw zoom level.
* @param scopeExpThreshold The exponent difference threshold for main scope influence.
* @param mainMeasure The designated main measure.
* @returns The maximum raw zoom value (exclusive) that falls within the current scope.
*/
export function getCurrentScopeUpperZoomBound(zoom, scopeExpThreshold, mainMeasure) {
// Use the exponent bounds to derive zoom bounds based on the new logic
const lowerExp = getCurrentScopeExponentLowerBound(zoom, scopeExpThreshold, mainMeasure);
// Scope 'i' applies when lowerExp <= -log10(zoom) < upperExp
// Which means -upperExp < log10(zoom) <= -lowerExp
// Upper zoom bound is 10^(-lowerExp)
return Math.pow(10, -lowerExp);
}
/**
* Converts a numeric value from one scope (unit) to its equivalent in another scope,
* preserving the actual physical magnitude.
* This uses the underlying scale factors relative to the base unit (meter).
*
* @param providedValue - The numeric value to convert.
* @param providedScope - The scope (unit) of the provided value.
* @param targetScope - The target scope (unit) to convert the value to.
* @returns The equivalent value in the target scope.
*/
export function getPrecisionValueForScope(providedValue, providedScope, targetScope) {
// If scopes are the same, no conversion needed
if (providedScope === targetScope) {
return providedValue;
}
// Get the translation factor between the scopes
const translationFactor = getTranslationFactor(providedScope, targetScope);
// Convert the value using the translation factor
// This maintains the relative scale between the scopes
return (providedValue * translationFactor);
}
/**
* Gets the percentage through the current scope's *exponent* range (0-100%)
* Percentage 0% means at the boundary with the next *larger* unit scope
* Percentage 100% means at the boundary with the next *smaller* unit scope
*/
export function getScopeThroughPercentage(zoom, scopeExpThreshold, mainMeasure) {
const zoomClamped = clampZoom(zoom);
const lowerZoom = getCurrentScopeLowerZoomBound(zoom, scopeExpThreshold, mainMeasure);
const upperZoom = getCurrentScopeUpperZoomBound(zoom, scopeExpThreshold, mainMeasure);
if (upperZoom <= lowerZoom) {
return 0;
}
const fraction = (zoomClamped - lowerZoom) / (upperZoom - lowerZoom);
return Math.min(100, Math.max(0, fraction * 100));
}
/**
* Gets proximity to scope change (0-1, where 1 means about to change)
* This determines animation of measurement indicators
*/
export function getProximityToScopeChange(zoom, scopeExpThreshold, mainMeasure) {
const zoomClamped = clampZoom(zoom);
const zoomExp = Math.log10(zoomClamped);
// Get current scope's exponent bounds
const lowerExp = getCurrentScopeExponentLowerBound(zoom, scopeExpThreshold, mainMeasure);
const upperExp = getCurrentScopeExponentUpperBound(zoom, scopeExpThreshold, mainMeasure);
// Calculate where we are in the current scope's range
const scopeRange = upperExp - lowerExp;
if (scopeRange <= 0)
return 0;
// Calculate position within scope (-log10(zoom) should be between lowerExp and upperExp)
const position = -zoomExp;
// Calculate relative position in the scope range (0 to 1)
const relativePos = (position - lowerExp) / scopeRange;
// We want the proximity to increase smoothly as we move away from the center of the scope
// Center is at 0.5, edges are at 0 and 1
const distanceFromCenter = Math.abs(relativePos - 0.5) * 2; // Will be 0 at center, 1 at edges
// Apply a smooth easing function to make the transition more gradual
// This creates a more continuous movement from center to edges
return Math.pow(distanceFromCenter, 1.5); // The power of 1.5 makes it more gradual
}
/**
* Determines direction of next scope change based on position within current scope
* 'up' -> higher index (larger units)
* 'down' -> lower index (smaller units)
*/
export function getNextScopeDirection(zoom, scopeExpThreshold, mainMeasure) {
const zoomClamped = clampZoom(zoom);
const zoomExp = Math.log10(zoomClamped);
// Get current scope's exponent bounds
const lowerExp = getCurrentScopeExponentLowerBound(zoom, scopeExpThreshold, mainMeasure);
const upperExp = getCurrentScopeExponentUpperBound(zoom, scopeExpThreshold, mainMeasure);
// If we're dealing with an almost zero range, use zoom direction instead
const scopeRange = upperExp - lowerExp;
if (scopeRange < 0.001) {
return zoomExp < 0 ? 'down' : 'up';
}
// Calculate position within scope (-log10(zoom) should be between lowerExp and upperExp)
const position = -zoomExp;
// Calculate relative position in the scope range (0 to 1)
const relativePos = (position - lowerExp) / scopeRange;
// Now use a stable approach - if in the lower 40% of the range, direction is up,
// if in the upper 40%, direction is down. This prevents rapid switching.
if (relativePos < 0.4)
return 'up';
if (relativePos > 0.6)
return 'down';
// In the middle zone (40-60%), maintain the previous direction to avoid oscillation
// Use the relative position compared to exact center to decide
return relativePos < 0.5 ? 'up' : 'down';
}
/**
* Get translation factor between two measures (unit scopes).
* Calculates the multiplicative factor needed to convert a value from `fromMeasure` to `toMeasure`.
* @param fromMeasure The source measure unit.
* @param toMeasure The target measure unit.
* @returns The translation factor.
*/
export function getTranslationFactor(fromMeasure, toMeasure) {
const fromFactor = ScaleFactors[fromMeasure];
const toFactor = ScaleFactors[toMeasure];
// Handle potential division by zero if a factor is missing or zero
if (toFactor === 0) {
console.error(`Attempted to divide by zero scale factor for unit: ${toMeasure}`);
return 1; // Or throw an error, depending on desired behavior
}
return fromFactor / toFactor;
}
/**
* Adjusts the raw zoom level when the main scope is changed, aiming to maintain
* the same visual center point or numeric value representation on screen.
* @param currentZoom The current raw zoom level.
* @param oldMainMeasure The previous main measure.
* @param newMainMeasure The newly selected main measure.
* @returns The adjusted raw zoom level.
*/
export function getAdjustedZoomForMainScopeChange(currentZoom, oldMainMeasure, newMainMeasure) {
// Get the translation factor between the old and new units
// Note the order: new to old, because zoom is inversely proportional to unit size
const translationFactor = getTranslationFactor(newMainMeasure, oldMainMeasure);
// Adjust the zoom to maintain the same numeric center value
// Since zoom is inversely proportional to the unit size,
// we multiply by the translation factor (in the inverse direction)
return currentZoom * translationFactor;
}
/**
* Converts a raw value, assumed to be in NEUTRAL_SCOPE (meters), into its equivalent in the `currentScope`.
* This effectively gives the value as if it were measured in `currentScope` units while maintaining its physical magnitude relative to the neutral reference.
*
* @param value - The raw numeric value, assumed to be in NEUTRAL_SCOPE (e.g., meters).
* @param currentScope - The target scope (unit) to express the value in.
* @returns The value converted to `currentScope`, branded as `ScopedValue`.
*/
export const getNeutralScopedValue = (value, currentScope) => {
return getPrecisionValueForScope(value, NEUTRAL_SCOPE, currentScope);
};
/**
* Constructs a `PrecisionValue` object from a value that is already scoped.
* A scoped value is typically expressed in `currentScope` units but is relative to `NEUTRAL_SCOPE`.
* This function converts this `newScopedValue` back to its original `RawValue` in its `providedScope`.
*
* @param newScopedValue - The value already expressed in `currentScope` units, relative to `NEUTRAL_SCOPE`.
* @param providedScope - The original scope (unit) the raw value should be in.
* @param currentScope - The scope (unit) in which `newScopedValue` is currently expressed.
* @returns A `PrecisionValue` object containing both the `scoped` (input) and calculated `value` (in `providedScope`).
*/
export const getPrecisionValueFromScoped = (newScopedValue, providedScope, currentScope) => {
return {
scoped: newScopedValue,
value: getPrecisionValueForScope(newScopedValue, currentScope, providedScope)
};
};
/**
* Constructs a `PrecisionValue` object from a `RawValue`.
* It calculates the `scoped` representation of the `rawValue` (which is in `providedScope`)
* by converting it to the `currentScope`.
*
* @param rawValue - The raw numeric value in its original `providedScope`.
* @param providedScope - The original scope (unit) of the `rawValue`.
* @param currentScope - The target scope (unit) to express the `scoped` value in.
* @returns A `PrecisionValue` object containing the calculated `scoped` value and the original `value` (raw).
*/
export const getPrecisionValueFromRaw = (rawValue, providedScope, currentScope) => {
return {
scoped: getPrecisionValueForScope(rawValue, providedScope, currentScope),
value: rawValue
};
};
/**
* Converts a raw zoom value into a `ScopedZoomValue`.
* Due to the inverse perception of zoom (higher raw zoom means smaller represented units),
* this conversion is from `currentScope` to `NEUTRAL_SCOPE`.
* It essentially calculates how many `NEUTRAL_SCOPE` units are equivalent to one `currentScope` unit at the given raw zoom.
*
* @param rawZoomValue - The raw, dimensionless zoom factor.
* @param currentScope - The current active measurement scope.
* @returns The zoom value scoped relative to `NEUTRAL_SCOPE`, branded as `ScopedZoomValue`.
*/
export const getScopedZoomValue = (rawZoomValue, currentScope) => {
return getPrecisionValueForScope(rawZoomValue, currentScope, NEUTRAL_SCOPE);
};
/**
* Calculates the real-world length a scale bar represents on the screen.
* `scaledZoom` is the value representing how many units of the current drawing scope one physical pixel covers.
* Multiplying this by the pixel width of the scale bar gives its total real-world length.
*
* @param scaleBarPxWidth - The width of the scale bar in physical pixels on the screen.
* @param scaledZoom - The current scaled zoom value (units of current drawing scope per physical pixel).
* @returns The real-world length the scale bar represents, in the units of the `currentScope` implied by `scaledZoom`.
*/
export const getScaledBarZoomValue = (scaleBarPxWidth, scaledZoom) => {
// scaledZoom is (units of currentScope / 1 physical pixel)
// scaleBarPxWidth is (physical pixels)
// result is (units of currentScope)
return scaleBarPxWidth * scaledZoom;
};
/**
* Calculates the `ScaledZoom` value for a given `scopedZoom` in a `currentScope`.
* `scopedZoom` is the Z-axis zoom value in `currentScope` units, already relative to `NEUTRAL_SCOPE` (higher means more zoomed in).
* `ScaledZoom` (the return value) represents the distance in `currentScope` units
* that a single logical screen pixel covers on the X/Y drawing plane.
* This value is determined by the `scopedZoom` (which reflects the Z-axis viewing
* depth relative to `NEUTRAL_SCOPE`) and the `currentScope`.
*
* @param scopedZoom - The Z-axis zoom value, effectively `rawZoom` expressed in `NEUTRAL_SCOPE` units relative to `currentScope`.
* @param currentScope - The current active measurement scope.
* @returns The calculated `ScaledZoom` (units of `currentScope` per logical screen pixel).
*/
export const getScaledZoomValueForScope = (scopedZoom, currentScope) => {
// Step 1: Convert scopedZoom back to the equivalent rawZoom.
// scopedZoom = rawZoom * SF[currentScope] / SF[NEUTRAL_SCOPE]
// actualRawZoom = scopedZoom * SF[NEUTRAL_SCOPE] / SF[currentScope]
const actualRawZoom = getPrecisionValueForScope(scopedZoom, NEUTRAL_SCOPE, currentScope);
// Handle edge case: If actualRawZoom is 0 (or non-finite), it implies maximum zoom-out.
// In this scenario, 1 pixel covers a very large distance. We represent this large
// distance in NEUTRAL_SCOPE using MAX_ZOOM (since 1/MIN_ZOOM_raw = MAX_ZOOM_distance).
if (actualRawZoom === 0 || !isFinite(actualRawZoom)) {
return getPrecisionValueForScope(MAX_ZOOM, NEUTRAL_SCOPE, currentScope);
}
// Step 2: Calculate distance 1 logical pixel covers in NEUTRAL_SCOPE units.
// This is 1 / actualRawZoom.
const distanceInNeutralScope = 1 / actualRawZoom;
// Step 3: Convert this distance to currentScope units.
// Clamp the distanceInNeutralScope to ensure it's within valid MIN/MAX zoom bounds
// before converting, to prevent extreme values if actualRawZoom was very small/large.
return getPrecisionValueForScope(clampZoom(distanceInNeutralScope), NEUTRAL_SCOPE, currentScope);
};
/**
* Calculates the raw, dimensionless zoom factor from a `ScaledZoom` value.
* `ScaledZoom` represents the distance in `currentScope` units that a single
* logical screen pixel covers on the drawing plane.
* This function is the inverse of `getScaledZoomValueForScope`.
*
* @param scaledZoom - The scaled zoom value (units of `currentScope` per logical screen pixel on the drawing).
* @param currentScope - The scope (unit) in which `scaledZoom` is expressed.
* @returns The raw, dimensionless zoom factor, clamped within MIN_ZOOM and MAX_ZOOM.
*/
export const getRawZoomFromScaledZoom = (scaledZoom, currentScope) => {
// Handle edge case: If scaledZoom is 0, it means 1 pixel covers 0 distance.
// This implies infinite zoom-in.
if (scaledZoom === 0 || !isFinite(scaledZoom)) {
return MAX_ZOOM;
}
// Step 1: Convert scaledZoom (distance in currentScope) to its equivalent distance in NEUTRAL_SCOPE.
// distanceInNeutralScope = scaledZoom * SF[currentScope] / SF[NEUTRAL_SCOPE]
const distanceInNeutralScope = getPrecisionValueForScope(scaledZoom, currentScope, NEUTRAL_SCOPE);
// Handle edge case: If the converted distance in neutral scope is 0.
// This could happen if scaledZoom was extremely small and precision was lost,
// or if currentScope is vastly different from NEUTRAL_SCOPE.
// If 1 pixel covers 0 neutral distance, it implies infinite zoom-in.
if (distanceInNeutralScope === 0 || !isFinite(distanceInNeutralScope)) {
return MAX_ZOOM;
}
// Step 2: Calculate rawZoom.
// Since distanceInNeutralScope = 1 / rawZoom, then rawZoom = 1 / distanceInNeutralScope.
const rawZoom = 1 / distanceInNeutralScope;
// Step 3: Clamp the result to ensure it's a valid zoom value.
return clampZoom(rawZoom);
};
export const getScopedBezierPointFromDucPoint = (point) => {
return Object.assign(Object.assign({}, point), { x: point.x.scoped, y: point.y.scoped });
};
export const getPrecisionPointsFromScoped = (points, targetScope, currentScope) => {
return points.map(point => ({
x: getPrecisionValueFromScoped(point.x, targetScope, currentScope),
y: getPrecisionValueFromScoped(point.y, targetScope, currentScope),
}));
};