chart2txt 0.6.0 → 0.7.0

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.
Files changed (49) hide show
  1. package/README.md +101 -34
  2. package/dist/chart2txt.d.ts +9 -0
  3. package/dist/chart2txt.js +30 -0
  4. package/dist/chart2txt.min.js +1 -1
  5. package/dist/config/ChartSettings.d.ts +12 -6
  6. package/dist/config/ChartSettings.js +36 -11
  7. package/dist/constants.d.ts +17 -2
  8. package/dist/constants.js +301 -34
  9. package/dist/core/analysis.d.ts +6 -0
  10. package/dist/core/analysis.js +235 -0
  11. package/dist/core/aspectPatterns.d.ts +8 -3
  12. package/dist/core/aspectPatterns.js +234 -218
  13. package/dist/core/aspects.d.ts +14 -11
  14. package/dist/core/aspects.js +49 -32
  15. package/dist/core/dignities.d.ts +2 -27
  16. package/dist/core/dignities.js +56 -121
  17. package/dist/core/dispositors.d.ts +6 -19
  18. package/dist/core/dispositors.js +45 -131
  19. package/dist/core/grouping.d.ts +9 -0
  20. package/dist/core/grouping.js +45 -0
  21. package/dist/core/signDistributions.d.ts +20 -30
  22. package/dist/core/signDistributions.js +22 -122
  23. package/dist/core/stelliums.d.ts +10 -0
  24. package/dist/core/stelliums.js +108 -0
  25. package/dist/formatters/text/sections/aspectPatterns.d.ts +3 -1
  26. package/dist/formatters/text/sections/aspectPatterns.js +118 -94
  27. package/dist/formatters/text/sections/aspects.d.ts +3 -6
  28. package/dist/formatters/text/sections/aspects.js +35 -52
  29. package/dist/formatters/text/sections/dispositors.d.ts +4 -3
  30. package/dist/formatters/text/sections/dispositors.js +7 -8
  31. package/dist/formatters/text/sections/houseOverlays.d.ts +11 -6
  32. package/dist/formatters/text/sections/houseOverlays.js +37 -44
  33. package/dist/formatters/text/sections/metadata.d.ts +2 -0
  34. package/dist/formatters/text/sections/metadata.js +54 -0
  35. package/dist/formatters/text/sections/planets.d.ts +3 -5
  36. package/dist/formatters/text/sections/planets.js +11 -22
  37. package/dist/formatters/text/sections/signDistributions.d.ts +9 -25
  38. package/dist/formatters/text/sections/signDistributions.js +9 -55
  39. package/dist/formatters/text/textFormatter.d.ts +4 -5
  40. package/dist/formatters/text/textFormatter.js +81 -141
  41. package/dist/index.d.ts +7 -4
  42. package/dist/index.js +11 -6
  43. package/dist/types.d.ts +100 -15
  44. package/dist/types.js +15 -0
  45. package/dist/utils/formatting.d.ts +4 -0
  46. package/dist/utils/formatting.js +43 -0
  47. package/dist/utils/houseCalculations.d.ts +10 -13
  48. package/dist/utils/houseCalculations.js +15 -57
  49. package/package.json +1 -1
@@ -2,31 +2,70 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.detectAspectPatterns = detectAspectPatterns;
4
4
  const astrology_1 = require("./astrology");
5
- const houseCalculations_1 = require("../utils/houseCalculations");
6
5
  /**
7
- * Helper function to calculate orb between two planets for a specific aspect angle
6
+ * Create a consistent key for planet+chart combinations
8
7
  */
9
- function calculateOrb(planet1, planet2, aspectAngle) {
10
- const degree1 = (0, astrology_1.normalizeDegree)(planet1.degree);
11
- const degree2 = (0, astrology_1.normalizeDegree)(planet2.degree);
12
- let diff = Math.abs(degree1 - degree2);
13
- if (diff > 180)
14
- diff = 360 - diff;
15
- return Math.abs(diff - aspectAngle);
8
+ function createPlanetKey(planetName, chartName) {
9
+ return chartName ? `${planetName}-${chartName}` : planetName;
16
10
  }
17
11
  /**
18
- * Convert Point to PlanetPosition
12
+ * Create a lookup map for aspect relationships between planets
19
13
  */
20
- function pointToPlanetPosition(point, houseCusps) {
14
+ function createAspectLookup(aspects) {
15
+ const lookup = new Map();
16
+ aspects.forEach((aspect) => {
17
+ const keyA = createPlanetKey(aspect.planetA, aspect.p1ChartName);
18
+ const keyB = createPlanetKey(aspect.planetB, aspect.p2ChartName);
19
+ if (!lookup.has(keyA)) {
20
+ lookup.set(keyA, new Map());
21
+ }
22
+ if (!lookup.has(keyB)) {
23
+ lookup.set(keyB, new Map());
24
+ }
25
+ lookup.get(keyA).set(keyB, aspect);
26
+ lookup.get(keyB).set(keyA, aspect);
27
+ });
28
+ return lookup;
29
+ }
30
+ /**
31
+ * Convert Point to PlanetPosition for aspect patterns
32
+ * Note: House information is optional for aspect patterns (except stelliums which are handled separately)
33
+ */
34
+ function pointToPlanetPosition(point, houseCusps, chartName) {
21
35
  const sign = (0, astrology_1.getDegreeSign)(point.degree);
22
- const house = houseCusps
23
- ? (0, houseCalculations_1.getHouseForPoint)(point.degree, houseCusps) || undefined
36
+ // For multi-chart patterns, house information may not be meaningful
37
+ // Only calculate house if houseCusps are provided and we're dealing with a single-chart context
38
+ const house = houseCusps && houseCusps.length === 12
39
+ ? (() => {
40
+ // Simple house calculation without importing the utility
41
+ const normalizedDegree = point.degree % 360;
42
+ for (let i = 0; i < 12; i++) {
43
+ const currentCusp = houseCusps[i];
44
+ const nextCusp = houseCusps[(i + 1) % 12];
45
+ if (nextCusp > currentCusp) {
46
+ // Normal case: house doesn't cross 0°
47
+ if (normalizedDegree >= currentCusp &&
48
+ normalizedDegree < nextCusp) {
49
+ return i + 1;
50
+ }
51
+ }
52
+ else {
53
+ // House crosses 0° (e.g., 350° to 20°)
54
+ if (normalizedDegree >= currentCusp ||
55
+ normalizedDegree < nextCusp) {
56
+ return i + 1;
57
+ }
58
+ }
59
+ }
60
+ return undefined;
61
+ })()
24
62
  : undefined;
25
63
  return {
26
64
  name: point.name,
27
65
  degree: point.degree,
28
66
  sign,
29
67
  house,
68
+ chartName,
30
69
  };
31
70
  }
32
71
  /**
@@ -35,7 +74,6 @@ function pointToPlanetPosition(point, houseCusps) {
35
74
  function getSignModality(sign) {
36
75
  const cardinal = ['Aries', 'Cancer', 'Libra', 'Capricorn'];
37
76
  const fixed = ['Taurus', 'Leo', 'Scorpio', 'Aquarius'];
38
- const mutable = ['Gemini', 'Virgo', 'Sagittarius', 'Pisces'];
39
77
  if (cardinal.includes(sign))
40
78
  return 'Cardinal';
41
79
  if (fixed.includes(sign))
@@ -49,7 +87,6 @@ function getSignElement(sign) {
49
87
  const fire = ['Aries', 'Leo', 'Sagittarius'];
50
88
  const earth = ['Taurus', 'Virgo', 'Capricorn'];
51
89
  const air = ['Gemini', 'Libra', 'Aquarius'];
52
- const water = ['Cancer', 'Scorpio', 'Pisces'];
53
90
  if (fire.includes(sign))
54
91
  return 'Fire';
55
92
  if (earth.includes(sign))
@@ -59,34 +96,54 @@ function getSignElement(sign) {
59
96
  return 'Water';
60
97
  }
61
98
  /**
62
- * Check if two planets form a specific aspect within orb
99
+ * Check if two planets have a specific aspect type using the pre-calculated aspects
63
100
  */
64
- function hasAspect(planet1, planet2, aspectAngle, orb) {
65
- const calculatedOrb = calculateOrb(planet1, planet2, aspectAngle);
66
- return calculatedOrb <= orb;
101
+ function hasSpecificAspect(p1, p2, aspectType, aspectLookup) {
102
+ const key1 = createPlanetKey(p1[0].name, p1[1]);
103
+ const key2 = createPlanetKey(p2[0].name, p2[1]);
104
+ const planet1Aspects = aspectLookup.get(key1);
105
+ if (!planet1Aspects)
106
+ return false;
107
+ const aspectData = planet1Aspects.get(key2);
108
+ return aspectData !== undefined && aspectData.aspectType === aspectType;
109
+ }
110
+ /**
111
+ * Get aspect data between two planets if it exists
112
+ */
113
+ function getAspectBetween(p1, p2, aspectLookup) {
114
+ const key1 = createPlanetKey(p1[0].name, p1[1]);
115
+ const key2 = createPlanetKey(p2[0].name, p2[1]);
116
+ const planet1Aspects = aspectLookup.get(key1);
117
+ if (!planet1Aspects)
118
+ return undefined;
119
+ return planet1Aspects.get(key2);
67
120
  }
68
121
  /**
69
122
  * Detect T-Square patterns
70
123
  */
71
- function detectTSquares(planets, houseCusps, maxOrb = 8) {
124
+ function detectTSquares(unionedPoints, aspectLookup, houseCusps) {
72
125
  const patterns = [];
73
- for (let i = 0; i < planets.length; i++) {
74
- for (let j = i + 1; j < planets.length; j++) {
126
+ for (let i = 0; i < unionedPoints.length; i++) {
127
+ for (let j = i + 1; j < unionedPoints.length; j++) {
75
128
  // Check for opposition
76
- if (hasAspect(planets[i], planets[j], 180, maxOrb)) {
129
+ if (hasSpecificAspect(unionedPoints[i], unionedPoints[j], 'opposition', aspectLookup)) {
77
130
  // Look for a third planet that squares both
78
- for (let k = 0; k < planets.length; k++) {
131
+ for (let k = 0; k < unionedPoints.length; k++) {
79
132
  if (k === i || k === j)
80
133
  continue;
81
- if (hasAspect(planets[i], planets[k], 90, maxOrb) &&
82
- hasAspect(planets[j], planets[k], 90, maxOrb)) {
83
- const apex = pointToPlanetPosition(planets[k], houseCusps);
84
- const opp1 = pointToPlanetPosition(planets[i], houseCusps);
85
- const opp2 = pointToPlanetPosition(planets[j], houseCusps);
86
- const orb1 = calculateOrb(planets[i], planets[j], 180);
87
- const orb2 = calculateOrb(planets[i], planets[k], 90);
88
- const orb3 = calculateOrb(planets[j], planets[k], 90);
89
- const averageOrb = (orb1 + orb2 + orb3) / 3;
134
+ if (hasSpecificAspect(unionedPoints[i], unionedPoints[k], 'square', aspectLookup) &&
135
+ hasSpecificAspect(unionedPoints[j], unionedPoints[k], 'square', aspectLookup)) {
136
+ const [pApex, cApex] = unionedPoints[k];
137
+ const [pOpp1, cOpp1] = unionedPoints[i];
138
+ const [pOpp2, cOpp2] = unionedPoints[j];
139
+ const apex = pointToPlanetPosition(pApex, houseCusps, cApex);
140
+ const opp1 = pointToPlanetPosition(pOpp1, houseCusps, cOpp1);
141
+ const opp2 = pointToPlanetPosition(pOpp2, houseCusps, cOpp2);
142
+ // Get actual orbs from pre-calculated aspects
143
+ const oppAspect = getAspectBetween(unionedPoints[i], unionedPoints[j], aspectLookup);
144
+ const square1Aspect = getAspectBetween(unionedPoints[i], unionedPoints[k], aspectLookup);
145
+ const square2Aspect = getAspectBetween(unionedPoints[j], unionedPoints[k], aspectLookup);
146
+ const averageOrb = (oppAspect.orb + square1Aspect.orb + square2Aspect.orb) / 3;
90
147
  // Determine modality from apex planet
91
148
  const mode = getSignModality(apex.sign);
92
149
  patterns.push({
@@ -106,22 +163,26 @@ function detectTSquares(planets, houseCusps, maxOrb = 8) {
106
163
  /**
107
164
  * Detect Grand Trine patterns
108
165
  */
109
- function detectGrandTrines(planets, houseCusps, maxOrb = 8) {
166
+ function detectGrandTrines(unionedPoints, aspectLookup, houseCusps) {
110
167
  const patterns = [];
111
- for (let i = 0; i < planets.length; i++) {
112
- for (let j = i + 1; j < planets.length; j++) {
113
- for (let k = j + 1; k < planets.length; k++) {
168
+ for (let i = 0; i < unionedPoints.length; i++) {
169
+ for (let j = i + 1; j < unionedPoints.length; j++) {
170
+ for (let k = j + 1; k < unionedPoints.length; k++) {
114
171
  // Check if all three planets form trines with each other
115
- if (hasAspect(planets[i], planets[j], 120, maxOrb) &&
116
- hasAspect(planets[j], planets[k], 120, maxOrb) &&
117
- hasAspect(planets[k], planets[i], 120, maxOrb)) {
118
- const planet1 = pointToPlanetPosition(planets[i], houseCusps);
119
- const planet2 = pointToPlanetPosition(planets[j], houseCusps);
120
- const planet3 = pointToPlanetPosition(planets[k], houseCusps);
121
- const orb1 = calculateOrb(planets[i], planets[j], 120);
122
- const orb2 = calculateOrb(planets[j], planets[k], 120);
123
- const orb3 = calculateOrb(planets[k], planets[i], 120);
124
- const averageOrb = (orb1 + orb2 + orb3) / 3;
172
+ if (hasSpecificAspect(unionedPoints[i], unionedPoints[j], 'trine', aspectLookup) &&
173
+ hasSpecificAspect(unionedPoints[j], unionedPoints[k], 'trine', aspectLookup) &&
174
+ hasSpecificAspect(unionedPoints[k], unionedPoints[i], 'trine', aspectLookup)) {
175
+ const [p1, c1] = unionedPoints[i];
176
+ const [p2, c2] = unionedPoints[j];
177
+ const [p3, c3] = unionedPoints[k];
178
+ const planet1 = pointToPlanetPosition(p1, houseCusps, c1);
179
+ const planet2 = pointToPlanetPosition(p2, houseCusps, c2);
180
+ const planet3 = pointToPlanetPosition(p3, houseCusps, c3);
181
+ // Get actual orbs from pre-calculated aspects
182
+ const trine1Aspect = getAspectBetween(unionedPoints[i], unionedPoints[j], aspectLookup);
183
+ const trine2Aspect = getAspectBetween(unionedPoints[j], unionedPoints[k], aspectLookup);
184
+ const trine3Aspect = getAspectBetween(unionedPoints[k], unionedPoints[i], aspectLookup);
185
+ const averageOrb = (trine1Aspect.orb + trine2Aspect.orb + trine3Aspect.orb) / 3;
125
186
  // Determine element from the planets (should be same element for proper grand trine)
126
187
  const element = getSignElement(planet1.sign);
127
188
  patterns.push({
@@ -136,121 +197,60 @@ function detectGrandTrines(planets, houseCusps, maxOrb = 8) {
136
197
  }
137
198
  return patterns;
138
199
  }
139
- /**
140
- * Detect Stellium patterns (3+ planets in same sign or adjacent houses)
141
- */
142
- function detectStelliums(planets, houseCusps, minPlanets = 3) {
143
- const patterns = [];
144
- // Group by sign
145
- const signGroups = new Map();
146
- planets.forEach((planet) => {
147
- const sign = (0, astrology_1.getDegreeSign)(planet.degree);
148
- if (!signGroups.has(sign)) {
149
- signGroups.set(sign, []);
150
- }
151
- signGroups.get(sign).push(planet);
152
- });
153
- // Check sign-based stelliums
154
- signGroups.forEach((planetsInSign, sign) => {
155
- if (planetsInSign.length >= minPlanets) {
156
- const planetPositions = planetsInSign.map((p) => pointToPlanetPosition(p, houseCusps));
157
- const houses = planetPositions
158
- .map((p) => p.house)
159
- .filter((h) => h !== undefined);
160
- const degrees = planetsInSign.map((p) => p.degree);
161
- const span = Math.max(...degrees) - Math.min(...degrees);
162
- patterns.push({
163
- type: 'Stellium',
164
- planets: planetPositions,
165
- sign,
166
- houses: [...new Set(houses)].sort(),
167
- span,
168
- });
169
- }
170
- });
171
- // Check house-based stelliums (if house cusps available)
172
- if (houseCusps) {
173
- const houseGroups = new Map();
174
- planets.forEach((planet) => {
175
- const house = (0, houseCalculations_1.getHouseForPoint)(planet.degree, houseCusps);
176
- if (house) {
177
- if (!houseGroups.has(house)) {
178
- houseGroups.set(house, []);
179
- }
180
- houseGroups.get(house).push(planet);
181
- }
182
- });
183
- houseGroups.forEach((planetsInHouse, house) => {
184
- if (planetsInHouse.length >= minPlanets) {
185
- const planetPositions = planetsInHouse.map((p) => pointToPlanetPosition(p, houseCusps));
186
- const degrees = planetsInHouse.map((p) => p.degree);
187
- const span = Math.max(...degrees) - Math.min(...degrees);
188
- // Only add if not already covered by sign stellium
189
- const existingSignStellium = patterns.find((p) => p.type === 'Stellium' &&
190
- p.planets.some((planet) => planetPositions.some((pp) => pp.name === planet.name)));
191
- if (!existingSignStellium) {
192
- patterns.push({
193
- type: 'Stellium',
194
- planets: planetPositions,
195
- houses: [house],
196
- span,
197
- });
198
- }
199
- }
200
- });
201
- }
202
- return patterns;
203
- }
204
200
  /**
205
201
  * Detect Grand Cross patterns
206
202
  */
207
- function detectGrandCrosses(planets, houseCusps, maxOrb = 8) {
203
+ function detectGrandCrosses(unionedPoints, aspectLookup, houseCusps) {
208
204
  const patterns = [];
209
- for (let i = 0; i < planets.length; i++) {
210
- for (let j = i + 1; j < planets.length; j++) {
211
- for (let k = j + 1; k < planets.length; k++) {
212
- for (let l = k + 1; l < planets.length; l++) {
205
+ for (let i = 0; i < unionedPoints.length; i++) {
206
+ for (let j = i + 1; j < unionedPoints.length; j++) {
207
+ for (let k = j + 1; k < unionedPoints.length; k++) {
208
+ for (let l = k + 1; l < unionedPoints.length; l++) {
209
+ const group = [
210
+ unionedPoints[i],
211
+ unionedPoints[j],
212
+ unionedPoints[k],
213
+ unionedPoints[l],
214
+ ];
213
215
  // Check if planets form two oppositions and four squares
214
216
  const pairs = [
215
- [i, j],
216
- [k, l],
217
+ [0, 1],
218
+ [2, 3],
217
219
  ];
218
220
  const otherPairs = [
219
- [i, k],
220
- [j, l],
221
- [i, l],
222
- [j, k],
221
+ [0, 2],
222
+ [1, 3],
223
+ [0, 3],
224
+ [1, 2],
223
225
  ];
224
226
  // Check for two oppositions
225
227
  let oppositions = 0;
226
228
  let squares = 0;
229
+ const aspectData = [];
227
230
  pairs.forEach(([a, b]) => {
228
- if (hasAspect(planets[a], planets[b], 180, maxOrb)) {
231
+ if (hasSpecificAspect(group[a], group[b], 'opposition', aspectLookup)) {
229
232
  oppositions++;
233
+ aspectData.push(getAspectBetween(group[a], group[b], aspectLookup));
230
234
  }
231
235
  });
232
236
  otherPairs.forEach(([a, b]) => {
233
- if (hasAspect(planets[a], planets[b], 90, maxOrb)) {
237
+ if (hasSpecificAspect(group[a], group[b], 'square', aspectLookup)) {
234
238
  squares++;
239
+ aspectData.push(getAspectBetween(group[a], group[b], aspectLookup));
235
240
  }
236
241
  });
237
242
  if (oppositions === 2 && squares === 4) {
238
- const planet1 = pointToPlanetPosition(planets[i], houseCusps);
239
- const planet2 = pointToPlanetPosition(planets[j], houseCusps);
240
- const planet3 = pointToPlanetPosition(planets[k], houseCusps);
241
- const planet4 = pointToPlanetPosition(planets[l], houseCusps);
242
- // Calculate average orb
243
- let totalOrb = 0;
244
- let aspectCount = 0;
245
- pairs.forEach(([a, b]) => {
246
- totalOrb += calculateOrb(planets[a], planets[b], 180);
247
- aspectCount++;
248
- });
249
- otherPairs.forEach(([a, b]) => {
250
- totalOrb += calculateOrb(planets[a], planets[b], 90);
251
- aspectCount++;
252
- });
253
- const averageOrb = totalOrb / aspectCount;
243
+ const [p1, c1] = group[0];
244
+ const [p2, c2] = group[1];
245
+ const [p3, c3] = group[2];
246
+ const [p4, c4] = group[3];
247
+ const planet1 = pointToPlanetPosition(p1, houseCusps, c1);
248
+ const planet2 = pointToPlanetPosition(p2, houseCusps, c2);
249
+ const planet3 = pointToPlanetPosition(p3, houseCusps, c3);
250
+ const planet4 = pointToPlanetPosition(p4, houseCusps, c4);
251
+ // Calculate average orb from actual aspect data
252
+ const totalOrb = aspectData.reduce((sum, aspect) => sum + aspect.orb, 0);
253
+ const averageOrb = totalOrb / aspectData.length;
254
254
  const mode = getSignModality(planet1.sign); // Determine from first planet
255
255
  patterns.push({
256
256
  type: 'Grand Cross',
@@ -268,25 +268,30 @@ function detectGrandCrosses(planets, houseCusps, maxOrb = 8) {
268
268
  /**
269
269
  * Detect Yod patterns (two quincunxes to apex planet and one sextile between base planets)
270
270
  */
271
- function detectYods(planets, houseCusps, maxOrb = 5) {
271
+ function detectYods(unionedPoints, aspectLookup, houseCusps) {
272
272
  const patterns = [];
273
- for (let i = 0; i < planets.length; i++) {
274
- for (let j = i + 1; j < planets.length; j++) {
273
+ for (let i = 0; i < unionedPoints.length; i++) {
274
+ for (let j = i + 1; j < unionedPoints.length; j++) {
275
275
  // Check for sextile between base planets
276
- if (hasAspect(planets[i], planets[j], 60, maxOrb)) {
276
+ if (hasSpecificAspect(unionedPoints[i], unionedPoints[j], 'sextile', aspectLookup)) {
277
277
  // Look for apex planet that forms quincunxes with both
278
- for (let k = 0; k < planets.length; k++) {
278
+ for (let k = 0; k < unionedPoints.length; k++) {
279
279
  if (k === i || k === j)
280
280
  continue;
281
- if (hasAspect(planets[i], planets[k], 150, maxOrb) &&
282
- hasAspect(planets[j], planets[k], 150, maxOrb)) {
283
- const apex = pointToPlanetPosition(planets[k], houseCusps);
284
- const base1 = pointToPlanetPosition(planets[i], houseCusps);
285
- const base2 = pointToPlanetPosition(planets[j], houseCusps);
286
- const orb1 = calculateOrb(planets[i], planets[j], 60);
287
- const orb2 = calculateOrb(planets[i], planets[k], 150);
288
- const orb3 = calculateOrb(planets[j], planets[k], 150);
289
- const averageOrb = (orb1 + orb2 + orb3) / 3;
281
+ if (hasSpecificAspect(unionedPoints[i], unionedPoints[k], 'quincunx', aspectLookup) &&
282
+ hasSpecificAspect(unionedPoints[j], unionedPoints[k], 'quincunx', aspectLookup)) {
283
+ const [pApex, cApex] = unionedPoints[k];
284
+ const [pBase1, cBase1] = unionedPoints[i];
285
+ const [pBase2, cBase2] = unionedPoints[j];
286
+ const apex = pointToPlanetPosition(pApex, houseCusps, cApex);
287
+ const base1 = pointToPlanetPosition(pBase1, houseCusps, cBase1);
288
+ const base2 = pointToPlanetPosition(pBase2, houseCusps, cBase2);
289
+ // Get actual orbs from pre-calculated aspects
290
+ const sextileAspect = getAspectBetween(unionedPoints[i], unionedPoints[j], aspectLookup);
291
+ const quincunx1Aspect = getAspectBetween(unionedPoints[i], unionedPoints[k], aspectLookup);
292
+ const quincunx2Aspect = getAspectBetween(unionedPoints[j], unionedPoints[k], aspectLookup);
293
+ const averageOrb = (sextileAspect.orb + quincunx1Aspect.orb + quincunx2Aspect.orb) /
294
+ 3;
290
295
  patterns.push({
291
296
  type: 'Yod',
292
297
  apex,
@@ -303,84 +308,88 @@ function detectYods(planets, houseCusps, maxOrb = 5) {
303
308
  /**
304
309
  * Detect Mystic Rectangle patterns (two oppositions with sextiles and trines)
305
310
  */
306
- function detectMysticRectangles(planets, houseCusps, maxOrb = 8) {
311
+ function detectMysticRectangles(unionedPoints, aspectLookup, houseCusps) {
307
312
  const patterns = [];
308
- for (let i = 0; i < planets.length; i++) {
309
- for (let j = i + 1; j < planets.length; j++) {
310
- for (let k = j + 1; k < planets.length; k++) {
311
- for (let l = k + 1; l < planets.length; l++) {
313
+ for (let i = 0; i < unionedPoints.length; i++) {
314
+ for (let j = i + 1; j < unionedPoints.length; j++) {
315
+ for (let k = j + 1; k < unionedPoints.length; k++) {
316
+ for (let l = k + 1; l < unionedPoints.length; l++) {
317
+ const group = [
318
+ unionedPoints[i],
319
+ unionedPoints[j],
320
+ unionedPoints[k],
321
+ unionedPoints[l],
322
+ ];
312
323
  // Check for two oppositions and appropriate sextiles/trines
313
324
  const combinations = [
314
325
  {
315
326
  oppositions: [
316
- [i, j],
317
- [k, l],
327
+ [0, 1],
328
+ [2, 3],
318
329
  ],
319
330
  sextiles: [
320
- [i, k],
321
- [i, l],
322
- [j, k],
323
- [j, l],
331
+ [0, 2],
332
+ [0, 3],
333
+ [1, 2],
334
+ [1, 3],
324
335
  ],
325
336
  },
326
337
  {
327
338
  oppositions: [
328
- [i, k],
329
- [j, l],
339
+ [0, 2],
340
+ [1, 3],
330
341
  ],
331
342
  sextiles: [
332
- [i, j],
333
- [i, l],
334
- [k, j],
335
- [k, l],
343
+ [0, 1],
344
+ [0, 3],
345
+ [2, 1],
346
+ [2, 3],
336
347
  ],
337
348
  },
338
349
  {
339
350
  oppositions: [
340
- [i, l],
341
- [j, k],
351
+ [0, 3],
352
+ [1, 2],
342
353
  ],
343
354
  sextiles: [
344
- [i, j],
345
- [i, k],
346
- [l, j],
347
- [l, k],
355
+ [0, 1],
356
+ [0, 2],
357
+ [3, 1],
358
+ [3, 2],
348
359
  ],
349
360
  },
350
361
  ];
351
362
  for (const combo of combinations) {
352
363
  let validOppositions = 0;
353
364
  let validSextiles = 0;
365
+ const aspectData = [];
354
366
  combo.oppositions.forEach(([a, b]) => {
355
- if (hasAspect(planets[a], planets[b], 180, maxOrb)) {
367
+ if (hasSpecificAspect(group[a], group[b], 'opposition', aspectLookup)) {
356
368
  validOppositions++;
369
+ aspectData.push(getAspectBetween(group[a], group[b], aspectLookup));
357
370
  }
358
371
  });
359
372
  combo.sextiles.forEach(([a, b]) => {
360
- if (hasAspect(planets[a], planets[b], 60, maxOrb) ||
361
- hasAspect(planets[a], planets[b], 120, maxOrb)) {
373
+ const sextileAspect = getAspectBetween(group[a], group[b], aspectLookup);
374
+ if (sextileAspect &&
375
+ (sextileAspect.aspectType === 'sextile' ||
376
+ sextileAspect.aspectType === 'trine')) {
362
377
  validSextiles++;
378
+ aspectData.push(sextileAspect);
363
379
  }
364
380
  });
365
381
  if (validOppositions === 2 && validSextiles === 4) {
366
- const pos1 = pointToPlanetPosition(planets[combo.oppositions[0][0]], houseCusps);
367
- const pos2 = pointToPlanetPosition(planets[combo.oppositions[0][1]], houseCusps);
368
- const pos3 = pointToPlanetPosition(planets[combo.oppositions[1][0]], houseCusps);
369
- const pos4 = pointToPlanetPosition(planets[combo.oppositions[1][1]], houseCusps);
370
- // Calculate average orb
371
- let totalOrb = 0;
372
- let aspectCount = 0;
373
- combo.oppositions.forEach(([a, b]) => {
374
- totalOrb += calculateOrb(planets[a], planets[b], 180);
375
- aspectCount++;
376
- });
377
- combo.sextiles.forEach(([a, b]) => {
378
- const sextileOrb = calculateOrb(planets[a], planets[b], 60);
379
- const trineOrb = calculateOrb(planets[a], planets[b], 120);
380
- totalOrb += Math.min(sextileOrb, trineOrb);
381
- aspectCount++;
382
- });
383
- const averageOrb = totalOrb / aspectCount;
382
+ const [p1, c1] = group[combo.oppositions[0][0]];
383
+ const [p2, c2] = group[combo.oppositions[0][1]];
384
+ const [p3, c3] = group[combo.oppositions[1][0]];
385
+ const [p4, c4] = group[combo.oppositions[1][1]];
386
+ const pos1 = pointToPlanetPosition(p1, houseCusps, c1);
387
+ const pos2 = pointToPlanetPosition(p2, houseCusps, c2);
388
+ const pos3 = pointToPlanetPosition(p3, houseCusps, c3);
389
+ const pos4 = pointToPlanetPosition(p4, houseCusps, c4);
390
+ // Calculate average orb from actual aspect data
391
+ const totalOrb = aspectData.reduce((sum, aspect) => sum + aspect.orb, 0);
392
+ const averageOrb = totalOrb / aspectData.length;
384
393
  patterns.push({
385
394
  type: 'Mystic Rectangle',
386
395
  oppositions: [
@@ -400,21 +409,23 @@ function detectMysticRectangles(planets, houseCusps, maxOrb = 8) {
400
409
  /**
401
410
  * Detect Kite patterns (Grand Trine with one opposition)
402
411
  */
403
- function detectKites(planets, houseCusps, maxOrb = 8) {
412
+ function detectKites(unionedPoints, aspectLookup, houseCusps) {
404
413
  const patterns = [];
405
- const grandTrines = detectGrandTrines(planets, houseCusps, maxOrb);
414
+ const grandTrines = detectGrandTrines(unionedPoints, aspectLookup, houseCusps);
406
415
  grandTrines.forEach((grandTrine) => {
407
416
  // For each planet in the grand trine, look for opposition to another planet
408
417
  grandTrine.planets.forEach((trinePoint) => {
409
- planets.forEach((planet) => {
410
- const isPartOfTrine = grandTrine.planets.some((tp) => tp.name === planet.name);
418
+ unionedPoints.forEach((unionedPoint) => {
419
+ const [planet, chartName] = unionedPoint;
420
+ const isPartOfTrine = grandTrine.planets.some((tp) => tp.name === planet.name && tp.chartName === chartName);
411
421
  if (!isPartOfTrine) {
412
- const trinePointOriginal = planets.find((p) => p.name === trinePoint.name);
413
- if (trinePointOriginal &&
414
- hasAspect(trinePointOriginal, planet, 180, maxOrb)) {
415
- const oppositionPlanet = pointToPlanetPosition(planet, houseCusps);
416
- const orbToOpposition = calculateOrb(trinePointOriginal, planet, 180);
417
- const averageOrb = (grandTrine.averageOrb + orbToOpposition) / 2;
422
+ // Find the original UnionedPoint for this trine point
423
+ const trineUnionedPoint = unionedPoints.find(([p, c]) => p.name === trinePoint.name && c === trinePoint.chartName);
424
+ if (trineUnionedPoint &&
425
+ hasSpecificAspect(trineUnionedPoint, unionedPoint, 'opposition', aspectLookup)) {
426
+ const oppositionPlanet = pointToPlanetPosition(planet, houseCusps, chartName);
427
+ const oppositionAspect = getAspectBetween(trineUnionedPoint, unionedPoint, aspectLookup);
428
+ const averageOrb = (grandTrine.averageOrb + oppositionAspect.orb) / 2;
418
429
  patterns.push({
419
430
  type: 'Kite',
420
431
  grandTrine: grandTrine.planets,
@@ -429,16 +440,21 @@ function detectKites(planets, houseCusps, maxOrb = 8) {
429
440
  return patterns;
430
441
  }
431
442
  /**
432
- * Main function to detect all aspect patterns
443
+ * Main function to detect aspect patterns (excluding stelliums which are handled separately)
444
+ * This function works with both single-chart and multi-chart scenarios
445
+ * @param planets Array of planets to analyze
446
+ * @param aspects Pre-calculated aspects between planets
447
+ * @param houseCusps Optional house cusps for single-chart reference
448
+ * @param planetChartMap Optional mapping from planet name to chart name for multichart ownership context
433
449
  */
434
- function detectAspectPatterns(planets, houseCusps) {
450
+ function detectAspectPatterns(unionedPoints, aspects, houseCusps) {
435
451
  const patterns = [];
436
- patterns.push(...detectTSquares(planets, houseCusps));
437
- patterns.push(...detectGrandTrines(planets, houseCusps));
438
- patterns.push(...detectStelliums(planets, houseCusps));
439
- patterns.push(...detectGrandCrosses(planets, houseCusps));
440
- patterns.push(...detectYods(planets, houseCusps));
441
- patterns.push(...detectMysticRectangles(planets, houseCusps));
442
- patterns.push(...detectKites(planets, houseCusps));
452
+ const aspectLookup = createAspectLookup(aspects);
453
+ patterns.push(...detectTSquares(unionedPoints, aspectLookup, houseCusps));
454
+ patterns.push(...detectGrandTrines(unionedPoints, aspectLookup, houseCusps));
455
+ patterns.push(...detectGrandCrosses(unionedPoints, aspectLookup, houseCusps));
456
+ patterns.push(...detectYods(unionedPoints, aspectLookup, houseCusps));
457
+ patterns.push(...detectMysticRectangles(unionedPoints, aspectLookup, houseCusps));
458
+ patterns.push(...detectKites(unionedPoints, aspectLookup, houseCusps));
443
459
  return patterns;
444
460
  }