@itwin/ecschema-rpcinterface-tests 5.6.0-dev.12 → 5.6.0-dev.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -63738,6 +63738,18 @@ class JsonParser extends _AbstractParser__WEBPACK_IMPORTED_MODULE_2__.AbstractPa
63738
63738
  throw new _Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaStatus.InvalidECJson, `The Format ${this._currentItemFullName} has an invalid 'uomSeparator' attribute. It should be of type 'string'.`);
63739
63739
  if (undefined !== jsonObj.scientificType && typeof (jsonObj.scientificType) !== "string")
63740
63740
  throw new _Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaStatus.InvalidECJson, `The Format ${this._currentItemFullName} has an invalid 'scientificType' attribute. It should be of type 'string'.`);
63741
+ if (undefined !== jsonObj.ratioType && typeof (jsonObj.ratioType) !== "string")
63742
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaStatus.InvalidECJson, `The Format ${this._currentItemFullName} has an invalid 'ratioType' attribute. It should be of type 'string'.`);
63743
+ if (undefined !== jsonObj.ratioSeparator && typeof (jsonObj.ratioSeparator) !== "string")
63744
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaStatus.InvalidECJson, `The Format ${this._currentItemFullName} has an invalid 'ratioSeparator' attribute. It should be of type 'string'.`);
63745
+ if (undefined !== jsonObj.ratioFormatType && typeof (jsonObj.ratioFormatType) !== "string")
63746
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaStatus.InvalidECJson, `The Format ${this._currentItemFullName} has an invalid 'ratioFormatType' attribute. It should be of type 'string'.`);
63747
+ // Validate EC version if ratio properties exist - they require EC version 3.3+
63748
+ if (jsonObj.ratioType !== undefined || jsonObj.ratioSeparator !== undefined || jsonObj.ratioFormatType !== undefined) {
63749
+ if (this._ecSpecVersion === undefined || this._ecSpecVersion.readVersion < 3 || (this._ecSpecVersion.readVersion === 3 && this._ecSpecVersion.writeVersion < 3)) {
63750
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaStatus.InvalidECJson, `The Format ${this._currentItemFullName} has ratio properties that require EC version 3.3 or newer.`);
63751
+ }
63752
+ }
63741
63753
  if (undefined !== jsonObj.stationOffsetSize && typeof (jsonObj.stationOffsetSize) !== "number")
63742
63754
  throw new _Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_0__.ECSchemaStatus.InvalidECJson, `The Format ${this._currentItemFullName} has an invalid 'stationOffsetSize' attribute. It should be of type 'number'.`);
63743
63755
  if (undefined !== jsonObj.stationSeparator && typeof (jsonObj.stationSeparator) !== "string")
@@ -64451,6 +64463,15 @@ class XmlParser extends _AbstractParser__WEBPACK_IMPORTED_MODULE_5__.AbstractPar
64451
64463
  const thousandSeparator = this.getOptionalAttribute(xmlElement, "thousandSeparator");
64452
64464
  const uomSeparator = this.getOptionalAttribute(xmlElement, "uomSeparator");
64453
64465
  const scientificType = this.getOptionalAttribute(xmlElement, "scientificType");
64466
+ const ratioType = this.getOptionalAttribute(xmlElement, "ratioType");
64467
+ const ratioSeparator = this.getOptionalAttribute(xmlElement, "ratioSeparator");
64468
+ const ratioFormatType = this.getOptionalAttribute(xmlElement, "ratioFormatType");
64469
+ // Validate EC version if ratio properties exist - they require EC version 3.3+
64470
+ if (ratioType !== undefined || ratioSeparator !== undefined || ratioFormatType !== undefined) {
64471
+ if (this._ecSpecVersion === undefined || this._ecSpecVersion.readVersion < 3 || (this._ecSpecVersion.readVersion === 3 && this._ecSpecVersion.writeVersion < 3)) {
64472
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidSchemaXML, `The Format ${this._currentItemFullName} has ratio properties that require EC version 3.3 or newer.`);
64473
+ }
64474
+ }
64454
64475
  const stationOffsetSize = this.getOptionalIntAttribute(xmlElement, "stationOffsetSize", `The Format ${this._currentItemFullName} has an invalid 'stationOffsetSize' attribute. It should be a numeric value.`);
64455
64476
  const stationSeparator = this.getOptionalAttribute(xmlElement, "stationSeparator");
64456
64477
  let composite;
@@ -64493,6 +64514,9 @@ class XmlParser extends _AbstractParser__WEBPACK_IMPORTED_MODULE_5__.AbstractPar
64493
64514
  thousandSeparator,
64494
64515
  uomSeparator,
64495
64516
  scientificType,
64517
+ ratioType,
64518
+ ratioSeparator,
64519
+ ratioFormatType,
64496
64520
  stationOffsetSize,
64497
64521
  stationSeparator,
64498
64522
  composite,
@@ -66236,7 +66260,7 @@ class SchemaFormatsProvider {
66236
66260
  // If no matching presentation format was found, use persistence unit format if it matches unit system.
66237
66261
  const persistenceUnit = await kindOfQuantity.persistenceUnit;
66238
66262
  const persistenceUnitSystem = await persistenceUnit?.unitSystem;
66239
- if (persistenceUnitSystem && unitSystemMatchers.some((matcher) => matcher(persistenceUnitSystem))) {
66263
+ if (persistenceUnit && persistenceUnitSystem && unitSystemMatchers.some((matcher) => matcher(persistenceUnitSystem))) {
66240
66264
  this._formatsRetrieved.add(itemKey.fullName);
66241
66265
  const props = getPersistenceUnitFormatProps(persistenceUnit);
66242
66266
  return this.convertToFormatDefinition(props, kindOfQuantity);
@@ -68833,6 +68857,7 @@ __webpack_require__.r(__webpack_exports__);
68833
68857
 
68834
68858
 
68835
68859
 
68860
+ const loggingCategory = "ECClass";
68836
68861
  /**
68837
68862
  * A common abstract class for all of the ECClass types.
68838
68863
  * @public @preview
@@ -68887,7 +68912,13 @@ class ECClass extends _SchemaItem__WEBPACK_IMPORTED_MODULE_9__.SchemaItem {
68887
68912
  async getDerivedClasses() {
68888
68913
  const derivedClasses = [];
68889
68914
  for (const derivedClassKey of this.schema.context.classHierarchy.getDerivedClassKeys(this.key)) {
68890
- const derivedClass = await this.schema.context.getSchemaItem(derivedClassKey, ECClass);
68915
+ let derivedClass = await this.schema.getItem(derivedClassKey.name, ECClass); // if the derived class is in the same schema this will get it without going to the context
68916
+ if (derivedClass) {
68917
+ derivedClasses.push(derivedClass);
68918
+ continue;
68919
+ }
68920
+ _itwin_core_bentley__WEBPACK_IMPORTED_MODULE_0__.Logger.logInfo(loggingCategory, `Derived class ${derivedClassKey.name} not found in schema ${this.schema.name}, looking in schema context.`);
68921
+ derivedClass = await this.schema.context.getSchemaItem(derivedClassKey, ECClass);
68891
68922
  if (derivedClass)
68892
68923
  derivedClasses.push(derivedClass);
68893
68924
  }
@@ -69275,18 +69306,56 @@ class ECClass extends _SchemaItem__WEBPACK_IMPORTED_MODULE_9__.SchemaItem {
69275
69306
  */
69276
69307
  async *getAllBaseClasses() {
69277
69308
  for (const baseClassKey of this.schema.context.classHierarchy.getBaseClassKeys(this.key)) {
69278
- const baseClass = await this.schema.lookupItem(baseClassKey, ECClass);
69309
+ const baseClass = await this.getClassFromReferencesRecursively(baseClassKey); // Search in schema ref tree all the way to the top
69279
69310
  if (baseClass)
69280
69311
  yield baseClass;
69281
69312
  }
69282
69313
  }
69314
+ /**
69315
+ * gets a class from this schema or its references recursively using the item key
69316
+ * @param itemKey
69317
+ * @returns ECClass if it could be found, undefined otherwise
69318
+ * @internal
69319
+ */
69320
+ async getClassFromReferencesRecursively(itemKey) {
69321
+ const schemaList = [this.schema];
69322
+ while (schemaList.length > 0) {
69323
+ const currentSchema = schemaList.shift();
69324
+ if (currentSchema.schemaKey.compareByName(itemKey.schemaKey)) {
69325
+ const baseClass = await currentSchema.getItem(itemKey.name, ECClass);
69326
+ schemaList.splice(0); // clear the list
69327
+ return baseClass;
69328
+ }
69329
+ schemaList.push(...currentSchema.references);
69330
+ }
69331
+ return undefined;
69332
+ }
69283
69333
  *getAllBaseClassesSync() {
69284
69334
  for (const baseClassKey of this.schema.context.classHierarchy.getBaseClassKeys(this.key)) {
69285
- const baseClass = this.schema.lookupItemSync(baseClassKey, ECClass);
69335
+ const baseClass = this.getClassFromReferencesRecursivelySync(baseClassKey); // Search in schema ref tree all the way to the top
69286
69336
  if (baseClass)
69287
69337
  yield baseClass;
69288
69338
  }
69289
69339
  }
69340
+ /**
69341
+ * gets a class from this schema or its references recursively using the item key synchronously
69342
+ * @param itemKey
69343
+ * @returns ECClass if it could be found, undefined otherwise
69344
+ * @internal
69345
+ */
69346
+ getClassFromReferencesRecursivelySync(itemKey) {
69347
+ const schemaList = [this.schema];
69348
+ while (schemaList.length > 0) {
69349
+ const currentSchema = schemaList.shift();
69350
+ if (currentSchema.schemaKey.compareByName(itemKey.schemaKey)) {
69351
+ const baseClass = currentSchema.getItemSync(itemKey.name, ECClass);
69352
+ schemaList.splice(0); // clear the list
69353
+ return baseClass;
69354
+ }
69355
+ schemaList.push(...currentSchema.references);
69356
+ }
69357
+ return undefined;
69358
+ }
69290
69359
  /**
69291
69360
  *
69292
69361
  * @param cache
@@ -70422,6 +70491,9 @@ class Format extends _SchemaItem__WEBPACK_IMPORTED_MODULE_5__.SchemaItem {
70422
70491
  get stationSeparator() { return this._base.stationSeparator; }
70423
70492
  get stationOffsetSize() { return this._base.stationOffsetSize; }
70424
70493
  get stationBaseFactor() { return this._base.stationBaseFactor; }
70494
+ get ratioType() { return this._base.ratioType; }
70495
+ get ratioSeparator() { return this._base.ratioSeparator; }
70496
+ get ratioFormatType() { return this._base.ratioFormatType; }
70425
70497
  get formatTraits() { return this._base.formatTraits; }
70426
70498
  get spacer() { return this._base.spacer; }
70427
70499
  get includeZero() { return this._base.includeZero; }
@@ -70443,10 +70515,13 @@ class Format extends _SchemaItem__WEBPACK_IMPORTED_MODULE_5__.SchemaItem {
70443
70515
  addUnit(unit, label) {
70444
70516
  if (undefined === this._units)
70445
70517
  this._units = [];
70446
- else { // Validate that a duplicate is not added.
70447
- for (const existingUnit of this._units) {
70448
- if (unit.fullName.toLowerCase() === existingUnit[0].fullName.toLowerCase())
70449
- throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidECJson, `The Format ${this.fullName} has duplicate units, '${unit.fullName}'.`); // TODO: Validation - this should be a validation error not a hard failure.
70518
+ else {
70519
+ const isDuplicateAllowed = this.type === _itwin_core_quantity__WEBPACK_IMPORTED_MODULE_3__.FormatType.Ratio;
70520
+ if (!isDuplicateAllowed) {
70521
+ for (const existingUnit of this._units) {
70522
+ if (unit.fullName.toLowerCase() === existingUnit[0].fullName.toLowerCase())
70523
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidECJson, `The Format ${this.fullName} has duplicate units, '${unit.fullName}'.`); // TODO: Validation - this should be a validation error not a hard failure.
70524
+ }
70450
70525
  }
70451
70526
  }
70452
70527
  this._units.push([unit, label]);
@@ -70471,37 +70546,87 @@ class Format extends _SchemaItem__WEBPACK_IMPORTED_MODULE_5__.SchemaItem {
70471
70546
  if (formatProps.composite.units.length <= 0 || formatProps.composite.units.length > 4)
70472
70547
  throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidECJson, `The Format ${this.fullName} has an invalid 'Composite' attribute. It should have 1-4 units.`);
70473
70548
  }
70549
+ // For Ratio formats: validate that composite is provided
70550
+ if (this.type === _itwin_core_quantity__WEBPACK_IMPORTED_MODULE_3__.FormatType.Ratio) {
70551
+ const hasComposite = undefined !== formatProps.composite && formatProps.composite.units.length > 0;
70552
+ if (!hasComposite) {
70553
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidECJson, `The Format ${this.fullName} is 'Ratio' type and must have 'composite' units.`);
70554
+ }
70555
+ }
70474
70556
  }
70475
70557
  fromJSONSync(formatProps) {
70476
70558
  super.fromJSONSync(formatProps);
70477
70559
  this.typecheck(formatProps);
70478
- if (undefined === formatProps.composite)
70479
- return;
70480
- // Units are separated from the rest of the deserialization because of the need to have separate sync and async implementation
70481
- for (const unit of formatProps.composite.units) {
70482
- const newUnit = this.schema.lookupItemSync(unit.name);
70483
- if (undefined === newUnit || (!_Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(newUnit) && !_InvertedUnit__WEBPACK_IMPORTED_MODULE_4__.InvertedUnit.isInvertedUnit(newUnit)))
70484
- throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidECJson, ``);
70485
- if (_Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(newUnit))
70486
- this.addUnit(new _DelayedPromise__WEBPACK_IMPORTED_MODULE_7__.DelayedPromiseWithProps(newUnit.key, async () => newUnit), unit.label);
70487
- else if (_InvertedUnit__WEBPACK_IMPORTED_MODULE_4__.InvertedUnit.isInvertedUnit(newUnit))
70488
- this.addUnit(new _DelayedPromise__WEBPACK_IMPORTED_MODULE_7__.DelayedPromiseWithProps(newUnit.key, async () => newUnit), unit.label);
70560
+ // Process composite units
70561
+ if (undefined !== formatProps.composite) {
70562
+ for (const unit of formatProps.composite.units) {
70563
+ const newUnit = this.schema.lookupItemSync(unit.name);
70564
+ if (undefined === newUnit || (!_Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(newUnit) && !_InvertedUnit__WEBPACK_IMPORTED_MODULE_4__.InvertedUnit.isInvertedUnit(newUnit)))
70565
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidECJson, ``);
70566
+ const lazyUnit = _Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(newUnit)
70567
+ ? new _DelayedPromise__WEBPACK_IMPORTED_MODULE_7__.DelayedPromiseWithProps(newUnit.key, async () => newUnit)
70568
+ : new _DelayedPromise__WEBPACK_IMPORTED_MODULE_7__.DelayedPromiseWithProps(newUnit.key, async () => newUnit);
70569
+ this.addUnit(lazyUnit, unit.label);
70570
+ }
70571
+ // For Ratio formats with 2 units: validate both units have the same phenomenon
70572
+ if (this.type === _itwin_core_quantity__WEBPACK_IMPORTED_MODULE_3__.FormatType.Ratio && this._units && this._units.length === 2) {
70573
+ const unit1Item = this.schema.lookupItemSync(this._units[0][0].fullName);
70574
+ const unit2Item = this.schema.lookupItemSync(this._units[1][0].fullName);
70575
+ if (!unit1Item || !unit2Item || (!_Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(unit1Item) && !_InvertedUnit__WEBPACK_IMPORTED_MODULE_4__.InvertedUnit.isInvertedUnit(unit1Item)) || (!_Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(unit2Item) && !_InvertedUnit__WEBPACK_IMPORTED_MODULE_4__.InvertedUnit.isInvertedUnit(unit2Item)))
70576
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidECJson, `The Format ${this.fullName} has invalid units.`);
70577
+ const getPhenomenon = (unitItem) => {
70578
+ if (_Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(unitItem)) {
70579
+ return unitItem.phenomenon;
70580
+ }
70581
+ const invertsUnit = unitItem.invertsUnit;
70582
+ if (invertsUnit) {
70583
+ const resolvedUnit = this.schema.lookupItemSync(invertsUnit.fullName);
70584
+ return resolvedUnit && _Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(resolvedUnit) ? resolvedUnit.phenomenon : undefined;
70585
+ }
70586
+ return undefined;
70587
+ };
70588
+ const phenomenon1 = getPhenomenon(unit1Item);
70589
+ const phenomenon2 = getPhenomenon(unit2Item);
70590
+ if (phenomenon1?.fullName !== phenomenon2?.fullName) {
70591
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidECJson, `The Format ${this.fullName} has 2-unit composite with different phenomena. Both units must have the same phenomenon.`);
70592
+ }
70593
+ }
70489
70594
  }
70490
70595
  }
70491
70596
  async fromJSON(formatProps) {
70492
70597
  await super.fromJSON(formatProps);
70493
70598
  this.typecheck(formatProps);
70494
- if (undefined === formatProps.composite)
70495
- return;
70496
- // Units are separated from the rest of the deserialization because of the need to have separate sync and async implementation
70497
- for (const unit of formatProps.composite.units) {
70498
- const newUnit = await this.schema.lookupItem(unit.name);
70499
- if (undefined === newUnit || (!_Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(newUnit) && !_InvertedUnit__WEBPACK_IMPORTED_MODULE_4__.InvertedUnit.isInvertedUnit(newUnit)))
70500
- throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidECJson, ``);
70501
- if (_Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(newUnit))
70502
- this.addUnit(new _DelayedPromise__WEBPACK_IMPORTED_MODULE_7__.DelayedPromiseWithProps(newUnit.key, async () => newUnit), unit.label);
70503
- else if (_InvertedUnit__WEBPACK_IMPORTED_MODULE_4__.InvertedUnit.isInvertedUnit(newUnit))
70504
- this.addUnit(new _DelayedPromise__WEBPACK_IMPORTED_MODULE_7__.DelayedPromiseWithProps(newUnit.key, async () => newUnit), unit.label);
70599
+ // Process composite units
70600
+ if (undefined !== formatProps.composite) {
70601
+ for (const unit of formatProps.composite.units) {
70602
+ const newUnit = await this.schema.lookupItem(unit.name);
70603
+ if (undefined === newUnit || (!_Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(newUnit) && !_InvertedUnit__WEBPACK_IMPORTED_MODULE_4__.InvertedUnit.isInvertedUnit(newUnit)))
70604
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidECJson, ``);
70605
+ const lazyUnit = _Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(newUnit)
70606
+ ? new _DelayedPromise__WEBPACK_IMPORTED_MODULE_7__.DelayedPromiseWithProps(newUnit.key, async () => newUnit)
70607
+ : new _DelayedPromise__WEBPACK_IMPORTED_MODULE_7__.DelayedPromiseWithProps(newUnit.key, async () => newUnit);
70608
+ this.addUnit(lazyUnit, unit.label);
70609
+ }
70610
+ // For Ratio formats with 2 units: validate both units have the same phenomenon
70611
+ if (this.type === _itwin_core_quantity__WEBPACK_IMPORTED_MODULE_3__.FormatType.Ratio && this._units && this._units.length === 2) {
70612
+ const unit1Item = await this.schema.lookupItem(this._units[0][0].fullName);
70613
+ const unit2Item = await this.schema.lookupItem(this._units[1][0].fullName);
70614
+ if (!unit1Item || !unit2Item || (!_Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(unit1Item) && !_InvertedUnit__WEBPACK_IMPORTED_MODULE_4__.InvertedUnit.isInvertedUnit(unit1Item)) || (!_Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(unit2Item) && !_InvertedUnit__WEBPACK_IMPORTED_MODULE_4__.InvertedUnit.isInvertedUnit(unit2Item)))
70615
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidECJson, `The Format ${this.fullName} has invalid units.`);
70616
+ // Helper to extract phenomenon from Unit or InvertedUnit
70617
+ const getPhenomenon = async (unitItem) => {
70618
+ if (_Unit__WEBPACK_IMPORTED_MODULE_6__.Unit.isUnit(unitItem)) {
70619
+ return unitItem.phenomenon;
70620
+ }
70621
+ const invertsUnit = await unitItem.invertsUnit;
70622
+ return invertsUnit ? invertsUnit.phenomenon : undefined;
70623
+ };
70624
+ const phenomenon1 = await getPhenomenon(unit1Item);
70625
+ const phenomenon2 = await getPhenomenon(unit2Item);
70626
+ if (phenomenon1?.fullName !== phenomenon2?.fullName) {
70627
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_2__.ECSchemaStatus.InvalidECJson, `The Format ${this.fullName} has 2-unit composite with different phenomena. Both units must have the same phenomenon.`);
70628
+ }
70629
+ }
70505
70630
  }
70506
70631
  }
70507
70632
  /**
@@ -70538,6 +70663,15 @@ class Format extends _SchemaItem__WEBPACK_IMPORTED_MODULE_5__.SchemaItem {
70538
70663
  if (" " !== this.stationSeparator)
70539
70664
  schemaJson.stationSeparator = this.stationSeparator;
70540
70665
  }
70666
+ // Only include ratio properties for EC version 3.3+
70667
+ if (_itwin_core_quantity__WEBPACK_IMPORTED_MODULE_3__.FormatType.Ratio === this.type && this.schema.originalECSpecMajorVersion === 3 && this.schema.originalECSpecMinorVersion !== undefined && this.schema.originalECSpecMinorVersion >= 3) {
70668
+ if (undefined !== this.ratioType)
70669
+ schemaJson.ratioType = this.ratioType;
70670
+ if (undefined !== this.ratioSeparator)
70671
+ schemaJson.ratioSeparator = this.ratioSeparator;
70672
+ if (undefined !== this.ratioFormatType)
70673
+ schemaJson.ratioFormatType = this.ratioFormatType;
70674
+ }
70541
70675
  if (undefined === this.units)
70542
70676
  return schemaJson;
70543
70677
  schemaJson.composite = {};
@@ -70569,6 +70703,15 @@ class Format extends _SchemaItem__WEBPACK_IMPORTED_MODULE_5__.SchemaItem {
70569
70703
  itemElement.setAttribute("minWidth", this.minWidth.toString());
70570
70704
  if (undefined !== this.scientificType)
70571
70705
  itemElement.setAttribute("scientificType", this.scientificType);
70706
+ // Only include ratio properties for EC version 3.3+
70707
+ if (this.schema.originalECSpecMajorVersion === 3 && this.schema.originalECSpecMinorVersion !== undefined && this.schema.originalECSpecMinorVersion >= 3) {
70708
+ if (undefined !== this.ratioType)
70709
+ itemElement.setAttribute("ratioType", this.ratioType);
70710
+ if (undefined !== this.ratioSeparator)
70711
+ itemElement.setAttribute("ratioSeparator", this.ratioSeparator);
70712
+ if (undefined !== this.ratioFormatType)
70713
+ itemElement.setAttribute("ratioFormatType", this.ratioFormatType);
70714
+ }
70572
70715
  if (undefined !== this.stationOffsetSize)
70573
70716
  itemElement.setAttribute("stationOffsetSize", this.stationOffsetSize.toString());
70574
70717
  const formatTraits = (0,_itwin_core_quantity__WEBPACK_IMPORTED_MODULE_3__.formatTraitsToArray)(this.formatTraits);
@@ -71315,6 +71458,9 @@ class OverrideFormat {
71315
71458
  get type() { return this.parent.type; }
71316
71459
  get minWidth() { return this.parent.minWidth; }
71317
71460
  get scientificType() { return this.parent.scientificType; }
71461
+ get ratioType() { return this.parent.ratioType; }
71462
+ get ratioSeparator() { return this.parent.ratioSeparator; }
71463
+ get ratioFormatType() { return this.parent.ratioFormatType; }
71318
71464
  get showSignOption() { return this.parent.showSignOption; }
71319
71465
  get decimalSeparator() { return this.parent.decimalSeparator; }
71320
71466
  get thousandSeparator() { return this.parent.thousandSeparator; }
@@ -71339,7 +71485,7 @@ class OverrideFormat {
71339
71485
  for (const [unit, unitLabel] of this._units) {
71340
71486
  const unitSchema = koqSchema.context.getSchemaSync(unit.schemaKey);
71341
71487
  if (unitSchema === undefined)
71342
- throw new _Exception__WEBPACK_IMPORTED_MODULE_3__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_3__.ECSchemaStatus.InvalidECJson, `The unit schema ${unit.schemaKey} is not found in the context.`);
71488
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_3__.ECSchemaError(_Exception__WEBPACK_IMPORTED_MODULE_3__.ECSchemaStatus.InvalidECJson, `The unit schema ${unit.schemaKey.toString()} is not found in the context.`);
71343
71489
  fullName += "[";
71344
71490
  fullName += _Deserialization_XmlSerializationUtils__WEBPACK_IMPORTED_MODULE_0__.XmlSerializationUtils.createXmlTypedName(koqSchema, unitSchema, unit.name);
71345
71491
  if (unitLabel !== undefined)
@@ -108185,7 +108331,9 @@ function createMeshArgs(mesh) {
108185
108331
  if (!mesh.triangles || mesh.triangles.isEmpty || mesh.points.length === 0)
108186
108332
  return undefined;
108187
108333
  const texture = mesh.displayParams.textureMapping?.texture;
108188
- const textureMapping = texture && mesh.uvParams.length > 0 ? { texture, uvParams: mesh.uvParams } : undefined;
108334
+ const useConstantLod = mesh.displayParams.textureMapping?.params?.useConstantLod;
108335
+ const constantLodParams = mesh.displayParams.textureMapping?.params?.constantLodParams;
108336
+ const textureMapping = texture && mesh.uvParams.length > 0 ? { texture, uvParams: mesh.uvParams, useConstantLod, constantLodParams } : undefined;
108189
108337
  const colors = new _itwin_core_common__WEBPACK_IMPORTED_MODULE_2__.ColorIndex();
108190
108338
  mesh.colorMap.toColorIndex(colors, mesh.colors);
108191
108339
  const features = new _itwin_core_common__WEBPACK_IMPORTED_MODULE_2__.FeatureIndex();
@@ -161403,14 +161551,47 @@ class GltfReader {
161403
161551
  }
161404
161552
  }
161405
161553
  createDisplayParams(material, hasBakedLighting, isPointPrimitive = false) {
161554
+ let constantLodParamProps;
161555
+ let normalMapUseConstantLod = false;
161556
+ if (!(0,_common_gltf_GltfSchema__WEBPACK_IMPORTED_MODULE_12__.isGltf1Material)(material)) {
161557
+ // NOTE: EXT_textureInfo_constant_lod is not supported for occlusionTexture and metallicRoughnessTexture
161558
+ // Use the same texture fallback logic as extractTextureId
161559
+ const textureInfo = material.pbrMetallicRoughness?.baseColorTexture ?? material.emissiveTexture;
161560
+ const extConstantLod = textureInfo?.extensions?.EXT_textureInfo_constant_lod;
161561
+ const offset = extConstantLod?.offset;
161562
+ extConstantLod ? constantLodParamProps = {
161563
+ repetitions: extConstantLod?.repetitions,
161564
+ offset: offset ? { x: offset[0], y: offset[1] } : undefined,
161565
+ minDistClamp: extConstantLod?.minClampDistance,
161566
+ maxDistClamp: extConstantLod?.maxClampDistance,
161567
+ } : undefined;
161568
+ // Normal map only uses constant LOD if both the base texture and normal texture have the extension
161569
+ normalMapUseConstantLod = extConstantLod !== undefined && material.normalTexture?.extensions?.EXT_textureInfo_constant_lod !== undefined;
161570
+ }
161406
161571
  const isTransparent = this.isMaterialTransparent(material);
161407
161572
  const textureId = this.extractTextureId(material);
161408
161573
  const normalMapId = this.extractNormalMapId(material);
161409
- let textureMapping = (undefined !== textureId || undefined !== normalMapId) ? this.findTextureMapping(textureId, isTransparent, normalMapId) : undefined;
161574
+ let textureMapping = (undefined !== textureId || undefined !== normalMapId) ? this.findTextureMapping(textureId, isTransparent, normalMapId, constantLodParamProps, normalMapUseConstantLod) : undefined;
161410
161575
  const color = colorFromMaterial(material, isTransparent);
161411
161576
  let renderMaterial;
161412
- if (undefined !== textureMapping && undefined !== textureMapping.normalMapParams) {
161413
- const args = { diffuse: { color }, specular: { color: _itwin_core_common__WEBPACK_IMPORTED_MODULE_2__.ColorDef.white }, textureMapping };
161577
+ if (undefined !== textureMapping) {
161578
+ // Convert result of findTextureMapping (TextureMapping object) to MaterialTextureMappingProps interface
161579
+ const textureMappingProps = {
161580
+ texture: textureMapping.texture,
161581
+ normalMapParams: textureMapping.normalMapParams,
161582
+ mode: textureMapping.params.mode,
161583
+ transform: textureMapping.params.textureMatrix,
161584
+ weight: textureMapping.params.weight,
161585
+ worldMapping: textureMapping.params.worldMapping,
161586
+ useConstantLod: textureMapping.params.useConstantLod,
161587
+ constantLodProps: textureMapping.params.useConstantLod ? {
161588
+ repetitions: textureMapping.params.constantLodParams.repetitions,
161589
+ offset: textureMapping.params.constantLodParams.offset,
161590
+ minDistClamp: textureMapping.params.constantLodParams.minDistClamp,
161591
+ maxDistClamp: textureMapping.params.constantLodParams.maxDistClamp,
161592
+ } : undefined,
161593
+ };
161594
+ const args = { diffuse: { color }, specular: { color: _itwin_core_common__WEBPACK_IMPORTED_MODULE_2__.ColorDef.white }, textureMapping: textureMappingProps };
161414
161595
  renderMaterial = _IModelApp__WEBPACK_IMPORTED_MODULE_3__.IModelApp.renderSystem.createRenderMaterial(args);
161415
161596
  // DisplayParams doesn't want a separate texture mapping if the material already has one.
161416
161597
  textureMapping = undefined;
@@ -162368,7 +162549,7 @@ class GltfReader {
162368
162549
  });
162369
162550
  return renderTexture ?? false;
162370
162551
  }
162371
- findTextureMapping(id, isTransparent, normalMapId) {
162552
+ findTextureMapping(id, isTransparent, normalMapId, constantLodParamProps, normalMapUseConstantLod = false) {
162372
162553
  if (undefined === id && undefined === normalMapId)
162373
162554
  return undefined;
162374
162555
  let texture;
@@ -162390,16 +162571,18 @@ class GltfReader {
162390
162571
  nMap = {
162391
162572
  normalMap,
162392
162573
  greenUp,
162574
+ useConstantLod: normalMapUseConstantLod,
162393
162575
  };
162394
162576
  }
162395
162577
  else {
162396
162578
  texture = normalMap;
162397
- nMap = { greenUp };
162579
+ nMap = { greenUp, useConstantLod: normalMapUseConstantLod };
162398
162580
  }
162399
162581
  }
162400
162582
  if (!texture)
162401
162583
  return undefined;
162402
- const textureMapping = new _itwin_core_common__WEBPACK_IMPORTED_MODULE_2__.TextureMapping(texture, new _itwin_core_common__WEBPACK_IMPORTED_MODULE_2__.TextureMapping.Params());
162584
+ const useConstantLod = constantLodParamProps !== undefined;
162585
+ const textureMapping = new _itwin_core_common__WEBPACK_IMPORTED_MODULE_2__.TextureMapping(texture, new _itwin_core_common__WEBPACK_IMPORTED_MODULE_2__.TextureMapping.Params({ useConstantLod, constantLodProps: constantLodParamProps }));
162403
162586
  textureMapping.normalMapParams = nMap;
162404
162587
  return textureMapping;
162405
162588
  }
@@ -304095,6 +304278,8 @@ class BaseFormat {
304095
304278
  _stationOffsetSize; // required when type is station; positive integer > 0
304096
304279
  _stationBaseFactor; // optional positive integer base factor for station formatting; default is 1
304097
304280
  _ratioType; // required if type is ratio; options: oneToN, NToOne, ValueBased, useGreatestCommonDivisor
304281
+ _ratioFormatType; // defaults to Decimal if not specified
304282
+ _ratioSeparator; // default is ":"; separator character used in ratio formatting
304098
304283
  _azimuthBase; // value always clockwise from north
304099
304284
  _azimuthBaseUnit; // unit for azimuthBase value
304100
304285
  _azimuthCounterClockwise; // if set to true, azimuth values are returned counter-clockwise from base
@@ -304116,6 +304301,10 @@ class BaseFormat {
304116
304301
  set scientificType(scientificType) { this._scientificType = scientificType; }
304117
304302
  get ratioType() { return this._ratioType; }
304118
304303
  set ratioType(ratioType) { this._ratioType = ratioType; }
304304
+ get ratioFormatType() { return this._ratioFormatType; }
304305
+ set ratioFormatType(ratioFormatType) { this._ratioFormatType = ratioFormatType; }
304306
+ get ratioSeparator() { return this._ratioSeparator; }
304307
+ set ratioSeparator(ratioSeparator) { this._ratioSeparator = ratioSeparator; }
304119
304308
  get showSignOption() { return this._showSignOption; }
304120
304309
  set showSignOption(showSignOption) { this._showSignOption = showSignOption; }
304121
304310
  get decimalSeparator() { return this._decimalSeparator; }
@@ -304184,6 +304373,22 @@ class BaseFormat {
304184
304373
  if (undefined === formatProps.ratioType)
304185
304374
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidJson, `The Format ${this.name} is 'Ratio' type therefore the attribute 'ratioType' is required.`);
304186
304375
  this._ratioType = (0,_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.parseRatioType)(formatProps.ratioType, this.name);
304376
+ if (undefined !== formatProps.ratioSeparator) {
304377
+ if (typeof (formatProps.ratioSeparator) !== "string")
304378
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidJson, `The Format ${this.name} has an invalid 'ratioSeparator' attribute. It should be of type 'string'.`);
304379
+ if (formatProps.ratioSeparator.length !== 1)
304380
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidJson, `The Format ${this.name} has an invalid 'ratioSeparator' attribute. It should be a one character string.`);
304381
+ this._ratioSeparator = formatProps.ratioSeparator;
304382
+ }
304383
+ else {
304384
+ this._ratioSeparator = ":"; // Apply default
304385
+ }
304386
+ if (undefined !== formatProps.ratioFormatType) {
304387
+ this._ratioFormatType = (0,_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.parseRatioFormatType)(formatProps.ratioFormatType, this.name);
304388
+ }
304389
+ else {
304390
+ this._ratioFormatType = _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.RatioFormatType.Decimal; // Apply default
304391
+ }
304187
304392
  }
304188
304393
  if (undefined !== formatProps.roundFactor) { // optional; default is 0.0
304189
304394
  if (typeof (formatProps.roundFactor) !== "number")
@@ -304308,6 +304513,8 @@ class Format extends BaseFormat {
304308
304513
  newFormat._azimuthBaseUnit = this._azimuthBaseUnit;
304309
304514
  newFormat._azimuthCounterClockwise = this._azimuthCounterClockwise;
304310
304515
  newFormat._ratioType = this._ratioType;
304516
+ newFormat._ratioFormatType = this._ratioFormatType;
304517
+ newFormat._ratioSeparator = this._ratioSeparator;
304311
304518
  newFormat._revolutionUnit = this._revolutionUnit;
304312
304519
  newFormat._customProps = this._customProps;
304313
304520
  this._units && (newFormat._units = [...this._units]);
@@ -304368,25 +304575,28 @@ class Format extends BaseFormat {
304368
304575
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidJson, `The Format ${this.name} has a Composite with an invalid 'units' attribute. It must be of type 'array'`);
304369
304576
  }
304370
304577
  if (jsonObj.composite.units.length > 0 && jsonObj.composite.units.length <= 4) { // Composite requires 1-4 units
304371
- for (const nextUnit of jsonObj.composite.units) {
304372
- if (this._units) {
304373
- for (const existingUnit of this._units) {
304374
- const unitObj = existingUnit[0].name;
304375
- if (unitObj.toLowerCase() === nextUnit.unit.name.toLowerCase()) {
304376
- throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidJson, `The unit ${unitObj} has a duplicate name.`);
304377
- }
304578
+ const isDuplicateAllowed = this.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Ratio;
304579
+ const seenUnits = new Set();
304580
+ this._units = [];
304581
+ for (const unitSpec of jsonObj.composite.units) {
304582
+ if (!isDuplicateAllowed) {
304583
+ const unitName = unitSpec.unit.name.toLowerCase();
304584
+ const existingName = seenUnits.has(unitName);
304585
+ if (existingName) {
304586
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidJson, `The Format ${this.name} contains duplicate units: '${unitSpec.unit.name}'`);
304378
304587
  }
304588
+ seenUnits.add(unitName);
304379
304589
  }
304380
- if (undefined === this._units) {
304381
- this._units = [];
304382
- }
304383
- this._units.push([nextUnit.unit, nextUnit.label]);
304590
+ this._units.push([unitSpec.unit, unitSpec.label]);
304384
304591
  }
304385
304592
  }
304386
304593
  }
304387
304594
  if (undefined === this.units || this.units.length === 0)
304388
304595
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidJson, `The Format ${this.name} has a Composite with no valid 'units'`);
304389
304596
  }
304597
+ if (this.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Ratio && (!this._units || this._units.length === 0)) {
304598
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidJson, `The Format ${this.name} is 'Ratio' type and must have 'composite' units.`);
304599
+ }
304390
304600
  if (this.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Azimuth || this.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Bearing) {
304391
304601
  this._azimuthBaseUnit = jsonObj.azimuthBaseUnit;
304392
304602
  this._revolutionUnit = jsonObj.revolutionUnit;
@@ -304453,6 +304663,8 @@ class Format extends BaseFormat {
304453
304663
  uomSeparator: this.uomSeparator,
304454
304664
  scientificType: this.scientificType ? this.scientificType : undefined,
304455
304665
  ratioType: this.ratioType,
304666
+ ratioFormatType: this.ratioFormatType,
304667
+ ratioSeparator: this.ratioSeparator,
304456
304668
  stationOffsetSize: this.stationOffsetSize,
304457
304669
  stationSeparator: this.stationSeparator,
304458
304670
  stationBaseFactor: this.stationBaseFactor,
@@ -304497,6 +304709,15 @@ async function resolveFormatProps(formatName, unitsProvider, jsonObj) {
304497
304709
  const unit = await resolveCompositeUnit(unitsProvider, entry.name);
304498
304710
  return { unit, label: entry.label };
304499
304711
  }));
304712
+ // For Ratio formats with 2 units: validate both units have the same phenomenon
304713
+ const formatType = (0,_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.parseFormatType)(jsonObj.type, formatName);
304714
+ if (formatType === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Ratio && units.length === 2) {
304715
+ const phenomenon1 = units[0].unit.phenomenon;
304716
+ const phenomenon2 = units[1].unit.phenomenon;
304717
+ if (phenomenon1 !== phenomenon2) {
304718
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidJson, `The Format ${formatName} has 2-unit composite with different phenomena. Both units must have the same phenomenon. Found '${phenomenon1}' and '${phenomenon2}'.`);
304719
+ }
304720
+ }
304500
304721
  }
304501
304722
  let azimuthBaseUnit, revolutionUnit;
304502
304723
  const type = (0,_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.parseFormatType)(jsonObj.type, formatName);
@@ -304537,6 +304758,7 @@ __webpack_require__.r(__webpack_exports__);
304537
304758
  /* harmony export */ FormatTraits: () => (/* binding */ FormatTraits),
304538
304759
  /* harmony export */ FormatType: () => (/* binding */ FormatType),
304539
304760
  /* harmony export */ FractionalPrecision: () => (/* binding */ FractionalPrecision),
304761
+ /* harmony export */ RatioFormatType: () => (/* binding */ RatioFormatType),
304540
304762
  /* harmony export */ RatioType: () => (/* binding */ RatioType),
304541
304763
  /* harmony export */ ScientificType: () => (/* binding */ ScientificType),
304542
304764
  /* harmony export */ ShowSignOption: () => (/* binding */ ShowSignOption),
@@ -304550,6 +304772,7 @@ __webpack_require__.r(__webpack_exports__);
304550
304772
  /* harmony export */ parseFormatType: () => (/* binding */ parseFormatType),
304551
304773
  /* harmony export */ parseFractionalPrecision: () => (/* binding */ parseFractionalPrecision),
304552
304774
  /* harmony export */ parsePrecision: () => (/* binding */ parsePrecision),
304775
+ /* harmony export */ parseRatioFormatType: () => (/* binding */ parseRatioFormatType),
304553
304776
  /* harmony export */ parseRatioType: () => (/* binding */ parseRatioType),
304554
304777
  /* harmony export */ parseScientificType: () => (/* binding */ parseScientificType),
304555
304778
  /* harmony export */ parseShowSignOption: () => (/* binding */ parseShowSignOption),
@@ -304694,6 +304917,16 @@ var RatioType;
304694
304917
  /** scales the input ratio to its simplest integer form using the greatest common divisor (GCD) of the values. e.g. 0.3 turns into 3:10 */
304695
304918
  RatioType["UseGreatestCommonDivisor"] = "UseGreatestCommonDivisor";
304696
304919
  })(RatioType || (RatioType = {}));
304920
+ /** The format type for the numbers within a ratio.
304921
+ * @beta
304922
+ */
304923
+ var RatioFormatType;
304924
+ (function (RatioFormatType) {
304925
+ /** Decimal display (ie 2.125) */
304926
+ RatioFormatType["Decimal"] = "Decimal";
304927
+ /** Fractional display (ie 2-1/8) */
304928
+ RatioFormatType["Fractional"] = "Fractional";
304929
+ })(RatioFormatType || (RatioFormatType = {}));
304697
304930
  /** Determines how the sign of values are displayed
304698
304931
  * @beta */
304699
304932
  var ShowSignOption;
@@ -304739,6 +304972,18 @@ function parseRatioType(ratioType, formatName) {
304739
304972
  }
304740
304973
  throw new _Exception__WEBPACK_IMPORTED_MODULE_0__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_0__.QuantityStatus.InvalidJson, `The Format ${formatName} has an invalid 'ratioType' attribute.`);
304741
304974
  }
304975
+ /** @beta */
304976
+ function parseRatioFormatType(ratioFormatType, formatName) {
304977
+ const normalizedValue = ratioFormatType.toLowerCase();
304978
+ for (const key in RatioFormatType) {
304979
+ if (RatioFormatType.hasOwnProperty(key)) {
304980
+ const enumValue = RatioFormatType[key];
304981
+ if (enumValue.toLowerCase() === normalizedValue)
304982
+ return enumValue;
304983
+ }
304984
+ }
304985
+ throw new _Exception__WEBPACK_IMPORTED_MODULE_0__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_0__.QuantityStatus.InvalidJson, `The Format ${formatName} has an invalid 'ratioFormatType' attribute.`);
304986
+ }
304742
304987
  /** @beta */
304743
304988
  function parseShowSignOption(showSignOption, formatName) {
304744
304989
  switch (showSignOption.toLowerCase()) {
@@ -304900,10 +305145,18 @@ function parsePrecision(precision, type, formatName) {
304900
305145
  case FormatType.Decimal:
304901
305146
  case FormatType.Scientific:
304902
305147
  case FormatType.Station:
304903
- case FormatType.Ratio:
304904
305148
  case FormatType.Bearing:
304905
305149
  case FormatType.Azimuth:
304906
305150
  return parseDecimalPrecision(precision, formatName);
305151
+ case FormatType.Ratio:
305152
+ // Ratio type can use either decimal or fractional precision depending on ratioFormatType
305153
+ // Try decimal first, if it fails, try fractional
305154
+ try {
305155
+ return parseDecimalPrecision(precision, formatName);
305156
+ }
305157
+ catch {
305158
+ return parseFractionalPrecision(precision, formatName);
305159
+ }
304907
305160
  case FormatType.Fractional:
304908
305161
  return parseFractionalPrecision(precision, formatName);
304909
305162
  default:
@@ -304927,8 +305180,9 @@ __webpack_require__.r(__webpack_exports__);
304927
305180
  /* harmony export */ });
304928
305181
  /* harmony import */ var _Constants__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Constants */ "../../core/quantity/lib/esm/Constants.js");
304929
305182
  /* harmony import */ var _Exception__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../Exception */ "../../core/quantity/lib/esm/Exception.js");
304930
- /* harmony import */ var _FormatEnums__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./FormatEnums */ "../../core/quantity/lib/esm/Formatter/FormatEnums.js");
304931
- /* harmony import */ var _Quantity__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../Quantity */ "../../core/quantity/lib/esm/Quantity.js");
305183
+ /* harmony import */ var _FormatterSpec__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./FormatterSpec */ "../../core/quantity/lib/esm/Formatter/FormatterSpec.js");
305184
+ /* harmony import */ var _FormatEnums__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./FormatEnums */ "../../core/quantity/lib/esm/Formatter/FormatEnums.js");
305185
+ /* harmony import */ var _Quantity__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../Quantity */ "../../core/quantity/lib/esm/Quantity.js");
304932
305186
  /*---------------------------------------------------------------------------------------------
304933
305187
  * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
304934
305188
  * See LICENSE.md in the project root for license terms and full copyright notice.
@@ -304940,6 +305194,7 @@ __webpack_require__.r(__webpack_exports__);
304940
305194
 
304941
305195
 
304942
305196
 
305197
+
304943
305198
  /** rounding additive
304944
305199
  * @internal
304945
305200
  */
@@ -305040,7 +305295,7 @@ class Formatter {
305040
305295
  static integerPartToText(wholePart, spec) {
305041
305296
  // build invariant string represent wholePart
305042
305297
  let formattedValue = wholePart.toFixed(0);
305043
- if ((formattedValue.length > 3) && (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.Use1000Separator) && (spec.format.thousandSeparator.length > 0))) {
305298
+ if ((formattedValue.length > 3) && (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.Use1000Separator) && (spec.format.thousandSeparator.length > 0))) {
305044
305299
  let numSeparators = Math.floor(formattedValue.length / 3);
305045
305300
  let groupLength = formattedValue.length % 3;
305046
305301
  if (groupLength === 0) {
@@ -305087,7 +305342,7 @@ class Formatter {
305087
305342
  else {
305088
305343
  componentText = Formatter.formatMagnitude(compositeValue, spec);
305089
305344
  }
305090
- if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.ShowUnitLabel)) {
305345
+ if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.ShowUnitLabel)) {
305091
305346
  componentText = componentText + spec.format.uomSeparator + label;
305092
305347
  }
305093
305348
  return componentText;
@@ -305107,12 +305362,11 @@ class Formatter {
305107
305362
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidCompositeFormat, `The Format ${spec.format.name} has a invalid unit specification.`);
305108
305363
  if (i > 0 && unitConversion.offset !== 0) // offset should only ever be defined for major unit
305109
305364
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidCompositeFormat, `The Format ${spec.format.name} has a invalid unit specification.`);
305110
- let unitValue = 0.0;
305111
- if (spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Ratio) {
305112
- if (1 !== (spec.format.units?.length ?? 0))
305113
- throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidCompositeFormat, `The Format '${spec.format.name}' with type 'ratio' must have exactly one unit.`);
305365
+ // Handle ratio format with composite units
305366
+ if (spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Ratio) {
305367
+ let ratioUnitValue = 0.0;
305114
305368
  try {
305115
- unitValue = (0,_Quantity__WEBPACK_IMPORTED_MODULE_3__.applyConversion)(remainingMagnitude, unitConversion) + this.FPV_MINTHRESHOLD;
305369
+ ratioUnitValue = (0,_Quantity__WEBPACK_IMPORTED_MODULE_4__.applyConversion)(remainingMagnitude, unitConversion) + this.FPV_MINTHRESHOLD;
305116
305370
  }
305117
305371
  catch (e) {
305118
305372
  // The "InvertingZero" error is thrown when the value is zero and the conversion factor is inverted.
@@ -305120,12 +305374,13 @@ class Formatter {
305120
305374
  if (e instanceof _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError && e.errorNumber === _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvertingZero) {
305121
305375
  return { componentText: "1:0", isNegative: false };
305122
305376
  }
305377
+ throw e;
305123
305378
  }
305124
- compositeStrings.push(this.formatRatio(unitValue, spec));
305125
- isNegative = unitValue < 0;
305379
+ compositeStrings.push(this.formatRatio(ratioUnitValue, spec));
305380
+ isNegative = ratioUnitValue < 0;
305126
305381
  continue;
305127
305382
  }
305128
- unitValue = (0,_Quantity__WEBPACK_IMPORTED_MODULE_3__.applyConversion)(remainingMagnitude, unitConversion) + this.FPV_MINTHRESHOLD;
305383
+ let unitValue = (0,_Quantity__WEBPACK_IMPORTED_MODULE_4__.applyConversion)(remainingMagnitude, unitConversion) + this.FPV_MINTHRESHOLD;
305129
305384
  if (0 === i) {
305130
305385
  // Only set isNegative from the first (major) unit conversion
305131
305386
  isNegative = unitValue < 0;
@@ -305133,16 +305388,16 @@ class Formatter {
305133
305388
  // but use higher precision if the format specifies it
305134
305389
  const precisionScale = Math.pow(10, Math.max(8, spec.format.precision));
305135
305390
  unitValue = Math.floor(unitValue * precisionScale + FPV_ROUNDFACTOR) / precisionScale;
305136
- if ((Math.abs(unitValue) < 0.0001) && spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.ZeroEmpty))
305391
+ if ((Math.abs(unitValue) < 0.0001) && spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.ZeroEmpty))
305137
305392
  return { componentText: "", isNegative: false };
305138
305393
  }
305139
305394
  if (i < (spec.format.units?.length ?? 0) - 1) {
305140
305395
  let wholePart = Math.trunc(unitValue);
305141
305396
  // Check if the remaining fractional part will round up to a full unit in the next (smaller) component
305142
- if (spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Fractional && i === spec.unitConversions.length - 2) {
305397
+ if (spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Fractional && i === spec.unitConversions.length - 2) {
305143
305398
  // For the second-to-last unit with fractional formatting, check if rounding causes carry-over
305144
305399
  const fractionalPart = unitValue - wholePart;
305145
- const nextUnitValue = (0,_Quantity__WEBPACK_IMPORTED_MODULE_3__.applyConversion)(fractionalPart, spec.unitConversions[i + 1].conversion);
305400
+ const nextUnitValue = (0,_Quantity__WEBPACK_IMPORTED_MODULE_4__.applyConversion)(fractionalPart, spec.unitConversions[i + 1].conversion);
305146
305401
  // Create a FractionalNumeric to determine what the rounded value would be
305147
305402
  const fn = new FractionalNumeric(Math.abs(nextUnitValue), spec.format.precision, true);
305148
305403
  // If the fractional numeric rounds to a whole unit (integral part increased due to rounding)
@@ -305172,18 +305427,18 @@ class Formatter {
305172
305427
  */
305173
305428
  static formatMagnitude(magnitude, spec) {
305174
305429
  let posMagnitude = Math.abs(magnitude);
305175
- if ((Math.abs(posMagnitude) < 0.0001) && spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.ZeroEmpty))
305430
+ if ((Math.abs(posMagnitude) < 0.0001) && spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.ZeroEmpty))
305176
305431
  return "";
305177
- if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.ApplyRounding))
305432
+ if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.ApplyRounding))
305178
305433
  posMagnitude = Math.abs(Formatter.roundDouble(magnitude, spec.format.roundFactor));
305179
- const isSci = ((posMagnitude > 1.0e12) || spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Scientific);
305180
- const isDecimal = (isSci || spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Decimal || spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Bearing || spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Azimuth) || spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Ratio;
305181
- const isFractional = (!isDecimal && spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Fractional);
305434
+ const isSci = ((posMagnitude > 1.0e12) || spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Scientific);
305435
+ const isDecimal = (isSci || spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Decimal || spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Bearing || spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Azimuth) || spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Ratio;
305436
+ const isFractional = (!isDecimal && spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Fractional);
305182
305437
  /* const usesStops = spec.format.type === FormatType.Station; */
305183
- const isPrecisionZero = spec.format.precision === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.DecimalPrecision.Zero;
305184
- const isKeepSingleZero = spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.KeepSingleZero);
305438
+ const isPrecisionZero = spec.format.precision === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.DecimalPrecision.Zero;
305439
+ const isKeepSingleZero = spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.KeepSingleZero);
305185
305440
  const precisionScale = Math.pow(10.0, spec.format.precision);
305186
- const isKeepTrailingZeroes = spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.TrailZeroes);
305441
+ const isKeepTrailingZeroes = spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.TrailZeroes);
305187
305442
  let expInt = 0.0;
305188
305443
  if (isSci && (posMagnitude !== 0.0)) {
305189
305444
  let exp = Math.log10(posMagnitude);
@@ -305193,10 +305448,10 @@ class Formatter {
305193
305448
  negativeExp = true;
305194
305449
  }
305195
305450
  expInt = Math.floor(exp);
305196
- if (spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Scientific) {
305197
- if (spec.format.scientificType === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.ScientificType.ZeroNormalized && posMagnitude > 1.0)
305451
+ if (spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Scientific) {
305452
+ if (spec.format.scientificType === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.ScientificType.ZeroNormalized && posMagnitude > 1.0)
305198
305453
  expInt += 1.0;
305199
- else if (spec.format.scientificType === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.ScientificType.Normalized && posMagnitude < 1.0)
305454
+ else if (spec.format.scientificType === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.ScientificType.Normalized && posMagnitude < 1.0)
305200
305455
  expInt += 1.0;
305201
305456
  if (negativeExp)
305202
305457
  expInt = -expInt;
@@ -305218,7 +305473,7 @@ class Formatter {
305218
305473
  }
305219
305474
  formattedValue = Formatter.integerPartToText(wholePart, spec);
305220
305475
  if (isPrecisionZero) {
305221
- if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.KeepDecimalPoint) && !isKeepSingleZero)
305476
+ if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.KeepDecimalPoint) && !isKeepSingleZero)
305222
305477
  formattedValue = formattedValue + spec.format.decimalSeparator;
305223
305478
  else if (isKeepSingleZero)
305224
305479
  formattedValue = `${formattedValue + spec.format.decimalSeparator}0`;
@@ -305233,7 +305488,7 @@ class Formatter {
305233
305488
  if (fractionString.length > 0)
305234
305489
  formattedValue = formattedValue + spec.format.decimalSeparator + fractionString;
305235
305490
  else {
305236
- if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.KeepDecimalPoint))
305491
+ if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.KeepDecimalPoint))
305237
305492
  formattedValue = formattedValue + spec.format.decimalSeparator + (isKeepSingleZero ? "0" : "");
305238
305493
  }
305239
305494
  }
@@ -305246,7 +305501,7 @@ class Formatter {
305246
305501
  const fn = new FractionalNumeric(posMagnitude, spec.format.precision, true);
305247
305502
  formattedValue = fn.getIntegralString();
305248
305503
  if (!fn.isZero && fn.hasFractionPart) {
305249
- const wholeFractionSeparator = spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.FractionDash) ? "-" : " ";
305504
+ const wholeFractionSeparator = spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.FractionDash) ? "-" : " ";
305250
305505
  const fractionString = `${fn.getNumeratorString()}/${fn.getDenominatorString()}`;
305251
305506
  formattedValue = formattedValue + wholeFractionSeparator + fractionString;
305252
305507
  }
@@ -305275,7 +305530,7 @@ class Formatter {
305275
305530
  else {
305276
305531
  if (isKeepTrailingZeroes)
305277
305532
  fractionString = spec.format.decimalSeparator + "".padEnd(spec.format.precision, "0");
305278
- else if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.KeepDecimalPoint))
305533
+ else if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.KeepDecimalPoint))
305279
305534
  fractionString = spec.format.decimalSeparator;
305280
305535
  formattedValue = stationString + fractionString;
305281
305536
  }
@@ -305304,21 +305559,21 @@ class Formatter {
305304
305559
  let prefix = "";
305305
305560
  let suffix = "";
305306
305561
  switch (showSignOption) {
305307
- case _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.ShowSignOption.NegativeParentheses:
305562
+ case _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.ShowSignOption.NegativeParentheses:
305308
305563
  if (isNegative) {
305309
305564
  prefix = "(";
305310
305565
  suffix = ")";
305311
305566
  }
305312
305567
  break;
305313
- case _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.ShowSignOption.OnlyNegative:
305314
- if (isNegative && formatType !== _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Bearing && formatType !== _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Azimuth) {
305568
+ case _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.ShowSignOption.OnlyNegative:
305569
+ if (isNegative && formatType !== _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Bearing && formatType !== _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Azimuth) {
305315
305570
  prefix = "-";
305316
305571
  }
305317
305572
  break;
305318
- case _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.ShowSignOption.SignAlways:
305573
+ case _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.ShowSignOption.SignAlways:
305319
305574
  prefix = isNegative ? "-" : "+";
305320
305575
  break;
305321
- case _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.ShowSignOption.NoSign:
305576
+ case _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.ShowSignOption.NoSign:
305322
305577
  default:
305323
305578
  break;
305324
305579
  }
@@ -305334,14 +305589,20 @@ class Formatter {
305334
305589
  let suffix = "";
305335
305590
  let formattedValue = "";
305336
305591
  // Handle bearing/azimuth special formatting
305337
- if (spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Bearing || spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Azimuth) {
305592
+ if (spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Bearing || spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Azimuth) {
305338
305593
  const result = this.processBearingAndAzimuth(magnitude, spec);
305339
305594
  magnitude = result.magnitude;
305340
305595
  prefix = result.prefix ?? "";
305341
305596
  suffix = result.suffix ?? "";
305342
305597
  }
305343
305598
  let formattedMagnitude = "";
305344
- if (spec.format.hasUnits) {
305599
+ if (spec.format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Ratio && spec.unitConversions.length >= 3) {
305600
+ // Handle ratio formatting separately when 2-unit composite provides 3 conversion specs
305601
+ const ratioResult = this.formatRatioQuantity(magnitude, spec);
305602
+ formattedMagnitude = ratioResult.componentText;
305603
+ valueIsNegative = ratioResult.isNegative;
305604
+ }
305605
+ else if (spec.format.hasUnits) {
305345
305606
  const compositeResult = Formatter.formatComposite(magnitude, spec);
305346
305607
  formattedMagnitude = compositeResult.componentText;
305347
305608
  // Override the sign detection with the composite conversion result
@@ -305350,8 +305611,8 @@ class Formatter {
305350
305611
  else {
305351
305612
  // unitless quantity
305352
305613
  formattedMagnitude = Formatter.formatMagnitude(magnitude, spec);
305353
- if (formattedMagnitude.length > 0 && spec.unitConversions.length > 0 && spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.ShowUnitLabel)) {
305354
- if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatTraits.PrependUnitLabel))
305614
+ if (formattedMagnitude.length > 0 && spec.unitConversions.length > 0 && spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.ShowUnitLabel)) {
305615
+ if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.PrependUnitLabel))
305355
305616
  formattedMagnitude = spec.unitConversions[0].label + spec.format.uomSeparator + formattedMagnitude;
305356
305617
  else
305357
305618
  formattedMagnitude = formattedMagnitude + spec.format.uomSeparator + spec.unitConversions[0].label;
@@ -305371,12 +305632,12 @@ class Formatter {
305371
305632
  }
305372
305633
  static processBearingAndAzimuth(magnitude, spec) {
305373
305634
  const type = spec.format.type;
305374
- if (type !== _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Bearing && type !== _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Azimuth)
305635
+ if (type !== _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Bearing && type !== _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Azimuth)
305375
305636
  return { magnitude };
305376
305637
  const revolution = this.getRevolution(spec);
305377
305638
  magnitude = this.normalizeAngle(magnitude, revolution);
305378
305639
  const quarterRevolution = revolution / 4;
305379
- if (type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Bearing) {
305640
+ if (type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Bearing) {
305380
305641
  let quadrant = 0;
305381
305642
  while (magnitude > quarterRevolution) {
305382
305643
  magnitude -= quarterRevolution;
@@ -305403,7 +305664,7 @@ class Formatter {
305403
305664
  if (quadrant === 2 && spec.unitConversions.length > 0) {
305404
305665
  // To determine if value is small, we need to convert it to the smallest unit presented and use the provided precision on it
305405
305666
  const unitConversion = spec.unitConversions[spec.unitConversions.length - 1].conversion;
305406
- const smallestFormattedDelta = (0,_Quantity__WEBPACK_IMPORTED_MODULE_3__.applyConversion)((quarterRevolution - magnitude), unitConversion) + this.FPV_MINTHRESHOLD;
305667
+ const smallestFormattedDelta = (0,_Quantity__WEBPACK_IMPORTED_MODULE_4__.applyConversion)((quarterRevolution - magnitude), unitConversion) + this.FPV_MINTHRESHOLD;
305407
305668
  const precisionScale = Math.pow(10.0, spec.format.precision);
305408
305669
  const floor = Math.floor((smallestFormattedDelta) * precisionScale + FPV_ROUNDFACTOR) / precisionScale;
305409
305670
  if (floor === 0) {
@@ -305412,13 +305673,13 @@ class Formatter {
305412
305673
  }
305413
305674
  return { magnitude, prefix, suffix };
305414
305675
  }
305415
- if (type === _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Azimuth) {
305676
+ if (type === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Azimuth) {
305416
305677
  let azimuthBase = 0; // default base is North
305417
305678
  if (spec.format.azimuthBase !== undefined) {
305418
305679
  if (spec.azimuthBaseConversion === undefined) {
305419
305680
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.MissingRequiredProperty, `Missing azimuth base conversion for interpreting ${spec.name}'s azimuth base.`);
305420
305681
  }
305421
- const azBaseQuantity = new _Quantity__WEBPACK_IMPORTED_MODULE_3__.Quantity(spec.format.azimuthBaseUnit, spec.format.azimuthBase);
305682
+ const azBaseQuantity = new _Quantity__WEBPACK_IMPORTED_MODULE_4__.Quantity(spec.format.azimuthBaseUnit, spec.format.azimuthBase);
305422
305683
  const azBaseConverted = azBaseQuantity.convertTo(spec.persistenceUnit, spec.azimuthBaseConversion);
305423
305684
  if (azBaseConverted === undefined || !azBaseConverted.isValid) {
305424
305685
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.UnsupportedUnit, `Failed to convert azimuth base unit to ${spec.persistenceUnit.name}.`);
@@ -305448,40 +305709,87 @@ class Formatter {
305448
305709
  if (spec.revolutionConversion === undefined) {
305449
305710
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.MissingRequiredProperty, `Missing revolution unit conversion for calculating ${spec.name}'s revolution.`);
305450
305711
  }
305451
- const revolution = new _Quantity__WEBPACK_IMPORTED_MODULE_3__.Quantity(spec.format.revolutionUnit, 1.0);
305712
+ const revolution = new _Quantity__WEBPACK_IMPORTED_MODULE_4__.Quantity(spec.format.revolutionUnit, 1.0);
305452
305713
  const converted = revolution.convertTo(spec.persistenceUnit, spec.revolutionConversion);
305453
305714
  if (converted === undefined || !converted.isValid) {
305454
305715
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.UnsupportedUnit, `Failed to convert revolution unit to ${spec.persistenceUnit.name}.`);
305455
305716
  }
305456
305717
  return converted.magnitude;
305457
305718
  }
305719
+ static formatRatioPart(value, spec, side) {
305720
+ const formatType = spec.format.ratioFormatType === "Fractional" ? _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Fractional : _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Decimal;
305721
+ const tempFormat = spec.format.clone({ type: formatType });
305722
+ const tempSpec = new _FormatterSpec__WEBPACK_IMPORTED_MODULE_2__.FormatterSpec(spec.name, tempFormat, spec.unitConversions, spec.persistenceUnit);
305723
+ let formattedValue = this.formatMagnitude(value, tempSpec);
305724
+ // For fractional ratio formatting, suppress leading "0" if the value is purely fractional
305725
+ if (formatType === _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatType.Fractional && formattedValue.startsWith("0 ")) {
305726
+ formattedValue = formattedValue.substring(2); // Remove "0 " prefix
305727
+ }
305728
+ // Add unit label if ShowUnitLabel trait is set
305729
+ // unitConversions[0] = ratio scale factor, [1] = numerator unit, [2] = denominator unit
305730
+ if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.ShowUnitLabel) && spec.unitConversions.length >= 3) {
305731
+ const labelToAdd = side === "numerator" ? spec.unitConversions[1].label : spec.unitConversions[2].label;
305732
+ formattedValue = formattedValue + labelToAdd;
305733
+ }
305734
+ return formattedValue;
305735
+ }
305736
+ /** Format a ratio quantity value (separate from composite formatting) */
305737
+ static formatRatioQuantity(magnitude, spec) {
305738
+ const unitConversion = spec.unitConversions[0].conversion;
305739
+ let unitValue = 0.0;
305740
+ try {
305741
+ unitValue = (0,_Quantity__WEBPACK_IMPORTED_MODULE_4__.applyConversion)(magnitude, unitConversion) + this.FPV_MINTHRESHOLD;
305742
+ }
305743
+ catch (e) {
305744
+ // The "InvertingZero" error is thrown when the value is zero and the conversion factor is inverted.
305745
+ // For ratio, we return "1:0" as the formatted value.
305746
+ if (e instanceof _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError && e.errorNumber === _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvertingZero) {
305747
+ return { componentText: "1:0", isNegative: false };
305748
+ }
305749
+ throw e;
305750
+ }
305751
+ const componentText = this.formatRatio(unitValue, spec);
305752
+ const isNegative = unitValue < 0;
305753
+ return { componentText, isNegative };
305754
+ }
305458
305755
  static formatRatio(magnitude, spec) {
305459
305756
  if (null === spec.format.ratioType)
305460
305757
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidCompositeFormat, `The Format ${spec.format.name} must have a ratio type specified.`);
305461
305758
  const precisionScale = Math.pow(10.0, spec.format.precision);
305759
+ const separator = spec.format.ratioSeparator;
305462
305760
  let reciprocal = 0;
305761
+ // Helper to get unit labels if ShowUnitLabel is set
305762
+ const getUnitLabels = () => {
305763
+ if (spec.format.hasFormatTraitSet(_FormatEnums__WEBPACK_IMPORTED_MODULE_3__.FormatTraits.ShowUnitLabel) && spec.unitConversions.length >= 3) {
305764
+ return { numeratorLabel: spec.unitConversions[1].label, denominatorLabel: spec.unitConversions[2].label };
305765
+ }
305766
+ return { numeratorLabel: "", denominatorLabel: "" };
305767
+ };
305768
+ const { numeratorLabel, denominatorLabel } = getUnitLabels();
305463
305769
  if (magnitude === 0.0)
305464
- return "0:1";
305770
+ return `0${separator}1`;
305465
305771
  else
305466
305772
  reciprocal = 1.0 / magnitude;
305467
305773
  switch (spec.format.ratioType) {
305468
- case _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.RatioType.OneToN:
305469
- return `1:${this.formatMagnitude(reciprocal, spec)}`;
305470
- case _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.RatioType.NToOne:
305471
- return `${this.formatMagnitude(magnitude, spec)}:1`;
305472
- case _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.RatioType.ValueBased:
305473
- if (magnitude > 1.0)
305474
- return `${this.formatMagnitude(magnitude, spec)}:1`;
305475
- else
305476
- return `1:${this.formatMagnitude(reciprocal, spec)}`;
305477
- case _FormatEnums__WEBPACK_IMPORTED_MODULE_2__.RatioType.UseGreatestCommonDivisor:
305774
+ case _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.RatioType.OneToN:
305775
+ return `1${numeratorLabel}${separator}${this.formatRatioPart(reciprocal, spec, "denominator")}`;
305776
+ case _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.RatioType.NToOne:
305777
+ return `${this.formatRatioPart(magnitude, spec, "numerator")}${separator}1${denominatorLabel}`;
305778
+ case _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.RatioType.ValueBased:
305779
+ if (magnitude > 1.0) {
305780
+ return `${this.formatRatioPart(magnitude, spec, "numerator")}${separator}1${denominatorLabel}`;
305781
+ }
305782
+ else {
305783
+ return `1${numeratorLabel}${separator}${this.formatRatioPart(reciprocal, spec, "denominator")}`;
305784
+ }
305785
+ case _FormatEnums__WEBPACK_IMPORTED_MODULE_3__.RatioType.UseGreatestCommonDivisor:
305478
305786
  magnitude = Math.round(magnitude * precisionScale) / precisionScale;
305479
305787
  let numerator = magnitude * precisionScale;
305480
305788
  let denominator = precisionScale;
305481
305789
  const gcd = FractionalNumeric.getGreatestCommonFactor(numerator, denominator);
305482
305790
  numerator /= gcd;
305483
305791
  denominator /= gcd;
305484
- return `${this.formatMagnitude(numerator, spec)}:${this.formatMagnitude(denominator, spec)}`;
305792
+ return `${this.formatRatioPart(numerator, spec, "numerator")}${separator}${this.formatRatioPart(denominator, spec, "denominator")}`;
305485
305793
  default:
305486
305794
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvalidCompositeFormat, `The Format ${spec.format.name} has an invalid ratio type specified.`);
305487
305795
  }
@@ -305502,7 +305810,8 @@ __webpack_require__.r(__webpack_exports__);
305502
305810
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
305503
305811
  /* harmony export */ FormatterSpec: () => (/* binding */ FormatterSpec)
305504
305812
  /* harmony export */ });
305505
- /* harmony import */ var _Formatter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./Formatter */ "../../core/quantity/lib/esm/Formatter/Formatter.js");
305813
+ /* harmony import */ var _FormatEnums__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./FormatEnums */ "../../core/quantity/lib/esm/Formatter/FormatEnums.js");
305814
+ /* harmony import */ var _Formatter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Formatter */ "../../core/quantity/lib/esm/Formatter/Formatter.js");
305506
305815
  /*---------------------------------------------------------------------------------------------
305507
305816
  * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
305508
305817
  * See LICENSE.md in the project root for license terms and full copyright notice.
@@ -305511,6 +305820,7 @@ __webpack_require__.r(__webpack_exports__);
305511
305820
  * @module Quantity
305512
305821
  */
305513
305822
 
305823
+
305514
305824
  // cSpell:ignore ZERONORMALIZED, nosign, onlynegative, signalways, negativeparentheses
305515
305825
  // cSpell:ignore trailzeroes, keepsinglezero, zeroempty, keepdecimalpoint, applyrounding, fractiondash, showunitlabel, prependunitlabel, exponentonlynegative
305516
305826
  /** A class that contains both formatting information and the conversion factors necessary to convert from an input unit to the units specified in the format.
@@ -305557,6 +305867,49 @@ class FormatterSpec {
305557
305867
  get persistenceUnit() { return this._persistenceUnit; }
305558
305868
  get azimuthBaseConversion() { return this._azimuthBaseConversion; }
305559
305869
  get revolutionConversion() { return this._revolutionConversion; }
305870
+ /** Build conversion specs for ratio format with 2 composite units (numerator/denominator). */
305871
+ static async getRatioUnitConversions(units, unitsProvider, persistenceUnit) {
305872
+ const conversions = [];
305873
+ const [numeratorUnit, numeratorLabel] = units[0];
305874
+ const [denominatorUnit, denominatorLabel] = units[1];
305875
+ // Compute ratio scale: how many numerator units per denominator unit (e.g., IN:FT = 12)
305876
+ const denominatorToNumerator = await unitsProvider.getConversion(denominatorUnit, numeratorUnit);
305877
+ const displayRatioScale = denominatorToNumerator.factor;
305878
+ // Avoid double-scaling: if persistence unit already encodes the display ratio, use factor 1.
305879
+ // Check by name heuristic (e.g., IN_PER_FT with ratioUnits [IN, FT] → no scaling needed)
305880
+ const persistenceName = persistenceUnit.name.toUpperCase();
305881
+ const numName = numeratorUnit.name.toUpperCase().split(".").pop() ?? "";
305882
+ const denName = denominatorUnit.name.toUpperCase().split(".").pop() ?? "";
305883
+ // Split by word boundaries (underscores, dots) and check for exact token matches
305884
+ const persistenceTokens = persistenceName.split(/[._]/);
305885
+ const isPersistenceMatchingRatio = persistenceTokens.includes(numName) && persistenceTokens.includes(denName);
305886
+ const ratioScaleFactor = isPersistenceMatchingRatio ? 1.0 : displayRatioScale;
305887
+ // First conversion spec: effective ratio unit conversion
305888
+ const ratioConversionSpec = {
305889
+ name: `${numeratorUnit.name}_per_${denominatorUnit.name}`,
305890
+ label: "",
305891
+ system: numeratorUnit.system,
305892
+ conversion: { factor: ratioScaleFactor, offset: 0.0 },
305893
+ };
305894
+ conversions.push(ratioConversionSpec);
305895
+ // Numerator unit for label lookup
305896
+ const numeratorSpec = {
305897
+ name: numeratorUnit.name,
305898
+ label: numeratorLabel?.length ? numeratorLabel : numeratorUnit.label,
305899
+ system: numeratorUnit.system,
305900
+ conversion: { factor: 1.0, offset: 0.0 },
305901
+ };
305902
+ conversions.push(numeratorSpec);
305903
+ // Denominator unit for label lookup
305904
+ const denominatorSpec = {
305905
+ name: denominatorUnit.name,
305906
+ label: denominatorLabel?.length ? denominatorLabel : denominatorUnit.label,
305907
+ system: denominatorUnit.system,
305908
+ conversion: { factor: 1.0, offset: 0.0 },
305909
+ };
305910
+ conversions.push(denominatorSpec);
305911
+ return conversions;
305912
+ }
305560
305913
  /** Get an array of UnitConversionSpecs, one for each unit that is to be shown in the formatted quantity string. */
305561
305914
  static async getUnitConversions(format, unitsProvider, inputUnit) {
305562
305915
  const conversions = [];
@@ -305570,6 +305923,10 @@ class FormatterSpec {
305570
305923
  throw new Error("Formatter Spec needs persistence unit to be specified");
305571
305924
  }
305572
305925
  }
305926
+ // Handle 2-unit composite for ratio formats (scale factors)
305927
+ if (format.type === _FormatEnums__WEBPACK_IMPORTED_MODULE_0__.FormatType.Ratio && format.units && format.units.length === 2) {
305928
+ return FormatterSpec.getRatioUnitConversions(format.units, unitsProvider, persistenceUnit);
305929
+ }
305573
305930
  if (format.units) {
305574
305931
  let convertFromUnit = inputUnit;
305575
305932
  for (const unit of format.units) {
@@ -305626,7 +305983,7 @@ class FormatterSpec {
305626
305983
  }
305627
305984
  /** Format a quantity value. */
305628
305985
  applyFormatting(magnitude) {
305629
- return _Formatter__WEBPACK_IMPORTED_MODULE_0__.Formatter.formatQuantity(magnitude, this);
305986
+ return _Formatter__WEBPACK_IMPORTED_MODULE_1__.Formatter.formatQuantity(magnitude, this);
305630
305987
  }
305631
305988
  }
305632
305989
 
@@ -305734,13 +306091,14 @@ var ParseError;
305734
306091
  ParseError[ParseError["BearingPrefixOrSuffixMissing"] = 7] = "BearingPrefixOrSuffixMissing";
305735
306092
  ParseError[ParseError["MathematicOperationFoundButIsNotAllowed"] = 8] = "MathematicOperationFoundButIsNotAllowed";
305736
306093
  ParseError[ParseError["BearingAngleOutOfRange"] = 9] = "BearingAngleOutOfRange";
306094
+ ParseError[ParseError["InvalidMathResult"] = 10] = "InvalidMathResult";
305737
306095
  })(ParseError || (ParseError = {}));
305738
306096
  var Operator;
305739
306097
  (function (Operator) {
305740
306098
  Operator["addition"] = "+";
305741
306099
  Operator["subtraction"] = "-";
305742
306100
  Operator["multiplication"] = "*";
305743
- Operator["division"] = "/"; // unsupported but we recognize it during parsing
306101
+ Operator["division"] = "/";
305744
306102
  })(Operator || (Operator = {}));
305745
306103
  function isOperator(char) {
305746
306104
  if (typeof char === "number") {
@@ -306495,36 +306853,107 @@ class Parser {
306495
306853
  magnitude = this.normalizeAngle(magnitude, revolution);
306496
306854
  return { ok: true, value: magnitude };
306497
306855
  }
306856
+ /**
306857
+ * Parse a ratio part string (numerator or denominator) to extract the numeric value and optional unit label.
306858
+ * This method processes tokens without applying unit conversions, allowing the ratio format
306859
+ * handler to manage conversions at the ratio level.
306860
+ *
306861
+ *
306862
+ * @note Fractions are already handled by parseQuantitySpecification, which converts them to
306863
+ * single numeric tokens (e.g., "1/2" becomes 0.5).
306864
+ *
306865
+ * @param partStr The string to parse, which may contain a number, fraction, or mixed fraction with optional unit label.
306866
+ * @param format The format specification used for token parsing.
306867
+ * @returns An object containing the parsed numeric value and optional unit label. Returns NaN for value if no number is found.
306868
+ */
306869
+ static parseRatioPart(partStr, format) {
306870
+ partStr = partStr.trim();
306871
+ // Parse tokens - fractions are automatically converted to decimal values by parseQuantitySpecification
306872
+ const tempFormat = format.clone({ type: _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_2__.FormatType.Decimal });
306873
+ const tokens = Parser.parseQuantitySpecification(partStr, tempFormat);
306874
+ let value = NaN;
306875
+ let unitLabel;
306876
+ // Pre-process: merge negative operators with following numbers
306877
+ const processedTokens = [];
306878
+ for (let i = 0; i < tokens.length; i++) {
306879
+ const token = tokens[i];
306880
+ if (token.isOperator && i === 0 && token.value === "-" &&
306881
+ i + 1 < tokens.length && tokens[i + 1].isNumber) {
306882
+ // Merge negative sign with number
306883
+ processedTokens.push(new ParseToken(-tokens[i + 1].value));
306884
+ i++; // Skip the number token since we consumed it
306885
+ }
306886
+ else {
306887
+ processedTokens.push(token);
306888
+ }
306889
+ }
306890
+ // Extract numeric value and unit label from processed tokens
306891
+ for (const token of processedTokens) {
306892
+ if (token.isNumber && isNaN(value)) {
306893
+ value = token.value;
306894
+ }
306895
+ else if (token.isString && !token.isOperator) {
306896
+ // String token that's not an operator - treat as unit label
306897
+ unitLabel = token.value;
306898
+ }
306899
+ }
306900
+ return { value, unitLabel };
306901
+ }
306498
306902
  static parseRatioFormat(inString, spec) {
306499
306903
  if (!inString)
306500
306904
  return { ok: false, error: ParseError.NoValueOrUnitFoundInString };
306501
- const parts = inString.split(":");
306905
+ const separator = spec.format.ratioSeparator ?? ":";
306906
+ const parts = inString.split(separator);
306502
306907
  if (parts.length > 2)
306503
306908
  return { ok: false, error: ParseError.UnableToConvertParseTokensToQuantity };
306504
- const numerator = parseFloat(parts[0]);
306505
- let denominator;
306506
- if (parts.length === 1) {
306507
- denominator = 1.0;
306508
- }
306509
- else {
306510
- denominator = parseFloat(parts[1]);
306909
+ // If the string doesn't contain the expected separator but contains other ratio-like separators,
306910
+ // return an error since the wrong separator was used
306911
+ if (parts.length === 1 && !inString.includes(separator)) {
306912
+ // Check if the string contains other common ratio separators
306913
+ const otherSeparators = [":", "=", "/"];
306914
+ for (const otherSep of otherSeparators) {
306915
+ if (otherSep !== separator && inString.includes(otherSep)) {
306916
+ // The string looks like a ratio but uses the wrong separator
306917
+ return { ok: false, error: ParseError.UnableToConvertParseTokensToQuantity };
306918
+ }
306919
+ }
306920
+ // Parse as a regular quantity value (numerator only, denominator = 1)
306921
+ const result = this.parseAndProcessTokens(inString, spec.format, spec.unitConversions);
306922
+ return result;
306511
306923
  }
306512
- if (isNaN(numerator) || isNaN(denominator))
306924
+ // Parse numerator and denominator parts which may include unit labels
306925
+ const numeratorPart = this.parseRatioPart(parts[0], spec.format);
306926
+ const denominatorPart = parts.length === 1 ? { value: 1.0 } : this.parseRatioPart(parts[1], spec.format);
306927
+ if (isNaN(numeratorPart.value) || isNaN(denominatorPart.value))
306513
306928
  return { ok: false, error: ParseError.NoValueOrUnitFoundInString };
306929
+ // Handle 2-unit composite case - simpler conversion using the pre-computed scale factor
306930
+ if (spec.format.units && spec.format.units.length === 2 && spec.unitConversions.length >= 3) {
306931
+ const ratioConvSpec = spec.unitConversions[0];
306932
+ const scaleFactor = ratioConvSpec.conversion.factor;
306933
+ if (denominatorPart.value === 0) {
306934
+ return { ok: false, error: ParseError.InvalidMathResult };
306935
+ }
306936
+ // The ratio value is numerator/denominator in the display units (e.g., 12 for 12"=1')
306937
+ // Divide by scale factor to get persistence unit value (e.g., 12/12 = 1.0)
306938
+ const ratioValue = numeratorPart.value / denominatorPart.value;
306939
+ const convertedValue = ratioValue / scaleFactor;
306940
+ return { ok: true, value: convertedValue };
306941
+ }
306942
+ // Original flow for 1-unit composite - use Quantity.convertTo for proper unit conversion
306514
306943
  const defaultUnit = spec.format.units && spec.format.units.length > 0 ? spec.format.units[0][0] : undefined;
306515
306944
  const unitConversion = defaultUnit ? Parser.tryFindUnitConversion(defaultUnit.label, spec.unitConversions, defaultUnit) : undefined;
306516
306945
  if (!unitConversion) {
306517
306946
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.MissingRequiredProperty, `Missing input unit or unit conversion for interpreting ${spec.format.name}.`);
306518
306947
  }
306519
- if (denominator === 0) {
306520
- if (unitConversion.inversion && numerator === 1)
306948
+ if (denominatorPart.value === 0) {
306949
+ if (unitConversion.inversion && numeratorPart.value === 1)
306521
306950
  return { ok: true, value: 0.0 };
306522
306951
  else
306523
- return { ok: false, error: ParseError.MathematicOperationFoundButIsNotAllowed };
306952
+ return { ok: false, error: ParseError.InvalidMathResult };
306524
306953
  }
306525
306954
  let quantity;
306526
306955
  if (spec.format.units && spec.outUnit) {
306527
- quantity = new _Quantity__WEBPACK_IMPORTED_MODULE_3__.Quantity(spec.format.units[0][0], numerator / denominator);
306956
+ quantity = new _Quantity__WEBPACK_IMPORTED_MODULE_3__.Quantity(spec.format.units[0][0], numeratorPart.value / denominatorPart.value);
306528
306957
  }
306529
306958
  else {
306530
306959
  throw new _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError(_Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.MissingRequiredProperty, "Missing presentation unit or persistence unit for ratio format.");
@@ -306536,7 +306965,7 @@ class Parser {
306536
306965
  catch (err) {
306537
306966
  // for input of "0:N" with reversed unit
306538
306967
  if (err instanceof _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError && err.errorNumber === _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus.InvertingZero) {
306539
- return { ok: false, error: ParseError.MathematicOperationFoundButIsNotAllowed };
306968
+ return { ok: false, error: ParseError.InvalidMathResult };
306540
306969
  }
306541
306970
  }
306542
306971
  if (converted === undefined || !converted.isValid) {
@@ -306662,7 +307091,8 @@ __webpack_require__.r(__webpack_exports__);
306662
307091
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
306663
307092
  /* harmony export */ ParserSpec: () => (/* binding */ ParserSpec)
306664
307093
  /* harmony export */ });
306665
- /* harmony import */ var _Parser__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./Parser */ "../../core/quantity/lib/esm/Parser.js");
307094
+ /* harmony import */ var _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./Formatter/FormatEnums */ "../../core/quantity/lib/esm/Formatter/FormatEnums.js");
307095
+ /* harmony import */ var _Parser__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Parser */ "../../core/quantity/lib/esm/Parser.js");
306666
307096
  /*---------------------------------------------------------------------------------------------
306667
307097
  * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
306668
307098
  * See LICENSE.md in the project root for license terms and full copyright notice.
@@ -306671,6 +307101,7 @@ __webpack_require__.r(__webpack_exports__);
306671
307101
  * @module Quantity
306672
307102
  */
306673
307103
 
307104
+
306674
307105
  /** A ParserSpec holds information needed to parse a string into a quantity synchronously.
306675
307106
  * @beta
306676
307107
  */
@@ -306696,6 +307127,51 @@ class ParserSpec {
306696
307127
  get outUnit() { return this._outUnit; }
306697
307128
  get azimuthBaseConversion() { return this._azimuthBaseConversion; }
306698
307129
  get revolutionConversion() { return this._revolutionConversion; }
307130
+ /** Build conversion specs for ratio format with 2 composite units (numerator/denominator). */
307131
+ static async getRatioUnitConversions(units, unitsProvider, outUnit, altUnitLabelsProvider) {
307132
+ const conversions = [];
307133
+ const [numeratorUnit, numeratorLabel] = units[0];
307134
+ const [denominatorUnit, denominatorLabel] = units[1];
307135
+ // Compute ratio scale: how many numerator units per denominator unit (e.g., IN:FT = 12)
307136
+ const denominatorToNumerator = await unitsProvider.getConversion(denominatorUnit, numeratorUnit);
307137
+ const displayRatioScale = denominatorToNumerator.factor;
307138
+ // Avoid double-scaling: if persistence unit already encodes the display ratio, use factor 1.
307139
+ // Check by name heuristic (e.g., IN_PER_FT with ratioUnits [IN, FT] → no scaling needed)
307140
+ const persistenceName = outUnit.name.toUpperCase();
307141
+ const numName = numeratorUnit.name.toUpperCase().split(".").pop() ?? "";
307142
+ const denName = denominatorUnit.name.toUpperCase().split(".").pop() ?? "";
307143
+ // Split by word boundaries (underscores, dots) and check for exact token matches
307144
+ const persistenceTokens = persistenceName.split(/[._]/);
307145
+ const isPersistenceMatchingRatio = persistenceTokens.includes(numName) && persistenceTokens.includes(denName);
307146
+ const ratioScaleFactor = isPersistenceMatchingRatio ? 1.0 : displayRatioScale;
307147
+ // First conversion spec: effective ratio unit conversion
307148
+ const ratioConversionSpec = {
307149
+ name: `${numeratorUnit.name}_per_${denominatorUnit.name}`,
307150
+ label: "",
307151
+ system: numeratorUnit.system,
307152
+ conversion: { factor: ratioScaleFactor, offset: 0.0 },
307153
+ };
307154
+ conversions.push(ratioConversionSpec);
307155
+ // Numerator unit for label lookup
307156
+ const numeratorSpec = {
307157
+ name: numeratorUnit.name,
307158
+ label: numeratorLabel?.length ? numeratorLabel : numeratorUnit.label,
307159
+ system: numeratorUnit.system,
307160
+ conversion: { factor: 1.0, offset: 0.0 },
307161
+ parseLabels: altUnitLabelsProvider?.getAlternateUnitLabels(numeratorUnit),
307162
+ };
307163
+ conversions.push(numeratorSpec);
307164
+ // Denominator unit for label lookup
307165
+ const denominatorSpec = {
307166
+ name: denominatorUnit.name,
307167
+ label: denominatorLabel?.length ? denominatorLabel : denominatorUnit.label,
307168
+ system: denominatorUnit.system,
307169
+ conversion: { factor: 1.0, offset: 0.0 },
307170
+ parseLabels: altUnitLabelsProvider?.getAlternateUnitLabels(denominatorUnit),
307171
+ };
307172
+ conversions.push(denominatorSpec);
307173
+ return conversions;
307174
+ }
306699
307175
  /** Static async method to create a ParserSpec given the format and unit of the quantity that will be passed to the Parser. The input unit will
306700
307176
  * be used to generate conversion information for each unit specified in the Format. This method is async due to the fact that the units provider must make
306701
307177
  * async calls to lookup unit definitions.
@@ -306704,7 +307180,14 @@ class ParserSpec {
306704
307180
  * @param outUnit The unit a value will be formatted to. This unit is often referred to as persistence unit.
306705
307181
  */
306706
307182
  static async create(format, unitsProvider, outUnit, altUnitLabelsProvider) {
306707
- const conversions = await _Parser__WEBPACK_IMPORTED_MODULE_0__.Parser.createUnitConversionSpecsForUnit(unitsProvider, outUnit, altUnitLabelsProvider);
307183
+ let conversions;
307184
+ // For ratio formats with 2 composite units, use private helper method
307185
+ if (format.type === _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_0__.FormatType.Ratio && format.units && format.units.length === 2) {
307186
+ conversions = await ParserSpec.getRatioUnitConversions(format.units, unitsProvider, outUnit, altUnitLabelsProvider);
307187
+ }
307188
+ else {
307189
+ conversions = await _Parser__WEBPACK_IMPORTED_MODULE_1__.Parser.createUnitConversionSpecsForUnit(unitsProvider, outUnit, altUnitLabelsProvider);
307190
+ }
306708
307191
  const spec = new ParserSpec(outUnit, format, conversions);
306709
307192
  if (format.azimuthBaseUnit !== undefined) {
306710
307193
  if (outUnit !== undefined) {
@@ -306726,7 +307209,7 @@ class ParserSpec {
306726
307209
  }
306727
307210
  /** Do the parsing. Done this way to allow Custom Parser Specs to parse custom formatted strings into their quantities. */
306728
307211
  parseToQuantityValue(inString) {
306729
- return _Parser__WEBPACK_IMPORTED_MODULE_0__.Parser.parseQuantityString(inString, this);
307212
+ return _Parser__WEBPACK_IMPORTED_MODULE_1__.Parser.parseQuantityString(inString, this);
306730
307213
  }
306731
307214
  }
306732
307215
 
@@ -306924,6 +307407,7 @@ __webpack_require__.r(__webpack_exports__);
306924
307407
  /* harmony export */ QuantityConstants: () => (/* reexport safe */ _Constants__WEBPACK_IMPORTED_MODULE_0__.QuantityConstants),
306925
307408
  /* harmony export */ QuantityError: () => (/* reexport safe */ _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityError),
306926
307409
  /* harmony export */ QuantityStatus: () => (/* reexport safe */ _Exception__WEBPACK_IMPORTED_MODULE_1__.QuantityStatus),
307410
+ /* harmony export */ RatioFormatType: () => (/* reexport safe */ _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_9__.RatioFormatType),
306927
307411
  /* harmony export */ RatioType: () => (/* reexport safe */ _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_9__.RatioType),
306928
307412
  /* harmony export */ ScientificType: () => (/* reexport safe */ _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_9__.ScientificType),
306929
307413
  /* harmony export */ ShowSignOption: () => (/* reexport safe */ _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_9__.ShowSignOption),
@@ -306942,6 +307426,7 @@ __webpack_require__.r(__webpack_exports__);
306942
307426
  /* harmony export */ parseFormatType: () => (/* reexport safe */ _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_9__.parseFormatType),
306943
307427
  /* harmony export */ parseFractionalPrecision: () => (/* reexport safe */ _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_9__.parseFractionalPrecision),
306944
307428
  /* harmony export */ parsePrecision: () => (/* reexport safe */ _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_9__.parsePrecision),
307429
+ /* harmony export */ parseRatioFormatType: () => (/* reexport safe */ _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_9__.parseRatioFormatType),
306945
307430
  /* harmony export */ parseRatioType: () => (/* reexport safe */ _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_9__.parseRatioType),
306946
307431
  /* harmony export */ parseScientificType: () => (/* reexport safe */ _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_9__.parseScientificType),
306947
307432
  /* harmony export */ parseShowSignOption: () => (/* reexport safe */ _Formatter_FormatEnums__WEBPACK_IMPORTED_MODULE_9__.parseShowSignOption),
@@ -320651,7 +321136,7 @@ var loadLanguages = instance.loadLanguages;
320651
321136
  /***/ ((module) => {
320652
321137
 
320653
321138
  "use strict";
320654
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@itwin/core-frontend","version":"5.6.0-dev.12","description":"iTwin.js frontend components","main":"lib/cjs/core-frontend.js","module":"lib/esm/core-frontend.js","typings":"lib/cjs/core-frontend","license":"MIT","scripts":{"build":"npm run -s copy:public && npm run -s build:cjs && npm run -s build:esm && npm run -s webpackWorkers && npm run -s copy:workers && npm run -s copy:draco","build:cjs":"npm run -s copy:js:cjs && tsc 1>&2 --outDir lib/cjs","build:esm":"npm run -s copy:js:esm && tsc 1>&2 --module ES2022 --outDir lib/esm","clean":"rimraf -g lib .rush/temp/package-deps*.json","copy:public":"cpx \\"./src/public/**/*\\" ./lib/public","copy:js:cjs":"cpx \\"./src/**/*.js\\" ./lib/cjs","copy:js:esm":"cpx \\"./src/**/*.js\\" ./lib/esm","copy:workers":"cpx \\"./lib/workers/webpack/parse-imdl-worker.js\\" ./lib/public/scripts","copy:draco":"cpx \\"./node_modules/@loaders.gl/draco/dist/libs/*\\" ./lib/public/scripts","docs":"betools docs --json=../../generated-docs/core/core-frontend/file.json --tsIndexFile=./core-frontend.ts --onlyJson --excludes=webgl/**/*,**/map/*.d.ts,**/tile/*.d.ts,**/*-css.ts","extract-api":"betools extract-api --entry=core-frontend && npm run extract-extension-api","extract-extension-api":"eslint --no-inline-config -c extraction.eslint.config.js \\"./src/**/*.ts\\" 1>&2","lint":"eslint \\"./src/**/*.ts\\" 1>&2","lint-fix":"eslint --fix -f visualstudio \\"./src/**/*.ts\\" 1>&2","lint-deprecation":"eslint --fix -f visualstudio --no-inline-config -c ../../common/config/eslint/eslint.config.deprecation-policy.js \\"./src/**/*.ts\\"","pseudolocalize":"betools pseudolocalize --englishDir ./src/public/locales/en --out ./public/locales/en-PSEUDO","test":"npm run webpackTestWorker && vitest --run","cover":"npm run webpackTestWorker && vitest --run","webpackTests":"webpack --config ./src/test/utils/webpack.config.js 1>&2 && npm run -s webpackTestWorker","webpackTestWorker":"webpack --config ./src/test/worker/webpack.config.js 1>&2 && cpx \\"./lib/test/test-worker.js\\" ./lib/test","webpackWorkers":"webpack --config ./src/workers/ImdlParser/webpack.config.js 1>&2"},"repository":{"type":"git","url":"https://github.com/iTwin/itwinjs-core.git","directory":"core/frontend"},"keywords":["Bentley","BIM","iModel","digital-twin","iTwin"],"author":{"name":"Bentley Systems, Inc.","url":"http://www.bentley.com"},"peerDependencies":{"@itwin/appui-abstract":"workspace:*","@itwin/core-bentley":"workspace:*","@itwin/core-common":"workspace:*","@itwin/core-geometry":"workspace:*","@itwin/core-orbitgt":"workspace:*","@itwin/core-quantity":"workspace:*","@itwin/ecschema-metadata":"workspace:*","@itwin/ecschema-rpcinterface-common":"workspace:*"},"//devDependencies":["NOTE: All peerDependencies should also be listed as devDependencies since peerDependencies are not considered by npm install","NOTE: All tools used by scripts in this package must be listed as devDependencies"],"devDependencies":{"@itwin/appui-abstract":"workspace:*","@itwin/build-tools":"workspace:*","@itwin/core-bentley":"workspace:*","@itwin/core-common":"workspace:*","@itwin/core-geometry":"workspace:*","@itwin/core-orbitgt":"workspace:*","@itwin/core-quantity":"workspace:*","@itwin/ecschema-metadata":"workspace:*","@itwin/ecschema-rpcinterface-common":"workspace:*","@itwin/object-storage-core":"^3.0.4","@itwin/eslint-plugin":"5.2.2-dev.2","@types/chai-as-promised":"^7","@types/draco3d":"^1.4.10","@types/sinon":"^17.0.2","@vitest/browser":"^3.0.6","@vitest/coverage-v8":"^3.0.6","cpx2":"^8.0.0","eslint":"^9.31.0","glob":"^10.5.0","playwright":"~1.56.1","rimraf":"^6.0.1","sinon":"^17.0.2","source-map-loader":"^5.0.0","typescript":"~5.6.2","typemoq":"^2.1.0","vitest":"^3.0.6","vite-multiple-assets":"^1.3.1","vite-plugin-static-copy":"2.2.0","webpack":"^5.97.1"},"//dependencies":["NOTE: these dependencies should be only for things that DO NOT APPEAR IN THE API","NOTE: core-frontend should remain UI technology agnostic, so no react/angular dependencies are allowed"],"dependencies":{"@itwin/core-i18n":"workspace:*","@itwin/webgl-compatibility":"workspace:*","@loaders.gl/core":"^4.3.4","@loaders.gl/draco":"^4.3.4","fuse.js":"^3.3.0","wms-capabilities":"0.4.0"}}');
321139
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@itwin/core-frontend","version":"5.6.0-dev.14","description":"iTwin.js frontend components","main":"lib/cjs/core-frontend.js","module":"lib/esm/core-frontend.js","typings":"lib/cjs/core-frontend","license":"MIT","scripts":{"build":"npm run -s copy:public && npm run -s build:cjs && npm run -s build:esm && npm run -s webpackWorkers && npm run -s copy:workers && npm run -s copy:draco","build:cjs":"npm run -s copy:js:cjs && tsc 1>&2 --outDir lib/cjs","build:esm":"npm run -s copy:js:esm && tsc 1>&2 --module ES2022 --outDir lib/esm","clean":"rimraf -g lib .rush/temp/package-deps*.json","copy:public":"cpx \\"./src/public/**/*\\" ./lib/public","copy:js:cjs":"cpx \\"./src/**/*.js\\" ./lib/cjs","copy:js:esm":"cpx \\"./src/**/*.js\\" ./lib/esm","copy:workers":"cpx \\"./lib/workers/webpack/parse-imdl-worker.js\\" ./lib/public/scripts","copy:draco":"cpx \\"./node_modules/@loaders.gl/draco/dist/libs/*\\" ./lib/public/scripts","docs":"betools docs --json=../../generated-docs/core/core-frontend/file.json --tsIndexFile=./core-frontend.ts --onlyJson --excludes=webgl/**/*,**/map/*.d.ts,**/tile/*.d.ts,**/*-css.ts","extract-api":"betools extract-api --entry=core-frontend && npm run extract-extension-api","extract-extension-api":"eslint --no-inline-config -c extraction.eslint.config.js \\"./src/**/*.ts\\" 1>&2","lint":"eslint \\"./src/**/*.ts\\" 1>&2","lint-fix":"eslint --fix -f visualstudio \\"./src/**/*.ts\\" 1>&2","lint-deprecation":"eslint --fix -f visualstudio --no-inline-config -c ../../common/config/eslint/eslint.config.deprecation-policy.js \\"./src/**/*.ts\\"","pseudolocalize":"betools pseudolocalize --englishDir ./src/public/locales/en --out ./public/locales/en-PSEUDO","test":"npm run webpackTestWorker && vitest --run","cover":"npm run webpackTestWorker && vitest --run","webpackTests":"webpack --config ./src/test/utils/webpack.config.js 1>&2 && npm run -s webpackTestWorker","webpackTestWorker":"webpack --config ./src/test/worker/webpack.config.js 1>&2 && cpx \\"./lib/test/test-worker.js\\" ./lib/test","webpackWorkers":"webpack --config ./src/workers/ImdlParser/webpack.config.js 1>&2"},"repository":{"type":"git","url":"https://github.com/iTwin/itwinjs-core.git","directory":"core/frontend"},"keywords":["Bentley","BIM","iModel","digital-twin","iTwin"],"author":{"name":"Bentley Systems, Inc.","url":"http://www.bentley.com"},"peerDependencies":{"@itwin/appui-abstract":"workspace:*","@itwin/core-bentley":"workspace:*","@itwin/core-common":"workspace:*","@itwin/core-geometry":"workspace:*","@itwin/core-orbitgt":"workspace:*","@itwin/core-quantity":"workspace:*","@itwin/ecschema-metadata":"workspace:*","@itwin/ecschema-rpcinterface-common":"workspace:*"},"//devDependencies":["NOTE: All peerDependencies should also be listed as devDependencies since peerDependencies are not considered by npm install","NOTE: All tools used by scripts in this package must be listed as devDependencies"],"devDependencies":{"@itwin/appui-abstract":"workspace:*","@itwin/build-tools":"workspace:*","@itwin/core-bentley":"workspace:*","@itwin/core-common":"workspace:*","@itwin/core-geometry":"workspace:*","@itwin/core-orbitgt":"workspace:*","@itwin/core-quantity":"workspace:*","@itwin/ecschema-metadata":"workspace:*","@itwin/ecschema-rpcinterface-common":"workspace:*","@itwin/object-storage-core":"^3.0.4","@itwin/eslint-plugin":"^6.0.0","@types/chai-as-promised":"^7","@types/draco3d":"^1.4.10","@types/sinon":"^17.0.2","@vitest/browser":"^3.0.6","@vitest/coverage-v8":"^3.0.6","cpx2":"^8.0.0","eslint":"^9.31.0","glob":"^10.5.0","playwright":"~1.56.1","rimraf":"^6.0.1","sinon":"^17.0.2","source-map-loader":"^5.0.0","typescript":"~5.6.2","typemoq":"^2.1.0","vitest":"^3.0.6","vite-multiple-assets":"^1.3.1","vite-plugin-static-copy":"2.2.0","webpack":"^5.97.1"},"//dependencies":["NOTE: these dependencies should be only for things that DO NOT APPEAR IN THE API","NOTE: core-frontend should remain UI technology agnostic, so no react/angular dependencies are allowed"],"dependencies":{"@itwin/core-i18n":"workspace:*","@itwin/webgl-compatibility":"workspace:*","@loaders.gl/core":"^4.3.4","@loaders.gl/draco":"^4.3.4","fuse.js":"^3.3.0","wms-capabilities":"0.4.0"}}');
320655
321140
 
320656
321141
  /***/ })
320657
321142