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