Загрузка данных


# ============================================================
# НЕБЕСНЫЙ ГЛОБУС Ø1500 мм
# 24 ЛЕПЕСТКА
# ЗВЁЗДЫ ТОЛЬКО mag < 5.0
#
# Blender 4.x
# ============================================================

import bpy
import math
import csv
import io
import urllib.request
from mathutils import Vector

# ------------------------------------------------------------
# НАСТРОЙКИ
# ------------------------------------------------------------

DIAMETER_MM = 1500.0
RADIUS_MM = DIAMETER_MM / 2.0

NUM_GORES = 24
GORE_WIDTH_DEG = 360.0 / NUM_GORES
HALF_GORE_DEG = GORE_WIDTH_DEG / 2.0

MAG_LIMIT = 5.0

# URL официального HYG 4.1
HYG_URL = (
    "https://raw.githubusercontent.com/astronexus/"
    "HYG-Database/main/hyg/CURRENT/hyg_v41.csv"
)

# Размеры звёзд на 3D-сфере
STAR_MIN_MM = 1.0
STAR_MAX_MM = 5.0

# Звёзды на развёртке
FLAT_STAR_MIN_MM = 0.8
FLAT_STAR_MAX_MM = 4.5

# Количество сегментов маленьких сфер-звёзд
STAR_SEGMENTS = 6
STAR_RINGS = 4

# Развёртка:
# синусоидальная схема:
# x = R * sin(lambda) * cos(latitude)
# y = R * latitude
#
# Это даёт ширину лепестка:
# 2 * R * sin(7.5°) = 196.35 мм на экваторе.

# ------------------------------------------------------------
# ОЧИСТКА СЦЕНЫ
# ------------------------------------------------------------

bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

for datablocks in (
    bpy.data.meshes,
    bpy.data.curves,
    bpy.data.materials,
    bpy.data.cameras,
    bpy.data.lights,
):
    pass

# ------------------------------------------------------------
# КОЛЛЕКЦИИ
# ------------------------------------------------------------

def new_collection(name):
    col = bpy.data.collections.new(name)
    bpy.context.scene.collection.children.link(col)
    return col

COL_SPHERE = new_collection("01_SKY_SPHERE")
COL_STARS_3D = new_collection("02_STARS_MAG_LT_5")
COL_GORES = new_collection("03_GORES_24")
COL_FLAT_STARS = new_collection("04_FLAT_STARS_MAG_LT_5")
COL_CUT = new_collection("05_CUT_LINES")
COL_LABELS = new_collection("06_LABELS")

# ------------------------------------------------------------
# МАТЕРИАЛЫ
# ------------------------------------------------------------

def make_material(name, color, metallic=0.0, roughness=0.5):
    mat = bpy.data.materials.new(name)
    mat.diffuse_color = (*color, 1.0)
    mat.metallic = metallic
    mat.roughness = roughness
    return mat

MAT_SPHERE = make_material(
    "Sky sphere",
    (0.005, 0.008, 0.02),
    0.0,
    0.8
)

MAT_STAR = make_material(
    "Stars",
    (1.0, 0.85, 0.45),
    0.0,
    0.25
)

MAT_GORE = make_material(
    "Gore paper",
    (0.025, 0.035, 0.07),
    0.0,
    0.9
)

MAT_FLAT_STAR = make_material(
    "Printed stars",
    (1.0, 0.8, 0.25),
    0.0,
    0.4
)

MAT_LINE = make_material(
    "Cut lines",
    (1.0, 0.15, 0.05),
    0.0,
    0.5
)

MAT_LABEL = make_material(
    "Labels",
    (0.8, 0.9, 1.0),
    0.0,
    0.5
)

# ------------------------------------------------------------
# ЗАГРУЗКА HYG
# ------------------------------------------------------------

print("Downloading HYG 4.1...")

request = urllib.request.Request(
    HYG_URL,
    headers={"User-Agent": "Blender-Star-Globe/1.0"}
)

with urllib.request.urlopen(request, timeout=60) as response:
    data = response.read().decode("utf-8")

print("HYG downloaded:", len(data), "bytes")

# ------------------------------------------------------------
# ЧТЕНИЕ ЗВЁЗД
# ------------------------------------------------------------

stars = []

reader = csv.DictReader(io.StringIO(data))

for row in reader:

    try:
        ra = float(row["ra"])
        dec = float(row["dec"])
        mag = float(row["mag"])
    except:
        continue

    # Главное условие:
    # 5.0 НЕ включается
    if not (mag < MAG_LIMIT):
        continue

    # Некорректные значения пропускаем
    if not (-90.0 <= dec <= 90.0):
        continue

    stars.append({
        "ra": ra,
        "dec": dec,
        "mag": mag,
        "name": row.get("proper", ""),
        "con": row.get("con", ""),
    })

print("Stars with mag < 5.0:", len(stars))

# ------------------------------------------------------------
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
# ------------------------------------------------------------

def star_radius(mag, min_size=STAR_MIN_MM, max_size=STAR_MAX_MM):
    """
    Чем ярче звезда, тем больше её диаметр.
    Диапазон ограничен для удобства печати.
    """

    # Нормировка примерно от -1.5 до 5
    t = (5.0 - mag) / 6.5
    t = max(0.0, min(1.0, t))

    # Немного нелинейности
    t = t ** 0.55

    return min_size + t * (max_size - min_size)


def ra_dec_to_xyz(ra_hours, dec_deg, radius):
    """
    RA в часах.
    Dec в градусах.

    X = направление RA 0
    Y = RA 6h
    Z = северный полюс
    """

    ra = math.radians(ra_hours * 15.0)
    dec = math.radians(dec_deg)

    x = radius * math.cos(dec) * math.cos(ra)
    y = radius * math.cos(dec) * math.sin(ra)
    z = radius * math.sin(dec)

    return Vector((x, y, z))


def normalize_ra_deg(deg):
    """
    RA в диапазон 0...360.
    """
    return deg % 360.0


# ------------------------------------------------------------
# 3D СФЕРА
# ------------------------------------------------------------

bpy.ops.mesh.primitive_uv_sphere_add(
    segments=96,
    ring_count=48,
    radius=RADIUS_MM,
    location=(0, 0, 0)
)

sphere = bpy.context.object
sphere.name = "SKY_SPHERE_DIAMETER_1500mm"

sphere.data.materials.append(MAT_SPHERE)

# Перемещаем в нужную коллекцию
for col in list(sphere.users_collection):
    col.objects.unlink(sphere)

COL_SPHERE.objects.link(sphere)

# ------------------------------------------------------------
# СОЗДАНИЕ 3D ЗВЁЗД
# ------------------------------------------------------------

print("Creating 3D stars...")

for i, star in enumerate(stars):

    pos = ra_dec_to_xyz(
        star["ra"],
        star["dec"],
        RADIUS_MM + 1.0
    )

    diameter = star_radius(star["mag"])
    radius = diameter / 2.0

    bpy.ops.mesh.primitive_ico_sphere_add(
        subdivisions=1,
        radius=radius,
        location=pos
    )

    obj = bpy.context.object
    obj.name = f"STAR_{i:05d}_mag_{star['mag']:.2f}"

    obj.data.materials.append(MAT_STAR)

    # Перемещаем в коллекцию
    for col in list(obj.users_collection):
        col.objects.unlink(obj)

    COL_STARS_3D.objects.link(obj)

# ------------------------------------------------------------
# ГЕОМЕТРИЯ ЛЕПЕСТКА
# ------------------------------------------------------------

def gore_xy(lat_deg, local_lon_deg):
    """
    Синусоидальная развёртка.

    lat = -90...90
    local_lon = -7.5...7.5

    x = R sin(lambda) cos(phi)
    y = R phi

    Все размеры в мм.
    """

    phi = math.radians(lat_deg)
    lam = math.radians(local_lon_deg)

    x = RADIUS_MM * math.sin(lam) * math.cos(phi)
    y = RADIUS_MM * phi

    return x, y


def gore_width_at_lat(lat_deg):
    """
    Полная ширина лепестка на заданной широте.
    """

    phi = math.radians(lat_deg)

    return (
        2.0
        * RADIUS_MM
        * math.sin(math.radians(HALF_GORE_DEG))
        * math.cos(phi)
    )

# ------------------------------------------------------------
# СОЗДАНИЕ 24 ЛЕПЕСТКОВ
# ------------------------------------------------------------

print("Creating 24 gores...")

LAT_STEPS = 72

for gore_index in range(NUM_GORES):

    # Центральная долгота лепестка
    center_lon = gore_index * GORE_WIDTH_DEG

    vertices = []
    faces = []

    # Левая и правая границы
    for side in (-1, 1):

        for j in range(LAT_STEPS + 1):

            lat = -90.0 + (
                180.0 * j / LAT_STEPS
            )

            x, y = gore_xy(
                lat,
                side * HALF_GORE_DEG
            )

            # Размещаем лепестки отдельно,
            # чтобы их было удобно рассматривать.
            spacing = 2200.0

            offset_x = (
                (gore_index % 6) * spacing
            )

            offset_y = (
                (gore_index // 6) * 1900.0
            )

            vertices.append(
                (
                    x + offset_x,
                    y + offset_y,
                    0
                )
            )

    # Индексы:
    # левая сторона 0...LAT_STEPS
    # правая сторона LAT_STEPS+1...
    right_start = LAT_STEPS + 1

    for j in range(LAT_STEPS):

        faces.append(
            (
                j,
                j + 1,
                right_start + j + 1,
                right_start + j
            )
        )

    mesh = bpy.data.meshes.new(
        f"GORE_{gore_index+1:02d}_MESH"
    )

    mesh.from_pydata(
        vertices,
        [],
        faces
    )

    mesh.update()

    obj = bpy.data.objects.new(
        f"GORE_{gore_index+1:02d}_15deg",
        mesh
    )

    COL_GORES.objects.link(obj)
    obj.data.materials.append(MAT_GORE)

    # Записываем полезные параметры как Custom Properties
    obj["gore_number"] = gore_index + 1
    obj["center_longitude_deg"] = center_lon
    obj["width_deg"] = GORE_WIDTH_DEG
    obj["sphere_diameter_mm"] = DIAMETER_MM

# ------------------------------------------------------------
# ПЕРЕНОС ЗВЁЗД НА ЛЕПЕСТКИ
# ------------------------------------------------------------

print("Creating flat star positions...")

def create_flat_star_mesh(name, x, y, diameter):

    r = diameter / 2.0

    segments = 12

    verts = []

    for k in range(segments):
        a = 2.0 * math.pi * k / segments

        verts.append(
            (
                x + r * math.cos(a),
                y + r * math.sin(a),
                0.5
            )
        )

    face = tuple(range(segments))

    mesh = bpy.data.meshes.new(name + "_MESH")

    mesh.from_pydata(
        verts,
        [],
        [face]
    )

    mesh.update()

    obj = bpy.data.objects.new(
        name,
        mesh
    )

    COL_FLAT_STARS.objects.link(obj)
    obj.data.materials.append(MAT_FLAT_STAR)

    return obj


for i, star in enumerate(stars):

    ra_deg = star["ra"] * 15.0
    dec = star["dec"]

    # Номер лепестка.
    # Границы идут по 15 градусов.
    gore_index = int(
        math.floor(
            normalize_ra_deg(ra_deg)
            / GORE_WIDTH_DEG
        )
    )

    if gore_index >= NUM_GORES:
        gore_index = NUM_GORES - 1

    center_lon = (
        gore_index * GORE_WIDTH_DEG
        + HALF_GORE_DEG
    )

    local_lon = (
        normalize_ra_deg(ra_deg)
        - center_lon
    )

    # Если объект на границе,
    # он может оказаться ровно на краю.
    # Для бумажной версии это нормально.
    x, y = gore_xy(
        dec,
        local_lon
    )

    # То же расположение лепестков,
    # что использовалось выше.
    spacing = 2200.0

    offset_x = (
        (gore_index % 6) * spacing
    )

    offset_y = (
        (gore_index // 6) * 1900.0
    )

    diameter = star_radius(
        star["mag"],
        FLAT_STAR_MIN_MM,
        FLAT_STAR_MAX_MM
    )

    obj = create_flat_star_mesh(
        f"FLAT_STAR_{i:05d}_mag_{star['mag']:.2f}",
        x + offset_x,
        y + offset_y,
        diameter
    )

    obj["magnitude"] = star["mag"]
    obj["RA_hours"] = star["ra"]
    obj["Dec_deg"] = star["dec"]
    obj["constellation"] = star["con"]

# ------------------------------------------------------------
# ЛИНИИ РЕЗА ЛЕПЕСТКОВ
# ------------------------------------------------------------

def create_line_object(name, points, material):

    curve = bpy.data.curves.new(
        name + "_CURVE",
        type='CURVE'
    )

    curve.dimensions = '3D'
    curve.resolution_u = 1

    curve.bevel_depth = 0.8
    curve.bevel_resolution = 1

    spline = curve.splines.new('POLY')
    spline.points.add(len(points) - 1)

    for p, co in zip(spline.points, points):
        p.co = (
            co[0],
            co[1],
            co[2],
            1.0
        )

    obj = bpy.data.objects.new(
        name,
        curve
    )

    COL_CUT.objects.link(obj)
    curve.materials.append(material)

    return obj


# Границы всех лепестков
for gore_index in range(NUM_GORES):

    spacing = 2200.0

    ox = (
        (gore_index % 6) * spacing
    )

    oy = (
        (gore_index // 6) * 1900.0
    )

    for side in (-1, 1):

        points = []

        for j in range(LAT_STEPS + 1):

            lat = -90.0 + (
                180.0 * j / LAT_STEPS
            )

            x, y = gore_xy(
                lat,
                side * HALF_GORE_DEG
            )

            points.append(
                (
                    x + ox,
                    y + oy,
                    1.5
                )
            )

        create_line_object(
            f"GORe_{gore_index+1:02d}_CUT_{side}",
            points,
            MAT_LINE
        )

# ------------------------------------------------------------
# ЭКВАТОР И ПОЛЮСА НА ЛЕПЕСТКАХ
# ------------------------------------------------------------

# Экватор
for gore_index in range(NUM_GORES):

    spacing = 2200.0

    ox = (
        (gore_index % 6) * spacing
    )

    oy = (
        (gore_index // 6) * 1900.0
    )

    points = []

    for k in range(21):

        local_lon = (
            -HALF_GORE_DEG
            + 2.0 * HALF_GORE_DEG * k / 20.0
        )

        x, y = gore_xy(
            0.0,
            local_lon
        )

        points.append(
            (
                x + ox,
                y + oy,
                1.0
            )
        )

    create_line_object(
        f"GORE_{gore_index+1:02d}_EQUATOR",
        points,
        MAT_LINE
    )

# ------------------------------------------------------------
# НОМЕРА ЛЕПЕСТКОВ
# ------------------------------------------------------------

def add_text(
    text,
    location,
    size=50.0
):

    curve = bpy.data.curves.new(
        "TEXT_" + text,
        type='FONT'
    )

    curve.body = text
    curve.align_x = 'CENTER'
    curve.align_y = 'CENTER'
    curve.size = size
    curve.extrude = 0.2

    obj = bpy.data.objects.new(
        "LABEL_" + text,
        curve
    )

    obj.location = location

    COL_LABELS.objects.link(obj)

    curve.materials.append(MAT_LABEL)

    return obj


for gore_index in range(NUM_GORES):

    spacing = 2200.0

    ox = (
        (gore_index % 6) * spacing
    )

    oy = (
        (gore_index // 6) * 1900.0
    )

    add_text(
        f"GORE {gore_index+1:02d}",
        (
            ox,
            oy - RADIUS_MM - 80,
            2.0
        ),
        45.0
    )

# ------------------------------------------------------------
# ИНФОРМАЦИОННЫЙ ОБЪЕКТ
# ------------------------------------------------------------

scene_info = add_text(
    "CELESTIAL GLOBE 1500mm / MAG < 5.0 / 24 GORES",
    (0, -1100, 0),
    35.0
)

# ------------------------------------------------------------
# НАСТРОЙКИ СЦЕНЫ
# ------------------------------------------------------------

scene = bpy.context.scene

scene["Globe_Diameter_mm"] = DIAMETER_MM
scene["Globe_Radius_mm"] = RADIUS_MM
scene["Number_of_Gores"] = NUM_GORES
scene["Gore_Width_Deg"] = GORE_WIDTH_DEG
scene["Magnitude_Limit"] = MAG_LIMIT
scene["Magnitude_Filter"] = "mag < 5.0"
scene["Star_Catalog"] = "HYG Database 4.1"
scene["Epoch"] = "J2000.0"

# Единицы — миллиметры
scene.unit_settings.system = 'METRIC'
scene.unit_settings.length_unit = 'MILLIMETERS'

# ------------------------------------------------------------
# СОЗДАЁМ КАМЕРУ ДЛЯ ОБЗОРА
# ------------------------------------------------------------

bpy.ops.object.camera_add(
    location=(0, -2600, 1800)
)

camera = bpy.context.object
camera.name = "CAMERA_PREVIEW"

camera.rotation_euler = (
    math.radians(65),
    0,
    0
)

scene.camera = camera

# ------------------------------------------------------------
# СТАТИСТИКА
# ------------------------------------------------------------

print("")
print("============================================")
print("ГОТОВО")
print("============================================")
print(f"Диаметр сферы:       {DIAMETER_MM} мм")
print(f"Радиус:              {RADIUS_MM} мм")
print(f"Лепестков:           {NUM_GORES}")
print(f"Ширина лепестка:     {GORE_WIDTH_DEG}°")
print(f"Ширина на экваторе:  "
      f"{2*RADIUS_MM*math.sin(math.radians(HALF_GORE_DEG)):.2f} мм")
print(f"Фильтр звёзд:        mag < {MAG_LIMIT}")
print(f"Звёзд:               {len(stars)}")
print("============================================")

# ------------------------------------------------------------
# СОХРАНЕНИЕ BLEND
# ------------------------------------------------------------

filepath = bpy.path.abspath(
    "//celestial_globe_1500mm_mag_lt_5_24_gores.blend"
)

bpy.ops.wm.save_as_mainfile(
    filepath=filepath
)

print("")
print("BLEND СОХРАНЁН:")
print(filepath)