"""Stage order of the T-pier app and the Rhino step list for a build up to a given gate. Gates follow the required work order: G1 shape -> G2 column rebar -> G3 footing rebar -> G4 coping rebar -> G5 sole plates & bearings. A build "up to Gk" always rebuilds G1..Gk in a fresh document, so every gate model is reproducible from params alone. Per-pier options (2026-10-13, Ulsan P1 = second T-pier form): params["scope"] gates modelled for this pier (default all); others are skipped ("범위 밖") params["builders"] {gate: script} replacing the default builder (e.g. G1 -> concrete_tapered.py) expected["checkers"] {gate: script} replacing the default checker (e.g. G1 -> g1_tapered.py) """ import json from pathlib import Path RHINO_DIR = Path(__file__).resolve().parent / "rhino" GATES = [ ("G1", "형상(콘크리트·말뚝·받침대좌·패드)", "concrete.py"), ("G2", "기둥 철근", "column_rebar.py"), ("G3", "확대기초 철근", "footing_rebar.py"), ("G4", "코핑 철근", "coping_rebar.py"), ("G5", "교량받침·솔플레이트", "bearings.py"), ] CHECKERS = {"G1": "g1_shape.py", "G2": "g2_column_rebar.py", "G3": "g3_footing_rebar.py", "G4": "g4_coping_rebar.py", "G5": "g5_bearings.py"} # Object id prefixes created by each gate (after the pier id), used for negative tests and the # "earlier gates unchanged" fingerprint comparison. GATE_PREFIXES = {"G1": ["LC", "FT", "CL", "CP", "PL", "BS", "BP"], "G2": ["R-P", "RC-P"], "G3": ["R-B", "RC-B", "PT"], "G4": ["R-C", "RC-C", "R-S", "RC-S"], "G5": ["BB", "SP"]} # "PT": shaft cage plate (Ramp-A P1 G3, 2026-10-13). Ramp-A shaft cage bars carry "R-P1-1" etc. (marks P1-1 ...), # so they fall under the G2 prefixes in the unchanged check; that is harmless (all earlier gates are compared) REBAR_GATES = {"G2", "G3", "G4"} TOLERANCE_MM = 0.001 def gate_ids(): return [g for g, _, _ in GATES] def _json(path): return json.loads(Path(path).read_text(encoding="utf-8")) if path and Path(path).exists() else {} def scope_of(params_path): """Gates modelled for this pier (params["scope"], default all), in gate order.""" sc = _json(params_path).get("scope") return [g for g in gate_ids() if not sc or g in sc] def builder_of(params_path, gate): default = {g: s for g, _, s in GATES}[gate] return _json(params_path).get("builders", {}).get(gate, default) def checker_of(expected_path, gate): return _json(expected_path).get("checkers", {}).get(gate, CHECKERS[gate]) def build_steps(params_path, upto="G5"): """Rhino steps for G1..`upto`: builders, diameter colours after the last rebar stage, display state.""" ids = gate_ids() if upto not in ids: raise ValueError(f"unknown gate {upto}") scope = scope_of(params_path) chosen = [(g, n, builder_of(params_path, g)) for g, n, _ in GATES[: ids.index(upto) + 1] if g in scope] glb = {"PARAMS_PATH": str(Path(params_path).resolve())} steps = [] last_rebar = max((i for i, (g, _, _) in enumerate(chosen) if g in REBAR_GATES), default=None) for i, (g, _, script) in enumerate(chosen): steps.append({"name": f"{g}:{script}", "script": RHINO_DIR / script, "globals": dict(glb, GATE=g)}) if i == last_rebar: steps.append({"name": f"{g}:rebar_colours.py", "script": RHINO_DIR / "rebar_colours.py", "globals": glb}) steps.append({"name": "display.py", "script": RHINO_DIR / "display.py", "globals": glb}) return steps def check_steps(expected_path, out_dir, gates=None, tag=""): """Rhino steps running the gate checkers; each writes /.json.""" steps = [] for g in gates or gate_ids(): chk = checker_of(expected_path, g) steps.append({"name": f"{g}:{chk}", "script": RHINO_DIR / "checks" / chk, "globals": {"EXPECTED_PATH": str(Path(expected_path).resolve()), "RESULT_PATH": str(Path(out_dir).resolve() / f"{g}{tag}.json")}}) return steps