|
16 | 16 | from typing import Any, Iterable, Mapping, Optional, Sequence |
17 | 17 |
|
18 | 18 |
|
19 | | -ALGORITHM_VERSION = "galaxy-v12-responsive-compact-orbits" |
| 19 | +ALGORITHM_VERSION = "galaxy-v13-responsive-compact-orbits" |
20 | 20 | PUBLIC_REFERENCE_ID_LIMIT = 200 |
21 | 21 | PUBLIC_FACET_LIMIT = 100 |
22 | 22 | PUBLIC_REPO_NAME_LIMIT = 100 |
@@ -707,47 +707,155 @@ def _community_positions( |
707 | 707 | # survive the overview cap. Rank-based assignment (rank/N) fails when only the top-K |
708 | 708 | # by mass are shown — they occupy a tight arc instead of spreading evenly. |
709 | 709 | GOLDEN_ANGLE_RAD = math.pi * (3.0 - math.sqrt(5.0)) |
710 | | - orbital_rank = 0 |
711 | | - for community in ordered: |
712 | | - community_id = str(community["id"]) |
713 | | - system_radius = _clamp( |
714 | | - _finite_float(community.get("radius"), 36.0), 36.0, 10_000.0 |
715 | | - ) |
716 | | - if community_id == global_community_id: |
| 710 | + non_global = [c for c in ordered if str(c["id"]) != global_community_id] |
| 711 | + non_global_count = len(non_global) |
| 712 | + |
| 713 | + if non_global_count <= 1: |
| 714 | + orbital_rank = 0 |
| 715 | + for community in ordered: |
| 716 | + community_id = str(community["id"]) |
| 717 | + system_radius = _clamp( |
| 718 | + _finite_float(community.get("radius"), 36.0), 36.0, 10_000.0 |
| 719 | + ) |
| 720 | + if community_id == global_community_id: |
| 721 | + specs.append({ |
| 722 | + "id": community_id, "system_radius": system_radius, |
| 723 | + "arm": -1, "nominal_x": 0.0, "nominal_y": 0.0, |
| 724 | + }) |
| 725 | + continue |
| 726 | + arm = orbital_rank % arm_count if arm_count > 0 else 0 |
| 727 | + digest = hashlib.sha256( |
| 728 | + f"{ALGORITHM_VERSION}:{layout_seed}:system:{community_id}".encode("utf-8") |
| 729 | + ).digest() |
| 730 | + # Small angular jitter for visual variety; kept tight so even spacing dominates. |
| 731 | + angular_jitter = ( |
| 732 | + int.from_bytes(digest[:4], "big") / float(1 << 32) - 0.5 |
| 733 | + ) * 0.06 |
| 734 | + radial_jitter = 0.95 + ( |
| 735 | + int.from_bytes(digest[4:8], "big") / float(1 << 32) |
| 736 | + ) * 0.10 |
| 737 | + # Golden-angle based placement: each successive system advances by ≈137.5°. |
| 738 | + # This guarantees that any contiguous or sampled subset fills the circle evenly. |
| 739 | + golden_angle = base_phase + orbital_rank * GOLDEN_ANGLE_RAD |
| 740 | + angle = golden_angle + angular_jitter |
| 741 | + # Ring radius clears the core envelope. Inter-system clearance is handled |
| 742 | + # per-pair in the collision pass using actual radii, not a pessimistic global max. |
| 743 | + baseline_radius = max( |
| 744 | + core_clearance_radius, |
| 745 | + spacing * 0.90 * radial_jitter, |
| 746 | + ) |
717 | 747 | specs.append({ |
718 | | - "id": community_id, "system_radius": system_radius, |
719 | | - "arm": -1, "nominal_x": 0.0, "nominal_y": 0.0, |
| 748 | + "id": community_id, |
| 749 | + "system_radius": system_radius, |
| 750 | + "arm": arm, |
| 751 | + "nominal_x": baseline_radius * math.cos(angle), |
| 752 | + "nominal_y": baseline_radius * math.sin(angle), |
720 | 753 | }) |
721 | | - continue |
722 | | - arm = orbital_rank % arm_count if arm_count > 0 else 0 |
723 | | - digest = hashlib.sha256( |
724 | | - f"{ALGORITHM_VERSION}:{layout_seed}:system:{community_id}".encode("utf-8") |
725 | | - ).digest() |
726 | | - # Small angular jitter for visual variety; kept tight so even spacing dominates. |
727 | | - angular_jitter = ( |
728 | | - int.from_bytes(digest[:4], "big") / float(1 << 32) - 0.5 |
729 | | - ) * 0.06 |
730 | | - radial_jitter = 0.95 + ( |
731 | | - int.from_bytes(digest[4:8], "big") / float(1 << 32) |
732 | | - ) * 0.10 |
733 | | - # Golden-angle based placement: each successive system advances by ≈137.5°. |
734 | | - # This guarantees that any contiguous or sampled subset fills the circle evenly. |
735 | | - golden_angle = base_phase + orbital_rank * GOLDEN_ANGLE_RAD |
736 | | - angle = golden_angle + angular_jitter |
737 | | - # Ring radius clears the core envelope. Inter-system clearance is handled |
738 | | - # per-pair in the collision pass using actual radii, not a pessimistic global max. |
739 | | - baseline_radius = max( |
740 | | - core_clearance_radius, |
741 | | - spacing * 1.10 * radial_jitter, |
| 754 | + orbital_rank += 1 |
| 755 | + else: |
| 756 | + # Multi-tiered concentric orbital lanes: distribute communities across radial bands |
| 757 | + # (inner, mid-inner, mid-outer, outer) filling the 2D disk from the core clearance radius |
| 758 | + # outward. Each tier accommodates as many systems as geometrically fit without overlap |
| 759 | + # before placing subsequent systems on the next radial tier, interleaved with golden-angle |
| 760 | + # angular offsets. This prevents all star systems from colliding onto a single outer hoop. |
| 761 | + for community in ordered: |
| 762 | + if str(community["id"]) == global_community_id: |
| 763 | + system_radius = _clamp( |
| 764 | + _finite_float(community.get("radius"), 36.0), 36.0, 10_000.0 |
| 765 | + ) |
| 766 | + specs.append({ |
| 767 | + "id": str(community["id"]), "system_radius": system_radius, |
| 768 | + "arm": -1, "nominal_x": 0.0, "nominal_y": 0.0, |
| 769 | + }) |
| 770 | + break |
| 771 | + |
| 772 | + avg_sys_radius = sum( |
| 773 | + _clamp(_finite_float(c.get("radius"), 36.0), 36.0, 10_000.0) |
| 774 | + for c in non_global |
| 775 | + ) / non_global_count |
| 776 | + tier_step = max(spacing * 0.65, 2.0 * avg_sys_radius + GALAXY_SYSTEM_MIN_GAP * 0.35) |
| 777 | + |
| 778 | + tiers: list[dict[str, float | int]] = [] |
| 779 | + curr_radius = core_clearance_radius + avg_sys_radius |
| 780 | + remaining = non_global_count |
| 781 | + while remaining > 0: |
| 782 | + circ = 2.0 * math.pi * curr_radius |
| 783 | + envelope_size = 2.0 * avg_sys_radius + GALAXY_SYSTEM_MIN_GAP * 0.35 |
| 784 | + capacity = max(2, int(circ / envelope_size)) |
| 785 | + take = min(capacity, remaining) |
| 786 | + tiers.append({ |
| 787 | + "radius": curr_radius, |
| 788 | + "count": take, |
| 789 | + }) |
| 790 | + remaining -= take |
| 791 | + curr_radius += tier_step |
| 792 | + |
| 793 | + # Homogeneous systems can share a true tier-local lattice: using the actual slot |
| 794 | + # count keeps every lane's angular gaps consistent and avoids radial fallback when |
| 795 | + # several similarly sized systems sit close to the core. Heterogeneous envelopes |
| 796 | + # retain the scene-wide low-discrepancy carrier so a large system does not create a |
| 797 | + # regular angular wall for the smaller systems. A three-or-more-tier scene is also |
| 798 | + # compact enough that local lane spacing is the safer choice even with modest size |
| 799 | + # variation. |
| 800 | + non_global_radii = [ |
| 801 | + _clamp(_finite_float(c.get("radius"), 36.0), 36.0, 10_000.0) |
| 802 | + for c in non_global |
| 803 | + ] |
| 804 | + homogeneous_envelopes = ( |
| 805 | + max(non_global_radii, default=0.0) - min(non_global_radii, default=0.0) |
| 806 | + <= max(2.0, avg_sys_radius * 0.08) |
742 | 807 | ) |
743 | | - specs.append({ |
744 | | - "id": community_id, |
745 | | - "system_radius": system_radius, |
746 | | - "arm": arm, |
747 | | - "nominal_x": baseline_radius * math.cos(angle), |
748 | | - "nominal_y": baseline_radius * math.sin(angle), |
749 | | - }) |
750 | | - orbital_rank += 1 |
| 808 | + use_tier_local_slots = len(tiers) >= 3 or homogeneous_envelopes |
| 809 | + |
| 810 | + sys_idx = 0 |
| 811 | + for tier_index, tier_info in enumerate(tiers): |
| 812 | + t_rad = float(tier_info["radius"]) |
| 813 | + t_count = int(tier_info["count"]) |
| 814 | + for tier_slot in range(t_count): |
| 815 | + community = non_global[sys_idx] |
| 816 | + community_id = str(community["id"]) |
| 817 | + system_radius = _clamp( |
| 818 | + _finite_float(community.get("radius"), 36.0), 36.0, 10_000.0 |
| 819 | + ) |
| 820 | + arm = sys_idx % arm_count if arm_count > 0 else 0 |
| 821 | + digest = hashlib.sha256( |
| 822 | + f"{ALGORITHM_VERSION}:{layout_seed}:system:{community_id}".encode("utf-8") |
| 823 | + ).digest() |
| 824 | + angular_jitter = ( |
| 825 | + int.from_bytes(digest[:4], "big") / float(1 << 32) - 0.5 |
| 826 | + ) * 0.06 |
| 827 | + radial_jitter = 0.96 + ( |
| 828 | + int.from_bytes(digest[4:8], "big") / float(1 << 32) |
| 829 | + ) * 0.08 |
| 830 | + if use_tier_local_slots: |
| 831 | + tier_phase = base_phase + tier_index * math.tau / 12.0 |
| 832 | + # Use the tier's actual slot count rather than continuing the previous |
| 833 | + # tier's golden-angle rank; the deterministic phase keeps the lanes |
| 834 | + # visually distinct while the exact local lattice prevents radial |
| 835 | + # fallback from an ID-dependent jitter collapse. |
| 836 | + angle = ( |
| 837 | + tier_phase |
| 838 | + + tier_slot * math.tau / max(1, t_count) |
| 839 | + ) |
| 840 | + else: |
| 841 | + # Mixed-size two-tier scenes keep a scene-wide low-discrepancy carrier so |
| 842 | + # the angular distribution remains stable across unequal envelopes. |
| 843 | + angle = base_phase + sys_idx * GOLDEN_ANGLE_RAD + angular_jitter |
| 844 | + |
| 845 | + # ``preferred_targets`` applies the user-facing compactness scale below. |
| 846 | + # Tier radii are already physical lane coordinates, so compensate here or a |
| 847 | + # compactness of 0.192 would shrink every tier back through the core floor and |
| 848 | + # make the collision walk, rather than the tier plan, choose the lanes. |
| 849 | + physical_tier_radius = max(core_clearance_radius, t_rad * radial_jitter) |
| 850 | + nominal_r = physical_tier_radius / max(clean_radius_scale, 1e-9) |
| 851 | + specs.append({ |
| 852 | + "id": community_id, |
| 853 | + "system_radius": system_radius, |
| 854 | + "arm": arm, |
| 855 | + "nominal_x": nominal_r * math.cos(angle), |
| 856 | + "nominal_y": nominal_r * math.sin(angle), |
| 857 | + }) |
| 858 | + sys_idx += 1 |
751 | 859 |
|
752 | 860 | def pack_with_radial_clearance( |
753 | 861 | targets: Mapping[str, tuple[float, float]], |
@@ -803,7 +911,7 @@ def collides(x: float, y: float, system_radius: float) -> bool: |
803 | 911 | # The radius_scale compactness pass may shrink preferred targets inside |
804 | 912 | # the core; clamp the walk's starting radius to the clearance floor so |
805 | 913 | # the collision search never considers orbits inside the black hole. |
806 | | - minimum_orbital_radius = core_outer_extent + GALAXY_SYSTEM_MIN_GAP |
| 914 | + minimum_orbital_radius = core_outer_extent + system_radius + GALAXY_SYSTEM_MIN_GAP |
807 | 915 | axis_radius = max(axis_radius, minimum_orbital_radius) |
808 | 916 | # Radial-only walk preserves the even angular distribution. Moving only |
809 | 917 | # the system centre outward (not angularly) keeps every local star/planet |
@@ -2341,7 +2449,7 @@ def _build_complete_scene( |
2341 | 2449 | str(all_nodes[global_anchor]["community_id"]) if global_anchor else "" |
2342 | 2450 | ) |
2343 | 2451 | positions, community_hints = _community_positions( |
2344 | | - communities, global_community_id, layout_seed, spacing=92.0 |
| 2452 | + communities, global_community_id, layout_seed, spacing=74.0 |
2345 | 2453 | ) |
2346 | 2454 | for community in communities: |
2347 | 2455 | community.update(community_hints[community["id"]]) |
@@ -2890,7 +2998,7 @@ def eligible(node_id: str) -> bool: |
2890 | 2998 | graph, set(graph["community_members"]), set(graph["nodes"]), _system_radii |
2891 | 2999 | ) |
2892 | 3000 | layout_positions, layout_hints = _community_positions( |
2893 | | - layout_communities, global_community_id, layout_seed, spacing=98.0 |
| 3001 | + layout_communities, global_community_id, layout_seed, spacing=78.0 |
2894 | 3002 | ) |
2895 | 3003 | seeded_positions = _orbital_layout_positions( |
2896 | 3004 | graph["nodes"], graph["community_members"], graph["community_anchors"], |
|
0 commit comments