Import S-CANVAS source + iter=1~7 lint cleanup

S-CANVAS (Saman Corp.) — DXF + DEM + AI 기반 3D 조감도 생성 엔진.
~24k LOC Python (scanvas_maker.py 7072 LOC GUI + 구조물 파서/빌더 다수).

이 커밋은 7-iter cleanup이 적용된 상태로 import:
- F821 8 + B023 6: 비동기 lambda + except/loop 변수 캡처 NameError
  (Py3.13에서 reproduce 확인된 진짜 버그)
- RUF012 4 + RUF013 1: ClassVar / implicit Optional 명시화
- F811/B905/B904/F401/F841/W293/F541/UP/SIM/RUF/PLR 700+ cleanup/modernization

신규 파일:
- ruff.toml: target=py313, Korean unicode/저자 스타일/도메인 복잡도 무력화
- requirements-py313.txt: pyproj>=3.7, scipy>=1.14, numpy>=2.0.2 (Py3.13 wheel)
- .gitignore: gcp-key.json, 캐시, 백업, 생성 이미지 제외

검증: ruff 0 errors, py_compile 0 errors, import 33/33 OK on Py3.13.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 10:29:08 +09:00
parent 53d8b53c2f
commit b9342f6726
92 changed files with 3413501 additions and 0 deletions

View File

@@ -0,0 +1,372 @@
"""옹벽 3D 파라메트릭 빌더.
구성요소:
1. 본체 (사다리꼴 단면을 길이방향으로 sweep)
2. 기초 slab (하부 넓은 base)
3. 뒤채움 지형 (배면 토사)
4. 배면 앵커바 × N (격자 배치)
5. 상단 안전난간 (parapet)
6. 수축이음 세로선 (표면에 시각화)
7. 배수공 (weep hole)
8. 전면 지반 + 바위
"""
from __future__ import annotations
import math
import numpy as np
import pyvista as pv
from retaining_wall_parser import RetainingWallParams
COLORS = {
"wall": "#A8A59B", # 콘크리트
"wall_face": "#B5B2A7", # 전면 (약간 밝게)
"base": "#8D8A80", # 기초 slab
"backfill": "#8B7355", # 뒤채움 토사
"anchor": "#2C3E50", # 앵커바 (철)
"anchor_plate": "#566573", # 앵커 판
"parapet": "#A8A59B",
"rail": "#4A4A4A", # 난간
"joint": "#5D4A33", # 수축이음 (진한 선)
"weep": "#2C3E50",
"ground": "#8B7D6B",
"rock": "#6B5D50", # 기초암반
}
class RetainingWallBuilder:
def __init__(self, params: RetainingWallParams):
self.p = params
self.meshes: list[tuple[pv.PolyData, str, float]] = []
def build_all(self):
self.meshes = []
self._build_base_slab()
self._build_wall_body()
self._build_backfill()
self._build_anchors()
self._build_parapet()
self._build_contraction_joints()
self._build_weep_holes()
self._build_ground()
return self.meshes
# 좌표계:
# X: 길이방향 (총 연장)
# Y: 전면(-Y) ↔ 배면(+Y) 방향
# Z: 높이 (EL)
# --- 기초 slab (바닥) ---
def _build_base_slab(self):
p = self.p
L = p.total_length
W = p.base_slab_width
T = p.base_slab_thickness
z0 = p.bottom_el
z1 = z0 + T
# 기초는 벽 하단에서 전면/배면 모두 확장
# 중심이 벽 중심과 같다고 가정
self._add_box(-L/2, L/2, -W/2, W/2, z0, z1, COLORS["base"])
# --- 본체 벽 (사다리꼴 단면 sweep) ---
def _build_wall_body(self):
p = self.p
L = p.total_length
z_bot = p.bottom_el + p.base_slab_thickness
z_top = p.top_el
W_bot = p.avg_bottom_width
W_top = p.avg_top_width
# 전면 경사: 하단이 더 앞으로 나온 사다리꼴
# 단면: YZ 평면에서
# 하단: (-W_bot/2, 0) ~ (W_bot/2, 0)
# 상단: (-W_top/2, H) ~ (W_top/2, H)
# 실제로는 배면은 수직, 전면만 경사
# 배면 벽 (Y+ 면)
# 단면:
# 전면(Y-): (-W_bot/2 + batter*H, z_bot) → (-W_top/2, z_top)
# 배면(Y+): (W_bot/2, z_bot) → (W_bot/2, z_top) (수직)
# batter: 전면이 안쪽으로 기움 (현재는 W_top - W_bot 차이로 묵시적 처리)
# 8개 코너 점
y_front_bot = -W_bot / 2
y_front_top = -W_top / 2 # 전면은 상단에서 얇아짐
y_back_bot = W_bot / 2
y_back_top = W_top / 2 # 배면도 상단에서 얇아짐 (대칭 사다리꼴) 아니면 수직
# 실제 옹벽은 배면 수직이 더 흔함. 수직으로 고정:
y_back_bot = W_bot / 2
y_back_top = W_bot / 2
pts = np.array([
[-L/2, y_front_bot, z_bot], # 0 좌하전
[L/2, y_front_bot, z_bot], # 1 우하전
[L/2, y_back_bot, z_bot], # 2 우하배
[-L/2, y_back_bot, z_bot], # 3 좌하배
[-L/2, y_front_top, z_top], # 4 좌상전
[L/2, y_front_top, z_top], # 5 우상전
[L/2, y_back_top, z_top], # 6 우상배
[-L/2, y_back_top, z_top], # 7 좌상배
])
faces = np.hstack([
[4, 0, 3, 2, 1], # 바닥
[4, 4, 5, 6, 7], # 상단
[4, 0, 1, 5, 4], # 전면 (경사)
[4, 2, 3, 7, 6], # 배면
[4, 1, 2, 6, 5], # 우측 단부
[4, 0, 4, 7, 3], # 좌측 단부
])
self.meshes.append((pv.PolyData(pts, faces), COLORS["wall"], 1.0))
# --- 뒤채움 (배면 토사) ---
def _build_backfill(self):
p = self.p
L = p.total_length
z_top = p.top_el
# 배면에서 뒤로 뻗은 토사 (30 길이)
back_depth = 15.0
y_start = p.avg_bottom_width / 2 # 벽 배면
y_end = y_start + back_depth
# 토사는 상단부터 아래로 경사 (자연 지형)
# 단면: 상단 수평, 경사면, 바닥 수평
pts = np.array([
[-L/2, y_start, z_top],
[ L/2, y_start, z_top],
[ L/2, y_end, z_top], # 뒤쪽 상단
[-L/2, y_end, z_top],
# 바닥은 z_top - 1 정도로 살짝 낮게
[-L/2, y_start, z_top - 0.1],
[ L/2, y_start, z_top - 0.1],
[ L/2, y_end, z_top - 3],
[-L/2, y_end, z_top - 3],
])
faces = np.hstack([
[4, 0, 3, 2, 1],
[4, 4, 5, 6, 7],
[4, 0, 1, 5, 4],
[4, 2, 3, 7, 6],
[4, 1, 2, 6, 5],
[4, 0, 4, 7, 3],
])
self.meshes.append((pv.PolyData(pts, faces), COLORS["backfill"], 1.0))
# --- 배면 앵커바 (격자 배치) ---
def _build_anchors(self):
p = self.p
if not p.has_anchors:
return
L = p.total_length
z_bot = p.bottom_el + p.base_slab_thickness
z_top = p.top_el - 1.0 # 상단은 난간 영역 제외
# 격자 개수 결정
dx = p.anchor_spacing_h
dz = p.anchor_spacing_v
nx = max(int(L / dx), 2)
nz = max(int((z_top - z_bot) / dz), 2)
# 개수 상한 (너무 많으면 안 만듦)
max_total = 200
if nx * nz > max_total:
# 간격 늘리기
ratio = math.sqrt(nx * nz / max_total)
nx = int(nx / ratio)
nz = int(nz / ratio)
# 격자 배치
y_wall_back = p.avg_bottom_width / 2 # 벽 배면 Y 좌표
angle_rad = math.radians(p.anchor_angle_deg)
anchor_L = p.anchor_length
# 앵커는 배면에서 아래쪽으로 경사져 안으로 매입
dx_anchor = math.cos(angle_rad) * anchor_L
dz_anchor = -math.sin(angle_rad) * anchor_L
for i in range(nx):
for j in range(nz):
x = -L/2 + (i + 0.5) * (L / nx)
z = z_bot + (j + 0.5) * ((z_top - z_bot) / nz)
start = np.array([x, y_wall_back, z])
end = np.array([x, y_wall_back + dx_anchor, z + dz_anchor])
# 앵커바 (얇은 실린더)
try:
length = float(np.linalg.norm(end - start))
direction = (end - start) / length
anchor = pv.Cylinder(
center=tuple((start + end) / 2),
direction=tuple(direction),
radius=p.anchor_diameter / 2,
height=length,
resolution=8,
).extract_surface()
self.meshes.append((anchor, COLORS["anchor"], 1.0))
except Exception:
continue
# 앵커 헤드 판 (벽 면에 붙은 작은 사각)
plate_size = 0.2
self._add_box(
x - plate_size / 2, x + plate_size / 2,
y_wall_back, y_wall_back + 0.05,
z - plate_size / 2, z + plate_size / 2,
COLORS["anchor_plate"],
)
# --- 상단 파라펫 / 난간 ---
def _build_parapet(self):
p = self.p
if not p.has_parapet:
return
L = p.total_length
z0 = p.top_el
z1 = z0 + p.parapet_height
t = p.parapet_thickness
# 전면 파라펫
y_front = -p.avg_top_width / 2
self._add_box(-L/2, L/2, y_front, y_front + t, z0, z1, COLORS["parapet"])
# 배면 파라펫 (선택적)
y_back = p.avg_top_width / 2 # 상단 폭 기준
self._add_box(-L/2, L/2, y_back - t, y_back, z0, z1, COLORS["parapet"])
# 난간 (수평 바)
rail_t = 0.08
for rz in [z0 + p.parapet_height * 0.7, z0 + p.parapet_height * 0.4]:
self._add_box(-L/2, L/2, y_front, y_front + rail_t, rz, rz + rail_t, COLORS["rail"])
# --- 수축이음 (표면 세로선) ---
def _build_contraction_joints(self):
p = self.p
if not p.has_contraction_joints:
return
L = p.total_length
z_bot = p.bottom_el + p.base_slab_thickness
z_top = p.top_el
spacing = p.joint_spacing
n = max(int(L / spacing), 1)
# 전면(Y-)에 얇은 세로 띠 (시각적 이음)
joint_width = 0.05
y_front = -p.avg_bottom_width / 2 - 0.01 # 전면 살짝 앞
depth = 0.08
for i in range(1, n):
x = -L/2 + i * (L / n)
self._add_box(
x - joint_width / 2, x + joint_width / 2,
y_front, y_front + depth,
z_bot, z_top,
COLORS["joint"],
)
# --- 배수공 (전면 작은 구멍) ---
def _build_weep_holes(self):
p = self.p
if not p.has_weep_holes:
return
L = p.total_length
z_row = p.bottom_el + p.base_slab_thickness + 1.0 # 바닥 약간 위 한 줄
spacing = p.weep_hole_spacing
n = max(int(L / spacing), 1)
y_front = -p.avg_bottom_width / 2 - 0.05
r = p.weep_hole_diameter / 2
for i in range(n):
x = -L/2 + (i + 0.5) * (L / n)
try:
hole = pv.Cylinder(
center=(x, y_front, z_row),
direction=(0, 1, 0),
radius=r,
height=0.2,
resolution=12,
).extract_surface()
self.meshes.append((hole, COLORS["weep"], 1.0))
except Exception:
continue
# --- 전면 지반 ---
def _build_ground(self):
p = self.p
L = p.total_length
# 전면 지반 (앞쪽으로 15m)
y_start = -p.avg_bottom_width / 2 - 15
y_end = -p.avg_bottom_width / 2
z_ground = p.ground_level
pts = np.array([
[-L/2 - 5, y_start, z_ground],
[L/2 + 5, y_start, z_ground],
[L/2 + 5, y_end, z_ground],
[-L/2 - 5, y_end, z_ground],
])
self.meshes.append((pv.PolyData(pts, np.array([4, 0, 1, 2, 3])),
COLORS["ground"], 1.0))
# 기초암반 (벽 아래에서 전면으로 노출)
rock_depth = 3.0
rock_y_start = -p.avg_bottom_width / 2 - 3
rock_y_end = -p.avg_bottom_width / 2 - 0.5
z_rock_top = p.bottom_el
z_rock_bot = p.bottom_el - rock_depth
self._add_box(
-L/2 - 3, L/2 + 3,
rock_y_start, rock_y_end,
z_rock_bot, z_rock_top,
COLORS["rock"],
)
# --- 헬퍼 ---
def _add_box(self, x0, x1, y0, y1, z0, z1, color):
if x1 <= x0 or y1 <= y0 or z1 <= z0:
return
pts = np.array([
[x0, y0, z0], [x1, y0, z0], [x1, y1, z0], [x0, y1, z0],
[x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1],
])
faces = np.hstack([
[4, 0, 3, 2, 1], [4, 4, 5, 6, 7],
[4, 0, 1, 5, 4], [4, 2, 3, 7, 6],
[4, 1, 2, 6, 5], [4, 0, 4, 7, 3],
])
self.meshes.append((pv.PolyData(pts, faces), color, 1.0))
def build_retaining_wall_meshes(params: RetainingWallParams):
return RetainingWallBuilder(params).build_all()
if __name__ == "__main__":
from retaining_wall_parser import parse_retaining_wall
paths = ["SAMPLE_CAD/1. 좌안옹벽 일반도 작성(2026.0109).dxf"]
p = parse_retaining_wall(paths)
print(p.summary())
meshes = RetainingWallBuilder(p).build_all()
print(f"\n{len(meshes)}개 구성요소 생성")