chore: import 3d-drawing history from local Code/3d-drawing (subfolder, history preserved)
This commit is contained in:
commit
7b754e810b
51 files changed
+20300
No files matched your search
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Task 5 (user decision 2026-10-21): no pattern-station data exists. Create a
|
||||
PLACEHOLDER pattern table at 2 m segments over the modelled range 4+745..5+480,
|
||||
cycling the six pattern names. Every row is marked '임의 배정 (placeholder)'.
|
||||
The table is an editable CSV: replace the 'pattern' column later and rebuild.
|
||||
Output: data/pattern_zones_placeholder.csv (created only if absent; never overwritten)."""
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
|
||||
ROOT = sys.argv[1]
|
||||
OUT = f"{ROOT}/data/pattern_zones_placeholder.csv"
|
||||
PATTERNS = ["NP-4AD-1-D-2", "NP-4BD-1-D-2", "NP-5AE-1-WL-4", "NP-5BE-1-WL-4", "NP-6A", "NP-6B"]
|
||||
S0, S1, SEG = 4745, 5480, 2
|
||||
if not os.path.exists(OUT):
|
||||
with open(OUT, "w", newline="", encoding="utf-8") as fh:
|
||||
w = csv.writer(fh)
|
||||
w.writerow(["seg_from", "seg_to", "pattern", "status"])
|
||||
for k, s in enumerate(range(S0, S1, SEG)):
|
||||
w.writerow([s, min(s + SEG, S1), PATTERNS[k % len(PATTERNS)], "임의 배정 (placeholder, 실제 구간 아님)"])
|
||||
print("pattern table:", OUT)
|
||||
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Task 7: axis + standard-section sweep skeleton of the main line (본선), 4+745..5+480.
|
||||
|
||||
Inputs (all confirmed by the user, 2026-10-21):
|
||||
- centreline nodes data/alignment_plan_001_centre.csv (midpoint of side ticks)
|
||||
- profile data/profile_gl_002_draft.csv (A = 지반고 GL, B = 계획고 = 노면)
|
||||
- section table DOC/03_설계/설계_표준단면치수표_2026-10-21_v0.1.00.md
|
||||
R1 6,982 mm (lining inner), lining 300 mm -> outer R 7,282, excavation R 7,402
|
||||
crown A = 6.615 m above road (F = 0.000 = road reference)
|
||||
- cross slope: 4+745~5+105 -2 %, 5+105~5+165 linear -2 -> +2 %, 5+165~5+480 +2 %
|
||||
Model (skeleton, not as-built):
|
||||
- Section = arch (concentric centre c = 6.615 - 6.982) + vertical walls down to the road.
|
||||
Walls and invert are APPROXIMATIONS and are flagged in the metadata. No invert.
|
||||
- Superelevation: z += (S/100) * x across the section (sign convention ASSUMED, flagged).
|
||||
- Coordinates: plan XY in m (station spacing 100 m matches plan units), origin at 4+745.
|
||||
Outputs: model/tunnel_skeleton.glb, model/tunnel_skeleton_meta.json, data/verify_skeleton.json
|
||||
"""
|
||||
import csv
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import trimesh
|
||||
|
||||
ROOT = sys.argv[1] # Code/3d-drawing
|
||||
C_CSV = f"{ROOT}/data/alignment_plan_001_centre.csv"
|
||||
P_CSV = f"{ROOT}/data/profile_gl_002_draft.csv"
|
||||
R1, R_OUT, R_EX = 6.982, 7.282, 7.402 # m (mm / 1000)
|
||||
CROWN_A = 6.615 # m above road
|
||||
CENTRE_C = CROWN_A - R1 # arch centre height above road (m)
|
||||
S0, S1 = 4745, 5480 # modelled range (fully determined)
|
||||
DS = 1.0 # station step for rings (m); pattern segments are 2 m
|
||||
ANG = np.radians(np.linspace(40.0, 140.0, 41)) # arch arc, right springline -> left
|
||||
|
||||
def read_centre():
|
||||
rows = [r for r in csv.DictReader(open(C_CSV, encoding="utf-8"))]
|
||||
return np.array([[int(r["station_m"]), float(r["centre_x"]), float(r["centre_y"])] for r in rows])
|
||||
|
||||
def read_profile():
|
||||
gl, dg = {}, {}
|
||||
for r in csv.DictReader(open(P_CSV, encoding="utf-8")):
|
||||
sta = int(r["station_anchor"])
|
||||
(gl if float(r["across_m"]) < -25 else dg)[sta] = float(r["value"])
|
||||
return gl, dg
|
||||
|
||||
def slope_pct(s):
|
||||
if s < 5105: return -2.0
|
||||
if s < 5165: return -2.0 + 4.0 * (s - 5105) / 60.0
|
||||
return 2.0
|
||||
|
||||
cn = read_centre()
|
||||
gl_d, dg_d = read_profile()
|
||||
gl_s = np.array(sorted(gl_d)); gl_v = np.array([gl_d[k] for k in gl_s])
|
||||
dg_s = np.array(sorted(dg_d)); dg_v = np.array([dg_d[k] for k in dg_s])
|
||||
cn_s = cn[:, 0]
|
||||
|
||||
def axis_xy(s):
|
||||
x = np.interp(s, cn_s, cn[:, 1]); y = np.interp(s, cn_s, cn[:, 2])
|
||||
i = min(max(np.searchsorted(cn_s, s) - 1, 0), len(cn_s) - 2)
|
||||
dx, dy = cn[i + 1, 1] - cn[i, 1], cn[i + 1, 2] - cn[i, 2]
|
||||
L = math.hypot(dx, dy)
|
||||
return x, y, dx / L, dy / L
|
||||
|
||||
ORIGIN = np.array(axis_xy(S0)[:2])
|
||||
stations = np.arange(S0, S1 + 0.001, DS)
|
||||
|
||||
def section(R, s, slope):
|
||||
"""ring (x lateral m, z above road m) for radius R, including superelevation."""
|
||||
xs = np.concatenate([[R * math.cos(ANG[0])], R * np.cos(ANG), [R * math.cos(ANG[-1])]])
|
||||
zs = np.concatenate([[0.0], CENTRE_C + R * np.sin(ANG), [0.0]])
|
||||
zs = zs + slope * xs
|
||||
return xs, zs
|
||||
|
||||
def ring_world(R, s):
|
||||
x, y, tx, ty = axis_xy(s)
|
||||
nx, ny = -ty, tx # left normal (horizontal)
|
||||
zr = np.interp(s, dg_s, dg_v) # road (계획고 = 노면)
|
||||
xs, zs = section(R, s, slope_pct(s) / 100.0)
|
||||
P = np.column_stack([x - ORIGIN[0] + xs * nx, y - ORIGIN[1] + xs * ny, zr + zs])
|
||||
return P
|
||||
|
||||
PAL = {"A": (42, 157, 143, 255), "B-1": (231, 111, 81, 255), "B-2": (244, 162, 97, 255),
|
||||
"C-1": (233, 196, 106, 255), "C-2": (138, 177, 125, 255), "D-1": (38, 70, 83, 255),
|
||||
"D-2": (100, 130, 160, 255), "P-2": (190, 110, 140, 255), "P-3": (150, 90, 60, 255),
|
||||
"RP-1": (80, 170, 200, 255), "WL-1": (200, 200, 120, 255), "WL-2": (120, 90, 170, 255),
|
||||
"개착부": (170, 170, 170, 255)}
|
||||
ZONES = [(int(float(r["sta_from"])), int(float(r["sta_to"])), r["lining_pattern"], "엑셀 지보패턴표(당진방향)")
|
||||
for r in csv.DictReader(open(f"{ROOT}/data/pattern_stations_dangjin.csv", encoding="utf-8"))]
|
||||
|
||||
def pattern_of(sta):
|
||||
for a, b, pat, _ in ZONES:
|
||||
if a <= sta < b:
|
||||
return pat
|
||||
return ZONES[-1][2]
|
||||
|
||||
def make_band_pattern(R, name):
|
||||
rings = [ring_world(R, s) for s in stations]
|
||||
n = len(rings[0]); m = len(rings)
|
||||
V = np.vstack(rings)
|
||||
F, C = [], []
|
||||
for i in range(m - 1):
|
||||
col = PAL[pattern_of(stations[i])]
|
||||
for j in range(n - 1):
|
||||
a = i * n + j; b = a + 1; c = a + n; d = c + 1
|
||||
F += [[a, b, c], [b, d, c]]
|
||||
C += [col, col]
|
||||
mesh = trimesh.Trimesh(vertices=V, faces=np.array(F), process=False)
|
||||
mesh.visual.face_colors = np.array(C, dtype=np.uint8)
|
||||
mesh.metadata["name"] = name
|
||||
mesh.metadata["ring_points"] = n
|
||||
return mesh
|
||||
|
||||
def make_ribbon(z_fn, half_w, color, name):
|
||||
V, F = [], []
|
||||
for k, s in enumerate(stations):
|
||||
x, y, tx, ty = axis_xy(s)
|
||||
nx, ny = -ty, tx
|
||||
zr = z_fn(s)
|
||||
V.append([x - ORIGIN[0] - half_w * nx, y - ORIGIN[1] - half_w * ny, zr])
|
||||
V.append([x - ORIGIN[0] + half_w * nx, y - ORIGIN[1] + half_w * ny, zr])
|
||||
V = np.array(V)
|
||||
for k in range(len(stations) - 1):
|
||||
a = 2 * k; b = a + 1; c = a + 2; d = a + 3
|
||||
F += [[a, c, b], [b, c, d]]
|
||||
mesh = trimesh.Trimesh(vertices=V, faces=np.array(F), process=False)
|
||||
mesh.visual.face_colors = np.tile(np.array(color, dtype=np.uint8), (len(F), 1))
|
||||
mesh.metadata["name"] = name
|
||||
return mesh
|
||||
|
||||
def make_band(R, color, name):
|
||||
"""uniform-colour band (excavation outline)."""
|
||||
rings = [ring_world(R, s) for s in stations]
|
||||
n = len(rings[0]); m = len(rings)
|
||||
V = np.vstack(rings)
|
||||
F = []
|
||||
for i in range(m - 1):
|
||||
for j in range(n - 1):
|
||||
a = i * n + j; b = a + 1; c = a + n; d = c + 1
|
||||
F += [[a, b, c], [b, d, c]]
|
||||
mesh = trimesh.Trimesh(vertices=V, faces=np.array(F), process=False)
|
||||
mesh.visual.face_colors = np.tile(np.array(color, dtype=np.uint8), (len(F), 1))
|
||||
mesh.metadata["name"] = name
|
||||
return mesh
|
||||
|
||||
parts = [
|
||||
make_band_pattern(R_OUT, "lining_outer_R7282"),
|
||||
make_band_pattern(R1, "lining_inner_R6982"),
|
||||
make_band(R_EX, (150, 120, 90, 110), "excavation_R7402"),
|
||||
make_ribbon(lambda s: float(np.interp(s, dg_s, dg_v)), 5.0, (70, 70, 75, 255), "road_surface_계획고"),
|
||||
make_ribbon(lambda s: float(np.interp(s, gl_s, gl_v)), 20.0, (110, 150, 90, 200), "ground_GL_지반고"),
|
||||
]
|
||||
scene = trimesh.Scene()
|
||||
for p in parts:
|
||||
scene.add_geometry(p, geom_name=p.metadata["name"])
|
||||
glb = f"{ROOT}/model/tunnel_skeleton.glb"
|
||||
scene.export(glb)
|
||||
|
||||
# ---- verification (assertions recorded, not forced) ----
|
||||
checks = {}
|
||||
# radius check in section plane: distance from arch centre (lateral=0, z=centre_c)
|
||||
xs, zs = section(R1, S0, 0.0)
|
||||
rad = np.hypot(xs[1:-1], zs[1:-1] - CENTRE_C)
|
||||
checks["inner_radius_max_dev_m"] = float(np.max(np.abs(rad - R1)))
|
||||
checks["lining_thickness_crown_m"] = round(R_OUT - R1, 3)
|
||||
crown_z = ring_world(R1, 5000)[:, 2].max() - np.interp(5000, dg_s, dg_v)
|
||||
checks["crown_above_road_m (expect 6.615 at S=0 station 5000)"] = round(float(crown_z), 4)
|
||||
checks["road_z_4745_m"] = round(float(np.interp(S0, dg_s, dg_v)), 3)
|
||||
checks["road_z_5480_m"] = round(float(np.interp(S1, dg_s, dg_v)), 3)
|
||||
L_chord = float(np.sum([math.dist(cn[i, 1:], cn[i + 1, 1:]) for i in range(len(cn) - 1)]))
|
||||
checks["plan_chord_length_total_m (4+700..5+500)"] = round(L_chord, 3)
|
||||
checks["station_range_modelled"] = f"{S0} .. {S1} (ring step {DS} m, {len(stations)} rings)"
|
||||
checks["pattern_zones (지보패턴표 당진방향)"] = len(ZONES)
|
||||
checks["pattern_coverage_all_stations"] = bool(all(pattern_of(x) in PAL for x in stations))
|
||||
checks["pattern_counts"] = {k: sum(1 for z in ZONES if z[2] == k) for k in PAL}
|
||||
checks["glb_bytes"] = int(__import__("os").path.getsize(glb))
|
||||
reload = trimesh.load(glb)
|
||||
checks["glb_reload_geometries"] = len(reload.geometry)
|
||||
json.dump(checks, open(f"{ROOT}/data/verify_skeleton.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
meta = {
|
||||
"model": "tunnel_skeleton.glb", "scope": f"{S0}~{S1} (4+745~5+480)",
|
||||
"units": "m", "origin_plan_xy": [round(ORIGIN[0], 3), round(ORIGIN[1], 3)],
|
||||
"sources": {"centreline": "C0060302-001 CA-ALGN-STSM (side-tick midpoint)",
|
||||
"road_grade_B": "C0060302-002 CR-TABL-TEX2 (계획고=노면)",
|
||||
"GL_A": "C0060302-002 CR-TABL-TEX2 (지반고)",
|
||||
"section": "C0060303-001 CS-TABL-TEX1/2, CS-CONC-MAJR, CS-EXCV"},
|
||||
"approximations": ["walls vertical down to road; invert not modelled",
|
||||
"lining band thickness at walls is radial-consistent but not as-built",
|
||||
"centreline and grade linearly interpolated between nodes",
|
||||
"superelevation sign convention assumed (z += S/100 * x, x = left of tangent)"],
|
||||
"layers": [p.metadata["name"] for p in parts],
|
||||
"ring_step_m": DS, "ring_points": int(parts[0].metadata["ring_points"]), "first_station": S0,
|
||||
"pattern_status": "지보패턴: 엑셀 참고_지보패턴현황 당진방향 표(4+740~5+500, 42행, 빈틈 없음) 적용. 출처 data/pattern_stations_dangjin.csv (사용자 결정 2026-10-21)",
|
||||
"patterns": [{"from": a, "to": b, "pattern": pat} for a, b, pat, _ in ZONES],
|
||||
"open": ["4+700~4+745 and 5+480~5+500 excluded ([미확정])", "지보패턴 측점표 적용(당진). 도면 E-1~2, RP-2, WL-3~4 는 당진 표에 없어 [미확정]. 대산 표 WL-4(4+758.85~4+768.85)는 참고만 [확인 필요]",
|
||||
"lining reference recorded (meta.lining_reference, 04-004~009); sheet-to-station mapping [미확정]",
|
||||
"[확정 2026-10-21] R1 6,982 mm 유지 (사용자 결정). 라이닝 도면 6장의 R1 6.892 m 표기는 확인 대상 오기로 기록", "pattern-group to excavation-pattern mapping: user explanation conflicts with filename (WL-4) - re-confirm [확인 필요]"]
|
||||
}
|
||||
_ls = json.load(open(f"{ROOT}/data/lining_spec.json", encoding="utf-8"))
|
||||
meta["lining_reference"] = {
|
||||
"source": _ls["source"], "concrete": "fck 27 MPa", "rebar": "SD400 (fy 400 MPa)", "aggregate_mm": 25,
|
||||
"summary": "C0060304-004~009 | fck 27 MPa | SD400 | 입출구부 50 m 취약구간 철근 상세 (측점 대응 미확정)",
|
||||
"sheets": [{"sheet": r["file"][:14], "groups": [[g["group"], g["kind"]] for g in r["pattern_details"]]} for r in _ls["sheets"]]
|
||||
}
|
||||
json.dump(meta, open(f"{ROOT}/model/tunnel_skeleton_meta.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
print(json.dumps(checks, ensure_ascii=False, indent=1))
|
||||
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Task 8: B-rep solids of the main line (본선) 4+745..5+480 -> STEP (Rhino/Autodesk) + GLB (web viewer).
|
||||
|
||||
Section geometry: standard section C0060303-001 (drawing 03-001), revision rev2 approved 2026-10-21.
|
||||
- lining (라이닝): arch R6982 (inner) / R7282 (outer) about centre C; haunch arcs R3712/R3910 (inner),
|
||||
R4454/R4699 (outer) run below road level as drawn; inner walls and inner chord (road level) as drawn.
|
||||
Lining is symmetric (no superelevation). H/I boxes removed from lining (2D).
|
||||
- excavation band (굴착 여유대): R7402 / R4574 / R4819 minus lining outer, above road level. Symmetric.
|
||||
- road surface / 계획고 (노면, 포장 280 mm): cross slope S per station (superelevation). Only this part tilts.
|
||||
- base / invert (기층·인버트, ASSUMED: not in drawing): region between lining outer and inner chord minus
|
||||
pavement and side drains. Its top follows the tilted pavement bottom.
|
||||
- side drains (측구): box 300 x 836 mm minus pipe r150 (drawing circles).
|
||||
- H/I boxes (공동구, ASSUMED by user decision 2026-10-21).
|
||||
- 시설한계 50 mm: REFERENCE polylines only (not solids), as decided by the user.
|
||||
Coordinates: local section x = lateral along the LEFT normal of the alignment (sign convention flagged),
|
||||
z = height above road centre. Superelevation: z += (S/100)*x (sign flagged).
|
||||
Outputs: model/tunnel_solids.step, model/tunnel_solids.glb, model/tunnel_solids_meta.json, data/verify_solids.json
|
||||
Usage: python3 scripts/build_solids.py "$PWD"
|
||||
"""
|
||||
import csv, json, math, os, sys, tempfile
|
||||
import numpy as np
|
||||
from shapely.geometry import Polygon, box
|
||||
from build123d import Vector, Wire, Solid, Compound, Color, export_step, export_stl, import_step
|
||||
import trimesh
|
||||
|
||||
ROOT = sys.argv[1]
|
||||
MODE = sys.argv[2] if len(sys.argv) > 2 else 'all' # parts_a | parts_b | assemble (4 GB RAM: run in stages)
|
||||
PARTS = f"{ROOT}/model/parts"
|
||||
RANGE = (int(sys.argv[3]), int(sys.argv[4])) if len(sys.argv) > 4 else (0, 10**6) # run index range for parts_a
|
||||
os.makedirs(PARTS, exist_ok=True)
|
||||
C_CSV = f"{ROOT}/data/alignment_plan_001_centre.csv"
|
||||
P_CSV = f"{ROOT}/data/profile_gl_002_draft.csv"
|
||||
PAT_CSV = f"{ROOT}/data/pattern_stations_dangjin.csv"
|
||||
S0, S1 = 4745, 5480
|
||||
STEP_M = 10.0 # section spacing (m): ruled loft between sections (STEP size vs curvature)
|
||||
CHUNK_M = 100
|
||||
PIPE_SEG = 24
|
||||
CLEAR_OFFSET = 0.05
|
||||
CLEAR_LINES = 6
|
||||
CLEAR_RIB_W = 0.02
|
||||
|
||||
# ---------- drawing geometry (mm) -> local m ----------
|
||||
CX, CY, Y0 = 14126.29, 15305.02, 15672.0
|
||||
def P(x, y): return ((x - CX) / 1000.0, (y - Y0) / 1000.0)
|
||||
def arc(cx, cy, r, a0, a1, n=120):
|
||||
return [(cx + r * math.cos(math.radians(a0 + (a1 - a0) * i / n)),
|
||||
cy + r * math.sin(math.radians(a0 + (a1 - a0) * i / n))) for i in range(n + 1)]
|
||||
def loc(pts): return [P(x, y) for x, y in pts]
|
||||
WALL_L_INNER = [(8771, 15751), (8771, 15826), (8651, 16001), (8621, 16411), (8471, 16411),
|
||||
(8171, 16411), (8045, 16411), (8034, 16446)]
|
||||
WALL_R_INNER = [(19481, 15567), (19481, 15642), (19601, 15817), (19631, 16227), (19781, 16227),
|
||||
(20181, 16227), (20251, 16239)]
|
||||
INNER_MM = (WALL_L_INNER + arc(11621.3, 17406.9, 3712, 195.6, 140) + arc(CX, CY, 6982, 140, 40)
|
||||
+ arc(16479.6, 17279.7, 3910, 40, -15.6) + WALL_R_INNER[::-1])
|
||||
OUTER_MM = (arc(11959.9, 17122.8, 4454, 204.7, 140) + arc(CX, CY, 7282, 140, 40)
|
||||
+ arc(16105.0, 16965.3, 4699, 40, -23.7))
|
||||
EX_MM = (arc(11959.9, 17122.8, 4574, 204.0, 140) + arc(CX, CY, 7402, 140, 40)
|
||||
+ arc(16105.0, 16965.3, 4819, 40, -23.0))
|
||||
inner_poly = Polygon(loc(INNER_MM)).buffer(0)
|
||||
outer_poly = Polygon(loc(OUTER_MM)).buffer(0)
|
||||
ex_poly = Polygon(loc(EX_MM)).buffer(0)
|
||||
def _fill_slivers(g, tol=1e-3):
|
||||
if g.geom_type != 'Polygon': return g
|
||||
return Polygon(g.exterior, [h for h in g.interiors if Polygon(h).area > tol])
|
||||
ring = _fill_slivers(outer_poly.difference(inner_poly)) # 37 mm wall/haunch join leaves 1e-5 m2 slivers
|
||||
|
||||
CHORD_A, CHORD_B = P(8771, 15751), P(19481, 15567)
|
||||
_k = (CHORD_B[1] - CHORD_A[1]) / (CHORD_B[0] - CHORD_A[0])
|
||||
def chord_z(x): return CHORD_A[1] + _k * (x - CHORD_A[0])
|
||||
below_chord = Polygon([(-9, chord_z(-9)), (9, chord_z(9)), (9, -5), (-9, -5)])
|
||||
above_chord = Polygon([(-9, chord_z(-9)), (9, chord_z(9)), (9, 9), (-9, 9)])
|
||||
|
||||
XL = P(9526, 15764)[0]; XR = P(18726, 15580)[0]
|
||||
T_FIX = -0.02
|
||||
PAV_T = 0.28
|
||||
def pav_corners(t):
|
||||
return [(XL, t * XL), (XR, t * XR), (XR, t * XR - PAV_T), (XL, t * XL - PAV_T)]
|
||||
pav_fix = Polygon(pav_corners(T_FIX))
|
||||
|
||||
def rect_local(x0_mm, x1_mm, y0_mm, y1_mm):
|
||||
(a, b), (c, d) = P(x0_mm, y0_mm), P(x1_mm, y1_mm)
|
||||
return box(min(a, c), min(b, d), max(a, c), max(b, d))
|
||||
DRAIN_L = rect_local(9101, 9401, 14864, 15700)
|
||||
DRAIN_R = rect_local(18851, 19151, 14680, 15520)
|
||||
PIPE_L = (P(9251.3, 15064)[0], P(9251.3, 15064)[1], 0.150)
|
||||
PIPE_R = (P(19001.3, 14880)[0], P(19001.3, 14880)[1], 0.150)
|
||||
HBOX = rect_local(8171, 8471, 16051, 16411)
|
||||
IBOX = rect_local(19781, 20181, 15867, 16227)
|
||||
|
||||
lin_full = _fill_slivers(ring.intersection(above_chord))
|
||||
assert lin_full.geom_type == 'Polygon' and len(lin_full.interiors) == 0, f"lining topology: {lin_full.geom_type}"
|
||||
LINING_POLY = lin_full.simplify(0.01, preserve_topology=True) # 10 mm: removes 30 um edges (loft invalid) and cuts ~700 -> ~60 section vertices (STEP size)
|
||||
EXCAV_POLY = ex_poly.difference(outer_poly).intersection(box(-9, 0.0, 9, 9)).simplify(0.01, preserve_topology=True)
|
||||
assert EXCAV_POLY.geom_type == 'Polygon' and len(EXCAV_POLY.interiors) == 0, f"excavation topology: {EXCAV_POLY.geom_type}"
|
||||
base_fix = ring.intersection(below_chord).difference(pav_fix).difference(DRAIN_L).difference(DRAIN_R)
|
||||
assert base_fix.geom_type == 'Polygon' and len(base_fix.interiors) == 0, f"base topology: {base_fix.geom_type}"
|
||||
BASE_PTS_FIX = np.array(base_fix.exterior.coords)[:-1]
|
||||
def _find(pt):
|
||||
d = np.hypot(BASE_PTS_FIX[:, 0] - pt[0], BASE_PTS_FIX[:, 1] - pt[1])
|
||||
i = int(np.argmin(d)); assert d[i] < 1e-6, f"pavement corner not on base boundary: {d[i]}"
|
||||
return i
|
||||
IDX_BL = _find((XL, T_FIX * XL - PAV_T)); IDX_BR = _find((XR, T_FIX * XR - PAV_T))
|
||||
def base_pts(t):
|
||||
pts = BASE_PTS_FIX.copy()
|
||||
pts[IDX_BL, 1] = t * XL - PAV_T
|
||||
pts[IDX_BR, 1] = t * XR - PAV_T
|
||||
return pts
|
||||
LIN_PTS = np.array(LINING_POLY.exterior.coords)[:-1]
|
||||
EXC_PTS = np.array(EXCAV_POLY.exterior.coords)[:-1]
|
||||
def PIPE_PTS(cx, cz, r):
|
||||
return np.array([(cx + r * math.cos(2 * math.pi * k / PIPE_SEG), cz + r * math.sin(2 * math.pi * k / PIPE_SEG)) for k in range(PIPE_SEG)])
|
||||
DRAIN_L_PTS = np.array(DRAIN_L.exterior.coords)[:-1]
|
||||
DRAIN_R_PTS = np.array(DRAIN_R.exterior.coords)[:-1]
|
||||
HBOX_PTS = np.array(HBOX.exterior.coords)[:-1]
|
||||
IBOX_PTS = np.array(IBOX.exterior.coords)[:-1]
|
||||
|
||||
# ---------- alignment, profile ----------
|
||||
cn = np.array([[int(r["station_m"]), float(r["centre_x"]), float(r["centre_y"])]
|
||||
for r in csv.DictReader(open(C_CSV, encoding="utf-8"))])
|
||||
dg_d = {}
|
||||
for r in csv.DictReader(open(P_CSV, encoding="utf-8")):
|
||||
if float(r["across_m"]) >= -25:
|
||||
dg_d[int(r["station_anchor"])] = float(r["value"])
|
||||
dg_s = np.array(sorted(dg_d)); dg_v = np.array([dg_d[k] for k in dg_s])
|
||||
cn_s = cn[:, 0]
|
||||
def axis(s):
|
||||
x = np.interp(s, cn_s, cn[:, 1]); y = np.interp(s, cn_s, cn[:, 2])
|
||||
i = min(max(np.searchsorted(cn_s, s) - 1, 0), len(cn_s) - 2)
|
||||
dx, dy = cn[i + 1, 1] - cn[i, 1], cn[i + 1, 2] - cn[i, 2]
|
||||
L = math.hypot(dx, dy)
|
||||
return x, y, dx / L, dy / L
|
||||
ORX, ORY = axis(S0)[0], axis(S0)[1]
|
||||
def slope_pct(s):
|
||||
if s < 5105: return -2.0
|
||||
if s < 5165: return -2.0 + 4.0 * (s - 5105) / 60.0
|
||||
return 2.0
|
||||
def world(s, x, z):
|
||||
ax, ay, tx, ty = axis(s)
|
||||
nx, ny = -ty, tx
|
||||
zr = float(np.interp(s, dg_s, dg_v))
|
||||
return (ax - ORX + x * nx, ay - ORY + x * ny, zr + z)
|
||||
|
||||
# ---------- B-rep helpers ----------
|
||||
def loft(stations, pts2_fn):
|
||||
wires = []
|
||||
for s in stations:
|
||||
pts3 = [world(s, float(x), float(z)) for x, z in pts2_fn(s)]
|
||||
wires.append(Wire.make_polygon([Vector(*p) for p in pts3], close=True))
|
||||
sol = Solid.make_loft(wires, ruled=True)
|
||||
if not sol.is_valid: # end caps of lofts with near-degenerate edges; ShapeFix repairs them
|
||||
sol = sol.fix()
|
||||
return sol
|
||||
def stations_of(a, b, step=STEP_M):
|
||||
n = max(1, int(round((b - a) / step)))
|
||||
return [float(v) for v in np.linspace(a, b, n + 1)]
|
||||
def chunks():
|
||||
out = []; a = S0
|
||||
while a < S1:
|
||||
b = min(a + CHUNK_M, S1); out.append((a, b)); a = b
|
||||
return out
|
||||
|
||||
PAL = {"A": (42, 157, 143), "B-1": (231, 111, 81), "B-2": (244, 162, 97), "C-1": (233, 196, 106),
|
||||
"C-2": (138, 177, 125), "D-1": (38, 70, 83), "D-2": (100, 130, 160), "P-2": (190, 110, 140),
|
||||
"P-3": (150, 90, 60), "RP-1": (80, 170, 200), "WL-1": (200, 200, 120), "WL-2": (120, 90, 170),
|
||||
"개착부": (170, 170, 170)}
|
||||
COL = {"base": (141, 110, 99), "pav": (96, 125, 139), "drain": (30, 136, 229), "hi": (253, 216, 53),
|
||||
"clear": (255, 111, 0)}
|
||||
|
||||
runs = []
|
||||
for r in csv.DictReader(open(PAT_CSV, encoding="utf-8")):
|
||||
a, b = int(float(r["sta_from"])), int(float(r["sta_to"])); pat = r["lining_pattern"]
|
||||
a2, b2 = max(a, S0), min(b, S1)
|
||||
if b2 > a2: runs.append((a2, b2, pat))
|
||||
runs.sort()
|
||||
assert runs[0][0] == S0 and runs[-1][1] == S1, f"pattern coverage {runs[0][0]}..{runs[-1][1]}"
|
||||
for (a1, b1, _), (a2, _, _) in zip(runs, runs[1:]):
|
||||
assert b1 == a2, f"pattern gap/overlap at {b1} vs {a2}"
|
||||
|
||||
solids, meta_c, clear_wires = [], [], []
|
||||
def add(sol, cid, label, group, color, pattern=None, a=None, b=None):
|
||||
if not sol.is_valid:
|
||||
sol = sol.fix() # booleans on lofts can leave cap faces invalid
|
||||
assert sol.is_valid, f"invalid solid {cid}"
|
||||
sol.label = cid; sol.color = Color(*[c / 255 for c in color])
|
||||
solids.append(sol)
|
||||
meta_c.append({"id": cid, "label": label, "group": group, "pattern": pattern,
|
||||
"from_sta": a, "to_sta": b, "volume_m3": round(sol.volume, 4), "color": list(color)})
|
||||
|
||||
for i, (a, b, pat) in enumerate(runs):
|
||||
if MODE != 'parts_a' or not (RANGE[0] <= i < RANGE[1]): continue
|
||||
print('lining run', i + 1, pat, a, b, flush=True)
|
||||
lin_run = loft(stations_of(a, b), lambda s: LIN_PTS)
|
||||
lin_run = lin_run - loft(stations_of(a, b), lambda s: HBOX_PTS) - loft(stations_of(a, b), lambda s: IBOX_PTS)
|
||||
add(lin_run, f"LIN_{i+1:02d}_{pat}", f"라이닝 {pat} {a/1000:.3f}~{b/1000:.3f}",
|
||||
"라이닝", PAL[pat], pat, a, b)
|
||||
for i, (a, b, pat) in enumerate(runs):
|
||||
if MODE != 'parts_a' or not (RANGE[0] <= i < RANGE[1]): continue
|
||||
add(loft(stations_of(a, b), lambda s: EXC_PTS), f"EXC_{i+1:02d}_{pat}", f"굴착 여유대 {pat} {a/1000:.3f}~{b/1000:.3f}",
|
||||
"굴착면", (150, 120, 90), pat, a, b)
|
||||
for i, (a, b, pat) in enumerate(runs): # 기타 구성요소도 지보패턴 구간(run) 경계에서 분할
|
||||
if MODE != 'parts_b' or not (RANGE[0] <= i < RANGE[1]): continue
|
||||
sts = stations_of(a, b)
|
||||
rid = f"{i+1:02d}_{pat}"
|
||||
rng = f"{a/1000:.3f}~{b/1000:.3f}"
|
||||
add(loft(sts, lambda s: np.array(pav_corners(slope_pct(s) / 100.0))), f"PAV_{rid}", f"노면 계획고 포장 {pat} {rng}",
|
||||
"노면(계획고)", COL["pav"], pat, a, b)
|
||||
add(loft(sts, lambda s: base_pts(slope_pct(s) / 100.0)), f"BASE_{rid}", f"기층·인버트(가정) {pat} {rng}",
|
||||
"기층·인버트(가정)", COL["base"], pat, a, b)
|
||||
for side, dpts, pipe in (("L", DRAIN_L_PTS, PIPE_L), ("R", DRAIN_R_PTS, PIPE_R)):
|
||||
bx = loft(sts, lambda s, d=dpts: d)
|
||||
pp = loft(sts, lambda s, p=pipe: PIPE_PTS(p[0], p[1], p[2]))
|
||||
add(bx - pp, f"DRN{side}_{rid}", f"측구 {side} {pat} {rng}", "배수(측구)", COL["drain"], pat, a, b)
|
||||
add(loft(sts, lambda s: HBOX_PTS), f"HBOX_{rid}", f"공동구 H(가정) {pat} {rng}", "공동구(가정)", COL["hi"], pat, a, b)
|
||||
add(loft(sts, lambda s: IBOX_PTS), f"IBOX_{rid}", f"공동구 I(가정) {pat} {rng}", "공동구(가정)", COL["hi"], pat, a, b)
|
||||
|
||||
# 시설한계 50 mm reference lines (wires, not solids)
|
||||
clr = inner_poly.buffer(CLEAR_OFFSET).exterior
|
||||
clr_pts = [(x, z) for x, z in list(clr.coords)[:-1] if z >= chord_z(x) - 1e-9 and abs(x) <= 5.4]
|
||||
idxs = np.round(np.linspace(0, len(clr_pts) - 1, CLEAR_LINES)).astype(int)
|
||||
clear_lines = []
|
||||
sts_all = stations_of(S0, S1)
|
||||
for k, ii in enumerate(idxs):
|
||||
x0, z0 = clr_pts[ii]
|
||||
pts3 = [world(s, x0, z0) for s in sts_all]
|
||||
clear_lines.append(pts3)
|
||||
if MODE != 'parts_b' or RANGE[0] != 0:
|
||||
continue
|
||||
w = Wire.make_polygon([Vector(*p) for p in pts3], close=False)
|
||||
w.label = f"REF_CLEAR_{k+1}"; w.color = Color(1.0, 0.44, 0.0)
|
||||
clear_wires.append(w)
|
||||
meta_c.append({"id": f"REF_CLEAR_{k+1}", "label": f"시설한계 50 mm 참조선 {k+1}", "group": "시설한계(참조선)",
|
||||
"pattern": None, "from_sta": S0, "to_sta": S1, "volume_m3": 0, "color": list(COL["clear"]), "reference": True})
|
||||
|
||||
if MODE in ('parts_a', 'parts_b'):
|
||||
tag = f'lin_exc_{RANGE[0]:02d}' if MODE == 'parts_a' else f'oth_{RANGE[0]:02d}'
|
||||
export_step(Compound(children=solids + clear_wires, label=tag), f"{PARTS}/{tag}.step")
|
||||
json.dump(meta_c, open(f"{PARTS}/{tag}.json", 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
|
||||
print('parts written', tag, len(solids), 'solids', len(clear_wires), 'ref wires')
|
||||
sys.exit(0)
|
||||
# ---- post-processing modes (each call < 180 s, 4 GB RAM) ----
|
||||
import glob
|
||||
TAGS = sorted(os.path.basename(x)[:-5] for x in glob.glob(f"{PARTS}/*.json"))
|
||||
from build123d import Solid as _Solid
|
||||
if MODE == 'mesh': # python build_solids.py ROOT mesh <tag>
|
||||
tag = sys.argv[3]
|
||||
imp = import_step(f"{PARTS}/{tag}.step")
|
||||
items = [x for x in (imp.children or list(imp.solids())) if isinstance(x, _Solid)]
|
||||
pj = [m for m in json.load(open(f"{PARTS}/{tag}.json", encoding='utf-8')) if not m.get('reference')]
|
||||
assert len(items) == len(pj), f"{tag}: {len(items)} solids vs {len(pj)} meta"
|
||||
sc = trimesh.Scene(); tmp = tempfile.mkdtemp(prefix='stl_')
|
||||
for k, (sol, mm) in enumerate(zip(items, pj)):
|
||||
stl = os.path.join(tmp, f"s{k}.stl")
|
||||
export_stl(sol, stl, tolerance=0.002, angular_tolerance=0.2)
|
||||
m = trimesh.load(stl, force='mesh')
|
||||
m.visual.face_colors = np.tile(np.array(list(mm['color']) + [255], dtype=np.uint8), (len(m.faces), 1))
|
||||
sc.add_geometry(m, geom_name=mm['id'])
|
||||
sc.export(f"{PARTS}/{tag}.glb")
|
||||
print('mesh', tag, len(sc.geometry), 'geometries'); sys.exit(0)
|
||||
if MODE == 'step': # python build_solids.py ROOT step
|
||||
allitems = []
|
||||
for tag in TAGS:
|
||||
imp = import_step(f"{PARTS}/{tag}.step")
|
||||
allitems += list(imp.children or list(imp.solids()))
|
||||
os.makedirs(f"{ROOT}/model", exist_ok=True)
|
||||
export_step(Compound(children=allitems, label="본선 4+745~5+480 표준단면 모델"), f"{ROOT}/model/tunnel_solids.step")
|
||||
print('step merged', len(allitems), 'items', os.path.getsize(f"{ROOT}/model/tunnel_solids.step"), 'bytes'); sys.exit(0)
|
||||
if MODE == 'final': # python build_solids.py ROOT final
|
||||
meta_c = []
|
||||
for tag in TAGS:
|
||||
meta_c += json.load(open(f"{PARTS}/{tag}.json", encoding='utf-8'))
|
||||
scene = trimesh.Scene()
|
||||
for tag in TAGS:
|
||||
g = trimesh.load(f"{PARTS}/{tag}.glb")
|
||||
for name, geom in g.geometry.items():
|
||||
scene.add_geometry(geom, geom_name=name)
|
||||
glb_faces = {}
|
||||
for k, pts3 in enumerate(clear_lines): # 시설한계 reference ribbons (visual only; exact wires in STEP)
|
||||
P3 = np.array(pts3); V = []; F = []
|
||||
for i in range(len(P3)):
|
||||
d = P3[min(i + 1, len(P3) - 1)] - P3[max(i - 1, 0)]
|
||||
d = d / (np.linalg.norm(d) + 1e-12)
|
||||
w = np.cross(d, np.array([0, 0, 1.0])); w = w / (np.linalg.norm(w) + 1e-12) * CLEAR_RIB_W
|
||||
V += [P3[i] - w, P3[i] + w]
|
||||
for i in range(len(P3) - 1):
|
||||
a0, b0, c0, d0 = 2 * i, 2 * i + 1, 2 * i + 2, 2 * i + 3
|
||||
F += [[a0, c0, b0], [b0, c0, d0]]
|
||||
m = trimesh.Trimesh(vertices=np.array(V), faces=np.array(F), process=False)
|
||||
m.visual.face_colors = np.tile(np.array([255, 111, 0, 255], dtype=np.uint8), (len(m.faces), 1))
|
||||
scene.add_geometry(m, geom_name=f"REF_CLEAR_{k+1}")
|
||||
glb_path = f"{ROOT}/model/tunnel_solids.glb"
|
||||
scene.export(glb_path)
|
||||
step_path = f"{ROOT}/model/tunnel_solids.step"
|
||||
solid_meta = [x for x in meta_c if not x.get('reference')]
|
||||
meta = {
|
||||
"model": ["tunnel_solids.step (STEP B-rep, Rhino/Autodesk)", "tunnel_solids.glb (web viewer, same B-rep tessellated)"],
|
||||
"scope": f"{S0}~{S1} (4+745~5+480)", "units": "m",
|
||||
"origin_plan_xy": [round(ORX, 3), round(ORY, 3)],
|
||||
"sections": {"lining": "rev2: 라이닝 R6982/R7282, 하단 호 도면 끝점까지", "excavation": "굴착 여유대 R7402/R4574/R4819 (노면 위)",
|
||||
"road": "계획고 노면 포장 280 mm, 측점별 편경사 S (노면만)", "base": "기층·인버트: 가정 (도면 미표현)",
|
||||
"drains": "측구 300x836 mm, 관 r150", "hi": "H/I 박스: 공동구 가정(사용자 결정)", "clearance": "시설한계 50 mm 참조선 (솔리드 아님)"},
|
||||
"assumptions": ["기층·인버트 형상·두께는 가정 (도면 미표현, 사용자 결정 '가정하여 생성')",
|
||||
"H/I 박스 = 공동구 가정 (사용자 결정)",
|
||||
"편경사 부호 z = (S/100)·x, x = 접선 좌측 법선 (부호 확인 필요)",
|
||||
"라이닝·굴착 여유대는 대칭 (편경사 미적용)",
|
||||
"측점 간 축선은 선형 보간, 단면은 10 m 간격 Ruled loft (곡선부 근사)",
|
||||
"라이닝 H/I 박스 제외 부분은 3D 불리언 차감 (공동구 가정)"],
|
||||
"pattern_runs": [{"id": f"LIN_{i+1:02d}_{p}", "pattern": p, "from": a, "to": b} for i, (a, b, p) in enumerate(runs)],
|
||||
"components": meta_c,
|
||||
"open": ["E-1, E-2, RP-2, WL-3 패턴 배치 [미확정] (당진 표 밖)", "NP-5BE-1-WL-4 해석 [확인 필요]",
|
||||
"04 공동구·배수 도면 상세 미반영 (측구·공동구 치수는 03-001 단면 기준)", "편경사 부호(좌·우) 확인 필요"]
|
||||
}
|
||||
json.dump(meta, open(f"{ROOT}/model/tunnel_solids_meta.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
ver = {"solid_count": len(solid_meta), "runs": len(runs), "pattern_coverage": f"{runs[0][0]}..{runs[-1][1]} contiguous",
|
||||
"lining_runs": sum(1 for x in solid_meta if x['group'] == '라이닝'),
|
||||
"excavation_runs": sum(1 for x in solid_meta if x['group'] == '굴착면'),
|
||||
"volume_sum_m3": round(sum(x['volume_m3'] for x in solid_meta), 3),
|
||||
"step_bytes": os.path.getsize(step_path), "glb_bytes": os.path.getsize(glb_path),
|
||||
"glb_geometries": len(trimesh.load(glb_path).geometry)}
|
||||
def _overlap(sample):
|
||||
base = Polygon(base_pts(slope_pct(sample) / 100.0)); pav = Polygon(pav_corners(slope_pct(sample) / 100.0))
|
||||
return {"lining_vs_excavation": round(LINING_POLY.intersection(EXCAV_POLY).area, 9),
|
||||
"base_vs_pavement": round(base.intersection(pav).area, 9),
|
||||
"lining_vs_base": round(LINING_POLY.intersection(base).area, 9),
|
||||
"pavement_vs_lining": round(pav.intersection(LINING_POLY).area, 9),
|
||||
"base_vs_drainL": round(base.intersection(DRAIN_L).area, 9),
|
||||
"base_vs_drainR": round(base.intersection(DRAIN_R).area, 9)}
|
||||
ver["section_overlap_m2_by_station"] = {str(st): _overlap(st) for st in [4745, 5105, 5165, 5480]}
|
||||
json.dump(ver, open(f"{ROOT}/data/verify_solids.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
print(json.dumps(ver, ensure_ascii=False, indent=1)); sys.exit(0)
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare two plan sheets layer-by-layer and list elevation-like numeric
|
||||
texts (decimal values in 0..1000) per layer, to locate ground levels (지반고).
|
||||
Read-only on the source DXF."""
|
||||
import collections
|
||||
import re
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
|
||||
num = re.compile(r"^-?\d{1,4}\.\d{1,3}$")
|
||||
|
||||
|
||||
def scan(path):
|
||||
msp = ezdxf.readfile(path).modelspace()
|
||||
layers = collections.Counter(e.dxf.layer for e in msp)
|
||||
el = collections.defaultdict(list)
|
||||
for e in msp.query("TEXT"):
|
||||
s = e.dxf.text.strip()
|
||||
if num.match(s) and 0 <= float(s) <= 1000:
|
||||
el[e.dxf.layer].append((round(e.dxf.insert.x, 1), round(e.dxf.insert.y, 1), s))
|
||||
return layers, el
|
||||
|
||||
la, ea = scan(sys.argv[1])
|
||||
lb, eb = scan(sys.argv[2])
|
||||
print("layers only in 001:", sorted(set(la) - set(lb)))
|
||||
print("layers only in 002:", sorted(set(lb) - set(la)))
|
||||
for lay in sorted(set(la) | set(lb)):
|
||||
if la[lay] != lb[lay]:
|
||||
print(f" count diff {lay}: 001={la[lay]} 002={lb[lay]}")
|
||||
print("== elevation-like numeric texts (layer: n, sample)")
|
||||
for name, el in (("001", ea), ("002", eb)):
|
||||
for lay, items in sorted(el.items(), key=lambda kv: -len(kv[1])):
|
||||
print(f" {name} {lay}: n={len(items)} sample={sorted(items)[:6]}")
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Task 4: dump the variable table of the standard section (C0060303-001) as a
|
||||
coordinate grid. Rows = cross slope (S, 편경사) per user answer 2026-10-21; the
|
||||
first row = variable names. Every TEXT on the table layers is kept (no regex
|
||||
filter), so that the S column and the header row can be checked."""
|
||||
import csv
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
import ezdxf
|
||||
|
||||
SRC, OUT = sys.argv[1], sys.argv[2]
|
||||
msp = ezdxf.readfile(SRC).modelspace()
|
||||
cells = [(round(e.dxf.insert.x, 1), round(e.dxf.insert.y, 1), e.dxf.text.strip())
|
||||
for e in msp.query("TEXT") if e.dxf.layer in ("CS-TABL-TEX1", "CS-TABL-TEX2")]
|
||||
rows = defaultdict(list)
|
||||
for x, y, s in cells:
|
||||
rows[round(y / 50) * 50].append((x, s)) # bucket by ~50 mm
|
||||
with open(OUT, "w", newline="", encoding="utf-8") as fh:
|
||||
w = csv.writer(fh)
|
||||
for y in sorted(rows, reverse=True):
|
||||
line = [s for x, s in sorted(rows[y])]
|
||||
w.writerow([y] + line)
|
||||
print(f"Y{y:>7}: " + " | ".join(f"{x:.0f}:{s}" for x, s in sorted(rows[y])))
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Task 3 exploration: layer usage and long polylines / text clusters in a
|
||||
plan-profile DXF, to locate the tunnel centreline (본선 축선) candidates."""
|
||||
import collections
|
||||
import json
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
|
||||
path = sys.argv[1]
|
||||
doc = ezdxf.readfile(path)
|
||||
msp = doc.modelspace()
|
||||
by_layer = collections.Counter(e.dxf.layer for e in msp)
|
||||
print("layers used:", len(by_layer))
|
||||
for name, n in by_layer.most_common(25):
|
||||
print(f" {n:6d} {name}")
|
||||
# long polylines = candidate alignment / outline strings
|
||||
polys = []
|
||||
for e in msp:
|
||||
if e.dxftype() == "LWPOLYLINE":
|
||||
pts = list(e.get_points("xy"))
|
||||
if len(pts) >= 20:
|
||||
xs = [p[0] for p in pts]; ys = [p[1] for p in pts]
|
||||
polys.append({"layer": e.dxf.layer, "n": len(pts),
|
||||
"x": [round(min(xs), 1), round(max(xs), 1)],
|
||||
"y": [round(min(ys), 1), round(max(ys), 1)]})
|
||||
polys.sort(key=lambda d: -d["n"])
|
||||
print("long LWPOLYLINE (>=20 vertices):", len(polys))
|
||||
for p in polys[:12]:
|
||||
print(" ", p)
|
||||
# text that looks like station (STA) or level (EL) labels
|
||||
import re
|
||||
pat = re.compile(r"(STA|No\.?|EL|E\.L|\d+\+\d+|\+\d{2,3}\.\d{3}|\d{2,3}\.\d{2,3})", re.I)
|
||||
hits = []
|
||||
for e in msp:
|
||||
if e.dxftype() in ("TEXT", "MTEXT"):
|
||||
s = e.dxf.text if e.dxftype() == "TEXT" else e.plain_text()
|
||||
if s and pat.search(s):
|
||||
loc = e.dxf.insert if e.dxftype() == "TEXT" else e.dxf.insert
|
||||
hits.append((round(loc.x, 1), round(loc.y, 1), s.strip()[:40]))
|
||||
print("station/level-like texts:", len(hits))
|
||||
for h in sorted(hits)[:25]:
|
||||
print(" ", h)
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Explore the concrete-lining structure drawings (04-004 .. 04-009).
|
||||
|
||||
Read-only on the source RAG folder. For each DXF prints layers with entity
|
||||
counts, INSERT block names, dimension measurements and every text string with
|
||||
its position, and writes a JSON dump for later use.
|
||||
Usage: explore_lining.py <RAG_04_DIR> <OUT_JSON> [max_texts_per_file]
|
||||
"""
|
||||
import collections
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
from ezdxf import recover
|
||||
|
||||
RAG_04 = sys.argv[1]
|
||||
OUT_JSON = sys.argv[2]
|
||||
MAX_TEXT = int(sys.argv[3]) if len(sys.argv) > 3 else 400
|
||||
|
||||
|
||||
def load(path):
|
||||
try:
|
||||
return ezdxf.readfile(path), None
|
||||
except Exception as exc:
|
||||
try:
|
||||
doc, _ = recover.readfile(path)
|
||||
return doc, f"recovered after strict error: {type(exc).__name__}"
|
||||
except Exception as exc2:
|
||||
return None, f"{type(exc).__name__} | recover: {type(exc2).__name__}: {exc2}"
|
||||
|
||||
|
||||
def explore(path):
|
||||
rec = {"file": os.path.basename(path)}
|
||||
doc, note = load(path)
|
||||
if doc is None:
|
||||
rec["error"] = note
|
||||
return rec
|
||||
rec["note"] = note
|
||||
msp = doc.modelspace()
|
||||
layers = collections.Counter()
|
||||
counts = collections.Counter()
|
||||
for e in msp:
|
||||
counts[e.dxftype()] += 1
|
||||
layers[e.dxf.layer] += 1
|
||||
rec["layers"] = dict(layers.most_common())
|
||||
rec["entity_counts"] = dict(counts.most_common())
|
||||
rec["inserts"] = dict(collections.Counter(e.dxf.name for e in msp if e.dxftype() == "INSERT").most_common(30))
|
||||
dims, texts = [], []
|
||||
for e in msp:
|
||||
t = e.dxftype()
|
||||
if t == "DIMENSION":
|
||||
try:
|
||||
val = round(float(e.get_measurement()), 3)
|
||||
except Exception:
|
||||
val = None
|
||||
dims.append({"layer": e.dxf.layer, "value": val, "text": e.dxf.get("text", "")})
|
||||
elif t in ("TEXT", "MTEXT"):
|
||||
s = e.dxf.text if t == "TEXT" else e.plain_text()
|
||||
if s and s.strip():
|
||||
pos = e.dxf.insert
|
||||
texts.append({"layer": e.dxf.layer, "x": round(pos.x, 2), "y": round(pos.y, 2),
|
||||
"text": s.strip().replace("\n", " | ")[:200]})
|
||||
rec["dimensions"] = dims[:200]
|
||||
rec["texts_total"] = len(texts)
|
||||
rec["texts"] = texts[:MAX_TEXT]
|
||||
return rec
|
||||
|
||||
|
||||
def main():
|
||||
files = sorted(glob.glob(os.path.join(RAG_04, "C0060304-00[4-9]*.dxf")))
|
||||
out = [explore(f) for f in files]
|
||||
with open(OUT_JSON, "w", encoding="utf-8") as fh:
|
||||
json.dump(out, fh, ensure_ascii=False, indent=1)
|
||||
for r in out:
|
||||
print("==", r["file"])
|
||||
if "error" in r:
|
||||
print(" ERROR", r["error"])
|
||||
continue
|
||||
print(" note:", r["note"])
|
||||
print(" entities:", r["entity_counts"])
|
||||
print(" layers:", dict(list(r["layers"].items())[:25]))
|
||||
print(" inserts:", r["inserts"])
|
||||
print(" dims:", len(r["dimensions"]), "texts:", r["texts_total"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Task 4 exploration: layers, arcs (radius + centre) and dimension-like texts
|
||||
of the standard section drawing (C0060303-001). Read-only."""
|
||||
import collections
|
||||
import re
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
from ezdxf import bbox
|
||||
|
||||
msp = ezdxf.readfile(sys.argv[1]).modelspace()
|
||||
print("layers:", collections.Counter(e.dxf.layer for e in msp).most_common(12))
|
||||
ext = bbox.extents(msp)
|
||||
print("extents:", [round(v, 1) for v in ext.extmin], [round(v, 1) for v in ext.extmax])
|
||||
print("== ARCs (layer, centre, radius, start/end deg) sorted by radius")
|
||||
arcs = sorted(msp.query("ARC"), key=lambda e: -e.dxf.radius)
|
||||
for e in arcs[:14]:
|
||||
c = e.dxf.center
|
||||
print(f" {e.dxf.layer:14s} c=({c.x:9.2f},{c.y:9.2f}) R={e.dxf.radius:9.2f} {e.dxf.start_angle:7.1f}->{e.dxf.end_angle:7.1f}")
|
||||
print("== R-labels and numeric dims")
|
||||
pat = re.compile(r"^(R\d'?|\d{2,5}(\.\d+)?|\d+\.\d+%?|-?\d+\.\d+)$")
|
||||
for e in msp.query("TEXT"):
|
||||
s = e.dxf.text.strip()
|
||||
if pat.match(s) or s.startswith("R"):
|
||||
print(f" {e.dxf.layer:14s} ({e.dxf.insert.x:9.2f},{e.dxf.insert.y:9.2f}) {s}")
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Task 3 (HITL draft): main-line centreline station table from a plan sheet.
|
||||
|
||||
Method (layers CA-ALGN-TEXT labels and CA-ALGN-STSM station ticks):
|
||||
1. Station labels: 'N+mmm' is explicit; bare numbers take the km of the next
|
||||
explicit label when they precede it (700 before 5+000 -> 4+700), otherwise
|
||||
the km of the previous explicit label.
|
||||
2. Each station has two labels (one per side of the road). Each label is
|
||||
paired with its nearest tick; the centreline point is the midpoint of the
|
||||
two tick points.
|
||||
3. Checks reported (not forced): side offset per station, and consecutive
|
||||
centreline distance vs station difference (residual).
|
||||
DRAFT for HITL review. No value is adjusted to fit."""
|
||||
import csv
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
import ezdxf
|
||||
|
||||
SRC, OUT = sys.argv[1], sys.argv[2]
|
||||
msp = ezdxf.readfile(SRC).modelspace()
|
||||
explicit_re = re.compile(r"^(\d+)\+(\d{3})$")
|
||||
|
||||
labels = [(e.dxf.insert.x, e.dxf.insert.y, e.dxf.text.strip())
|
||||
for e in msp.query("TEXT") if e.dxf.layer == "CA-ALGN-TEXT"]
|
||||
ticks = []
|
||||
for e in msp.query("LINE"):
|
||||
if e.dxf.layer == "CA-ALGN-STSM":
|
||||
a, b = e.dxf.start, e.dxf.end
|
||||
ticks.append(((a.x + b.x) / 2, (a.y + b.y) / 2))
|
||||
|
||||
labels = [l for l in labels if explicit_re.match(l[2]) or l[2].isdigit()]
|
||||
labels.sort(key=lambda t: -t[1]) # along the sheet, NNE -> SSW
|
||||
explicit_idx = [i for i, l in enumerate(labels) if explicit_re.match(l[2])]
|
||||
station_of = {}
|
||||
for i, (x, y, s) in enumerate(labels):
|
||||
if explicit_re.match(s):
|
||||
m = explicit_re.match(s)
|
||||
station_of[i] = int(m.group(1)) * 1000 + int(m.group(2))
|
||||
continue
|
||||
v = int(s)
|
||||
nxt = next((j for j in explicit_idx if j > i), None)
|
||||
prv = next((j for j in reversed(explicit_idx) if j < i), None)
|
||||
if prv is not None:
|
||||
km = int(explicit_re.match(labels[prv][2]).group(1))
|
||||
else:
|
||||
km = int(explicit_re.match(labels[nxt][2]).group(1)) - 1
|
||||
station_of[i] = km * 1000 + v
|
||||
|
||||
groups = defaultdict(list)
|
||||
for i, (x, y, s) in enumerate(labels):
|
||||
groups[station_of[i]].append((x, y))
|
||||
|
||||
|
||||
def nearest_tick(pt):
|
||||
return min(ticks, key=lambda t: math.hypot(t[0] - pt[0], t[1] - pt[1]))
|
||||
|
||||
centre = []
|
||||
for sta in sorted(groups):
|
||||
pts = groups[sta]
|
||||
if len(pts) != 2:
|
||||
continue
|
||||
t1, t2 = nearest_tick(pts[0]), nearest_tick(pts[1])
|
||||
side_gap = math.dist(t1, t2)
|
||||
mid = ((t1[0] + t2[0]) / 2, (t1[1] + t2[1]) / 2)
|
||||
centre.append((sta, mid[0], mid[1], side_gap))
|
||||
|
||||
with open(OUT, "w", newline="", encoding="utf-8") as fh:
|
||||
w = csv.writer(fh)
|
||||
w.writerow(["station_m", "centre_x", "centre_y", "side_gap_m"])
|
||||
for sta, x, y, g in centre:
|
||||
w.writerow([sta, round(x, 3), round(y, 3), round(g, 3)])
|
||||
|
||||
print(f"stations={len(centre)} (labels={len(labels)}, ticks={len(ticks)})")
|
||||
prev = None
|
||||
for sta, x, y, g in centre:
|
||||
step = "" if prev is None else f" d={math.dist(prev[1:3], (x, y)):8.3f} dSTA={sta - prev[0]:5d} res={math.dist(prev[1:3], (x, y)) - (sta - prev[0]):+.3f}"
|
||||
print(f" STA {sta:>5} ({x:12.3f}, {y:12.3f}) side_gap={g:6.2f}{step}")
|
||||
prev = (sta, x, y)
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract structured lining-reinforcement data from the concrete-lining
|
||||
structure drawings 04-004..009 (read-only on RAG).
|
||||
|
||||
Per sheet: pattern groups and their detail kind (입출구부 50 m / 온도미고려),
|
||||
note-5 scope sentence, concrete and rebar grade, rebar sizes present,
|
||||
radius and lap dimensions as printed, EL datum texts.
|
||||
Output: data/lining_spec.json (+ printed summary).
|
||||
Usage: extract_lining_spec.py <RAG_04_DIR> <OUT_JSON>
|
||||
"""
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
from ezdxf import recover
|
||||
|
||||
RAG_04, OUT = sys.argv[1], sys.argv[2]
|
||||
BAR_RE = re.compile(r"^0?H(\d{2})$")
|
||||
PAT_RE = re.compile(r"\(([^()]*?)\s*패턴\)")
|
||||
GROUP_RE = re.compile(r"주철근 조립도\(([^()]*?)\s*패턴\)")
|
||||
|
||||
|
||||
def load(path):
|
||||
try:
|
||||
return ezdxf.readfile(path)
|
||||
except Exception:
|
||||
doc, _ = recover.readfile(path)
|
||||
return doc
|
||||
|
||||
|
||||
def texts_of(doc):
|
||||
out = []
|
||||
for e in doc.modelspace():
|
||||
if e.dxftype() == "TEXT":
|
||||
s = e.dxf.text
|
||||
elif e.dxftype() == "MTEXT":
|
||||
s = e.plain_text()
|
||||
else:
|
||||
continue
|
||||
if s and s.strip():
|
||||
out.append((e.dxf.layer, round(e.dxf.insert.x, 1), round(e.dxf.insert.y, 1), s.strip()))
|
||||
return out
|
||||
|
||||
|
||||
def sheet_no(name):
|
||||
return int(re.search(r"C0060304-(\d{3})", name).group(1))
|
||||
|
||||
|
||||
def main():
|
||||
files = sorted(glob.glob(os.path.join(RAG_04, "C0060304-00[4-9]*.dxf")))
|
||||
sheets = []
|
||||
for f in files:
|
||||
doc = load(f)
|
||||
tx = texts_of(doc)
|
||||
rec = {"sheet": sheet_no(f), "file": os.path.basename(f)}
|
||||
# pattern groups: detail kind from the text itself
|
||||
groups = []
|
||||
for layer, x, y, s in tx:
|
||||
if layer != "CS-TABL-TEX1":
|
||||
continue
|
||||
if "패턴" in s:
|
||||
kind = "입출구부 50m" if "입출구부" in s else ("온도미고려" if "온도미고려" in s else "기타")
|
||||
m = PAT_RE.search(s)
|
||||
groups.append({"group": m.group(1) if m else None, "kind": kind, "raw": s})
|
||||
rec["pattern_details"] = groups
|
||||
# scope note 5 (full line, joined with the continuation line)
|
||||
note5 = [s for _, _, _, s in tx if s.startswith("5. 본 콘크리트 라이닝 구조도")]
|
||||
cont = [s for _, _, _, s in tx if s.startswith("설계조건과 상이할 경우")]
|
||||
rec["note5"] = (note5[0] + " " + (cont[0] if cont else "")).strip() if note5 else None
|
||||
rec["concrete"] = sorted({s for _, _, _, s in tx if "fck" in s})
|
||||
rec["rebar_grade"] = sorted({s for _, _, _, s in tx if "fy=" in s})
|
||||
rec["aggregate"] = sorted({s for _, _, _, s in tx if "최대치수" in s})
|
||||
bars = {}
|
||||
for layer, x, y, s in tx:
|
||||
m = BAR_RE.match(s)
|
||||
if m:
|
||||
bars[f"H{m.group(1)}"] = bars.get(f"H{m.group(1)}", 0) + 1
|
||||
rec["rebar_sizes_label_count"] = dict(sorted(bars.items()))
|
||||
rec["radius_dims"] = sorted({s for layer, _, _, s in tx if layer == "CS-DIMT" and re.match(r"R\d?'?=", s)})
|
||||
rec["lap_dims"] = sorted({s for layer, _, _, s in tx if layer == "CS-DIMT" and "mm" in s and "이음장" not in s and "600" in s})
|
||||
rec["el_texts"] = sorted({s for _, _, _, s in tx if s.startswith("EL")})
|
||||
rec["entrance_exit_50m"] = any("입출구부 50m" in s for _, _, _, s in tx)
|
||||
sheets.append(rec)
|
||||
with open(OUT, "w", encoding="utf-8") as fh:
|
||||
json.dump({"source": "C0060304-004..009 (콘크리트라이닝구조도)", "sheets": sheets},
|
||||
fh, ensure_ascii=False, indent=1)
|
||||
for r in sheets:
|
||||
print("==", r["sheet"], "| groups:", [(g["group"], g["kind"]) for g in r["pattern_details"]])
|
||||
print(" note5:", (r["note5"] or "")[:140])
|
||||
print(" concrete:", r["concrete"], "| rebar:", r["rebar_grade"], "| agg:", r["aggregate"])
|
||||
print(" bars:", r["rebar_sizes_label_count"], "| R:", r["radius_dims"], "| EL:", r["el_texts"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Task 5: support-pattern (지보패턴) station ranges from drawings 03-003..008.
|
||||
Collects every TEXT that names a station (STA. / N+mmm / N+mmm.mm) and every
|
||||
pattern name (NP-...) with its layer and position, without interpreting ranges."""
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
|
||||
RAG = sys.argv[1]
|
||||
pat_re = re.compile(r"NP-\d[A-Z0-9-]*|WL-\d")
|
||||
sta_re = re.compile(r"(STA\.?\s*)?\d{1,2}\+\d{2,3}(\.\d{2})?")
|
||||
files = sorted(glob.glob(os.path.join(RAG, "03_본선터널굴착", "C0060303-00[3-8]*.dxf")))
|
||||
for f in files:
|
||||
msp = ezdxf.readfile(f).modelspace()
|
||||
stas, pats = [], set()
|
||||
for e in msp.query("TEXT MTEXT"):
|
||||
s = e.dxf.text if e.dxftype() == "TEXT" else e.plain_text()
|
||||
for m in sta_re.finditer(s):
|
||||
stas.append((round(e.dxf.insert.x), round(e.dxf.insert.y), m.group(0).strip()))
|
||||
for m in pat_re.finditer(s):
|
||||
pats.add(m.group(0))
|
||||
print(os.path.basename(f)[:28], "| patterns:", sorted(pats))
|
||||
for x, y, t in sorted(stas, key=lambda r: (r[0], r[1]))[:12]:
|
||||
print(f" ({x},{y}) {t}")
|
||||
print(f" station-like texts: {len(stas)}")
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract the 당진방향 station pattern table from 참고_지보패턴현황 (read-only).
|
||||
Rows 55..96 of the sheet: C=start STA, G=end STA, J=length, M=lining pattern
|
||||
(지보패턴), O=실시설계 excavation class (blank = continues the row above),
|
||||
Q=굴착공법, T=비고. Also extracts the 대산방향 table (rows 13..49) for reference.
|
||||
Usage: extract_pattern_table.py <xlsx> <out_dir>"""
|
||||
import csv, os, sys
|
||||
import openpyxl
|
||||
|
||||
SRC, OUT = sys.argv[1], sys.argv[2]
|
||||
wb = openpyxl.load_workbook(SRC, read_only=True, data_only=True)
|
||||
ws = wb["참고_지보패턴현황"]
|
||||
rows = {r: row for r, row in enumerate(ws.iter_rows(min_row=1, max_row=114, max_col=33, values_only=True), start=1)}
|
||||
|
||||
|
||||
def table(r0, r1, tag):
|
||||
out, last_o = [], None
|
||||
for r in range(r0, r1 + 1):
|
||||
row = rows[r]
|
||||
try:
|
||||
a, b = float(row[2]), float(row[6])
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
m = (row[12] or "").strip() if isinstance(row[12], str) else row[12]
|
||||
o_raw = row[14]
|
||||
o = o_raw if o_raw not in (None, "") else last_o
|
||||
last_o = o
|
||||
remark = (row[19] or "") if isinstance(row[19], str) else ""
|
||||
out.append({"direction": tag, "source_row": r, "sta_from": a, "sta_to": b,
|
||||
"length_m": row[9], "lining_pattern": m or ("개착부" if "개착" in remark else ""),
|
||||
"excavation": o or "", "excavation_inherited": o_raw in (None, ""),
|
||||
"method": (row[16] or "") if isinstance(row[16], str) else "",
|
||||
"remark": remark})
|
||||
return out
|
||||
|
||||
|
||||
south = table(55, 96, "당진")
|
||||
north = table(13, 49, "대산")
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
cols = list(south[0].keys())
|
||||
for name, data in [("pattern_stations_dangjin.csv", south), ("pattern_stations_daesan_ref.csv", north)]:
|
||||
with open(os.path.join(OUT, name), "w", encoding="utf-8", newline="") as fh:
|
||||
w = csv.DictWriter(fh, fieldnames=cols)
|
||||
w.writeheader()
|
||||
w.writerows(data)
|
||||
print("dangjin rows", len(south), "range", south[0]["sta_from"], "->", south[-1]["sta_to"])
|
||||
print("daesan rows", len(north), "range", north[0]["sta_from"], "->", north[-1]["sta_to"])
|
||||
print("blank lining pattern rows (dangjin):", [(d["source_row"], d["remark"]) for d in south if not d["lining_pattern"]])
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Task 3 (user-confirmed reading): ground-level and design-grade values per
|
||||
20 m station from sheet 02-002 (당진방향).
|
||||
|
||||
Confirmed by the user (2026-10-21): decimal texts in CR-TABL-TEX2 are 지반고 (GL),
|
||||
sheet 002 values used; the ruler 740.00..980.00 is chainage 4+740..4+980, and
|
||||
0.00 is 5+000 (ruler continues in 20 m steps).
|
||||
Method: ruler texts give anchors (chainage = 4000+v for 740..980; 5000+v after 0.00).
|
||||
Each decimal text is projected onto the ruler axis; its station is the nearest anchor.
|
||||
Two decimal series appear at each station (|across| ~ 17-21 and ~ 32-39 m);
|
||||
the series names are NOT assigned here (HITL). Checks are reported, not forced."""
|
||||
import csv
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
|
||||
SRC, OUT = sys.argv[1], sys.argv[2]
|
||||
msp = ezdxf.readfile(SRC).modelspace()
|
||||
texts = [(e.dxf.insert.x, e.dxf.insert.y, e.dxf.text.strip())
|
||||
for e in msp.query("TEXT") if e.dxf.layer == "CR-TABL-TEX2"]
|
||||
|
||||
ruler = {}
|
||||
for x, y, s in texts:
|
||||
if re.fullmatch(r"\d{1,3}\.00", s):
|
||||
ruler[int(s.split(".")[0])] = (x, y)
|
||||
anchors = {}
|
||||
for v in range(740, 1000, 20):
|
||||
if v in ruler:
|
||||
anchors[4000 + v] = ruler[v]
|
||||
for v in range(0, 1000, 20):
|
||||
if v in ruler:
|
||||
anchors[5000 + v] = ruler[v]
|
||||
else:
|
||||
break
|
||||
stas = sorted(anchors)
|
||||
print("anchors:", len(stas), "range", stas[0], "..", stas[-1])
|
||||
gaps = [(stas[i + 1] - stas[i], round(math.dist(anchors[stas[i]], anchors[stas[i + 1]]), 2))
|
||||
for i in range(len(stas) - 1)]
|
||||
print("consecutive (dSTA, chord m):", gaps)
|
||||
|
||||
A0, A1 = anchors[stas[0]], anchors[stas[-1]]
|
||||
L = math.dist(A0, A1)
|
||||
u = ((A1[0] - A0[0]) / L, (A1[1] - A0[1]) / L)
|
||||
v_ = (-u[1], u[0])
|
||||
rows = []
|
||||
for x, y, s in texts:
|
||||
if not re.fullmatch(r"\d{1,3}\.\d{3}", s):
|
||||
continue
|
||||
dx, dy = x - A0[0], y - A0[1]
|
||||
along = dx * u[0] + dy * u[1]
|
||||
across = dx * v_[0] + dy * v_[1]
|
||||
if not (-5.0 <= along <= L + 5.0 and abs(across) < 60):
|
||||
continue # outside the ruler band (table noise)
|
||||
sta_est = stas[0] + along * (stas[-1] - stas[0]) / L
|
||||
near = min(stas, key=lambda k: abs(k - sta_est))
|
||||
rows.append((near, round(sta_est, 1), round(across, 2), float(s), round(x, 2), round(y, 2)))
|
||||
|
||||
with open(OUT, "w", newline="", encoding="utf-8") as fh:
|
||||
w = csv.writer(fh)
|
||||
w.writerow(["station_anchor", "est_station", "across_m", "value", "x", "y"])
|
||||
w.writerows(sorted(rows))
|
||||
per = {}
|
||||
for near, est, acr, val, _, _ in rows:
|
||||
per.setdefault(near, []).append((acr, val))
|
||||
for k in sorted(per):
|
||||
print(f"STA {k}: " + " ".join(f"[{a:+.1f}m {val:.3f}]" for a, val in sorted(per[k])))
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Inventory the main-line (본선) DXF drawings: header, layers, entity counts,
|
||||
extents, 3D flag and text samples. Read-only on the source RAG folder."""
|
||||
import collections
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
from ezdxf import bbox, recover
|
||||
|
||||
RAG_DIR = sys.argv[1]
|
||||
OUT_JSON = sys.argv[2]
|
||||
# 본선 scope (approved 2026-10-21): 02 평면·종단, 03 본선굴착, 04 본선기타
|
||||
SUBDIRS = ["02_평면 및 종단면도", "03_본선터널굴착", "04_본선터널기타"]
|
||||
|
||||
|
||||
def inventory(path):
|
||||
rec = {"file": os.path.basename(path), "ok": False}
|
||||
rec["recovered"] = False
|
||||
try:
|
||||
doc = ezdxf.readfile(path)
|
||||
except Exception as exc: # strict load failed: retry in recover mode and record it
|
||||
strict_error = f"{type(exc).__name__}: {exc}"
|
||||
try:
|
||||
doc, _auditor = recover.readfile(path)
|
||||
rec["recovered"] = True
|
||||
rec["strict_error"] = strict_error
|
||||
except Exception as exc2:
|
||||
rec["error"] = f"{strict_error} | recover: {type(exc2).__name__}: {exc2}"
|
||||
return rec
|
||||
hdr = doc.header
|
||||
rec["dxfversion"] = doc.dxfversion
|
||||
rec["insunits"] = hdr.get("$INSUNITS")
|
||||
rec["codepage"] = hdr.get("$DWGCODEPAGE")
|
||||
rec["layers"] = len(doc.layers)
|
||||
msp = doc.modelspace()
|
||||
counts = collections.Counter(e.dxftype() for e in msp)
|
||||
rec["entity_counts"] = dict(counts.most_common())
|
||||
rec["blocks"] = len([b for b in doc.blocks if not b.name.startswith("*")])
|
||||
z_vals = []
|
||||
texts = []
|
||||
for e in msp:
|
||||
t = e.dxftype()
|
||||
if t == "LINE":
|
||||
z_vals += [e.dxf.start.z, e.dxf.end.z]
|
||||
elif t in ("TEXT", "MTEXT"):
|
||||
s = e.dxf.text if t == "TEXT" else e.plain_text()
|
||||
if s and len(texts) < 40:
|
||||
texts.append(s.strip()[:80])
|
||||
elif t in ("POINT", "INSERT", "CIRCLE", "ARC"):
|
||||
z_vals.append(e.dxf.location.z if t == "POINT" else e.dxf.insert.z if t == "INSERT" else e.dxf.center.z)
|
||||
rec["z_min"] = round(min(z_vals), 3) if z_vals else None
|
||||
rec["z_max"] = round(max(z_vals), 3) if z_vals else None
|
||||
rec["has_3d_geometry"] = bool(z_vals) and (max(z_vals) - min(z_vals)) > 1e-6
|
||||
try:
|
||||
ext = bbox.extents(msp)
|
||||
rec["extents_min"] = [round(v, 2) for v in ext.extmin]
|
||||
rec["extents_max"] = [round(v, 2) for v in ext.extmax]
|
||||
except Exception as exc:
|
||||
rec["extents_error"] = str(exc)
|
||||
rec["text_sample"] = texts
|
||||
rec["ok"] = True
|
||||
return rec
|
||||
|
||||
|
||||
def main():
|
||||
paths = []
|
||||
for sub in SUBDIRS:
|
||||
paths += sorted(glob.glob(os.path.join(RAG_DIR, sub, "*.dxf")))
|
||||
results = [inventory(p) for p in paths]
|
||||
os.makedirs(os.path.dirname(OUT_JSON), exist_ok=True)
|
||||
with open(OUT_JSON, "w", encoding="utf-8") as fh:
|
||||
json.dump(results, fh, ensure_ascii=False, indent=2)
|
||||
ok = sum(r["ok"] for r in results)
|
||||
print(f"files={len(results)} ok={ok} failed={len(results) - ok}")
|
||||
for r in results:
|
||||
status = "OK " if r["ok"] else "ERR"
|
||||
detail = r.get("error") or f"ents={sum(r['entity_counts'].values())} 3D={r['has_3d_geometry']} units={r['insunits']} cp={r['codepage']} recovered={r['recovered']}"
|
||||
print(f"{status} {r['file'][:60]:60s} {detail}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the Task 3 draft centreline report (DOC/03_설계) from the centreline
|
||||
CSVs written by extract_alignment_plan.py. Numbers are read, not typed."""
|
||||
import csv
|
||||
import math
|
||||
import sys
|
||||
|
||||
CSV_001, CSV_002, OUT_MD = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
|
||||
|
||||
def load(path):
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return [(int(r["station_m"]), float(r["centre_x"]), float(r["centre_y"]), float(r["side_gap_m"]))
|
||||
for r in csv.DictReader(fh)]
|
||||
|
||||
a, b = load(CSV_001), load(CSV_002)
|
||||
same = a == b
|
||||
lines = ["# 본선 중심선 측점 좌표 (초안)\n",
|
||||
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
|
||||
"| v0.1.00 | 2026-10-21 | 최초 작성. 평면도 02-001 기준 측점 4+700~5+500 중심선 초안 (HITL 검토 대상) |\n",
|
||||
"## 1. 추출 방법",
|
||||
"- 소스: 레이어 `CA-ALGN-TEXT`(측점 라벨), `CA-ALGN-STSM`(측점 눈금) [출처: C0060302-001]",
|
||||
"- 측점 라벨은 측점당 2개(좌우 측). 각 라벨의 최근접 눈금 2점의 **중점**을 중심선 점으로 채택 (정의 HITL 확인 대상)",
|
||||
"- 좌표는 도면 좌표계 그대로 (단위: 도면 표기 따름, 사용자 승인 2026-10-21)\n",
|
||||
"## 2. 측점별 중심선 좌표 (02-001)\n",
|
||||
"| 측점(m) | 표기 | X | Y | 좌우 간격(m) | 인접 거리(m) | 측점차(m) | 잔차(m) |",
|
||||
"|---|---|---|---|---|---|---|---|"]
|
||||
prev = None
|
||||
for sta, x, y, g in a:
|
||||
label = f"{sta // 1000}+{sta % 1000:03d}"
|
||||
if prev is None:
|
||||
lines.append(f"| {sta} | {label} | {x:,.3f} | {y:,.3f} | {g:.2f} | — | — | — |")
|
||||
else:
|
||||
d = math.dist(prev[1:3], (x, y))
|
||||
lines.append(f"| {sta} | {label} | {x:,.3f} | {y:,.3f} | {g:.2f} | {d:.3f} | {sta - prev[0]} | {d - (sta - prev[0]):+.3f} |")
|
||||
prev = (sta, x, y)
|
||||
resid = [math.dist(a[i][1:3], a[i + 1][1:3]) - (a[i + 1][0] - a[i][0]) for i in range(len(a) - 1)]
|
||||
lines += ["",
|
||||
f"- 검증: 인접 측점 거리와 측점차의 잔차 최대 {max(abs(r) for r in resid):.3f} m (측점 9개, 8구간). 잔차는 보정하지 않고 그대로 기재",
|
||||
"- 범위: 4+700 ~ 5+500 (800m). **이 시트 밖의 연장은 확보된 도면이 없음**\n",
|
||||
"## 3. 02-002 시트 대조",
|
||||
f"- 02-002 추출 결과 {'02-001과 측점 좌표가 동일' if same else '02-001과 차이 있음'} ({'중복 시트 가능성' if same else '차이 확인 필요'}). 원본 파일 md5는 상이함 [출처: C0060302-002]",
|
||||
"- 02-002가 종단(프로파일) 시트인지 중복 시트인지는 사용자 확인 필요\n",
|
||||
"## 4. 미완 항목 (OPEN 등재 대상)",
|
||||
"- 종단 EL 추출 미실시 (측점별 계획고·지반고). `CR-TABL-TEX2`의 740.00~980.00 라벨은 20m 간격으로 보이나 측점 체계와 대응 미확인 [추론]",
|
||||
"- 허용오차 ±50mm 판정은 EL 추출 후 실시",
|
||||
"- 측점 4+700 이전·5+500 이후 구간 평면 도면 미확보\n"]
|
||||
open(OUT_MD, "w", encoding="utf-8").write("\n".join(lines) + "\n")
|
||||
print("written", OUT_MD, "same_as_002=", same, "max_resid=", round(max(abs(r) for r in resid), 3))
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the main-line drawing inventory report (DOC/02_분석) from the
|
||||
inventory JSON so that every number in the report comes from the extraction."""
|
||||
import collections
|
||||
import json
|
||||
import sys
|
||||
|
||||
SRC_JSON, OUT_MD = sys.argv[1], sys.argv[2]
|
||||
rows = json.load(open(SRC_JSON, encoding="utf-8"))
|
||||
ok = [r for r in rows if r["ok"]]
|
||||
err = [r for r in rows if not r["ok"]]
|
||||
units = collections.Counter(str(r.get("insunits")) for r in ok)
|
||||
n3d = sum(1 for r in ok if r["has_3d_geometry"])
|
||||
total_ents = sum(sum(r["entity_counts"].values()) for r in ok)
|
||||
|
||||
def top_types(r, k=3):
|
||||
return ", ".join(f"{t} {n}" for t, n in list(r["entity_counts"].items())[:k])
|
||||
|
||||
lines = []
|
||||
lines.append("# 대산2터널 본선 도면 인벤토리\n")
|
||||
lines.append("| 버전 | 일자 | 변경 내용 |\n|---|---|---|")
|
||||
lines.append("| v0.1.00 | 2026-10-21 | 최초 작성. 본선 21건(02 평면·종단 4, 03 본선굴착 8, 04 본선기타 9) DXF 인벤토리 |\n")
|
||||
lines.append("## 1. 개요\n")
|
||||
lines.append("- 입력: `RAG/03_터널명(대산2터널)/{02,03,04}` 하위 DXF 21건 (원본 DWG 변환본, 읽기 전용 사용)")
|
||||
lines.append(f"- 추출 결과: 성공 {len(ok)}건 / 실패 {len(err)}건 / 복구 모드 적용 {sum(1 for r in ok if r['recovered'])}건")
|
||||
lines.append(f"- 총 엔티티 {total_ents:,}개 (성공 파일 기준)")
|
||||
lines.append(f"- 3D 형상(Z 편차 있음) 보유 파일: **{n3d}건** — 전 도면이 2D이므로 3D 형상은 도면 해석으로 재구성해야 함 [출처: 본 인벤토리 `has_3d_geometry`]")
|
||||
lines.append(f"- `$INSUNITS` 분포: " + ", ".join(f"{k}={v}건" for k, v in units.items()) + " (0=미지정, 4=mm) — 단위는 도면 표기를 따름 (사용자 승인 2026-10-21)\n")
|
||||
lines.append("## 2. 파일별 추출 결과\n")
|
||||
lines.append("| # | 파일 | 상태 | 엔티티 | 주요 엔티티(상위 3) | 단위 | 코드페이지 |")
|
||||
lines.append("|---|---|---|---|---|---|---|")
|
||||
for i, r in enumerate(rows, 1):
|
||||
if r["ok"]:
|
||||
n = sum(r["entity_counts"].values())
|
||||
lines.append(f"| {i} | {r['file']} | OK | {n:,} | {top_types(r)} | {r['insunits']} | {r['codepage']} |")
|
||||
else:
|
||||
lines.append(f"| {i} | {r['file']} | **ERR** | — | {r.get('error','')} | — | — |")
|
||||
lines.append("")
|
||||
lines.append("## 3. 도면 주석 관찰 (정성, 판정 어휘: 있음 / 일부 있음 / 없음 / 확인 못함)\n")
|
||||
lines.append("| 항목 | 판정 | 근거 |\n|---|---|---|")
|
||||
lines.append("| 표준단면 편경사 기준 | 있음 | 「편경사 -2.0%를 기준으로 작성」 [출처: C0060303-001, 주석 1] |")
|
||||
lines.append("| 표준단면 방향 표기 범위 | 일부 있음 | 「당진방향 단면만 표기, 대산방향 단면은 생략」 [출처: C0060303-001, 주석 2] — 대산방향 형상은 이 도면으로 확정 불가 |")
|
||||
lines.append("| 숏크리트·라이닝 두께 수치 | 없음 | 「표준지보패턴도 참조」 [출처: C0060303-001, 주석 5] — 두께 값은 다른 도면(표준지보패턴) 필요 |")
|
||||
lines.append("| 굴착 순서 표기 | 있음 | N, N+1, N-1 단계 표기 및 1회 굴진장 [출처: C0060303-002] |")
|
||||
lines.append("| 굴진 1회당 길이 | 있음 | 「굴진 (1회당 0.8m)」 [출처: C0060303-008] |")
|
||||
lines.append("| 라이닝 구조도 적용 구간 | 일부 있음 | 「지보패턴 입출구부 A~C-2 구간의 구조적 취약구간에 적용」 [출처: C0060304-004, 주석 5] — 전 구간 적용 여부 확인 못함 |")
|
||||
lines.append("| 지보패턴 명칭 | 있음 | NP-4AD-1-D-2, NP-4BD-1-D-2, NP-5AE-1-WL-4, NP-5BE-1-WL-4, NP-6A, NP-6B [출처: 03-003~008 파일명] |")
|
||||
lines.append("| 평면·종단 지질·구배 표기 | 있음 | 구배 S=2.4913%, 지층명(붕적층·풍화토·풍화암·보통암) [출처: C0060302-001 텍스트] |")
|
||||
lines.append("| 구간 측점(STA) 정합 | 확인 못함 | 평면·종단·굴착 도면 간 측점 대응표 없음 — Task 3에서 대조 필요 [추론] |")
|
||||
lines.append("")
|
||||
lines.append("## 4. 확인·보류 항목 (OPEN 등재 대상)\n")
|
||||
lines.append("- **C0060304-003** (공동구 및 배수계획도 3): DXF 구조 오류(`Invalid sort handle code 331`), 복구 모드도 실패. 원본 DWG 재변환 필요. 본선 3D 골격에는 필수가 아니므로 배수 요소만 보류.")
|
||||
lines.append("- 대산방향 단면 형상 (C0060303-001 주석 2): 3D 대산방향 재구성 여부 결정 필요 [CLARIFICATION NEEDED].")
|
||||
lines.append("- 라이닝 두께 수치 출처 (표준지보패턴도 미보유): 두께 값 확정 전까지 `[미확정]`으로 표기.")
|
||||
lines.append("")
|
||||
lines.append("## 5. 다음 단계\n")
|
||||
lines.append("Task 3(평면·종단 선형 추출, HITL) 착수 전 위 4항의 확인 응답을 받는다. 본 인벤토리의 수치는 `Code/3d-drawing/data/inventory_mainline.json`에서 자동 생성되었다.")
|
||||
open(OUT_MD, "w", encoding="utf-8").write("\n".join(lines) + "\n")
|
||||
print("written", OUT_MD, "lines", len(lines))
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the lining-reinforcement reference report (DOC/03_설계) from
|
||||
data/lining_spec.json written by extract_lining_spec.py. Values are read from
|
||||
the JSON, not typed. Usage: make_lining_doc.py <lining_spec.json> <out.md>"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
SRC, OUT = sys.argv[1], sys.argv[2]
|
||||
d = json.load(open(SRC, encoding="utf-8"))
|
||||
S = d["sheets"]
|
||||
note5 = S[0]["note5"]
|
||||
conc = S[0]["concrete"][0].split("=")[-1]
|
||||
rebar = S[0]["rebar_grade"][0].split("=")[-1]
|
||||
agg = S[0]["aggregate"][0].split("Ø")[-1]
|
||||
r1_drawn = next(x for x in S[0]["radius_dims"] if x.startswith("R1="))
|
||||
r1_mm = float(r1_drawn.split("=")[1])
|
||||
lines = ["# 콘크리트 라이닝 구조도 판독 및 재료 사양 (초안)\n",
|
||||
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
|
||||
"| v0.1.00 | 2026-10-21 | 최초 작성. 콘크리트라이닝구조도 C0060304-004~009 판독 (HITL 검토 대상) |\n",
|
||||
"## 1. 개요",
|
||||
"- 6장은 **지보패턴 입출구부 취약구간**의 철근 상세와 공통 사양이다. 도면에 측점(STA) 표기가 없어 어느 측점 구간에 적용되는지는 판독되지 않았다 ([미확정]).",
|
||||
"- 적용 범위(주기 5, 원문): " + note5 + "\n",
|
||||
"## 2. 시트별 패턴군",
|
||||
"| 도면 | 패턴군 | 상세 종류 | 철근 라벨(H13~H29 개수) |",
|
||||
"|---|---|---|---|"]
|
||||
for r in S:
|
||||
bars = ", ".join(f"{k} {v}" for k, v in r["rebar_sizes_label_count"].items())
|
||||
for i, g in enumerate(r["pattern_details"]):
|
||||
lab = bars if i == 0 else ""
|
||||
lines.append(f"| {r['file'][:14]} | {g['group']} | {g['kind']} | {lab} |")
|
||||
lines += ["",
|
||||
"- 철근 라벨 개수는 도면 시트 단위 합계다 (패턴군별 분리 불가). 첫 행에만 표기했다.\n",
|
||||
"## 3. 공통 재료 사양 (6장 동일)",
|
||||
f"- 콘크리트: {conc} ({S[0]['concrete'][0]})",
|
||||
f"- 철근: {rebar}, 도면 표기 SD400 ({S[0]['rebar_grade'][0]})",
|
||||
f"- 굵은골재 최대치수: Ø{agg} mm",
|
||||
"- 설계방법: 강도설계법, 피복두께 6 cm 이상 확보 (주기 1)",
|
||||
"- 이음장 600 mm (도면 치수), 겹침이음 1 cycle (도면 상세)",
|
||||
"- 라이닝 뒷면 주입재 강도 1.0 MPa 기대 (주기 11)",
|
||||
"- 천정부 방수쉬트 부착형 앵커, 철근처짐방지 앵커 적용 (주기 4, 12)\n",
|
||||
"## 4. 기준선과 치수",
|
||||
f"- 도면 표기 R1 = {r1_drawn} (6장 모두 동일 표기)",
|
||||
"- 표준단면 치수표(설계_표준단면치수표) R1 = 6,982 mm, 골격 모델은 6,982 mm 사용",
|
||||
f"- 라이닝 외면 R 7.282 m 에서 내면 R 6.982 m 를 빼면 두께 0.300 m, 표기 6.892 m 를 쓰면 0.390 m 이다. 표준단면 두께 300 mm 와 맞는 값은 6.982 m 이다.",
|
||||
"- R2 = 3,712 mm, R2′ = 3,910 mm (도면 표기, 표준단면 치수표와 일치)",
|
||||
"- EL. 0.000 = 노면 기준 (계획고 F = 0.000 과 일치 기록)",
|
||||
"- EL. -0.367 = 아치 중심 높이 (골격의 중심 c = -0.367 m 와 일치)\n",
|
||||
"## 5. 충돌·미확정",
|
||||
"- [확정 2026-10-21, 사용자 결정] R1 = 6.982 m 유지. 도면의 6.892 m 표기는 확인 대상 오기로 기록한다.",
|
||||
"- [미확정] 시트별 패턴군이 어느 측점 구간인지 (도면에 측점 없음)",
|
||||
"- [확인 필요] 라이닝 패턴군(A~C-2, D-1~2, E-1~2, WL-1~4, RP-1~2, P-2~3)과 굴착 순서도 패턴명(NP-4AD-1-D-2 등)의 대응. 사용자는 파일명에 기록되어 있다고 답했다. 예시 풀이(NP-5BE-1-WL-4 = NP-5B 의 D-1·D-2)는 파일명 속 WL-4 및 시트 패턴군(WL-3~4)과 맞지 않아 재확인이 필요하다.",
|
||||
"- 재료표 수량(총 중량)은 이번 판독에서 추출하지 않았다. 필요하면 재료표 추출을 별도 Task로 한다.",
|
||||
"- 철근 배치는 골격 LOD 범위 밖이라 모델에 넣지 않았다 (사용자 승인 3D 상세 수준: 축선·표준단면 스윕 골격).",
|
||||
"- 설계 조건과 상이하면 구조해석 검토 후 철근 보강 가능 (주기 5, 시공 중 판단 대상)\n",
|
||||
"## 6. 확인 요청 (HITL)",
|
||||
"1. NP-5BE-1-WL-4 의 D-1·D-2 풀이가 맞는지, 아니면 WL-4 가 어느 라이닝 패턴군인지",
|
||||
"2. 측점별 지보패턴 구간표가 확보되면 그 값으로 placeholder 구간을 교체할지",
|
||||
""]
|
||||
open(OUT, "w", encoding="utf-8").write("\n".join(lines))
|
||||
print("written", OUT, len(lines), "lines")
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the station-by-station lining pattern report (DOC/03_설계) from the
|
||||
CSVs written by extract_pattern_table.py. Values are read, not typed.
|
||||
Usage: make_pattern_doc.py <dangjin.csv> <daesan_ref.csv> <lining_spec.json> <out.md>"""
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
|
||||
DJ, DS, LS, OUT = sys.argv[1:5]
|
||||
dj = list(csv.DictReader(open(DJ, encoding="utf-8")))
|
||||
ds = list(csv.DictReader(open(DS, encoding="utf-8")))
|
||||
lin = json.load(open(LS, encoding="utf-8"))["sheets"]
|
||||
drawing_groups = sorted({g["group"] for s in lin for g in s["pattern_details"] if g["group"]})
|
||||
dj_names = {r["lining_pattern"] for r in dj}
|
||||
ds_names = {r["lining_pattern"] for r in ds}
|
||||
# Drawing group labels (read from the sheet notes, 04-004..009) -> member patterns.
|
||||
# Explicit table: the labels mix letter ranges (A~C-2) and number ranges (WL-1~2).
|
||||
DRAWING_MEMBERS = {"A~C-2": ["A", "B-1", "B-2", "C-1", "C-2"], "D-1~2": ["D-1", "D-2"],
|
||||
"E-1~2": ["E-1", "E-2"], "WL-1~2": ["WL-1", "WL-2"], "WL-1~2,RP-1~2": ["WL-1", "WL-2", "RP-1", "RP-2"],
|
||||
"WL-3~4": ["WL-3", "WL-4"], "P-2~3": ["P-2", "P-3"], "RP-1~2": ["RP-1", "RP-2"]}
|
||||
members = set()
|
||||
for g in drawing_groups:
|
||||
members |= set(DRAWING_MEMBERS.get(g, [g]))
|
||||
missing_dj = sorted(m for m in members if m not in dj_names)
|
||||
only_ds = sorted(m for m in missing_dj if m in ds_names)
|
||||
none_any = sorted(m for m in missing_dj if m not in ds_names)
|
||||
L = ["# 측점별 콘크리트 라이닝 지보패턴표 (당진방향) (초안)\n",
|
||||
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
|
||||
"| v0.1.00 | 2026-10-21 | 최초 작성. 엑셀 `참고_지보패턴현황` 당진방향 표를 측점별로 정리 (사용자 결정: 모델 placeholder 대체) |\n",
|
||||
"## 1. 출처와 범위",
|
||||
"- 원본: `RAG/03_터널명(대산2터널)/지보패턴현황_20260918.xlsx`, 시트 `참고_지보패턴현황`, 당진방향 표 (원본 행 55~96)",
|
||||
f"- 행 {len(dj)}개, 측점 {dj[0]['sta_from']} ~ {dj[-1]['sta_to']}, 빈틈 없음 (각 행 끝 = 다음 행 시작)",
|
||||
"- 모델 적용 범위 4+745 ~ 5+480 은 이 표로 전 구간 덮임 (735 m)",
|
||||
"- 지보패턴(M열) = 라이닝 패턴, 실시설계(O열) = 굴착 등급. 실시설계 칸이 빈 행은 위 행의 값을 이어 받음 (병합 셀 해석, [추론])\n",
|
||||
"## 2. 측점별 표",
|
||||
"| 측점 from | 측점 to | 연장(m) | 지보패턴 | 실시설계 | 굴착공법 | 비고 |",
|
||||
"|---|---|---|---|---|---|---|"]
|
||||
def sta(x):
|
||||
v = int(float(x)); return f"{v // 1000}+{v % 1000:03d}"
|
||||
for r in dj:
|
||||
exc = r["excavation"] + (" (이어받음)" if r["excavation_inherited"] == "True" and r["excavation"] else "")
|
||||
L.append(f"| {sta(r['sta_from'])} | {sta(r['sta_to'])} | {float(r['length_m']):g} | {r['lining_pattern']} | {exc} | {r['method'].replace(chr(10), ' ')} | {r['remark']} |")
|
||||
L += ["", "## 3. 도면과 표의 대응",
|
||||
f"- 라이닝 도면(04-004~009)의 패턴 {len(members)}개 중 당진 표에 있는 것: {len(members) - len(missing_dj)}개",
|
||||
f"- 당진 표에 없고 대산 표에만 있는 것: {', '.join(only_ds) if only_ds else '없음'} (대산 표 값 참고만, 적용 안 함 — 아래 4항)",
|
||||
f"- 두 표 모두 없는 것: {', '.join(none_any) if none_any else '없음'} → 측점 지정 불가, [미확정]\n",
|
||||
"## 4. 확인 필요 (HITL)",
|
||||
"- [확인 필요] 대산방향 표의 WL-4 (4+758.85~4+768.85, NP-5) 를 당진 측점에 적용할지. 두 방향이 별도 튜브라는 점은 [추론]이며 확인되지 않았다. 당진 표는 같은 측점에 WL-2/WL-1 을 보여 준다.",
|
||||
"- [확인 필요] 말씀하신 `NP-5BE-1-WL-4` 의 풀이 (NP-5B 의 E-1 ~ WL-4). 당진 NP-5 구간은 WL-2·WL-1 뿐이고, E-1 은 두 표 어디에도 없다.",
|
||||
"- [확인 필요] 엑셀 `개요2`·`1` 시트: 굴착 등급은 NP-4/5/6 (A·B 접미사 포함), 괄호 안은 지보 하위 패턴이라는 표기. 이 해석은 엑셀 표기와 일치하는 것으로 보이나 도면 명칭 대응은 확정하지 않았다.\n",
|
||||
"## 5. 적용 결과 (골격 모델)",
|
||||
"- placeholder 2 m 구간은 폐기하고 이 표로 대체 (사용자 결정 2026-10-21)",
|
||||
"- 뷰어의 라이닝 클릭 값: 측점, 지보패턴(본 표), 라이닝 사양 (C0060304-004~009)",
|
||||
""]
|
||||
open(OUT, "w", encoding="utf-8").write("\n".join(L))
|
||||
print("written", OUT, "rows", len(dj), "drawing groups", len(members), "missing in dangjin", missing_dj)
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the confirmed station table (지반고 = A열, 계획고 = B열, 20 m station
|
||||
numbering per user decision 2026-10-21) into DOC/03_설계 and check the design
|
||||
grade against the drawn label S=2.4913% (정상 구배 검산). No value is adjusted."""
|
||||
import csv
|
||||
import sys
|
||||
|
||||
SRC, OUT = sys.argv[1], sys.argv[2]
|
||||
rows = []
|
||||
with open(SRC, encoding="utf-8") as fh:
|
||||
rd = list(csv.DictReader(fh))
|
||||
gl, dg = {}, {}
|
||||
for r in rd:
|
||||
sta = int(r["station_anchor"])
|
||||
if float(r["across_m"]) < -25:
|
||||
gl[sta] = float(r["value"])
|
||||
else:
|
||||
dg[sta] = float(r["value"])
|
||||
stas = sorted(set(gl) & set(dg))
|
||||
n = len(stas)
|
||||
xs = [s for s in stas]
|
||||
mx = sum(xs) / n
|
||||
my = sum(dg[s] for s in stas) / n
|
||||
slope = sum((s - mx) * (dg[s] - my) for s in stas) / sum((s - mx) ** 2 for s in stas)
|
||||
grade_pct = slope * 100
|
||||
resid = [dg[s] - (my + slope * (s - mx)) for s in stas]
|
||||
max_res = max(abs(r) for r in resid)
|
||||
lines = ["# 본선 종단 측점값 — 지반고·계획고 (초안, 확정 해석 반영)\n",
|
||||
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
|
||||
"| v0.1.00 | 2026-10-21 | 최초 작성. 02-002 종단 눈금 4+740~5+480, 지반고(A열)·계획고(B열) 확정 해석 반영 |\n",
|
||||
"## 1. 확정 사항 (사용자 답변 2026-10-21)",
|
||||
"- 소수 A열(중심선 측 약 -33m) = **지반고**, B열(약 -16m) = **계획고** [출처: C0060302-002, CR-TABL-TEX2]",
|
||||
"- 측점은 **측점 번호 20m 기준**. 도면상 눈금 거리 21.27m는 사용하지 않음 (축척 1.0635배 불일치 기록)",
|
||||
"- 공백 구간(4+700~4+740, 5+480~5+500)은 **[미확정]**, 확보 구간만 모델링\n",
|
||||
"## 2. 측점별 지반고·계획고",
|
||||
"| 측점(m) | 표기 | 지반고 GL(m) | 계획고(m) |", "|---|---|---|---|"]
|
||||
for s in stas:
|
||||
lines.append(f"| {s} | {s // 1000}+{s % 1000:03d} | {gl[s]:.3f} | {dg[s]:.3f} |")
|
||||
lines += ["",
|
||||
"## 3. 검산",
|
||||
f"- 전구간 직선 회귀 구배 {grade_pct:.4f}%는 **단일 구배가 아님**을 뜻하므로 채택하지 않음 (잔차 최대 {max_res:.3f} m)",
|
||||
"- 구간별 구배 (계획고 2점 기울기, 도면 표기 S=2.4913%와 비교):"]
|
||||
for a, b in [(4740, 4800), (4800, 4900), (4900, 5000), (5000, 5100), (5100, 5200), (5200, 5300), (5300, 5400), (5400, 5480)]:
|
||||
seg = [k for k in stas if a <= k <= b]
|
||||
if len(seg) >= 2:
|
||||
g = (dg[seg[-1]] - dg[seg[0]]) / (seg[-1] - seg[0]) * 100
|
||||
lines.append(f" - {a // 1000}+{a % 1000:03d} ~ {b // 1000}+{b % 1000:03d}: {g:.4f}%")
|
||||
lines += ["- 해석: 4+740~5+300은 2.491%로 S=2.4913%와 일치. 5+300 이후 구배 감소는 종곡선 구간으로 보이나 [추론], 종곡선 제원은 도면에서 확인 못함 [확인 못함]",
|
||||
"- 지반고는 구배 검산 대상이 아님 (지형 값)\n",
|
||||
"## 4. 미확정·후속 (OPEN 등재 대상)",
|
||||
"- 4+700~4+740, 5+480~5+500 종단 값 없음 → [미확정]",
|
||||
"- 허용오차 ±50mm 판정: 본 초안은 도면 표기 값 그대로이며, 3D 대조 판정은 Task 7 이후 수행",
|
||||
"- 라이닝 두께 수치: 04-004~009 콘크리트라이닝 구조도 확인 예정 (사용자 지시 2026-10-21)\n"]
|
||||
open(OUT, "w", encoding="utf-8").write("\n".join(lines) + "\n")
|
||||
print(f"stations={n} grade={grade_pct:.4f}% max_resid={max_res:.3f}")
|
||||
@@ -0,0 +1,42 @@
|
||||
import sys, time, pickle
|
||||
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
|
||||
from build123d import import_step, Face
|
||||
import numpy as np
|
||||
shape = import_step("/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids.step")
|
||||
solids = shape.solids()
|
||||
pickle_out = {}
|
||||
import json
|
||||
meta=json.load(open('/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids_meta.json'))
|
||||
comps=[c for c in meta['components'] if c['group']!='시설한계(참조선)']
|
||||
print("non-clearance comps", len(comps))
|
||||
# match by volume greedily in meta order
|
||||
used=set(); mapping={}
|
||||
for c in comps:
|
||||
best=None;bd=1e9
|
||||
for j,s in enumerate(solids):
|
||||
if j in used: continue
|
||||
d=abs(s.volume-c['volume_m3'])
|
||||
if d<bd: bd=d;best=j
|
||||
if bd<0.02:
|
||||
used.add(best); mapping[c['id']]=best
|
||||
else:
|
||||
mapping[c['id']]=None
|
||||
print("matched", sum(v is not None for v in mapping.values()), "of", len(comps), "unused solids", len(solids)-len(used))
|
||||
print("first 10 mapping", list(mapping.items())[:10])
|
||||
pickle.dump({'mapping':mapping}, open('/tmp/verify_daesan2_run/mapping.pkl','wb'))
|
||||
# faces of solid 0 (LIN_01)
|
||||
for idx in [mapping['LIN_01_P-3']]:
|
||||
s=solids[idx]
|
||||
fs=s.faces()
|
||||
print("solid",idx,"nfaces",len(fs), "vol", round(s.volume,3))
|
||||
rows=[]
|
||||
for f in fs:
|
||||
gt=f.geom_type
|
||||
c=f.center(); a=f.area
|
||||
try:
|
||||
n=f.normal_at(c)
|
||||
except Exception as ex:
|
||||
n=None
|
||||
rows.append((gt, round(a,1), [round(x,3) for x in (c.X,c.Y,c.Z)], None if n is None else [round(x,4) for x in (n.X,n.Y,n.Z)]))
|
||||
rows.sort(key=lambda r: -r[1])
|
||||
for r in rows[:25]: print(r)
|
||||
@@ -0,0 +1,69 @@
|
||||
import sys
|
||||
exec(open('/tmp/verify_daesan2_run/scripts/hd2.py').read().split('allres={}')[0])
|
||||
from scipy.spatial import cKDTree
|
||||
import numpy as np
|
||||
extra={
|
||||
'PAV_sideL': seg(9526.3,15764.0,9526.3,15484.0), 'PAV_sideR': seg(18726.3,15580.0,18726.3,15300.0),
|
||||
'DRN_topL': seg(9026.3,15694.0,9476.3,15703.0), 'DRN_topR': seg(18776.3,15519.0,19226.3,15510.0),
|
||||
}
|
||||
REF.update(extra)
|
||||
def dd(A,B):
|
||||
d1,_=cKDTree(A).query(B); d2,_=cKDTree(B).query(A); return d1,d2
|
||||
for st in ['4745.0005','5100.0','5469.9995']:
|
||||
print("=== station", st, "sigma=+1 slope(dxf)", round(FIT[st]['slope_top'],5))
|
||||
L=to_dxf(pts(st,lambda g,i:g=='라이닝'),st,1); E=to_dxf(pts(st,lambda g,i:g=='굴착면'),st,1)
|
||||
P=to_dxf(pts(st,lambda g,i:g=='노면(계획고)'),st,1); B=to_dxf(pts(st,lambda g,i:g=='기층·인버트(가정)'),st,1)
|
||||
D=to_dxf(pts(st,lambda g,i:g=='배수(측구)'),st,1)
|
||||
# LIN full arcs (no clipping) and clipped to model extent
|
||||
arcs=['LIN_crown_outer','LIN_crown_inner','LIN_haunchL_outer','LIN_haunchL_inner','LIN_haunchR_outer','LIN_haunchR_inner']
|
||||
for k in arcs:
|
||||
d,_=cKDTree(L).query(REF[k]); print(f" LIN {k:18s} max={d.max():7.1f} mm (at y_min_ref={REF[k][:,1].min():.0f})")
|
||||
ymin_L=L[L[:,0]<C0[0]][:,1].min(); ymin_R=L[L[:,0]>C0[0]][:,1].min()
|
||||
lr=np.vstack([REF[k] for k in arcs]); d,_=cKDTree(L).query(lr)
|
||||
inside=((lr[:,0]<C0[0])&(lr[:,1]>=ymin_L))|((lr[:,0]>=C0[0])&(lr[:,1]>=ymin_R))
|
||||
print(f" LIN within model extent (y>={ymin_L:.0f}/{ymin_R:.0f}) max={d[inside].max():.1f} mm; drawn part below model extent (not modelled) n={int((~inside).sum())} pts, y range {lr[~inside,1].min():.0f}-{lr[~inside,1].max():.0f}")
|
||||
fl=np.vstack([REF['LIN_footL'],REF['LIN_footR']]); a,b=dd(L,fl); print(f" LIN foot lines (CS-CONC-MAJR wall/foot) ref->model max={a.max():.1f} model->ref(near band) ")
|
||||
# EXC
|
||||
ea=['EXC_crown','EXC_haunchL','EXC_haunchR']
|
||||
eymL=E[E[:,0]<C0[0]][:,1].min(); eymR=E[E[:,0]>C0[0]][:,1].min()
|
||||
er=np.vstack([REF[k] for k in ea]); d,_=cKDTree(E).query(er)
|
||||
ins=((er[:,0]<C0[0])&(er[:,1]>=eymL))|((er[:,0]>=C0[0])&(er[:,1]>=eymR))
|
||||
print(f" EXC arcs within model extent (y>={eymL:.0f}/{eymR:.0f}) max={d[ins].max():.1f} mm; crown max={cKDTree(E).query(REF['EXC_crown'])[0].max():.1f}; below model extent n={int((~ins).sum())} y {er[~ins,1].min():.0f}-{er[~ins,1].max():.0f}")
|
||||
# PAV model vs drawn top/bottom/sides
|
||||
pref=np.vstack([REF['PAV_top'],REF['PAV_bot'],REF['PAV_sideL'],REF['PAV_sideR']])
|
||||
Pm=P[(P[:,0]>9526.3-150)&(P[:,0]<18726.3+150)]
|
||||
a,b=dd(Pm,pref); print(f" PAV ref->model max={a.max():.1f} mm ; model->ref max={b.max():.1f} mm (top/bottom/sides)")
|
||||
# BASE: top envelope vs base top line; bottom envelope vs base bottom line, within road span
|
||||
bw=B[(B[:,0]>=9526.3)&(B[:,0]<=18726.3)]
|
||||
xs=np.linspace(9526.3,18726.3,921)
|
||||
top_line=15484.0+(xs-9526.3)*(15300-15484)/9200.0; bot_line=15334.0+(xs-9526.3)*(15150-15334)/9200.0
|
||||
# model envelope per x-bin (1 m bins for robust): max y (top) and min y (bottom)
|
||||
tops=[];bots=[]
|
||||
for x0 in np.arange(9526.3,18726.3,200):
|
||||
m=(bw[:,0]>=x0)&(bw[:,0]<x0+200)
|
||||
if m.sum()==0: continue
|
||||
xm=bw[m,0].mean(); tops.append((xm,bw[m,1].max())); bots.append((xm,bw[m,1].min()))
|
||||
tops=np.array(tops); bots=np.array(bots)
|
||||
et=np.abs(tops[:,1]-np.interp(tops[:,0],xs,top_line)); eb=np.abs(bots[:,1]-np.interp(bots[:,0],xs,bot_line))
|
||||
print(f" BASE top envelope vs drawn top (15484->15300): max={et.max():.1f} mm, mean={et.mean():.1f}; bottom envelope vs drawn bottom (15334->15150): max={eb.max():.1f} mm, mean={eb.mean():.1f}")
|
||||
print(f" BASE model bottom envelope at span ends: x={bots[0,0]:.0f} y={bots[0,1]:.1f} / x={bots[-1,0]:.0f} y={bots[-1,1]:.1f}; top envelope ends y={tops[0,1]:.1f}/{tops[-1,1]:.1f}")
|
||||
print(f" BASE model extent outside road span: x {B[:,0].min():.0f}..{B[:,0].max():.0f}, y {B[:,1].min():.0f}..{B[:,1].max():.0f}")
|
||||
# DRN
|
||||
dref=np.vstack([REF[k] for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR','DRN_topL','DRN_topR']])
|
||||
dm=D[(D[:,0]>8900)&(D[:,0]<19300)]
|
||||
a,b=dd(dm,dref); print(f" DRN ref->model max={a.max():.1f} mm ; model->ref max={b.max():.1f} mm (walls, bottoms, pipes, tops)")
|
||||
# vertical datum check at tunnel centre and road centre
|
||||
fit=FIT[st]; uc,vc,_=fit['outer']
|
||||
top_c=fit['slope_top']*(0-uc)+0 ;
|
||||
# top envelope at u for DXF tunnel centre (u=uc) and road centre (x=9776.3 -> u=uc-4.35)
|
||||
uu=P[:,0];
|
||||
def top_env(xdxf):
|
||||
um=uc+(xdxf-C0[0])/1000.0
|
||||
m=np.abs(pts(st,lambda g,i:g=='노면(계획고)')[:,0]-um)<0.05
|
||||
return pts(st,lambda g,i:g=='노면(계획고)')[m,1].max()
|
||||
Pm_raw=pts(st,lambda g,i:g=='노면(계획고)')
|
||||
for lab,xdxf,ydxf in [('tunnel centre x=14126.3',14126.3,15672.0),('road centre x=9776.3',9776.3,15759.0)]:
|
||||
um=uc+(xdxf-C0[0])/1000.0
|
||||
m=np.abs(Pm_raw[:,0]-um)<0.05
|
||||
yt=(Pm_raw[m,1].max()-vc)*1000; yb=(Pm_raw[m,1].min()-vc)*1000
|
||||
print(f" datum {lab}: model top={yt:.1f} mm, bottom={yb:.1f} mm above lining centre | DXF top at this x = {ydxf-15305:.1f} mm above lining centre")
|
||||
@@ -0,0 +1,23 @@
|
||||
import sys
|
||||
exec(open('/tmp/verify_daesan2_run/scripts/final.py').read().split("for st in ['4745.0005','5100.0','5469.9995']:")[0])
|
||||
for st in ['4745.0005','5100.0']:
|
||||
B=to_dxf(pts(st,lambda g,i:g=='기층·인버트(가정)'),st,1); D=to_dxf(pts(st,lambda g,i:g=='배수(측구)'),st,1)
|
||||
bw=B[(B[:,0]>=9526.3+150)&(B[:,0]<=18726.3-150)]
|
||||
xs=np.arange(9676.3,18576.3,50.0)
|
||||
top_line=lambda x: 15484.0+(x-9526.3)*(15300-15484)/9200.0
|
||||
bot_line=lambda x: 15334.0+(x-9526.3)*(15150-15334)/9200.0
|
||||
tt=[];bb=[]
|
||||
for x0 in xs:
|
||||
m=(bw[:,0]>=x0-25)&(bw[:,0]<x0+25)
|
||||
if m.sum()==0: continue
|
||||
tt.append(bw[m,1].max()-top_line(x0)); bb.append(bw[m,1].min()-bot_line(x0))
|
||||
tt=np.array(tt); bb=np.array(bb)
|
||||
print(f"{st}: BASE top (pavement-bottom interface), interior 150mm from ends: model-drawn max|dev|={np.abs(tt).max():.1f} mm, mean={tt.mean():.1f} mm (signed mean)")
|
||||
print(f"{st}: BASE bottom (drawn excavation floor 15334->15150), interior: max|dev|={np.abs(bb).max():.1f} mm, mean={bb.mean():.1f} mm (signed; negative=model below drawing)")
|
||||
# drains body vs lid
|
||||
body=np.vstack([REF[k] for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR']])
|
||||
lid=np.vstack([REF['DRN_topL'],REF['DRN_topR']])
|
||||
dm=D[(D[:,0]>8900)&(D[:,0]<19300)]
|
||||
a,_=cKDTree(dm).query(body); b,_=cKDTree(dm).query(lid)
|
||||
print(f"{st}: DRN body (walls/bottom/pipe) ref->model max={a.max():.1f} mm; lid (450-wide top line) ref->model max={b.max():.1f} mm")
|
||||
print(f"{st}: DRN model x-extent per ditch: left {dm[dm[:,0]<14126,0].min():.0f}-{dm[dm[:,0]<14126,0].max():.0f}, right {dm[dm[:,0]>14126,0].min():.0f}-{dm[dm[:,0]>14126,0].max():.0f}")
|
||||
@@ -0,0 +1,102 @@
|
||||
import sys, json, pickle, collections
|
||||
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
|
||||
import numpy as np
|
||||
from build123d import import_step, Shape
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Section
|
||||
from OCP.gp import gp_Pln, gp_Pnt, gp_Dir
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopAbs import TopAbs_EDGE
|
||||
solids = import_step("/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids.step").solids()
|
||||
mp = pickle.load(open('/tmp/verify_daesan2_run/mapping.pkl','rb'))['mapping']
|
||||
caps = json.load(open('/tmp/verify_daesan2_run/caps.json'))
|
||||
ch = json.load(open('/tmp/verify_daesan2_run/chain.json'))
|
||||
meta = json.load(open('/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids_meta.json'))
|
||||
comps = {c['id']: c for c in meta['components'] if c['group']!='시설한계(참조선)'}
|
||||
solid_comp = {v:k for k,v in mp.items()}
|
||||
# 1) boundary points from lining chain
|
||||
pts = []
|
||||
for k in ch['lin']:
|
||||
pts.append(np.array(ch['starts'][k])); pts.append(np.array(ch['ends'][k]))
|
||||
P = np.array(pts)[:, :2]
|
||||
uniq = []
|
||||
for p in P:
|
||||
if not any(np.linalg.norm(p-q) < 1e-3 for q in uniq): uniq.append(p)
|
||||
uniq = np.array(uniq)
|
||||
print("unique boundary points", len(uniq), "(expect 41)")
|
||||
D = P[-1]-P[0] if False else (np.array(ch['ends'][ch['lin'][-1]])[:2]-np.array(ch['starts'][ch['lin'][0]])[:2])
|
||||
proj = uniq@D/np.linalg.norm(D)
|
||||
order = np.argsort(proj); B = uniq[order]
|
||||
cum = np.concatenate([[0], np.cumsum(np.linalg.norm(np.diff(B,axis=0),axis=1))])
|
||||
Sb = 4745 + cum
|
||||
M = sorted(set([c['from_sta'] for c in comps.values()] + [5480]))
|
||||
print("boundary count", len(Sb), "meta boundary count", len(M))
|
||||
print("max |station_chain - meta_boundary| (m):", round(float(np.max(np.abs(Sb-np.array(M)))),3) if len(Sb)==len(M) else 'count mismatch')
|
||||
# 2) station for each solid by centroid projection
|
||||
def station_of(p):
|
||||
best=None
|
||||
for k in range(len(B)-1):
|
||||
a,b=B[k],B[k+1]; t=b-a; L2=t@t
|
||||
f=np.clip(((p-a)@t)/L2,0,1); q=a+f*t; d=np.linalg.norm(p-q)
|
||||
if best is None or d<best[0]: best=(d, Sb[k]+f*(Sb[k+1]-Sb[k]), k, f)
|
||||
return best
|
||||
sol_info={}
|
||||
for i,s in enumerate(solids):
|
||||
c=s.center(); d,st,k,f=station_of(np.array([c.X,c.Y]))
|
||||
cid=solid_comp[i]; sol_info[i]=dict(cid=cid, group=comps[cid]['group'], st=st, lat=d)
|
||||
# 3) pair within group by sorted mid
|
||||
assign={}
|
||||
groups=collections.defaultdict(list)
|
||||
for i,v in sol_info.items(): groups[v['group']].append(i)
|
||||
maxres=0
|
||||
for g,idxs in groups.items():
|
||||
cs=[c for c in comps.values() if c['group']==g]
|
||||
idxs_s=sorted(idxs, key=lambda i: sol_info[i]['st'])
|
||||
cs_s=sorted(cs, key=lambda c: (c['from_sta']+c['to_sta'])/2)
|
||||
if len(idxs_s)!=len(cs_s): print("COUNT MISMATCH", g, len(idxs_s), len(cs_s))
|
||||
for i,c in zip(idxs_s, cs_s):
|
||||
mid=(c['from_sta']+c['to_sta'])/2
|
||||
res=abs(sol_info[i]['st']-mid); maxres=max(maxres,res)
|
||||
assign[i]=dict(id=c['id'], group=g, frm=c['from_sta'], to=c['to_sta'], mid_chain=sol_info[i]['st'], lat=sol_info[i]['lat'])
|
||||
print("max residual solid-centre station vs comp mid (m):", round(maxres,3))
|
||||
# lateral offset sanity
|
||||
print("max lateral offset of solid centroid from chain (m):", round(max(v['lat'] for v in assign.values()),3))
|
||||
# 4) slice
|
||||
def slice_solid(idx, Q, T):
|
||||
nrm=np.array([T[0],T[1],0.0])
|
||||
pl=gp_Pln(gp_Pnt(float(Q[0]),float(Q[1]),0.0), gp_Dir(float(nrm[0]),float(nrm[1]),0.0))
|
||||
face=BRepBuilderAPI_MakeFace(pl,-80,80,-80,80).Face()
|
||||
sec=BRepAlgoAPI_Section(solids[idx].wrapped, face); sec.Build()
|
||||
polys=[]
|
||||
exp=TopExp_Explorer(sec.Shape(), TopAbs_EDGE)
|
||||
while exp.More():
|
||||
e=Shape.cast(exp.Current()); L=e.length; N=max(2,int(L/0.02))
|
||||
polys.append([[p.X,p.Y,p.Z] for p in (e.position_at(j/N) for j in range(N+1))])
|
||||
exp.Next()
|
||||
return polys
|
||||
def plane_at(s):
|
||||
k=int(np.searchsorted(Sb, s, side='right')-1); k=min(max(k,0),len(B)-2)
|
||||
f=(s-Sb[k])/(Sb[k+1]-Sb[k])
|
||||
Q=(1-f)*B[k]+f*B[k+1]; T=(B[k+1]-B[k]); T=T/np.linalg.norm(T)
|
||||
return Q, T
|
||||
out={}
|
||||
for s in [4745.0005, 5100.0, 5469.9995]:
|
||||
Q,T=plane_at(s); eh=np.array([-T[1],T[0]])
|
||||
print(f"== station {s}: Q=({Q[0]:.3f},{Q[1]:.3f}) T=({T[0]:.5f},{T[1]:.5f})")
|
||||
out[str(s)]={'Q':Q.tolist(),'T':T.tolist(),'eh':eh.tolist(),'solids':{}}
|
||||
cnt=collections.Counter()
|
||||
for i,a in assign.items():
|
||||
if not (a['frm'] <= s <= a['to']): continue
|
||||
polys=slice_solid(i,Q,T)
|
||||
uv=[]
|
||||
for pl_ in polys:
|
||||
arr=np.array(pl_); u=(arr[:,:2]-Q)@eh; uv.append(np.stack([u,arr[:,2]],1).tolist())
|
||||
out[str(s)]['solids'][str(i)]={'id':a['id'],'group':a['group'],'polys':uv}
|
||||
cnt[a['group']]+=1
|
||||
allp=np.vstack([np.array(x) for x in uv]) if uv else np.zeros((0,2))
|
||||
if len(allp): print(f" {a['group']:<12} {a['id']:<16} nloops={len(uv)} u[{allp[:,0].min():.3f},{allp[:,0].max():.3f}] v[{allp[:,1].min():.3f},{allp[:,1].max():.3f}]")
|
||||
else: print(" EMPTY", a['group'], a['id'])
|
||||
print(" counts", dict(cnt))
|
||||
json.dump(out, open('/tmp/verify_daesan2_run/sections_final.json','w'))
|
||||
json.dump({str(i):a for i,a in assign.items()}, open('/tmp/verify_daesan2_run/assign.json','w'), ensure_ascii=False)
|
||||
print("saved")
|
||||
@@ -0,0 +1,110 @@
|
||||
import sys, json, math
|
||||
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
|
||||
import numpy as np
|
||||
from scipy.spatial import cKDTree
|
||||
SEC=json.load(open('/tmp/verify_daesan2_run/sections_final.json'))
|
||||
FIT=json.load(open('/tmp/verify_daesan2_run/fits.json'))
|
||||
C0=(14126.3,15305.0)
|
||||
def pts(st, pred):
|
||||
out=[]
|
||||
for sid,v in SEC[st]['solids'].items():
|
||||
if pred(v['group'], v['id']):
|
||||
for poly in v['polys']: out.append(np.array(poly)[:,:2])
|
||||
return np.vstack(out)
|
||||
def arc(cx,cy,r,a0,a1,step=1.0):
|
||||
if a1<a0: a1+=360
|
||||
n=max(2,int(r*math.radians(a1-a0)/step)); th=np.radians(np.linspace(a0,a1,n))
|
||||
return np.c_[cx+r*np.cos(th), cy+r*np.sin(th)]
|
||||
def seg(x0,y0,x1,y1,step=1.0):
|
||||
n=max(2,int(math.hypot(x1-x0,y1-y0)/step)); t=np.linspace(0,1,n); return np.c_[x0+(x1-x0)*t,y0+(y1-y0)*t]
|
||||
def circ(cx,cy,r,step=1.0):
|
||||
n=max(8,int(2*math.pi*r/step)); th=np.linspace(0,2*math.pi,n,endpoint=False); return np.c_[cx+r*np.cos(th),cy+r*np.sin(th)]
|
||||
REF={
|
||||
'LIN_crown_outer': arc(*C0,7282,40,140), 'LIN_crown_inner': arc(*C0,6982,40,140),
|
||||
'LIN_haunchL_outer': arc(11959.9,17122.8,4454,140,204.67), 'LIN_haunchL_inner': arc(11621.3,17406.9,3712,140,195.56),
|
||||
'LIN_haunchR_outer': arc(16105.0,16965.3,4699,336.35,40), 'LIN_haunchR_inner': arc(16479.6,17279.7,3910,344.38,40),
|
||||
'LIN_footL': np.vstack([seg(8171.3,16410.9,8471.3,16410.9), seg(8471.3,16410.9,8621.3,16410.9), seg(8621.3,16410.9,8651.3,16000.9), seg(8651.3,16000.9,8771.3,15825.9), seg(8771.3,15825.9,8771.3,15750.9)]),
|
||||
'LIN_footR': np.vstack([seg(20181.3,16226.9,19781.3,16226.9), seg(19781.3,16226.9,19631.3,16226.9), seg(19631.3,16226.9,19601.3,15816.9), seg(19601.3,15816.9,19481.3,15641.9), seg(19481.3,15641.9,19481.3,15566.9)]),
|
||||
'EXC_crown': arc(*C0,7402,40,140), 'EXC_haunchL': arc(11959.9,17122.8,4574,140,203.98), 'EXC_haunchR': arc(16105.0,16965.3,4819,336.97,40),
|
||||
'PAV_top': seg(9526.3,15764.0,18726.3,15580.0), 'PAV_bot': seg(9526.3,15484.0,18726.3,15300.0), 'PAV_sideL': seg(9526.3,15764.0,9526.3,15484.0), 'PAV_sideR': seg(18726.3,15580.0,18726.3,15300.0),
|
||||
'BASE_top': seg(9526.3,15484.0,18726.3,15300.0), 'BASE_bot': seg(9526.3,15334.0,18726.3,15150.0),
|
||||
'DRN_wallL1': seg(9101.3,14864.0,9101.3,15695.5), 'DRN_wallL2': seg(9401.3,14864.0,9401.3,15701.5), 'DRN_botL': seg(9101.3,14864.0,9401.3,14864.0), 'DRN_pipeL': circ(9251.3,15064.0,150),
|
||||
'DRN_wallR1': seg(18851.3,14680.0,18851.3,15517.5), 'DRN_wallR2': seg(19151.3,14680.0,19151.3,15511.5), 'DRN_botR': seg(18851.3,14680.0,19151.3,14680.0), 'DRN_pipeR': circ(19001.3,14880.0,150),
|
||||
}
|
||||
def to_dxf(P, st, sig):
|
||||
uc,vc,_=FIT[st]['outer']
|
||||
return np.c_[C0[0]+sig*(P[:,0]-uc)*1000.0, C0[1]+(P[:,1]-vc)*1000.0]
|
||||
def dist_stats(M, R, label):
|
||||
"""M model pts (dxf frame), R reference pts. Returns d(ref->model) max, d(model->ref) max within band."""
|
||||
if len(M)==0 or len(R)==0: return None
|
||||
tM=cKDTree(M); tR=cKDTree(R)
|
||||
dR,_=tM.query(R); dM,_=tR.query(M)
|
||||
return dict(ref_to_model_max=float(dR.max()), ref_to_model_mean=float(dR.mean()), model_to_ref_max=float(dM.max()), model_to_ref_mean=float(dM.mean()), n_ref=len(R), n_model=len(M))
|
||||
def run(sig, st, feats):
|
||||
res={}
|
||||
L=to_dxf(pts(st,lambda g,i:g=='라이닝'),st,sig); E=to_dxf(pts(st,lambda g,i:g=='굴착면'),st,sig)
|
||||
P=to_dxf(pts(st,lambda g,i:g=='노면(계획고)'),st,sig); B=to_dxf(pts(st,lambda g,i:g=='기층·인버트(가정)'),st,sig)
|
||||
D=to_dxf(pts(st,lambda g,i:g=='배수(측구)'),st,sig)
|
||||
def side_clip(R, M, side):
|
||||
# keep reference points on given side (L: x<C0x, R: x>C0x) and above model's lowest y on that side minus 50mm
|
||||
sm = M[(M[:,0]<C0[0])] if side=='L' else M[(M[:,0]>C0[0])]
|
||||
sr = R[(R[:,0]<C0[0])] if side=='L' else R[(R[:,0]>C0[0])]
|
||||
if len(sm)==0: return sr, 0, None
|
||||
ymin=sm[:,1].min()-0.05*1000
|
||||
keep=sr[sr[:,1]>=ymin]
|
||||
dropped=len(sr)-len(keep)
|
||||
rng=None if dropped==0 else [round(float(sr[sr[:,1]<ymin,1].min())),round(float(sr[sr[:,1]<ymin,1].max()))]
|
||||
return keep, dropped, rng
|
||||
out={}
|
||||
# Lining: arcs and foot lines; clip below model extent per side
|
||||
lin_ref=np.vstack([REF[k] for k in ['LIN_crown_outer','LIN_crown_inner','LIN_haunchL_outer','LIN_haunchL_inner','LIN_haunchR_outer','LIN_haunchR_inner']])
|
||||
keepL,dL,rL=side_clip(lin_ref[lin_ref[:,0]<C0[0]],L,'L'); keepR,dR_,rR=side_clip(lin_ref[lin_ref[:,0]>C0[0]],L,'R')
|
||||
crown=REF['LIN_crown_outer'];
|
||||
out['LIN_arcs']=dist_stats(L, np.vstack([crown, REF['LIN_crown_inner'], keepL, keepR]), 'LIN')
|
||||
out['LIN_arcs']['clipped_below_model_extent']={'L':dL,'R':dR_,'yrange_L':rL,'yrange_R':rR}
|
||||
out['LIN_footlines']=dist_stats(L, np.vstack([REF['LIN_footL'],REF['LIN_footR']]), 'LIN_foot')
|
||||
# per-arc max deviations (ref->model), by arc name
|
||||
perarc={}
|
||||
for k in ['LIN_crown_outer','LIN_crown_inner','LIN_haunchL_outer','LIN_haunchL_inner','LIN_haunchR_outer','LIN_haunchR_inner']:
|
||||
R=REF[k]; tM=cKDTree(L); d,_=tM.query(R); perarc[k]=round(float(d.max())*1,1)
|
||||
out['LIN_perarc_ref2model_max_mm']=perarc
|
||||
# Excavation
|
||||
exc_ref=np.vstack([REF['EXC_crown'],REF['EXC_haunchL'],REF['EXC_haunchR']])
|
||||
keepE_L,eL,reL=side_clip(exc_ref,E,'L'); keepE_R,eR,reR=side_clip(exc_ref,E,'R')
|
||||
out['EXC_arcs']=dist_stats(E, np.vstack([REF['EXC_crown'],keepE_L,keepE_R]),'EXC')
|
||||
out['EXC_arcs']['clipped_below_model_extent']={'L':eL,'R':eR,'yrange_L':reL,'yrange_R':reR}
|
||||
tE=cKDTree(E); d,_=tE.query(REF['EXC_crown']); out['EXC_crown_ref2model_max_mm']=round(float(d.max()),1)
|
||||
# Pavement: top/bottom/sides
|
||||
pav_ref=np.vstack([REF['PAV_top'],REF['PAV_bot'],REF['PAV_sideL'],REF['PAV_sideR']])
|
||||
out['PAV']=dist_stats(P[(P[:,0]>9526.3-150)&(P[:,0]<18726.3+150)], pav_ref,'PAV')
|
||||
# Base: model points restricted to road width (drawn span)
|
||||
Bw=B[(B[:,0]>=9526.3-1)&(B[:,0]<=18726.3+1)]
|
||||
out['BASE_drawn_span']=dist_stats(Bw, np.vstack([REF['BASE_top'],REF['BASE_bot']]),'BASE')
|
||||
out['BASE_model_extent_x']=[round(float(B[:,0].min())),round(float(B[:,0].max()))]
|
||||
out['BASE_model_ymin_ymax']=[round(float(B[:,1].min())),round(float(B[:,1].max()))]
|
||||
# Drains: both ditches vs both references
|
||||
dref=np.vstack([REF[k] for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR']])
|
||||
dm=D[(D[:,0]>8900)&(D[:,0]<19300)]
|
||||
out['DRN']=dist_stats(dm, dref,'DRN')
|
||||
# per-ditch: ref walls/bottom/pipe to model
|
||||
tD=cKDTree(dm)
|
||||
out['DRN_perref_ref2model_max_mm']={k: round(float(tD.query(REF[k])[0].max()),1) for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR']}
|
||||
out['DRN_model_xrange']=[round(float(dm[:,0].min())),round(float(dm[:,0].max()))]
|
||||
out['DRN_model_yrange']=[round(float(dm[:,1].min())),round(float(dm[:,1].max()))]
|
||||
return out
|
||||
allres={}
|
||||
for sig in [1,-1]:
|
||||
for st in ['4745.0005','5100.0','5469.9995']:
|
||||
r=run(sig,st,None); allres[f"{sig}|{st}"]=r
|
||||
print(f"=== sigma={sig} st={st} slope={FIT[st]['slope_top']*sig:.5f}")
|
||||
for k in ['LIN_arcs','EXC_arcs','PAV','BASE_drawn_span','DRN']:
|
||||
v=r[k];
|
||||
if v is None: print(' ',k,'none'); continue
|
||||
print(f" {k:16s} ref->model max={v['ref_to_model_max']*1000:7.1f}mm mean={v['ref_to_model_mean']*1000:6.1f} | model->ref max={v['model_to_ref_max']*1000:7.1f}mm mean={v['model_to_ref_mean']*1000:6.1f}")
|
||||
if 'clipped_below_model_extent' in v: print(' clipped', v['clipped_below_model_extent'])
|
||||
print(' LIN foot lines model->ref / ref->model:', None if r['LIN_footlines'] is None else (round(r['LIN_footlines']['model_to_ref_max']*1000,1), round(r['LIN_footlines']['ref_to_model_max']*1000,1)))
|
||||
print(' LIN per-arc ref->model max mm:', r['LIN_perarc_ref2model_max_mm'])
|
||||
print(' EXC crown ref->model max mm:', r['EXC_crown_ref2model_max_mm'])
|
||||
print(' BASE model x-range', r['BASE_model_extent_x'], 'y-range', r['BASE_model_ymin_ymax'])
|
||||
print(' DRN per-ref ref->model max mm:', r['DRN_perref_ref2model_max_mm'], 'model x', r['DRN_model_xrange'], 'y', r['DRN_model_yrange'])
|
||||
json.dump(allres, open('/tmp/verify_daesan2_run/hd2_results.json','w'), indent=1, default=str)
|
||||
@@ -0,0 +1,44 @@
|
||||
import sys, json, math
|
||||
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
|
||||
import numpy as np
|
||||
from scipy.spatial import cKDTree
|
||||
from scipy.optimize import least_squares
|
||||
import matplotlib; matplotlib.use('Agg'); import matplotlib.pyplot as plt
|
||||
SEC = json.load(open('/tmp/verify_daesan2_run/sections_final.json'))
|
||||
def pts_of(st, pred):
|
||||
out=[]; ids=[]
|
||||
for sid,v in SEC[st]['solids'].items():
|
||||
if pred(v['group'], v['id']):
|
||||
for poly in v['polys']:
|
||||
out.append(np.array(poly)[:, :2]); ids.append(v['id'])
|
||||
return (np.vstack(out) if out else np.zeros((0,2))), ids
|
||||
def fit_circle(P):
|
||||
x,y=P[:,0],P[:,1]
|
||||
A=np.c_[2*x,2*y,np.ones(len(x))]; b=x*x+y*y
|
||||
c,_,_,_=np.linalg.lstsq(A,b,rcond=None)
|
||||
xc,yc=c[0],c[1]; r=math.sqrt(c[2]+xc*xc+yc*yc)
|
||||
return xc,yc,r
|
||||
# ---- lining crown fit per station
|
||||
fits={}
|
||||
for st in SEC:
|
||||
P,_=pts_of(st, lambda g,i: g=='라이닝')
|
||||
u0=(P[:,0].min()+P[:,0].max())/2; v0=P[:,1].max()-7.282
|
||||
d=np.hypot(P[:,0]-u0,P[:,1]-v0)
|
||||
sel_o=P[(np.abs(d-7.282)<0.03)&(P[:,1]-v0>4.0)]
|
||||
sel_i=P[(np.abs(d-6.982)<0.03)&(P[:,1]-v0>4.0)]
|
||||
co=fit_circle(sel_o); ci=fit_circle(sel_i)
|
||||
fits[st]={'outer':co,'inner':ci,'n_o':len(sel_o),'n_i':len(sel_i)}
|
||||
print(f"st {st}: outer n={len(sel_o)} centre=({co[0]:.4f},{co[1]:.4f}) R={co[2]:.4f} | inner n={len(sel_i)} centre=({ci[0]:.4f},{ci[1]:.4f}) R={ci[2]:.4f}")
|
||||
# ---- pavement slope (top/bottom envelopes) per station
|
||||
for st in SEC:
|
||||
P,_=pts_of(st, lambda g,i: g=='노면(계획고)')
|
||||
bins=np.arange(P[:,0].min(),P[:,0].max()+0.25,0.25)
|
||||
tops=[];bots=[]
|
||||
for a,b in zip(bins[:-1],bins[1:]):
|
||||
m=(P[:,0]>=a)&(P[:,0]<b)
|
||||
if m.sum()>0: tops.append((P[m,0].mean(),P[m,1].max())); bots.append((P[m,0].mean(),P[m,1].min()))
|
||||
tops=np.array(tops); bots=np.array(bots)
|
||||
mt=np.polyfit(tops[:,0],tops[:,1],1); mb=np.polyfit(bots[:,0],bots[:,1],1)
|
||||
fits[st]['slope_top']=mt[0]; fits[st]['slope_bot']=mb[0]
|
||||
print(f"st {st}: PAV top slope dv/du={mt[0]:.5f} (S={mt[0]*100:.3f}%), bottom slope={mb[0]:.5f}; top@u=0 {mt[1]:.4f}, bottom@u=0 {mb[1]:.4f}; u-range [{P[:,0].min():.3f},{P[:,0].max():.3f}]")
|
||||
json.dump(fits, open('/tmp/verify_daesan2_run/fits.json','w'))
|
||||
@@ -0,0 +1,77 @@
|
||||
import sys, json, pickle, math
|
||||
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
|
||||
from build123d import import_step, Edge, Shape
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Section
|
||||
from OCP.gp import gp_Pln, gp_Pnt, gp_Dir
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopAbs import TopAbs_EDGE
|
||||
from OCP.TopoDS import TopoDS
|
||||
import numpy as np
|
||||
solids = import_step("/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids.step").solids()
|
||||
mp = pickle.load(open('/tmp/verify_daesan2_run/mapping.pkl','rb'))['mapping']
|
||||
caps = json.load(open('/tmp/verify_daesan2_run/caps.json'))
|
||||
ch = json.load(open('/tmp/verify_daesan2_run/chain.json'))
|
||||
lin = ch['lin']
|
||||
starts = {k: np.array(v) for k,v in ch['starts'].items()}
|
||||
ends = {k: np.array(v) for k,v in ch['ends'].items()}
|
||||
# station knots
|
||||
S = [caps[k]['from_sta'] for k in lin] + [caps[lin[-1]]['to_sta']]
|
||||
P = [starts[k] for k in lin] + [ends[lin[-1]]]
|
||||
def plane_at(s):
|
||||
for i in range(len(lin)):
|
||||
if S[i] <= s <= S[i+1]:
|
||||
f=(s-S[i])/(S[i+1]-S[i])
|
||||
Q=(1-f)*P[i]+f*P[i+1]
|
||||
T=(P[i+1]-P[i])[:2]; T=T/np.linalg.norm(T)
|
||||
return Q, T, lin[i]
|
||||
raise ValueError(s)
|
||||
def slice_solid(idx, Q, T):
|
||||
nrm=np.array([T[0],T[1],0.0])
|
||||
pl=gp_Pln(gp_Pnt(*Q), gp_Dir(*nrm))
|
||||
face=BRepBuilderAPI_MakeFace(pl,-80,80,-80,80).Face()
|
||||
sec=BRepAlgoAPI_Section(solids[idx].wrapped, face); sec.Build()
|
||||
sh=sec.Shape()
|
||||
polys=[]
|
||||
exp=TopExp_Explorer(sh, TopAbs_EDGE)
|
||||
while exp.More():
|
||||
e=Shape.cast(exp.Current())
|
||||
L=e.length; N=max(2,int(L/0.02))
|
||||
pts=[e.position_at(i/N) for i in range(N+1)]
|
||||
polys.append([[p.X,p.Y,p.Z] for p in pts])
|
||||
exp.Next()
|
||||
return polys
|
||||
out={}
|
||||
stations=[4745.0005, 5100.0, 5469.9995]
|
||||
for s in stations:
|
||||
Q,T,lk=plane_at(s)
|
||||
eh=np.array([-T[1],T[0]])
|
||||
print(f"== station {s}: comp {lk}, Q=({Q[0]:.4f},{Q[1]:.4f},{Q[2]:.4f}) T=({T[0]:.5f},{T[1]:.5f}) ")
|
||||
out[str(s)]={'Q':Q.tolist(),'T':T.tolist(),'eh':eh.tolist(),'comps':{}}
|
||||
for cid,info in caps.items():
|
||||
if not (info['from_sta'] <= s <= info['to_sta']): continue
|
||||
polys=slice_solid(mp[cid], Q, T)
|
||||
uv=[]
|
||||
for pl_ in polys:
|
||||
arr=np.array(pl_)
|
||||
dx=arr[:,:2]-Q[:2]
|
||||
u=dx@eh; v=arr[:,2]
|
||||
uv.append(np.stack([u,v],1).tolist())
|
||||
out[str(s)]['comps'][cid]={'group':info['group'],'polys':uv}
|
||||
allp=np.vstack([np.array(x) for x in uv]) if uv else np.zeros((0,2))
|
||||
if info['group'] in ('라이닝','굴착면','노면(계획고)','기층·인버트(가정)','공동구(가정)') or cid.startswith('DRN') or 'DRN' in cid or 'HI' in cid:
|
||||
pass
|
||||
# summary per group
|
||||
import collections
|
||||
summ=collections.defaultdict(list)
|
||||
for cid,v in out[str(s)]['comps'].items():
|
||||
summ[v['group']].append(cid)
|
||||
for g,cids in summ.items():
|
||||
allp=[p for cid in cids for poly in out[str(s)]['comps'][cid]['polys'] for p in poly]
|
||||
A=np.array(allp) if allp else np.zeros((0,2))
|
||||
if len(A):
|
||||
print(f" {g}: n_comp={len(cids)} n_pts={len(A)} u[{A[:,0].min():.3f},{A[:,0].max():.3f}] v[{A[:,1].min():.3f},{A[:,1].max():.3f}] ids={cids[:4]}")
|
||||
else:
|
||||
print(f" {g}: n_comp={len(cids)} EMPTY ids={cids[:4]}")
|
||||
json.dump(out, open('/tmp/verify_daesan2_run/sections_raw.json','w'))
|
||||
print("saved sections_raw.json")
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Step 3 cross-check of the lining deliverables (Task 6).
|
||||
Re-reads the raw DXF texts (04-004..009 and 03-001) with a plain text search,
|
||||
independent of extract_lining_spec.py, and compares them with the report and
|
||||
the model meta. Prints PASS/FAIL per check with population counts.
|
||||
Usage: verify_lining.py <RAG_ROOT> <DOC_LINING_MD> <DOC_STD_MD> <META_JSON>
|
||||
"""
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
from ezdxf import recover
|
||||
|
||||
RAG, DOC_LIN, DOC_STD, META = sys.argv[1:5]
|
||||
RESULTS = []
|
||||
|
||||
|
||||
def check(name, ok, detail):
|
||||
RESULTS.append((name, ok, detail))
|
||||
print(f"[{'PASS' if ok else 'FAIL'}] {name}: {detail}")
|
||||
|
||||
|
||||
def all_texts(path):
|
||||
try:
|
||||
doc = ezdxf.readfile(path)
|
||||
except Exception:
|
||||
doc, _ = recover.readfile(path)
|
||||
out = []
|
||||
for e in doc.modelspace():
|
||||
if e.dxftype() == "TEXT":
|
||||
out.append(e.dxf.text)
|
||||
elif e.dxftype() == "MTEXT":
|
||||
out.append(e.plain_text())
|
||||
return out
|
||||
|
||||
|
||||
sheets = sorted(glob.glob(os.path.join(RAG, "04_본선터널기타", "C0060304-00[4-9]*.dxf")))
|
||||
std = glob.glob(os.path.join(RAG, "03_본선터널굴착", "C0060303-001*.dxf"))
|
||||
raw = {os.path.basename(p)[:14]: all_texts(p) for p in sheets}
|
||||
doc_lin = open(DOC_LIN, encoding="utf-8").read()
|
||||
doc_std = open(DOC_STD, encoding="utf-8").read()
|
||||
meta = json.load(open(META, encoding="utf-8"))
|
||||
|
||||
# C1 common values present in every sheet (population = 6 sheets)
|
||||
common = {"fck=27MPa": "fck 27", "fy=400MPa": "SD400", "최대치수 Ø25": "Ø25",
|
||||
"R1=6.892": "R1 6.892", "R2=3,712": "R2 3,712", "R2'=3,910": "R2' 3,910",
|
||||
"EL.-0.367": "EL -0.367", "EL. 0.000": "EL 0.000"}
|
||||
for token, label in common.items():
|
||||
hits = [k for k, t in raw.items() if any(token in s for s in t)]
|
||||
check(f"C1 {label} in all sheets", len(hits) == len(raw), f"{len(hits)}/{len(raw)} sheets (표본 아님, 전수)")
|
||||
|
||||
# C2 temperature-not-considered detail appears on 3 sheets (005, 006, 007)
|
||||
temp = [k for k, t in raw.items() if any("온도미고려" in s for s in t)]
|
||||
check("C2 온도미고려 detail sheets", sorted(temp) == sorted([k for k in raw if k[9:12] in ("005", "006", "007")]),
|
||||
f"{len(temp)} sheets: {[k[9:12] for k in sorted(temp)]} (전수)")
|
||||
|
||||
# C3 pattern-group count in raw texts equals report rows (9)
|
||||
groups = [s for t in raw.values() for s in t if "패턴" in s and "주철근 조립도" in s or (("패턴)" in s) and ("입출구부" in s or "온도미고려" in s))]
|
||||
rows = len([l for l in doc_lin.splitlines() if l.startswith("| C0060304-")])
|
||||
check("C3 pattern-detail count = report rows", len(groups) == rows,
|
||||
f"raw {len(groups)} vs report {rows} (전수)")
|
||||
|
||||
# C4 R1 cross-check against the standard section drawing and the report table
|
||||
std_txt = all_texts(std[0]) if std else []
|
||||
std_has = any(("6.982" in s or "6982" in s or "6,982" in s) for s in std_txt)
|
||||
check("C4 R1 6.982 in standard section (03-001 raw)", std_has, f"03-001 raw texts scanned: {len(std_txt)}")
|
||||
check("C4b standard table DOC states R1 6,982", "6,982" in doc_std, "설계_표준단면치수표 본문 대조")
|
||||
check("C4c report records decision", "6.982 m 유지" in doc_lin, "R1 결정 문구 존재")
|
||||
|
||||
# C5 thickness arithmetic: outer R 7.282 m minus inner R
|
||||
t982 = round(7.282 - 6.982, 3); t892 = round(7.282 - 6.892, 3)
|
||||
check("C5 thickness arithmetic", t982 == 0.300 and t892 == 0.390,
|
||||
f"7.282-6.982={t982} m (matches 300 mm), 7.282-6.892={t892} m (does not match)")
|
||||
|
||||
# C6 V9 existence + name version + key content of OUTPUT
|
||||
name = os.path.basename(DOC_LIN)
|
||||
m = re.match(r"^설계_콘크리트라이닝상세_(\d{4}-\d{2}-\d{2})_v(\d+\.\d+\.\d{2})\.md$", name)
|
||||
ver_hdr = re.search(r"\| (v\d+\.\d+\.\d{2}) \|", doc_lin)
|
||||
check("C6 file name pattern and version", bool(m) and ver_hdr is not None and ver_hdr.group(1) == "v" + m.group(2),
|
||||
f"name v{m.group(2) if m else '?'} vs header {ver_hdr.group(1) if ver_hdr else '?'}")
|
||||
check("C6b OUTPUT key items present", all(k in doc_lin for k in ["fck", "SD400", "입출구부", "확정 2026-10-21"]),
|
||||
"fck, SD400, 입출구부, 결정 문구")
|
||||
|
||||
# C7 meta: lining reference and open items
|
||||
lr = meta.get("lining_reference", {})
|
||||
check("C7 meta lining_reference 9 groups", sum(len(s["groups"]) for s in lr.get("sheets", [])) == 9,
|
||||
f"{sum(len(s['groups']) for s in lr.get('sheets', []))} groups, status={lr.get('summary', '')[:40]}...")
|
||||
check("C7b meta open records decision", any("확정 2026-10-21" in o for o in meta.get("open", [])), "open[] 결정 문구")
|
||||
|
||||
# C8 scripts never write to the RAG source tree
|
||||
writes = []
|
||||
for sc in glob.glob("scripts/*lining*.py"):
|
||||
txt = open(sc, encoding="utf-8").read()
|
||||
if re.search(r"open\([^)]*RAG[^)]*['\"]w", txt):
|
||||
writes.append(sc)
|
||||
check("C8 no script writes into RAG", not writes, f"scanned {len(glob.glob('scripts/*lining*.py'))} scripts")
|
||||
|
||||
n_fail = sum(1 for r in RESULTS if not r[1])
|
||||
print(f"SUMMARY: {len(RESULTS) - n_fail} PASS / {n_fail} FAIL of {len(RESULTS)}")
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Step 3 deterministic checks for the solid model (Task 8, 대산2본선3D변환).
|
||||
|
||||
Usage: python3 verify_solids.py <project_dir> <pattern_csv>
|
||||
project_dir : Code/3d-drawing folder (contains model/ and data/)
|
||||
pattern_csv : data/pattern_stations_dangjin.csv
|
||||
|
||||
Checks (each prints PASS/FAIL with counts, method and population):
|
||||
M1 meta counts per group M2 pattern runs contiguous 4745..5480
|
||||
M3 runs == CSV rows (in range) M4 component ids and from/to/pattern match runs
|
||||
M5 meta volume sum == verify json M6 pattern-run ids (PAV/BASE/DRN/HBOX/IBOX) present
|
||||
S1 STEP re-import: solid count, validity, total volume vs meta (0.1 %)
|
||||
S2 lining vs excavation overlap per run (volume, threshold 0.05 m2 x run length)
|
||||
G1 GLB reload: geometry count and non-empty meshes
|
||||
"""
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
PROJ, CSV = sys.argv[1], sys.argv[2]
|
||||
S0, S1 = 4745, 5480
|
||||
results = []
|
||||
|
||||
|
||||
def rec(code, verdict, method, note=""):
|
||||
results.append((code, verdict, method, note))
|
||||
print(f"{code} {verdict} | {method} | {note}")
|
||||
|
||||
|
||||
meta = json.load(open(os.path.join(PROJ, "model/tunnel_solids_meta.json"), encoding="utf-8"))
|
||||
verify = json.load(open(os.path.join(PROJ, "data/verify_solids.json"), encoding="utf-8"))
|
||||
comps = meta["components"]
|
||||
runs = meta["pattern_runs"]
|
||||
solids_meta = [c for c in comps if not c.get("reference")]
|
||||
refs = [c for c in comps if c.get("reference")]
|
||||
|
||||
# M1 counts per group
|
||||
cnt = Counter(c["group"] for c in solids_meta)
|
||||
exp = {"라이닝": 40, "굴착면": 40, "노면(계획고)": 40, "기층·인버트(가정)": 40, "배수(측구)": 80, "공동구(가정)": 80}
|
||||
bad = {g: (cnt.get(g, 0), n) for g, n in exp.items() if cnt.get(g, 0) != n}
|
||||
rec("M1", "PASS" if not bad and len(solids_meta) == 320 and len(refs) == 6 else "FAIL",
|
||||
f"meta components 전수 {len(comps)}", f"solids {len(solids_meta)} (expect 320), refs {len(refs)} (expect 6), group mismatch {bad}")
|
||||
|
||||
# M2 contiguity
|
||||
runs_sorted = sorted(runs, key=lambda r: r["from"])
|
||||
gaps = [(a["to"], b["from"]) for a, b in zip(runs_sorted, runs_sorted[1:]) if a["to"] != b["from"]]
|
||||
ok2 = runs_sorted[0]["from"] == S0 and runs_sorted[-1]["to"] == S1 and not gaps
|
||||
rec("M2", "PASS" if ok2 else "FAIL", f"pattern_runs 전수 {len(runs)}",
|
||||
f"range {runs_sorted[0]['from']}..{runs_sorted[-1]['to']}, gaps/overlaps {gaps}")
|
||||
|
||||
# M3 runs vs CSV rows inside the model range
|
||||
rows = list(csv.DictReader(open(CSV, encoding="utf-8")))
|
||||
csv_in = [r for r in rows if float(r["sta_to"]) > S0 and float(r["sta_from"]) < S1]
|
||||
# clip each CSV interval to the model range (row 95 runs 5468..5495 and is cut at 5480 by design)
|
||||
csv_set = Counter((max(int(float(r["sta_from"])), S0), min(int(float(r["sta_to"])), S1), r["lining_pattern"]) for r in csv_in)
|
||||
run_set = Counter((r["from"], r["to"], r["pattern"]) for r in runs)
|
||||
mism = (csv_set - run_set) + (run_set - csv_set)
|
||||
rec("M3", "PASS" if not mism else "FAIL",
|
||||
f"CSV 전수 {len(rows)} 행 중 범위 내 {len(csv_in)} 행과 pattern_runs {len(runs)} 행 대조",
|
||||
f"mismatch {dict(mism)}")
|
||||
|
||||
# M4 component from/to/pattern equal their run
|
||||
run_by_id = {r["id"]: r for r in runs}
|
||||
bad4 = []
|
||||
for c in solids_meta:
|
||||
if c["group"] == "라이닝":
|
||||
r = run_by_id.get(c["id"])
|
||||
if not r or (r["from"], r["to"], r["pattern"]) != (c["from_sta"], c["to_sta"], c["pattern"]):
|
||||
bad4.append(c["id"])
|
||||
rec("M4", "PASS" if not bad4 else "FAIL", f"라이닝 전수 {cnt['라이닝']} 대 run",
|
||||
f"mismatch {bad4[:5]}")
|
||||
|
||||
# M5 volume sum
|
||||
vol = sum(c["volume_m3"] for c in solids_meta)
|
||||
ok5 = abs(vol - verify["volume_sum_m3"]) < 0.001
|
||||
rec("M5", "PASS" if ok5 else "FAIL", f"meta 부피 합 전수 {len(solids_meta)} 개",
|
||||
f"sum {vol:.3f} vs verify {verify['volume_sum_m3']}")
|
||||
|
||||
# M6 per-run component ids
|
||||
need = ["LIN", "EXC", "PAV", "BASE", "DRNL", "DRNR", "HBOX", "IBOX"]
|
||||
ids = {c["id"] for c in solids_meta}
|
||||
miss = []
|
||||
for i, r in enumerate(runs_sorted):
|
||||
for pre in need:
|
||||
cid = f"{pre}_{i+1:02d}_{r['pattern']}"
|
||||
if pre in ("DRNL", "DRNR"):
|
||||
cid = f"{pre}_{i+1:02d}_{r['pattern']}"
|
||||
if cid not in ids and pre not in ("DRNL", "DRNR"):
|
||||
miss.append(cid)
|
||||
drn = [c for c in solids_meta if c["group"] == "배수(측구)"]
|
||||
rec("M6", "PASS" if not miss and len(drn) == 80 else "FAIL",
|
||||
f"run {len(runs_sorted)} x 부품 종류 {len(need)} 대조 (배수 L/R 포함 {len(drn)})",
|
||||
f"missing {miss[:5]}")
|
||||
|
||||
# S1 STEP re-import
|
||||
import build123d as bd # noqa: E402
|
||||
step_path = os.path.join(PROJ, "model/tunnel_solids.step")
|
||||
shape = bd.importers.import_step(step_path) if hasattr(bd, "importers") else bd.import_step(step_path)
|
||||
solids = shape.solids()
|
||||
n_valid = sum(1 for s in solids if s.is_valid)
|
||||
vol_step = sum(s.volume for s in solids)
|
||||
rel = abs(vol_step - vol) / vol
|
||||
rec("S1", "PASS" if len(solids) == 320 and n_valid == len(solids) and rel < 0.001 else "FAIL",
|
||||
f"STEP 재임포트 솔리드 전수 {len(solids)}, is_valid 전수 {n_valid}, 부피 재계산 합 vs meta",
|
||||
f"count {len(solids)}, valid {n_valid}, vol_step {vol_step:.3f} vs meta {vol:.3f} (rel {rel:.2e})")
|
||||
|
||||
# S2 overlap lining vs excavation per run, using the STEP solids
|
||||
def centroid(s):
|
||||
return s.center()
|
||||
|
||||
lin = [s for s in solids]
|
||||
# classify STEP solids by volume match to meta (only for overlap sampling)
|
||||
meta_vol = sorted([(c["volume_m3"], c["group"], c["id"], c["from_sta"], c["to_sta"]) for c in solids_meta])
|
||||
sample_ids = [r["id"] for r in runs_sorted[:: max(1, len(runs_sorted) // 4)]][:4]
|
||||
lines = []
|
||||
ok_overlap = True
|
||||
for rid in sample_ids:
|
||||
r = run_by_id[rid]
|
||||
k = rid.split("_", 1)[1]
|
||||
idx = [i for i, rr in enumerate(runs_sorted) if rr["id"] == rid][0] + 1
|
||||
lin_c = next(c for c in solids_meta if c["id"] == f"LIN_{idx:02d}_{r['pattern']}")
|
||||
exc_c = next(c for c in solids_meta if c["id"] == f"EXC_{idx:02d}_{r['pattern']}")
|
||||
def find(vol_target):
|
||||
best = min(solids, key=lambda s: abs(s.volume - vol_target))
|
||||
assert abs(best.volume - vol_target) < 1e-3, "no STEP solid matches meta volume"
|
||||
return best
|
||||
sl = find(lin_c["volume_m3"]); se = find(exc_c["volume_m3"])
|
||||
inter = sl.intersect(se)
|
||||
iv = sum(x.volume for x in inter) if inter is not None else 0.0
|
||||
length = r["to"] - r["from"]
|
||||
thr = 0.05 * length
|
||||
lines.append(f"{rid}: ovl {iv:.4f} m3 (thr {thr:.2f})")
|
||||
if iv > thr:
|
||||
ok_overlap = False
|
||||
rec("S2", "PASS" if ok_overlap else "FAIL", f"샘플 run {len(sample_ids)}건 라이닝∩굴착 부피 (부피 매칭 솔리드)",
|
||||
"; ".join(lines))
|
||||
|
||||
# G1 GLB reload
|
||||
import trimesh # noqa: E402
|
||||
glb = trimesh.load(os.path.join(PROJ, "model/tunnel_solids.glb"), force="scene")
|
||||
geoms = list(glb.geometry.values())
|
||||
empty = [g for g in geoms if len(g.faces) == 0]
|
||||
rec("G1", "PASS" if len(geoms) == verify["glb_geometries"] and not empty else "FAIL",
|
||||
f"GLB 로드 지오메트리 전수 {len(geoms)}", f"empty {len(empty)}, expect {verify['glb_geometries']}")
|
||||
|
||||
fails = [r for r in results if r[1] == "FAIL"]
|
||||
print("SUMMARY", len(results), "checks, FAIL", len(fails))
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Headless browser check of the 3D viewer (Task 6, V11).
|
||||
Needs Playwright + Chromium and the viewer/model served over HTTP, e.g.
|
||||
python3 -m http.server 8765 (from Code/3d-drawing)
|
||||
python3 verify_viewer_headless.py http://127.0.0.1:8765/viewer/index.html <chromium_path>
|
||||
Checks: page loads without page errors, canvas exists, meta text is shown,
|
||||
and a click on the lining (other layers hidden) returns station and pattern.
|
||||
"""
|
||||
import sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
URL, CHROME = sys.argv[1], sys.argv[2]
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(executable_path=CHROME, args=["--use-gl=swiftshader", "--enable-webgl", "--ignore-gpu-blocklist", "--no-sandbox"])
|
||||
pg = b.new_page(viewport={"width": 1280, "height": 800})
|
||||
errors = []
|
||||
pg.on("pageerror", lambda e: errors.append(str(e)))
|
||||
pg.goto(URL, wait_until="networkidle", timeout=90000)
|
||||
pg.wait_for_timeout(6000)
|
||||
canvas = pg.evaluate("() => !!document.querySelector('canvas')")
|
||||
body = pg.evaluate("() => document.body.innerText")
|
||||
print("page errors:", errors)
|
||||
print("canvas present:", canvas, "| meta shown:", "패턴:" in body and "범위:" in body)
|
||||
for label in ["excavation_R7402", "road_surface", "ground_GL"]:
|
||||
pg.locator("label", has_text=label).locator("input").uncheck()
|
||||
pg.wait_for_timeout(500)
|
||||
readout = None
|
||||
for fx in [0.5, 0.45, 0.55, 0.4, 0.6, 0.35, 0.65]:
|
||||
for fy in [0.5, 0.45, 0.55, 0.4, 0.6]:
|
||||
pg.mouse.click(1280 * fx, 800 * fy)
|
||||
pg.wait_for_timeout(200)
|
||||
t = pg.evaluate("() => { const els=[...document.querySelectorAll('div,pre,p')]; return els.map(e=>e.innerText).filter(s=>s && s.includes('교차점')).pop()||null }")
|
||||
if t and "측점" in t:
|
||||
readout = t
|
||||
break
|
||||
if readout:
|
||||
break
|
||||
print("lining readout:", (readout or "NO LINING HIT").replace("\n", " | ")[:700])
|
||||
b.close()
|
||||
Reference in new issue
Block a user