@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.
@@ -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-11-27T18:13:39.810Z
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.
@@ -2337,41 +2337,26 @@
2337
2337
  // Offset needed to place Ascendant (house 1 cusp) at 0 degrees (top side)
2338
2338
  ascendantAlignmentOffset = (360 - ascendantLon) % 360;
2339
2339
  }
2340
-
2341
- // Create array of houses with their rotated angles and original indices
2342
- const housesWithAngles = [];
2340
+
2341
+ // Render house numbers at the CENTER of each house sector.
2342
+ // If house cusp data is available, compute the midpoint between cusp[i] and cusp[i+1] (with proper 0/360 wrap).
2343
+ // Otherwise, fall back to equal 30° houses.
2343
2344
  for (let i = 0; i < 12; i++) {
2344
- let baseAngle;
2345
+ const houseNumber = i + 1;
2346
+
2347
+ let midAngle;
2345
2348
  if (this.houseData && this.houseData.length >= 12) {
2346
- baseAngle = this.getHouseLongitude(this.houseData[i]);
2349
+ const startLon = this.getHouseLongitude(this.houseData[i]);
2350
+ const endLon = this.getHouseLongitude(this.houseData[(i + 1) % 12]);
2351
+ const arc = (endLon - startLon + 360) % 360; // always move forward through the zodiac
2352
+ const midLon = (startLon + arc / 2) % 360;
2353
+ midAngle = (midLon + ascendantAlignmentOffset + rotationAngle) % 360;
2347
2354
  } else {
2348
- baseAngle = i * 30; // Default if no data
2355
+ // Equal houses: center of each 30° segment
2356
+ midAngle = (i * 30 + 15 + rotationAngle) % 360;
2349
2357
  }
2350
- const rotatedAngle = (baseAngle + ascendantAlignmentOffset + rotationAngle) % 360;
2351
2358
 
2352
- housesWithAngles.push({
2353
- originalIndex: i,
2354
- baseAngle: baseAngle,
2355
- rotatedAngle: rotatedAngle
2356
- });
2357
- }
2358
-
2359
- // Sort by rotated angle to determine visual order
2360
- housesWithAngles.sort((a, b) => a.rotatedAngle - b.rotatedAngle);
2361
-
2362
- // Find which house is Ascendant (originally index 0) after rotation
2363
- const ascendantVisualIndex = housesWithAngles.findIndex(h => h.originalIndex === 0);
2364
-
2365
- // Render houses with correct numbering based on visual position
2366
- housesWithAngles.forEach((house, visualIndex) => {
2367
- const houseAngle = house.rotatedAngle;
2368
-
2369
- // Calculate house number based on position relative to Ascendant
2370
- // Counter-clockwise from Ascendant
2371
- const houseNumber = ((visualIndex - ascendantVisualIndex + 12) % 12) + 1;
2372
-
2373
- // Offset text from house line clockwise
2374
- const angle = (houseAngle + 15) % 360; // Place in middle of house segment, apply modulo
2359
+ const angle = midAngle;
2375
2360
 
2376
2361
  // Calculate position for house number
2377
2362
  const point = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.numberRadius, angle);
@@ -2412,7 +2397,7 @@
2412
2397
 
2413
2398
  parentGroup.appendChild(text);
2414
2399
  elements.push(text);
2415
- });
2400
+ }
2416
2401
 
2417
2402
  return elements;
2418
2403
  }
@@ -2765,6 +2750,16 @@
2765
2750
  */
2766
2751
 
2767
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
+
2768
2763
  /**
2769
2764
  * Calculate position for a planet on a circle
2770
2765
  * @param {Object} params - Position parameters
@@ -2849,7 +2844,8 @@
2849
2844
  centerX,
2850
2845
  centerY,
2851
2846
  baseRadius,
2852
- iconSize = 24
2847
+ iconSize = 24,
2848
+ maxIterations = 5
2853
2849
  } = options;
2854
2850
 
2855
2851
  if (!positions || positions.length <= 1) {
@@ -2863,72 +2859,51 @@
2863
2859
 
2864
2860
  console.log(`PlanetPositionCalculator: Adjusting overlaps for ${positions.length} positions`);
2865
2861
 
2866
- // Make a copy to not modify originals
2867
- 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 }));
2868
2864
 
2869
2865
  // The minimum angular distance needed to prevent overlap at base radius
2870
- // Add safety factor to ensure visual separation
2871
- const minAngularDistance = (minDistance / baseRadius) * (180 / Math.PI) * 1.3; // 30% extra spacing
2866
+ // minDistance already includes the desired spacing (iconSize * 1.5)
2867
+ const minAngularDistance = (minDistance / baseRadius) * (180 / Math.PI);
2872
2868
  console.log(`PlanetPositionCalculator: Minimum angular distance: ${minAngularDistance.toFixed(2)}°`);
2873
-
2874
- // Sort positions by longitude for overlap detection
2875
- const sortedPositionIndices = adjustedPositions
2876
- .map((pos, idx) => ({ pos, idx }))
2877
- .sort((a, b) => a.pos.longitude - b.pos.longitude);
2878
-
2879
- const sortedPositions = sortedPositionIndices.map(item => ({
2880
- ...adjustedPositions[item.idx],
2881
- originalIndex: item.idx
2882
- }));
2883
-
2884
- // Find clusters of planets that are too close angularly
2885
- const clusters = this._findOverlappingClusters(sortedPositions, minAngularDistance);
2886
- console.log(`PlanetPositionCalculator: Found ${clusters.length} clusters of overlapping positions`);
2887
- clusters.forEach((cluster, i) => {
2888
- console.log(`PlanetPositionCalculator: Cluster ${i+1} has ${cluster.length} positions`);
2889
- });
2890
-
2891
- // Process each cluster
2892
- clusters.forEach((cluster, clusterIndex) => {
2893
- console.log(`PlanetPositionCalculator: Processing cluster ${clusterIndex+1}`);
2894
-
2895
- if (cluster.length <= 1) {
2896
- // Single planet - just place at exact base radius with no angle change
2897
- const planet = cluster[0];
2898
- console.log(`PlanetPositionCalculator: Single position in cluster, keeping at original longitude ${planet.longitude.toFixed(2)}°`);
2899
- this._setExactPosition(planet, planet.longitude, baseRadius, centerX, centerY, iconSize);
2900
- } else {
2901
- // Handle cluster with multiple planets - distribute by angle
2902
- console.log(`PlanetPositionCalculator: Distributing ${cluster.length} positions in cluster`);
2903
- this._distributeClusterByAngle(cluster, baseRadius, minAngularDistance, centerX, centerY, iconSize);
2904
-
2905
- // Log the distributions
2906
- cluster.forEach((pos, i) => {
2907
- console.log(`PlanetPositionCalculator: Position ${i+1} in cluster ${clusterIndex+1} adjusted from ${pos.longitude.toFixed(2)}° to ${pos.adjustedLongitude.toFixed(2)}°`);
2908
- });
2869
+
2870
+ // Initialize adjustedLongitude for all positions
2871
+ adjustedPositions.forEach(pos => {
2872
+ if (pos.adjustedLongitude === undefined) {
2873
+ pos.adjustedLongitude = pos.longitude;
2909
2874
  }
2875
+ this._setExactPosition(pos, this._getAngle(pos), baseRadius, centerX, centerY, iconSize);
2910
2876
  });
2911
-
2912
- // Copy adjusted positions back to the original array order
2913
- sortedPositions.forEach(pos => {
2914
- const origIndex = pos.originalIndex;
2915
-
2916
- // Only copy if we have valid data
2917
- if (origIndex !== undefined && origIndex >= 0 && origIndex < adjustedPositions.length) {
2918
- adjustedPositions[origIndex].x = pos.x;
2919
- adjustedPositions[origIndex].y = pos.y;
2920
- adjustedPositions[origIndex].iconX = pos.iconX;
2921
- adjustedPositions[origIndex].iconY = pos.iconY;
2922
- adjustedPositions[origIndex].iconCenterX = pos.iconCenterX;
2923
- adjustedPositions[origIndex].iconCenterY = pos.iconCenterY;
2924
-
2925
- // Also add any adjusted longitude for reference
2926
- if (pos.adjustedLongitude !== undefined) {
2927
- adjustedPositions[origIndex].adjustedLongitude = pos.adjustedLongitude;
2928
- }
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;
2929
2893
  }
2930
- });
2931
-
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
+
2932
2907
  return adjustedPositions;
2933
2908
  }
2934
2909
 
@@ -2958,7 +2933,9 @@
2958
2933
  const currPosition = sortedPositions[i];
2959
2934
 
2960
2935
  // Check angular distance, considering wrap-around at 360°
2961
- let angleDiff = currPosition.longitude - prevPosition.longitude;
2936
+ const prevAngle = this._getAngle(prevPosition);
2937
+ const currAngle = this._getAngle(currPosition);
2938
+ let angleDiff = currAngle - prevAngle;
2962
2939
  if (angleDiff < 0) angleDiff += 360;
2963
2940
 
2964
2941
  if (angleDiff < minAngularDistance) {
@@ -2982,7 +2959,9 @@
2982
2959
  const lastPlanet = sortedPositions[posCount - 1];
2983
2960
  const firstPlanetOriginal = sortedPositions[0];
2984
2961
 
2985
- 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;
2986
2965
  if (wrapDiff < 0) wrapDiff += 360;
2987
2966
 
2988
2967
  if (wrapDiff < minAngularDistance) {
@@ -3006,7 +2985,8 @@
3006
2985
  }
3007
2986
 
3008
2987
  /**
3009
- * 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.
3010
2990
  * @private
3011
2991
  * @param {Array} positions - Array of positions in the cluster
3012
2992
  * @param {number} radius - The exact radius to place all positions
@@ -3015,49 +2995,62 @@
3015
2995
  * @param {number} centerY - Y coordinate of center
3016
2996
  * @param {number} iconSize - Size of the icon
3017
2997
  */
3018
- static _distributeClusterByAngle(positions, radius, minAngularDistance, centerX, centerY, iconSize) {
2998
+ static _distributeClusterByPush(positions, radius, minAngularDistance, centerX, centerY, iconSize) {
3019
2999
  const n = positions.length;
3020
-
3021
- // If only one planet, keep its original position
3022
- if (n === 1) {
3023
- 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);
3024
3003
  return;
3025
3004
  }
3026
-
3027
- // Sort positions by their original longitude to maintain order
3028
- positions.sort((a, b) => a.longitude - b.longitude);
3029
-
3030
- // Calculate central angle and total span
3031
- const firstPos = positions[0].longitude;
3032
- const lastPos = positions[n-1].longitude;
3033
- let totalArc = lastPos - firstPos;
3034
-
3035
- // Handle wrap-around case (e.g., positions at 350° and 10°)
3036
- if (totalArc < 0 || totalArc > 180) {
3037
- 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
+ }
3038
3028
  }
3039
-
3040
- // Calculate the center of the cluster (weighted average of all positions)
3041
- let sumAngles = 0;
3042
- for (let i = 0; i < n; i++) {
3043
- 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
+ }
3044
3042
  }
3045
- let centerAngle = (sumAngles / n) % 360;
3046
-
3047
- // Determine minimum arc needed for n planets with minimum spacing
3048
- // Add extra spacing factor to ensure planets don't overlap
3049
- const minRequiredArc = (n - 1) * minAngularDistance * 1.2; // 20% extra spacing
3050
-
3051
- // Always use at least the minimum required arc
3052
- const spanToUse = Math.max(minRequiredArc, totalArc);
3053
-
3054
- // Calculate start angle (center - half of span)
3055
- const startAngle = (centerAngle - spanToUse/2 + 360) % 360;
3056
-
3057
- // 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
3058
3051
  for (let i = 0; i < n; i++) {
3059
- const angle = (startAngle + i * (spanToUse / (n-1))) % 360;
3060
- 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);
3061
3054
  }
3062
3055
  }
3063
3056
 
@@ -3134,7 +3127,8 @@
3134
3127
  // Define parameters for collision detection and distribution
3135
3128
  const iconSize = 24;
3136
3129
  const baseRadius = planets[0].iconRadius; // Use the iconRadius from the first planet
3137
- const minDistance = iconSize * 1.2;
3130
+ // Minimum distance = icon diameter + half diameter (0.5 * iconSize spacing between icons)
3131
+ const minDistance = iconSize * 1.5;
3138
3132
 
3139
3133
  // Prepare planets array in format expected by PlanetPositionCalculator
3140
3134
  const positions = planets.map((planet, index) => ({
@@ -3256,7 +3250,7 @@
3256
3250
  this.symbolRenderer.addPlanetTooltip(planetGroup, planet);
3257
3251
 
3258
3252
  // Render connector if needed
3259
- const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize);
3253
+ const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize, 0.9);
3260
3254
  if (connector) {
3261
3255
  planetGroup.appendChild(connector);
3262
3256
  }
@@ -3316,7 +3310,8 @@
3316
3310
  // Define parameters for collision detection and distribution
3317
3311
  const iconSize = 18; // Smaller size for secondary planets
3318
3312
  const baseRadius = planets[0].iconRadius; // Use the iconRadius from the first planet
3319
- const minDistance = iconSize * 1.1; // Slightly tighter packing
3313
+ // Minimum distance = icon diameter + half diameter (0.5 * iconSize spacing between icons)
3314
+ const minDistance = iconSize * 1.5;
3320
3315
 
3321
3316
  // Prepare planets array in format expected by PlanetPositionCalculator
3322
3317
  const positions = planets.map((planet, index) => ({
@@ -3437,7 +3432,7 @@
3437
3432
  this.symbolRenderer.addPlanetTooltip(planetGroup, planet);
3438
3433
 
3439
3434
  // Render connector if needed
3440
- const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize);
3435
+ const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize, 0.9);
3441
3436
  if (connector) {
3442
3437
  planetGroup.appendChild(connector);
3443
3438
  }
@@ -5476,8 +5471,13 @@
5476
5471
  const centerY = this.chart.config.svg.center.y;
5477
5472
  const c3Radius = this.chart.config.radius.innermost; // C3
5478
5473
 
5479
- // Only draw the innermost circle if secondary planets are enabled
5480
- 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) {
5481
5481
  this.drawInnermostCircle(zodiacGroup, this.svgUtils, centerX, centerY, c3Radius);
5482
5482
  }
5483
5483