@eaprelsky/nocturna-wheel 4.0.1 → 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-11-27T18:13:39.048Z
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.
@@ -2331,41 +2331,26 @@ class HouseRenderer extends BaseRenderer {
2331
2331
  // Offset needed to place Ascendant (house 1 cusp) at 0 degrees (top side)
2332
2332
  ascendantAlignmentOffset = (360 - ascendantLon) % 360;
2333
2333
  }
2334
-
2335
- // Create array of houses with their rotated angles and original indices
2336
- const housesWithAngles = [];
2334
+
2335
+ // Render house numbers at the CENTER of each house sector.
2336
+ // If house cusp data is available, compute the midpoint between cusp[i] and cusp[i+1] (with proper 0/360 wrap).
2337
+ // Otherwise, fall back to equal 30° houses.
2337
2338
  for (let i = 0; i < 12; i++) {
2338
- let baseAngle;
2339
+ const houseNumber = i + 1;
2340
+
2341
+ let midAngle;
2339
2342
  if (this.houseData && this.houseData.length >= 12) {
2340
- baseAngle = this.getHouseLongitude(this.houseData[i]);
2343
+ const startLon = this.getHouseLongitude(this.houseData[i]);
2344
+ const endLon = this.getHouseLongitude(this.houseData[(i + 1) % 12]);
2345
+ const arc = (endLon - startLon + 360) % 360; // always move forward through the zodiac
2346
+ const midLon = (startLon + arc / 2) % 360;
2347
+ midAngle = (midLon + ascendantAlignmentOffset + rotationAngle) % 360;
2341
2348
  } else {
2342
- baseAngle = i * 30; // Default if no data
2349
+ // Equal houses: center of each 30° segment
2350
+ midAngle = (i * 30 + 15 + rotationAngle) % 360;
2343
2351
  }
2344
- const rotatedAngle = (baseAngle + ascendantAlignmentOffset + rotationAngle) % 360;
2345
2352
 
2346
- housesWithAngles.push({
2347
- originalIndex: i,
2348
- baseAngle: baseAngle,
2349
- rotatedAngle: rotatedAngle
2350
- });
2351
- }
2352
-
2353
- // Sort by rotated angle to determine visual order
2354
- housesWithAngles.sort((a, b) => a.rotatedAngle - b.rotatedAngle);
2355
-
2356
- // Find which house is Ascendant (originally index 0) after rotation
2357
- const ascendantVisualIndex = housesWithAngles.findIndex(h => h.originalIndex === 0);
2358
-
2359
- // Render houses with correct numbering based on visual position
2360
- housesWithAngles.forEach((house, visualIndex) => {
2361
- const houseAngle = house.rotatedAngle;
2362
-
2363
- // Calculate house number based on position relative to Ascendant
2364
- // Counter-clockwise from Ascendant
2365
- const houseNumber = ((visualIndex - ascendantVisualIndex + 12) % 12) + 1;
2366
-
2367
- // Offset text from house line clockwise
2368
- const angle = (houseAngle + 15) % 360; // Place in middle of house segment, apply modulo
2353
+ const angle = midAngle;
2369
2354
 
2370
2355
  // Calculate position for house number
2371
2356
  const point = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.numberRadius, angle);
@@ -2406,7 +2391,7 @@ class HouseRenderer extends BaseRenderer {
2406
2391
 
2407
2392
  parentGroup.appendChild(text);
2408
2393
  elements.push(text);
2409
- });
2394
+ }
2410
2395
 
2411
2396
  return elements;
2412
2397
  }
@@ -2759,6 +2744,16 @@ class PlanetSymbolRenderer extends BasePlanetRenderer {
2759
2744
  */
2760
2745
 
2761
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
+
2762
2757
  /**
2763
2758
  * Calculate position for a planet on a circle
2764
2759
  * @param {Object} params - Position parameters
@@ -2843,7 +2838,8 @@ class PlanetPositionCalculator {
2843
2838
  centerX,
2844
2839
  centerY,
2845
2840
  baseRadius,
2846
- iconSize = 24
2841
+ iconSize = 24,
2842
+ maxIterations = 5
2847
2843
  } = options;
2848
2844
 
2849
2845
  if (!positions || positions.length <= 1) {
@@ -2857,72 +2853,51 @@ class PlanetPositionCalculator {
2857
2853
 
2858
2854
  console.log(`PlanetPositionCalculator: Adjusting overlaps for ${positions.length} positions`);
2859
2855
 
2860
- // Make a copy to not modify originals
2861
- 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 }));
2862
2858
 
2863
2859
  // The minimum angular distance needed to prevent overlap at base radius
2864
- // Add safety factor to ensure visual separation
2865
- const minAngularDistance = (minDistance / baseRadius) * (180 / Math.PI) * 1.3; // 30% extra spacing
2860
+ // minDistance already includes the desired spacing (iconSize * 1.5)
2861
+ const minAngularDistance = (minDistance / baseRadius) * (180 / Math.PI);
2866
2862
  console.log(`PlanetPositionCalculator: Minimum angular distance: ${minAngularDistance.toFixed(2)}°`);
2867
-
2868
- // Sort positions by longitude for overlap detection
2869
- const sortedPositionIndices = adjustedPositions
2870
- .map((pos, idx) => ({ pos, idx }))
2871
- .sort((a, b) => a.pos.longitude - b.pos.longitude);
2872
-
2873
- const sortedPositions = sortedPositionIndices.map(item => ({
2874
- ...adjustedPositions[item.idx],
2875
- originalIndex: item.idx
2876
- }));
2877
-
2878
- // Find clusters of planets that are too close angularly
2879
- const clusters = this._findOverlappingClusters(sortedPositions, minAngularDistance);
2880
- console.log(`PlanetPositionCalculator: Found ${clusters.length} clusters of overlapping positions`);
2881
- clusters.forEach((cluster, i) => {
2882
- console.log(`PlanetPositionCalculator: Cluster ${i+1} has ${cluster.length} positions`);
2883
- });
2884
-
2885
- // Process each cluster
2886
- clusters.forEach((cluster, clusterIndex) => {
2887
- console.log(`PlanetPositionCalculator: Processing cluster ${clusterIndex+1}`);
2888
-
2889
- if (cluster.length <= 1) {
2890
- // Single planet - just place at exact base radius with no angle change
2891
- const planet = cluster[0];
2892
- console.log(`PlanetPositionCalculator: Single position in cluster, keeping at original longitude ${planet.longitude.toFixed(2)}°`);
2893
- this._setExactPosition(planet, planet.longitude, baseRadius, centerX, centerY, iconSize);
2894
- } else {
2895
- // Handle cluster with multiple planets - distribute by angle
2896
- console.log(`PlanetPositionCalculator: Distributing ${cluster.length} positions in cluster`);
2897
- this._distributeClusterByAngle(cluster, baseRadius, minAngularDistance, centerX, centerY, iconSize);
2898
-
2899
- // Log the distributions
2900
- cluster.forEach((pos, i) => {
2901
- console.log(`PlanetPositionCalculator: Position ${i+1} in cluster ${clusterIndex+1} adjusted from ${pos.longitude.toFixed(2)}° to ${pos.adjustedLongitude.toFixed(2)}°`);
2902
- });
2863
+
2864
+ // Initialize adjustedLongitude for all positions
2865
+ adjustedPositions.forEach(pos => {
2866
+ if (pos.adjustedLongitude === undefined) {
2867
+ pos.adjustedLongitude = pos.longitude;
2903
2868
  }
2869
+ this._setExactPosition(pos, this._getAngle(pos), baseRadius, centerX, centerY, iconSize);
2904
2870
  });
2905
-
2906
- // Copy adjusted positions back to the original array order
2907
- sortedPositions.forEach(pos => {
2908
- const origIndex = pos.originalIndex;
2909
-
2910
- // Only copy if we have valid data
2911
- if (origIndex !== undefined && origIndex >= 0 && origIndex < adjustedPositions.length) {
2912
- adjustedPositions[origIndex].x = pos.x;
2913
- adjustedPositions[origIndex].y = pos.y;
2914
- adjustedPositions[origIndex].iconX = pos.iconX;
2915
- adjustedPositions[origIndex].iconY = pos.iconY;
2916
- adjustedPositions[origIndex].iconCenterX = pos.iconCenterX;
2917
- adjustedPositions[origIndex].iconCenterY = pos.iconCenterY;
2918
-
2919
- // Also add any adjusted longitude for reference
2920
- if (pos.adjustedLongitude !== undefined) {
2921
- adjustedPositions[origIndex].adjustedLongitude = pos.adjustedLongitude;
2922
- }
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;
2923
2887
  }
2924
- });
2925
-
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
+
2926
2901
  return adjustedPositions;
2927
2902
  }
2928
2903
 
@@ -2952,7 +2927,9 @@ class PlanetPositionCalculator {
2952
2927
  const currPosition = sortedPositions[i];
2953
2928
 
2954
2929
  // Check angular distance, considering wrap-around at 360°
2955
- let angleDiff = currPosition.longitude - prevPosition.longitude;
2930
+ const prevAngle = this._getAngle(prevPosition);
2931
+ const currAngle = this._getAngle(currPosition);
2932
+ let angleDiff = currAngle - prevAngle;
2956
2933
  if (angleDiff < 0) angleDiff += 360;
2957
2934
 
2958
2935
  if (angleDiff < minAngularDistance) {
@@ -2976,7 +2953,9 @@ class PlanetPositionCalculator {
2976
2953
  const lastPlanet = sortedPositions[posCount - 1];
2977
2954
  const firstPlanetOriginal = sortedPositions[0];
2978
2955
 
2979
- 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;
2980
2959
  if (wrapDiff < 0) wrapDiff += 360;
2981
2960
 
2982
2961
  if (wrapDiff < minAngularDistance) {
@@ -3000,7 +2979,8 @@ class PlanetPositionCalculator {
3000
2979
  }
3001
2980
 
3002
2981
  /**
3003
- * 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.
3004
2984
  * @private
3005
2985
  * @param {Array} positions - Array of positions in the cluster
3006
2986
  * @param {number} radius - The exact radius to place all positions
@@ -3009,49 +2989,62 @@ class PlanetPositionCalculator {
3009
2989
  * @param {number} centerY - Y coordinate of center
3010
2990
  * @param {number} iconSize - Size of the icon
3011
2991
  */
3012
- static _distributeClusterByAngle(positions, radius, minAngularDistance, centerX, centerY, iconSize) {
2992
+ static _distributeClusterByPush(positions, radius, minAngularDistance, centerX, centerY, iconSize) {
3013
2993
  const n = positions.length;
3014
-
3015
- // If only one planet, keep its original position
3016
- if (n === 1) {
3017
- 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);
3018
2997
  return;
3019
2998
  }
3020
-
3021
- // Sort positions by their original longitude to maintain order
3022
- positions.sort((a, b) => a.longitude - b.longitude);
3023
-
3024
- // Calculate central angle and total span
3025
- const firstPos = positions[0].longitude;
3026
- const lastPos = positions[n-1].longitude;
3027
- let totalArc = lastPos - firstPos;
3028
-
3029
- // Handle wrap-around case (e.g., positions at 350° and 10°)
3030
- if (totalArc < 0 || totalArc > 180) {
3031
- 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
+ }
3032
3022
  }
3033
-
3034
- // Calculate the center of the cluster (weighted average of all positions)
3035
- let sumAngles = 0;
3036
- for (let i = 0; i < n; i++) {
3037
- 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
+ }
3038
3036
  }
3039
- let centerAngle = (sumAngles / n) % 360;
3040
-
3041
- // Determine minimum arc needed for n planets with minimum spacing
3042
- // Add extra spacing factor to ensure planets don't overlap
3043
- const minRequiredArc = (n - 1) * minAngularDistance * 1.2; // 20% extra spacing
3044
-
3045
- // Always use at least the minimum required arc
3046
- const spanToUse = Math.max(minRequiredArc, totalArc);
3047
-
3048
- // Calculate start angle (center - half of span)
3049
- const startAngle = (centerAngle - spanToUse/2 + 360) % 360;
3050
-
3051
- // 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
3052
3045
  for (let i = 0; i < n; i++) {
3053
- const angle = (startAngle + i * (spanToUse / (n-1))) % 360;
3054
- 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);
3055
3048
  }
3056
3049
  }
3057
3050
 
@@ -3128,7 +3121,8 @@ class PrimaryPlanetRenderer extends BasePlanetRenderer {
3128
3121
  // Define parameters for collision detection and distribution
3129
3122
  const iconSize = 24;
3130
3123
  const baseRadius = planets[0].iconRadius; // Use the iconRadius from the first planet
3131
- const minDistance = iconSize * 1.2;
3124
+ // Minimum distance = icon diameter + half diameter (0.5 * iconSize spacing between icons)
3125
+ const minDistance = iconSize * 1.5;
3132
3126
 
3133
3127
  // Prepare planets array in format expected by PlanetPositionCalculator
3134
3128
  const positions = planets.map((planet, index) => ({
@@ -3250,7 +3244,7 @@ class PrimaryPlanetRenderer extends BasePlanetRenderer {
3250
3244
  this.symbolRenderer.addPlanetTooltip(planetGroup, planet);
3251
3245
 
3252
3246
  // Render connector if needed
3253
- const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize);
3247
+ const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize, 0.9);
3254
3248
  if (connector) {
3255
3249
  planetGroup.appendChild(connector);
3256
3250
  }
@@ -3310,7 +3304,8 @@ class SecondaryPlanetRenderer extends BasePlanetRenderer {
3310
3304
  // Define parameters for collision detection and distribution
3311
3305
  const iconSize = 18; // Smaller size for secondary planets
3312
3306
  const baseRadius = planets[0].iconRadius; // Use the iconRadius from the first planet
3313
- const minDistance = iconSize * 1.1; // Slightly tighter packing
3307
+ // Minimum distance = icon diameter + half diameter (0.5 * iconSize spacing between icons)
3308
+ const minDistance = iconSize * 1.5;
3314
3309
 
3315
3310
  // Prepare planets array in format expected by PlanetPositionCalculator
3316
3311
  const positions = planets.map((planet, index) => ({
@@ -3431,7 +3426,7 @@ class SecondaryPlanetRenderer extends BasePlanetRenderer {
3431
3426
  this.symbolRenderer.addPlanetTooltip(planetGroup, planet);
3432
3427
 
3433
3428
  // Render connector if needed
3434
- const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize);
3429
+ const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize, 0.9);
3435
3430
  if (connector) {
3436
3431
  planetGroup.appendChild(connector);
3437
3432
  }
@@ -5470,8 +5465,13 @@ class ChartRenderer {
5470
5465
  const centerY = this.chart.config.svg.center.y;
5471
5466
  const c3Radius = this.chart.config.radius.innermost; // C3
5472
5467
 
5473
- // Only draw the innermost circle if secondary planets are enabled
5474
- 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) {
5475
5475
  this.drawInnermostCircle(zodiacGroup, this.svgUtils, centerX, centerY, c3Radius);
5476
5476
  }
5477
5477