@eaprelsky/nocturna-wheel 4.0.2 → 4.0.3

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.
@@ -287,7 +287,7 @@ class IconProvider {
287
287
  /**
288
288
  * IconData.js
289
289
  * Auto-generated module containing inline SVG icons as data URLs
290
- * Generated at: 2025-12-21T14:49:27.435Z
290
+ * Generated at: 2025-12-21T15:10:29.083Z
291
291
  *
292
292
  * This file is automatically generated by the build process.
293
293
  * Do not edit manually - changes will be overwritten.
@@ -2744,6 +2744,16 @@ class PlanetSymbolRenderer extends BasePlanetRenderer {
2744
2744
  */
2745
2745
 
2746
2746
  class PlanetPositionCalculator {
2747
+ static _normalizeAngle(angle) {
2748
+ return ((angle % 360) + 360) % 360;
2749
+ }
2750
+
2751
+ static _getAngle(position) {
2752
+ return position && position.adjustedLongitude !== undefined
2753
+ ? position.adjustedLongitude
2754
+ : position.longitude;
2755
+ }
2756
+
2747
2757
  /**
2748
2758
  * Calculate position for a planet on a circle
2749
2759
  * @param {Object} params - Position parameters
@@ -2828,7 +2838,8 @@ class PlanetPositionCalculator {
2828
2838
  centerX,
2829
2839
  centerY,
2830
2840
  baseRadius,
2831
- iconSize = 24
2841
+ iconSize = 24,
2842
+ maxIterations = 5
2832
2843
  } = options;
2833
2844
 
2834
2845
  if (!positions || positions.length <= 1) {
@@ -2842,72 +2853,51 @@ class PlanetPositionCalculator {
2842
2853
 
2843
2854
  console.log(`PlanetPositionCalculator: Adjusting overlaps for ${positions.length} positions`);
2844
2855
 
2845
- // Make a copy to not modify originals
2846
- const adjustedPositions = [...positions];
2856
+ // Make a copy to not modify originals (also ensures we can add fields safely)
2857
+ const adjustedPositions = positions.map(p => ({ ...p }));
2847
2858
 
2848
2859
  // The minimum angular distance needed to prevent overlap at base radius
2849
2860
  // minDistance already includes the desired spacing (iconSize * 1.5)
2850
2861
  const minAngularDistance = (minDistance / baseRadius) * (180 / Math.PI);
2851
2862
  console.log(`PlanetPositionCalculator: Minimum angular distance: ${minAngularDistance.toFixed(2)}°`);
2852
-
2853
- // Sort positions by longitude for overlap detection
2854
- const sortedPositionIndices = adjustedPositions
2855
- .map((pos, idx) => ({ pos, idx }))
2856
- .sort((a, b) => a.pos.longitude - b.pos.longitude);
2857
-
2858
- const sortedPositions = sortedPositionIndices.map(item => ({
2859
- ...adjustedPositions[item.idx],
2860
- originalIndex: item.idx
2861
- }));
2862
-
2863
- // Find clusters of planets that are too close angularly
2864
- const clusters = this._findOverlappingClusters(sortedPositions, minAngularDistance);
2865
- console.log(`PlanetPositionCalculator: Found ${clusters.length} clusters of overlapping positions`);
2866
- clusters.forEach((cluster, i) => {
2867
- console.log(`PlanetPositionCalculator: Cluster ${i+1} has ${cluster.length} positions`);
2868
- });
2869
-
2870
- // Process each cluster
2871
- clusters.forEach((cluster, clusterIndex) => {
2872
- console.log(`PlanetPositionCalculator: Processing cluster ${clusterIndex+1}`);
2873
-
2874
- if (cluster.length <= 1) {
2875
- // Single planet - just place at exact base radius with no angle change
2876
- const planet = cluster[0];
2877
- console.log(`PlanetPositionCalculator: Single position in cluster, keeping at original longitude ${planet.longitude.toFixed(2)}°`);
2878
- this._setExactPosition(planet, planet.longitude, baseRadius, centerX, centerY, iconSize);
2879
- } else {
2880
- // Handle cluster with multiple planets - distribute by angle
2881
- console.log(`PlanetPositionCalculator: Distributing ${cluster.length} positions in cluster`);
2882
- this._distributeClusterByAngle(cluster, baseRadius, minAngularDistance, centerX, centerY, iconSize);
2883
-
2884
- // Log the distributions
2885
- cluster.forEach((pos, i) => {
2886
- console.log(`PlanetPositionCalculator: Position ${i+1} in cluster ${clusterIndex+1} adjusted from ${pos.longitude.toFixed(2)}° to ${pos.adjustedLongitude.toFixed(2)}°`);
2887
- });
2863
+
2864
+ // Initialize adjustedLongitude for all positions
2865
+ adjustedPositions.forEach(pos => {
2866
+ if (pos.adjustedLongitude === undefined) {
2867
+ pos.adjustedLongitude = pos.longitude;
2888
2868
  }
2869
+ this._setExactPosition(pos, this._getAngle(pos), baseRadius, centerX, centerY, iconSize);
2889
2870
  });
2890
-
2891
- // Copy adjusted positions back to the original array order
2892
- sortedPositions.forEach(pos => {
2893
- const origIndex = pos.originalIndex;
2894
-
2895
- // Only copy if we have valid data
2896
- if (origIndex !== undefined && origIndex >= 0 && origIndex < adjustedPositions.length) {
2897
- adjustedPositions[origIndex].x = pos.x;
2898
- adjustedPositions[origIndex].y = pos.y;
2899
- adjustedPositions[origIndex].iconX = pos.iconX;
2900
- adjustedPositions[origIndex].iconY = pos.iconY;
2901
- adjustedPositions[origIndex].iconCenterX = pos.iconCenterX;
2902
- adjustedPositions[origIndex].iconCenterY = pos.iconCenterY;
2903
-
2904
- // Also add any adjusted longitude for reference
2905
- if (pos.adjustedLongitude !== undefined) {
2906
- adjustedPositions[origIndex].adjustedLongitude = pos.adjustedLongitude;
2907
- }
2871
+
2872
+ // Iteratively resolve overlaps. This avoids "new overlaps" created after distributing a cluster.
2873
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
2874
+ // Sort by current (possibly adjusted) angle for overlap detection
2875
+ const sortedPositions = [...adjustedPositions].sort(
2876
+ (a, b) => this._getAngle(a) - this._getAngle(b)
2877
+ );
2878
+
2879
+ // Find clusters of planets that are too close angularly
2880
+ const clusters = this._findOverlappingClusters(sortedPositions, minAngularDistance);
2881
+ console.log(`PlanetPositionCalculator: Iteration ${iteration + 1}/${maxIterations}, clusters: ${clusters.length}`);
2882
+
2883
+ // No overlaps -> done
2884
+ const hasRealClusters = clusters.some(c => c.length > 1);
2885
+ if (!hasRealClusters) {
2886
+ break;
2908
2887
  }
2909
- });
2910
-
2888
+
2889
+ // Process each cluster
2890
+ clusters.forEach(cluster => {
2891
+ if (cluster.length <= 1) {
2892
+ const planet = cluster[0];
2893
+ this._setExactPosition(planet, this._getAngle(planet), baseRadius, centerX, centerY, iconSize);
2894
+ return;
2895
+ }
2896
+
2897
+ this._distributeClusterByPush(cluster, baseRadius, minAngularDistance, centerX, centerY, iconSize);
2898
+ });
2899
+ }
2900
+
2911
2901
  return adjustedPositions;
2912
2902
  }
2913
2903
 
@@ -2937,7 +2927,9 @@ class PlanetPositionCalculator {
2937
2927
  const currPosition = sortedPositions[i];
2938
2928
 
2939
2929
  // Check angular distance, considering wrap-around at 360°
2940
- let angleDiff = currPosition.longitude - prevPosition.longitude;
2930
+ const prevAngle = this._getAngle(prevPosition);
2931
+ const currAngle = this._getAngle(currPosition);
2932
+ let angleDiff = currAngle - prevAngle;
2941
2933
  if (angleDiff < 0) angleDiff += 360;
2942
2934
 
2943
2935
  if (angleDiff < minAngularDistance) {
@@ -2961,7 +2953,9 @@ class PlanetPositionCalculator {
2961
2953
  const lastPlanet = sortedPositions[posCount - 1];
2962
2954
  const firstPlanetOriginal = sortedPositions[0];
2963
2955
 
2964
- let wrapDiff = (firstPlanetOriginal.longitude + 360) - lastPlanet.longitude;
2956
+ const lastAngle = this._getAngle(lastPlanet);
2957
+ const firstAngle = this._getAngle(firstPlanetOriginal);
2958
+ let wrapDiff = (firstAngle + 360) - lastAngle;
2965
2959
  if (wrapDiff < 0) wrapDiff += 360;
2966
2960
 
2967
2961
  if (wrapDiff < minAngularDistance) {
@@ -2985,7 +2979,8 @@ class PlanetPositionCalculator {
2985
2979
  }
2986
2980
 
2987
2981
  /**
2988
- * Distribute positions in a cluster by adjusting only their angles
2982
+ * Distribute positions in a cluster using a "push-apart" algorithm.
2983
+ * This keeps adjustments as small as possible while ensuring minimum spacing.
2989
2984
  * @private
2990
2985
  * @param {Array} positions - Array of positions in the cluster
2991
2986
  * @param {number} radius - The exact radius to place all positions
@@ -2994,49 +2989,62 @@ class PlanetPositionCalculator {
2994
2989
  * @param {number} centerY - Y coordinate of center
2995
2990
  * @param {number} iconSize - Size of the icon
2996
2991
  */
2997
- static _distributeClusterByAngle(positions, radius, minAngularDistance, centerX, centerY, iconSize) {
2992
+ static _distributeClusterByPush(positions, radius, minAngularDistance, centerX, centerY, iconSize) {
2998
2993
  const n = positions.length;
2999
-
3000
- // If only one planet, keep its original position
3001
- if (n === 1) {
3002
- this._setExactPosition(positions[0], positions[0].longitude, radius, centerX, centerY, iconSize);
2994
+ if (n <= 1) {
2995
+ const p = positions[0];
2996
+ this._setExactPosition(p, this._getAngle(p), radius, centerX, centerY, iconSize);
3003
2997
  return;
3004
2998
  }
3005
-
3006
- // Sort positions by their original longitude to maintain order
3007
- positions.sort((a, b) => a.longitude - b.longitude);
3008
-
3009
- // Calculate central angle and total span
3010
- const firstPos = positions[0].longitude;
3011
- const lastPos = positions[n-1].longitude;
3012
- let totalArc = lastPos - firstPos;
3013
-
3014
- // Handle wrap-around case (e.g., positions at 350° and 10°)
3015
- if (totalArc < 0 || totalArc > 180) {
3016
- totalArc = (360 + lastPos - firstPos) % 360;
2999
+
3000
+ // Unwrap angles if cluster spans the 0°/360° boundary
3001
+ const baseAngles = positions.map(p => this._getAngle(p));
3002
+ const min = Math.min(...baseAngles);
3003
+ const max = Math.max(...baseAngles);
3004
+ const unwrappedAngles = (max - min > 180)
3005
+ ? baseAngles.map(a => (a < 180 ? a + 360 : a))
3006
+ : baseAngles;
3007
+
3008
+ // Sort by unwrapped angle
3009
+ const items = positions
3010
+ .map((p, idx) => ({ p, orig: unwrappedAngles[idx] }))
3011
+ .sort((a, b) => a.orig - b.orig);
3012
+
3013
+ const origAngles = items.map(it => it.orig);
3014
+ let adjusted = [...origAngles];
3015
+
3016
+ // Forward pass: enforce minimum distance
3017
+ for (let i = 1; i < n; i++) {
3018
+ const minAllowed = adjusted[i - 1] + minAngularDistance;
3019
+ if (adjusted[i] < minAllowed) {
3020
+ adjusted[i] = minAllowed;
3021
+ }
3017
3022
  }
3018
-
3019
- // Calculate the center of the cluster (weighted average of all positions)
3020
- let sumAngles = 0;
3021
- for (let i = 0; i < n; i++) {
3022
- sumAngles += positions[i].longitude;
3023
+
3024
+ // Shift the whole cluster to keep it centered around the original mean angle
3025
+ const origCenter = origAngles.reduce((s, a) => s + a, 0) / n;
3026
+ const adjustedCenter = adjusted.reduce((s, a) => s + a, 0) / n;
3027
+ const shift = origCenter - adjustedCenter;
3028
+ adjusted = adjusted.map(a => a + shift);
3029
+
3030
+ // Re-enforce constraints after shifting
3031
+ for (let i = 1; i < n; i++) {
3032
+ const minAllowed = adjusted[i - 1] + minAngularDistance;
3033
+ if (adjusted[i] < minAllowed) {
3034
+ adjusted[i] = minAllowed;
3035
+ }
3023
3036
  }
3024
- let centerAngle = (sumAngles / n) % 360;
3025
-
3026
- // Determine minimum arc needed for n planets with minimum spacing
3027
- // minAngularDistance already includes the desired spacing
3028
- const minRequiredArc = (n - 1) * minAngularDistance;
3029
-
3030
- // Always use at least the minimum required arc
3031
- const spanToUse = Math.max(minRequiredArc, totalArc);
3032
-
3033
- // Calculate start angle (center - half of span)
3034
- const startAngle = (centerAngle - spanToUse/2 + 360) % 360;
3035
-
3036
- // Distribute planets evenly from the start angle
3037
+ for (let i = n - 2; i >= 0; i--) {
3038
+ const maxAllowed = adjusted[i + 1] - minAngularDistance;
3039
+ if (adjusted[i] > maxAllowed) {
3040
+ adjusted[i] = maxAllowed;
3041
+ }
3042
+ }
3043
+
3044
+ // Apply results
3037
3045
  for (let i = 0; i < n; i++) {
3038
- const angle = (startAngle + i * (spanToUse / (n-1))) % 360;
3039
- this._setExactPosition(positions[i], angle, radius, centerX, centerY, iconSize);
3046
+ const angle = this._normalizeAngle(adjusted[i]);
3047
+ this._setExactPosition(items[i].p, angle, radius, centerX, centerY, iconSize);
3040
3048
  }
3041
3049
  }
3042
3050
 
@@ -3236,7 +3244,7 @@ class PrimaryPlanetRenderer extends BasePlanetRenderer {
3236
3244
  this.symbolRenderer.addPlanetTooltip(planetGroup, planet);
3237
3245
 
3238
3246
  // Render connector if needed
3239
- const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize);
3247
+ const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize, 0.9);
3240
3248
  if (connector) {
3241
3249
  planetGroup.appendChild(connector);
3242
3250
  }
@@ -3418,7 +3426,7 @@ class SecondaryPlanetRenderer extends BasePlanetRenderer {
3418
3426
  this.symbolRenderer.addPlanetTooltip(planetGroup, planet);
3419
3427
 
3420
3428
  // Render connector if needed
3421
- const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize);
3429
+ const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize, 0.9);
3422
3430
  if (connector) {
3423
3431
  planetGroup.appendChild(connector);
3424
3432
  }
@@ -5457,8 +5465,13 @@ class ChartRenderer {
5457
5465
  const centerY = this.chart.config.svg.center.y;
5458
5466
  const c3Radius = this.chart.config.radius.innermost; // C3
5459
5467
 
5460
- // Only draw the innermost circle if secondary planets are enabled
5461
- if (this.chart.config.planetSettings.secondaryEnabled !== false) {
5468
+ // Only draw the innermost circle if secondary planets are enabled AND we actually have secondary data
5469
+ const hasSecondaryPlanetsData =
5470
+ this.chart?.secondaryPlanets &&
5471
+ typeof this.chart.secondaryPlanets === 'object' &&
5472
+ Object.keys(this.chart.secondaryPlanets).length > 0;
5473
+
5474
+ if (this.chart.config.planetSettings.secondaryEnabled !== false && hasSecondaryPlanetsData) {
5462
5475
  this.drawInnermostCircle(zodiacGroup, this.svgUtils, centerX, centerY, c3Radius);
5463
5476
  }
5464
5477