"""E06 스테레오넷 투영 좌표 — 하반구 · 등각(Wulff) · 등면적(Schmidt) (PLAN 536-1). 화면(SVG)은 이 좌표를 그리기만 함 — 반지름 1 원 · x = 동 · y = 북(화면은 y 를 뒤집음). 극점 · 대원 · 교선 · 마찰원 · 일광 엔벨로프 · 위험 영역(Markland 부등식과 같은 경계 — `E06_StabRock_Engine.kinematic`)을 냄. 투영식 = 표준 구면 투영(mplstereonet 등은 대조만). """ from __future__ import annotations import math from typing import Any from E06_StabRock.E06_StabRock_Engine import ( LATERAL_PLANAR_DEG, LATERAL_TOPPLING_DEG, apparent_dip, intersection, pole, ) PROJECTIONS = ("equal_angle", "equal_area") STEP_DEG = 2.0 # 곡선 표본 간격 def radius(plunge: float, kind: str) -> float: """선주각 → 중심에서 거리(0 = 연직 · 1 = 수평).""" half = math.radians(90.0 - plunge) / 2.0 return math.tan(half) if kind == "equal_angle" else math.sqrt(2.0) * math.sin(half) def project(trend: float, plunge: float, kind: str) -> list[float]: r = radius(plunge, kind) t = math.radians(trend) return [round(r * math.sin(t), 5), round(r * math.cos(t), 5)] def inverse(x: float, y: float, kind: str) -> tuple[float, float]: """투영 좌표 → (추향 · 선주각) — 왕복 시험 · 화면 마우스 자리 읽기.""" r = math.hypot(x, y) half = math.atan(r) if kind == "equal_angle" else math.asin(min(1.0, r / math.sqrt(2.0))) return math.degrees(math.atan2(x, y)) % 360.0, 90.0 - math.degrees(2.0 * half) def _samples(lo: float, hi: float) -> list[float]: n = max(2, int(math.ceil((hi - lo) / STEP_DEG)) + 1) return [lo + (hi - lo) * k / (n - 1) for k in range(n)] def great_circle(dip_dir: float, dip: float, kind: str) -> list[list[float]]: """면의 대원 — 주향 한끝 → 경사 방향 → 다른 끝.""" return [ project(dip_dir + b, max(0.0, apparent_dip(dip, dip_dir, dip_dir + b)), kind) for b in _samples(-90.0, 90.0) ] def _zone(trend_lo, trend_hi, plunge_lo, plunge_hi, kind) -> list[list[float]] | None: """(추향 · 선주각) 띠 → 닫힌 다각형 — plunge_lo/hi 는 추향의 함수 · 비면 None.""" ts = _samples(trend_lo, trend_hi) lows = [plunge_lo(t) for t in ts] highs = [plunge_hi(t) for t in ts] keep = [k for k in range(len(ts)) if highs[k] > lows[k]] if len(keep) < 2: return None outer = [project(ts[k], lows[k], kind) for k in keep] inner = [project(ts[k], highs[k], kind) for k in reversed(keep)] return outer + inner def stereonet( slope: dict, joints: list[dict], phi: float, kind: str = "equal_angle", lateral_planar: float = LATERAL_PLANAR_DEG, lateral_toppling: float = LATERAL_TOPPLING_DEG, ) -> dict[str, Any]: """slope = {dip_direction, dip} · joints = [{id, dip_direction, dip}] · phi = 영역 마찰각.""" af, pf = float(slope["dip_direction"]), float(slope["dip"]) def face_app(t: float) -> float: return apparent_dip(pf, af, t) planes = [] for j in joints: tr, pl = pole(j["dip_direction"], j["dip"]) planes.append( { "id": j["id"], "pole": project(tr, pl, kind), "great_circle": great_circle(j["dip_direction"], j["dip"], kind), } ) lines = [] for i, a in enumerate(joints): for b in joints[i + 1 :]: line = intersection(a, b) if line is not None: lines.append({"ids": [a["id"], b["id"]], "point": project(*line, kind)}) # 일광 엔벨로프(극점 꼴) = 사면 겉보기 경사로 기운 면들의 극점 envelope = [ project(af + b + 180.0, 90.0 - face_app(af + b), kind) for b in _samples(-90.0, 90.0) ] zones = { # 평면(극점) — 극점 추향 αf + 180 ± 측방 · 선주각 90 − ψapp ~ 90 − φ "planar": _zone( af + 180.0 - lateral_planar, af + 180.0 + lateral_planar, lambda t: 90.0 - face_app(t - 180.0), lambda t: 90.0 - phi, kind, ), # 쐐기(교선) — 추향 αf ± 90 · 선주각 φ ~ ψapp "wedge": _zone(af - 90.0, af + 90.0, lambda t: phi, face_app, kind), # 전도(극점) — 극점 추향 αf ± 측방 · 선주각 0 ~ ψf − φ "toppling": _zone( af - lateral_toppling, af + lateral_toppling, lambda t: 0.0, lambda t: pf - phi, kind, ), } return { "projection": kind, "slope": { "pole": project(*pole(af, pf), kind), "great_circle": great_circle(af, pf, kind), }, "planes": planes, "intersections": lines, # 마찰원 — 극점 꼴 = 중심에서 φ(선주각 90 − φ) · 교선 꼴 = 선주각 φ "friction_pole_r": round(radius(90.0 - phi, kind), 5), "friction_line_r": round(radius(phi, kind), 5), "daylight_envelope": envelope, "zones": zones, }