@lovelace_lol/loom3 1.0.44 → 1.0.46

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.
package/README.md CHANGED
@@ -350,15 +350,20 @@ const loom = new Loom3({
350
350
 
351
351
  `annotationRegions` is the Loom3 field for camera/marker region defaults and profile overrides.
352
352
 
353
- If your app fetches a full saved `CharacterConfig` from Firestore or another backend, use `extendCharacterConfigWithPreset(...)` to build the runtime shape before handing that config to camera/marker tooling:
353
+ If your app fetches a saved model/profile record from Firestore or another backend, use `extendProfileConfigWithPreset(...)` to build the runtime shape before handing that profile config to camera/marker tooling:
354
354
 
355
355
  ```typescript
356
- import { extendCharacterConfigWithPreset } from '@lovelace_lol/loom3';
356
+ import { extendProfileConfigWithPreset } from '@lovelace_lol/loom3';
357
357
 
358
- const savedConfig = await fetchCharacterConfig();
359
- const runtimeConfig = extendCharacterConfigWithPreset(savedConfig);
358
+ const savedConfig = await fetchProfileConfig();
359
+ const runtimeConfig = extendProfileConfigWithPreset({
360
+ ...savedConfig,
361
+ profilePresetId: savedConfig.profilePresetId ?? 'cc4',
362
+ });
360
363
  ```
361
364
 
365
+ `CharacterConfig`, `auPresetType`, and `extendCharacterConfigWithPreset(...)` are still exported as deprecated compatibility aliases for apps migrating from older LoomLarge-style character records. New Loom3 integrations should model presets as base profiles, pass profile overrides through `profile`, `annotationRegions`, or other `Profile` fields, and use `profilePresetId` for preset selection.
366
+
362
367
  For the current runtime-oriented documentation, including:
363
368
 
364
369
  - `paddingFactor`
@@ -2091,7 +2096,7 @@ console.log(result.center, result.method, result.debugInfo);
2091
2096
  ### Resolving region-driven centers
2092
2097
 
2093
2098
  ```typescript
2094
- import type { CharacterConfig, Region } from '@lovelace_lol/loom3';
2099
+ import type { BoneResolutionProfile, Region } from '@lovelace_lol/loom3';
2095
2100
  import { resolveBoneName, resolveBoneNames, resolveFaceCenter } from '@lovelace_lol/loom3';
2096
2101
 
2097
2102
  const region: Region = {
@@ -2100,18 +2105,14 @@ const region: Region = {
2100
2105
  meshes: ['CC_Base_Body'],
2101
2106
  };
2102
2107
 
2103
- const config = {
2104
- characterId: 'demo',
2105
- characterName: 'Demo',
2106
- modelPath: '/demo.glb',
2107
- regions: [region],
2108
+ const profile: BoneResolutionProfile = {
2108
2109
  bonePrefix: 'CC_Base_',
2109
2110
  boneNodes: { HEAD: 'Head' },
2110
- } satisfies CharacterConfig;
2111
+ };
2111
2112
 
2112
- const headBone = resolveBoneName('HEAD', config);
2113
- const resolvedBones = resolveBoneNames(['HEAD'], config);
2114
- const faceCenter = resolveFaceCenter(gltf.scene, region, config);
2113
+ const headBone = resolveBoneName('HEAD', profile);
2114
+ const resolvedBones = resolveBoneNames(['HEAD'], profile);
2115
+ const faceCenter = resolveFaceCenter(gltf.scene, region, profile);
2115
2116
  ```
2116
2117
 
2117
2118
  ### Working with model orientation
@@ -2128,7 +2129,7 @@ const facing = detectFacingDirection(gltf.scene);
2128
2129
 
2129
2130
  Use these helpers when you need to:
2130
2131
  - place annotation markers using semantic regions instead of hard-coded coordinates
2131
- - resolve prefixed/suffixed bone names from a reusable character config
2132
+ - resolve prefixed/suffixed bone names from a reusable profile or minimal bone-resolution object
2132
2133
  - derive a face anchor for camera tooling or interaction layers
2133
2134
  - reason about model orientation before building your own camera or annotation system
2134
2135
 
package/dist/index.cjs CHANGED
@@ -7032,7 +7032,7 @@ function normalizeRegionTree(regions, disabledNames) {
7032
7032
  });
7033
7033
  }
7034
7034
 
7035
- // src/characters/extendCharacterConfigWithPreset.ts
7035
+ // src/profiles/resolveProfileConfig.ts
7036
7036
  var PROFILE_OVERRIDE_KEYS = [
7037
7037
  "name",
7038
7038
  "animalType",
@@ -7139,7 +7139,7 @@ function mergeRegion(base, override) {
7139
7139
  } : void 0
7140
7140
  };
7141
7141
  }
7142
- function mergeRegionsByName(base, override) {
7142
+ function mergeProfileRegionsByName(base, override) {
7143
7143
  if (!base && !override) return void 0;
7144
7144
  const merged = /* @__PURE__ */ new Map();
7145
7145
  for (const region of base ?? []) {
@@ -7154,6 +7154,10 @@ function mergeRegionsByName(base, override) {
7154
7154
  function getAnnotationRegions(value) {
7155
7155
  return Array.isArray(value) ? value : void 0;
7156
7156
  }
7157
+ var mergeRegionsByName = mergeProfileRegionsByName;
7158
+ function getProfilePresetId(config) {
7159
+ return config.profilePresetId ?? config.presetId ?? config.baseProfileId ?? config.auPresetType;
7160
+ }
7157
7161
  function getLegacyNestedOverrides(config) {
7158
7162
  return isPlainObject2(config.profile) ? config.profile : {};
7159
7163
  }
@@ -7161,12 +7165,12 @@ function getLegacyRuntimeRegions(config) {
7161
7165
  return Array.isArray(config.regions) && config.regions.length > 0 ? config.regions : void 0;
7162
7166
  }
7163
7167
  function getCanonicalAnnotationOverrides(config) {
7164
- return mergeRegionsByName(
7168
+ return mergeProfileRegionsByName(
7165
7169
  getAnnotationRegions(getLegacyNestedOverrides(config).annotationRegions),
7166
7170
  getAnnotationRegions(config.annotationRegions)
7167
7171
  );
7168
7172
  }
7169
- function extractProfileOverrides(config) {
7173
+ function extractLegacyCharacterProfileOverrides(config) {
7170
7174
  const topLevelConfig = config;
7171
7175
  const legacyNestedOverrides = getLegacyNestedOverrides(config);
7172
7176
  const canonicalAnnotationOverrides = getCanonicalAnnotationOverrides(config);
@@ -7189,12 +7193,18 @@ function extractProfileOverrides(config) {
7189
7193
  }
7190
7194
  return overrides;
7191
7195
  }
7192
- function applyCharacterProfileToPreset(config) {
7193
- const presetType = config.auPresetType;
7196
+ function extractProfileOverrides(config) {
7197
+ return extractLegacyCharacterProfileOverrides(config);
7198
+ }
7199
+ function resolveProfileFromPreset(config) {
7200
+ const presetType = getProfilePresetId(config);
7194
7201
  if (!presetType) {
7195
7202
  return null;
7196
7203
  }
7197
- return extendPresetWithProfile(getPreset(presetType), extractProfileOverrides(config));
7204
+ return extendPresetWithProfile(getPreset(presetType), extractLegacyCharacterProfileOverrides(config));
7205
+ }
7206
+ function applyCharacterProfileToPreset(config) {
7207
+ return resolveProfileFromPreset(config);
7198
7208
  }
7199
7209
  function orderExtendedRegions(extendedRegions, prioritizedLists) {
7200
7210
  if (!extendedRegions) return void 0;
@@ -7215,15 +7225,15 @@ function orderExtendedRegions(extendedRegions, prioritizedLists) {
7215
7225
  }
7216
7226
  return orderedNames.map((name) => extendedByName.get(name)).filter((region) => Boolean(region));
7217
7227
  }
7218
- function extendCharacterConfigWithPreset(config) {
7219
- const presetType = config.auPresetType;
7228
+ function extendProfileConfigWithPreset(config) {
7229
+ const presetType = getProfilePresetId(config);
7220
7230
  if (!presetType || presetType === "custom") {
7221
7231
  return config;
7222
7232
  }
7223
7233
  const canonicalAnnotationOverrides = getCanonicalAnnotationOverrides(config);
7224
7234
  const legacyRuntimeRegions = getLegacyRuntimeRegions(config);
7225
- const profileOverrides = extractProfileOverrides(config);
7226
- const extendedPresetProfile = applyCharacterProfileToPreset(config);
7235
+ const profileOverrides = extractLegacyCharacterProfileOverrides(config);
7236
+ const extendedPresetProfile = resolveProfileFromPreset(config);
7227
7237
  if (!extendedPresetProfile) {
7228
7238
  return config;
7229
7239
  }
@@ -7237,7 +7247,7 @@ function extendCharacterConfigWithPreset(config) {
7237
7247
  const extendedRegionNames = new Set((extendedAnnotationRegions ?? []).map((region) => region.name));
7238
7248
  const legacyExtraRegions = canonicalAnnotationOverrides && legacyRuntimeRegions ? legacyRuntimeRegions.filter((region) => !presetRegionNames.has(region.name) && !extendedRegionNames.has(region.name)).map((region) => cloneRegion(region)) : void 0;
7239
7249
  const mergedRegions = normalizeRegionTree(
7240
- mergeRegionsByName(extendedAnnotationRegions, legacyExtraRegions),
7250
+ mergeProfileRegionsByName(extendedAnnotationRegions, legacyExtraRegions),
7241
7251
  profileOverrides.disabledRegions
7242
7252
  );
7243
7253
  const extendedRegions = orderExtendedRegions(
@@ -7251,6 +7261,9 @@ function extendCharacterConfigWithPreset(config) {
7251
7261
  regions: extendedRegions ?? config.regions
7252
7262
  };
7253
7263
  }
7264
+ function extendCharacterConfigWithPreset(config) {
7265
+ return extendProfileConfigWithPreset(config);
7266
+ }
7254
7267
  var DEFAULT_EPSILON = 1e-4;
7255
7268
  var DEFAULT_YAW_WEIGHT = 0.35;
7256
7269
  var DEFAULT_PITCH_WEIGHT = 0.2;
@@ -7475,9 +7488,9 @@ function detectFacingDirection(model, eyeBoneNames = {
7475
7488
  function normalizeLooseName(value) {
7476
7489
  return value.replace(/\./g, "");
7477
7490
  }
7478
- function resolveBoneNameCandidates(semanticName, config) {
7479
- if (!config) return [semanticName];
7480
- const { bonePrefix, boneSuffix, boneNodes } = config;
7491
+ function resolveBoneNameCandidates(semanticName, profile) {
7492
+ if (!profile) return [semanticName];
7493
+ const { bonePrefix, boneSuffix, boneNodes } = profile;
7481
7494
  if (!boneNodes || !boneNodes[semanticName]) {
7482
7495
  return [semanticName];
7483
7496
  }
@@ -7491,14 +7504,14 @@ function resolveBoneNameCandidates(semanticName, config) {
7491
7504
  const fullyAffixed = suffix && !prefixedBase.endsWith(suffix) ? `${prefixedBase}${suffix}` : prefixedBase;
7492
7505
  return Array.from(/* @__PURE__ */ new Set([fullyAffixed, baseName]));
7493
7506
  }
7494
- function resolveBoneName(semanticName, config) {
7495
- return resolveBoneNameCandidates(semanticName, config)[0] ?? semanticName;
7507
+ function resolveBoneName(semanticName, profile) {
7508
+ return resolveBoneNameCandidates(semanticName, profile)[0] ?? semanticName;
7496
7509
  }
7497
- function resolveBoneNames(names, config) {
7510
+ function resolveBoneNames(names, profile) {
7498
7511
  if (!names || names.length === 0) return [];
7499
7512
  return Array.from(
7500
7513
  new Set(
7501
- names.flatMap((name) => resolveBoneNameCandidates(name, config))
7514
+ names.flatMap((name) => resolveBoneNameCandidates(name, profile))
7502
7515
  )
7503
7516
  );
7504
7517
  }
@@ -7511,8 +7524,8 @@ function fuzzyNameMatch(objectName, targetName, suffixPattern) {
7511
7524
  const regex = suffixPattern ? new RegExp(suffixPattern) : /^[_\.]\d+$/;
7512
7525
  return regex.test(suffix);
7513
7526
  }
7514
- function resolveFaceCenter(model, region, config) {
7515
- const resolvedBones = resolveBoneNames(region.bones, config);
7527
+ function resolveFaceCenter(model, region, profile) {
7528
+ const resolvedBones = resolveBoneNames(region.bones, profile);
7516
7529
  const headBoneNames = resolvedBones.filter((name) => name.toLowerCase().includes("head"));
7517
7530
  const result = findFaceCenter(model, {
7518
7531
  headBoneNames: headBoneNames.length > 0 ? headBoneNames : void 0,
@@ -7525,6 +7538,274 @@ function resolveFaceCenter(model, region, config) {
7525
7538
  };
7526
7539
  }
7527
7540
 
7541
+ // src/camera/annotationCameraAngles.ts
7542
+ var DEFAULT_LATERALITY = {
7543
+ leftSideX: -1,
7544
+ confidence: 0,
7545
+ evidence: ["default:left=-X"]
7546
+ };
7547
+ var SIDE_TOKEN_PATTERN = /(^|[_\s-])(left|right)(?=$|[_\s-])/i;
7548
+ var MIN_LATERAL_SEPARATION = 1e-3;
7549
+ function invertHorizontalSign(sign) {
7550
+ return sign === 1 ? -1 : 1;
7551
+ }
7552
+ function averagePoints(points) {
7553
+ if (points.length === 0) return null;
7554
+ const center = new THREE2__namespace.Vector3();
7555
+ for (const point of points) {
7556
+ center.add(point);
7557
+ }
7558
+ return center.divideScalar(points.length);
7559
+ }
7560
+ function replaceSemanticSideToken(name, replacement) {
7561
+ const match = SIDE_TOKEN_PATTERN.exec(name);
7562
+ if (!match) return null;
7563
+ const [fullMatch, prefix] = match;
7564
+ const start = match.index;
7565
+ const end = start + fullMatch.length;
7566
+ return `${name.slice(0, start)}${prefix}${replacement}${name.slice(end)}`;
7567
+ }
7568
+ function collectMatchedPoints(model, targetNames, matcher, extractor) {
7569
+ const points = [];
7570
+ for (const targetName of targetNames) {
7571
+ model.traverse((obj) => {
7572
+ if (!matcher(obj, targetName)) return;
7573
+ const point = extractor(obj);
7574
+ if (point) {
7575
+ points.push(point);
7576
+ }
7577
+ });
7578
+ }
7579
+ return points;
7580
+ }
7581
+ function getRegionWorldCenter(model, region, characterConfig) {
7582
+ if (region.customPosition) {
7583
+ return new THREE2__namespace.Vector3(
7584
+ region.customPosition.x,
7585
+ region.customPosition.y,
7586
+ region.customPosition.z
7587
+ );
7588
+ }
7589
+ if (region.objects?.includes("*")) {
7590
+ return null;
7591
+ }
7592
+ const suffixPattern = characterConfig?.suffixPattern;
7593
+ const points = [];
7594
+ if (region.bones?.length) {
7595
+ const boneNames = resolveBoneNames(region.bones, characterConfig ?? void 0);
7596
+ points.push(
7597
+ ...collectMatchedPoints(
7598
+ model,
7599
+ boneNames,
7600
+ (obj, targetName) => fuzzyNameMatch(obj.name, targetName, suffixPattern),
7601
+ (obj) => {
7602
+ const worldPos = new THREE2__namespace.Vector3();
7603
+ obj.getWorldPosition(worldPos);
7604
+ return worldPos;
7605
+ }
7606
+ )
7607
+ );
7608
+ }
7609
+ if (region.meshes?.length) {
7610
+ points.push(
7611
+ ...collectMatchedPoints(
7612
+ model,
7613
+ region.meshes,
7614
+ (obj, targetName) => obj.isMesh && fuzzyNameMatch(obj.name, targetName, suffixPattern),
7615
+ (obj) => {
7616
+ const box = new THREE2__namespace.Box3().setFromObject(obj);
7617
+ if (box.isEmpty()) {
7618
+ return null;
7619
+ }
7620
+ return box.getCenter(new THREE2__namespace.Vector3());
7621
+ }
7622
+ )
7623
+ );
7624
+ }
7625
+ if (region.objects?.length) {
7626
+ const objectTargets = region.objects.filter((target) => target !== "*");
7627
+ points.push(
7628
+ ...collectMatchedPoints(
7629
+ model,
7630
+ objectTargets,
7631
+ (obj, targetName) => fuzzyNameMatch(obj.name, targetName, suffixPattern),
7632
+ (obj) => {
7633
+ if (obj.isMesh) {
7634
+ const box = new THREE2__namespace.Box3().setFromObject(obj);
7635
+ if (!box.isEmpty()) {
7636
+ return box.getCenter(new THREE2__namespace.Vector3());
7637
+ }
7638
+ }
7639
+ const worldPos = new THREE2__namespace.Vector3();
7640
+ obj.getWorldPosition(worldPos);
7641
+ return worldPos;
7642
+ }
7643
+ )
7644
+ );
7645
+ }
7646
+ return averagePoints(points);
7647
+ }
7648
+ function getRegionLocalCenter(model, region, characterConfig) {
7649
+ const worldCenter = getRegionWorldCenter(model, region, characterConfig);
7650
+ return worldCenter ? model.worldToLocal(worldCenter.clone()) : null;
7651
+ }
7652
+ function cloneLaterality(value) {
7653
+ return {
7654
+ leftSideX: value.leftSideX,
7655
+ confidence: value.confidence,
7656
+ evidence: [...value.evidence]
7657
+ };
7658
+ }
7659
+ function getDefaultAnnotationLaterality() {
7660
+ return cloneLaterality(DEFAULT_LATERALITY);
7661
+ }
7662
+ function normalizeCameraAngle(angle) {
7663
+ return (angle % 360 + 360) % 360;
7664
+ }
7665
+ function getRegionSemanticSide(regionName) {
7666
+ if (!regionName) return null;
7667
+ const match = SIDE_TOKEN_PATTERN.exec(regionName);
7668
+ if (!match) return null;
7669
+ return match[2].toLowerCase() === "left" ? "left" : "right";
7670
+ }
7671
+ function getSemanticHorizontalSign(regionName, laterality) {
7672
+ const side = getRegionSemanticSide(regionName);
7673
+ if (side === "left") return laterality.leftSideX;
7674
+ if (side === "right") return invertHorizontalSign(laterality.leftSideX);
7675
+ return null;
7676
+ }
7677
+ function getSemanticHorizontalSignForSide(side, laterality) {
7678
+ return side === "left" ? laterality.leftSideX : invertHorizontalSign(laterality.leftSideX);
7679
+ }
7680
+ function detectAnnotationLaterality(model, regions, characterConfig) {
7681
+ if (!model || regions.length === 0) {
7682
+ return getDefaultAnnotationLaterality();
7683
+ }
7684
+ model.updateMatrixWorld(true);
7685
+ const regionsByName = new Map(
7686
+ regions.map((region) => [region.name.toLowerCase(), region])
7687
+ );
7688
+ const votes = [];
7689
+ const pairedKeys = /* @__PURE__ */ new Set();
7690
+ for (const region of regions) {
7691
+ if (getRegionSemanticSide(region.name) !== "left") continue;
7692
+ const mirroredName = replaceSemanticSideToken(region.name, "right");
7693
+ if (!mirroredName) continue;
7694
+ const other = regionsByName.get(mirroredName.toLowerCase());
7695
+ if (!other) continue;
7696
+ const pairKey = [region.name.toLowerCase(), other.name.toLowerCase()].sort().join("|");
7697
+ if (pairedKeys.has(pairKey)) continue;
7698
+ pairedKeys.add(pairKey);
7699
+ const leftCenter = getRegionLocalCenter(model, region, characterConfig);
7700
+ const rightCenter = getRegionLocalCenter(model, other, characterConfig);
7701
+ if (!leftCenter || !rightCenter) continue;
7702
+ const separation = Math.abs(leftCenter.x - rightCenter.x);
7703
+ if (separation < MIN_LATERAL_SEPARATION) continue;
7704
+ votes.push({
7705
+ sign: leftCenter.x > rightCenter.x ? 1 : -1,
7706
+ weight: separation,
7707
+ evidence: `pair:${region.name}/${other.name}:${leftCenter.x.toFixed(3)}/${rightCenter.x.toFixed(3)}`
7708
+ });
7709
+ }
7710
+ if (votes.length === 0) {
7711
+ for (const region of regions) {
7712
+ const side = getRegionSemanticSide(region.name);
7713
+ if (!side) continue;
7714
+ const center = getRegionLocalCenter(model, region, characterConfig);
7715
+ if (!center || Math.abs(center.x) < MIN_LATERAL_SEPARATION) continue;
7716
+ const sign = side === "left" ? center.x > 0 ? 1 : -1 : center.x > 0 ? -1 : 1;
7717
+ votes.push({
7718
+ sign,
7719
+ weight: Math.abs(center.x),
7720
+ evidence: `single:${region.name}:${center.x.toFixed(3)}`
7721
+ });
7722
+ }
7723
+ }
7724
+ if (votes.length === 0) {
7725
+ return getDefaultAnnotationLaterality();
7726
+ }
7727
+ let signedWeight = 0;
7728
+ let totalWeight = 0;
7729
+ for (const vote of votes) {
7730
+ signedWeight += vote.sign * vote.weight;
7731
+ totalWeight += vote.weight;
7732
+ }
7733
+ if (Math.abs(signedWeight) < MIN_LATERAL_SEPARATION) {
7734
+ return getDefaultAnnotationLaterality();
7735
+ }
7736
+ return {
7737
+ leftSideX: signedWeight > 0 ? 1 : -1,
7738
+ confidence: totalWeight > 0 ? Math.abs(signedWeight) / totalWeight : 0,
7739
+ evidence: votes.map((vote) => vote.evidence)
7740
+ };
7741
+ }
7742
+ function resolveRegionCameraAngle(region, laterality) {
7743
+ if (region.cameraAngle === void 0) {
7744
+ return void 0;
7745
+ }
7746
+ const normalizedAngle = normalizeCameraAngle(region.cameraAngle);
7747
+ const side = getRegionSemanticSide(region.name);
7748
+ if (side && (normalizedAngle === 90 || normalizedAngle === 270)) {
7749
+ return side === "left" ? laterality.leftSideX > 0 ? 90 : 270 : laterality.leftSideX > 0 ? 270 : 90;
7750
+ }
7751
+ return normalizedAngle;
7752
+ }
7753
+ function resolveRegionVisibilityCameraAngle(region, laterality) {
7754
+ const explicitAngle = resolveRegionCameraAngle(region, laterality);
7755
+ if (explicitAngle !== void 0) {
7756
+ return explicitAngle;
7757
+ }
7758
+ if (!region.parent) {
7759
+ return void 0;
7760
+ }
7761
+ const side = getRegionSemanticSide(region.name);
7762
+ if (!side) {
7763
+ return void 0;
7764
+ }
7765
+ return side === "left" ? laterality.leftSideX > 0 ? 90 : 270 : laterality.leftSideX > 0 ? 270 : 90;
7766
+ }
7767
+ function toWorldDirection(model, localDirection) {
7768
+ const worldDirection = localDirection.clone();
7769
+ if (model) {
7770
+ model.updateMatrixWorld(true);
7771
+ const worldQuaternion = new THREE2__namespace.Quaternion();
7772
+ model.getWorldQuaternion(worldQuaternion);
7773
+ worldDirection.applyQuaternion(worldQuaternion);
7774
+ }
7775
+ return worldDirection.normalize();
7776
+ }
7777
+ function toModelLocalDirection2(model, worldDirection) {
7778
+ const localDirection = worldDirection.clone();
7779
+ if (model) {
7780
+ model.updateMatrixWorld(true);
7781
+ const worldQuaternion = new THREE2__namespace.Quaternion();
7782
+ model.getWorldQuaternion(worldQuaternion);
7783
+ localDirection.applyQuaternion(worldQuaternion.invert());
7784
+ }
7785
+ return localDirection.normalize();
7786
+ }
7787
+ function getWorldDirectionForCameraAngle(model, cameraAngle) {
7788
+ const angleRad = normalizeCameraAngle(cameraAngle) * Math.PI / 180;
7789
+ return toWorldDirection(
7790
+ model,
7791
+ new THREE2__namespace.Vector3(Math.sin(angleRad), 0, Math.cos(angleRad))
7792
+ );
7793
+ }
7794
+ function getModelLocalOrbitAngle(model, modelCenter, worldPosition) {
7795
+ const worldOffset = new THREE2__namespace.Vector3().subVectors(worldPosition, modelCenter);
7796
+ const localOffset = toModelLocalDirection2(model, worldOffset);
7797
+ return normalizeCameraAngle(Math.atan2(localOffset.x, localOffset.z) * (180 / Math.PI));
7798
+ }
7799
+ function passesMarkerCameraAngleGate(params) {
7800
+ const { markerAngle, currentCameraAngle, rangeDegrees = 90 } = params;
7801
+ if (markerAngle === void 0 || markerAngle === 0 || currentCameraAngle === void 0) {
7802
+ return true;
7803
+ }
7804
+ let diff = Math.abs(currentCameraAngle - markerAngle);
7805
+ if (diff > 180) diff = 360 - diff;
7806
+ return diff <= rangeDegrees;
7807
+ }
7808
+
7528
7809
  // src/physics/HairPhysics.ts
7529
7810
  var DEFAULT_HAIR_PHYSICS_CONFIG = {
7530
7811
  mass: 1,
@@ -8668,36 +8949,54 @@ exports.buildMappingEditorModel = buildMappingEditorModel;
8668
8949
  exports.collectMorphMeshes = collectMorphMeshes;
8669
8950
  exports.compileVisemeKeys = compileVisemeKeys;
8670
8951
  exports.computeCameraRelativeGazeOffset = computeCameraRelativeGazeOffset;
8952
+ exports.detectAnnotationLaterality = detectAnnotationLaterality;
8671
8953
  exports.detectFacingDirection = detectFacingDirection;
8672
8954
  exports.extendCharacterConfigWithPreset = extendCharacterConfigWithPreset;
8673
8955
  exports.extendPresetWithProfile = extendPresetWithProfile;
8956
+ exports.extendProfileConfigWithPreset = extendProfileConfigWithPreset;
8674
8957
  exports.extractFromGLTF = extractFromGLTF;
8958
+ exports.extractLegacyCharacterProfileOverrides = extractLegacyCharacterProfileOverrides;
8675
8959
  exports.extractModelData = extractModelData;
8676
8960
  exports.extractProfileOverrides = extractProfileOverrides;
8677
8961
  exports.findFaceCenter = findFaceCenter;
8678
8962
  exports.fuzzyNameMatch = fuzzyNameMatch;
8679
8963
  exports.generateMappingCorrections = generateMappingCorrections;
8964
+ exports.getDefaultAnnotationLaterality = getDefaultAnnotationLaterality;
8680
8965
  exports.getMeshNamesForAUProfile = getMeshNamesForAUProfile;
8681
8966
  exports.getMeshNamesForVisemeProfile = getMeshNamesForVisemeProfile;
8682
8967
  exports.getModelForwardDirection = getModelForwardDirection;
8968
+ exports.getModelLocalOrbitAngle = getModelLocalOrbitAngle;
8683
8969
  exports.getPreset = getPreset;
8684
8970
  exports.getPresetWithProfile = getPresetWithProfile;
8971
+ exports.getProfilePresetId = getProfilePresetId;
8685
8972
  exports.getProfileVisemeSlots = getProfileVisemeSlots;
8973
+ exports.getRegionSemanticSide = getRegionSemanticSide;
8974
+ exports.getSemanticHorizontalSign = getSemanticHorizontalSign;
8975
+ exports.getSemanticHorizontalSignForSide = getSemanticHorizontalSignForSide;
8686
8976
  exports.getVisemeBindingTargets = getVisemeBindingTargets;
8687
8977
  exports.getVisemeJawAmounts = getVisemeJawAmounts;
8688
8978
  exports.getVisemeSlotIndex = getVisemeSlotIndex;
8979
+ exports.getWorldDirectionForCameraAngle = getWorldDirectionForCameraAngle;
8689
8980
  exports.hasLeftRightMorphs = hasLeftRightMorphs;
8690
8981
  exports.isMixedAU = isMixedAU;
8691
8982
  exports.isPresetCompatible = isPresetCompatible;
8692
8983
  exports.mapProviderVisemeToSlot = mapProviderVisemeToSlot;
8693
8984
  exports.mergeCharacterRegionsByName = mergeRegionsByName;
8985
+ exports.mergeProfileRegionsByName = mergeProfileRegionsByName;
8986
+ exports.normalizeCameraAngle = normalizeCameraAngle;
8987
+ exports.passesMarkerCameraAngleGate = passesMarkerCameraAngleGate;
8694
8988
  exports.resolveBoneName = resolveBoneName;
8695
8989
  exports.resolveBoneNames = resolveBoneNames;
8696
8990
  exports.resolveFaceCenter = resolveFaceCenter;
8697
8991
  exports.resolvePreset = resolvePreset;
8698
8992
  exports.resolvePresetWithOverrides = resolvePresetWithOverrides;
8993
+ exports.resolveProfileFromPreset = resolveProfileFromPreset;
8994
+ exports.resolveRegionCameraAngle = resolveRegionCameraAngle;
8995
+ exports.resolveRegionVisibilityCameraAngle = resolveRegionVisibilityCameraAngle;
8699
8996
  exports.resolveVisemeMeshCategory = resolveVisemeMeshCategory;
8700
8997
  exports.suggestBestPreset = suggestBestPreset;
8998
+ exports.toModelLocalDirection = toModelLocalDirection2;
8999
+ exports.toWorldDirection = toWorldDirection;
8701
9000
  exports.validateMappingConfig = validateMappingConfig;
8702
9001
  exports.validateMappings = validateMappings;
8703
9002
  //# sourceMappingURL=index.cjs.map