chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환 - Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용 - pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관) - worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md) - D:\Rhino-MCP 경로 참조를 새 위치로 치환 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
964a841de1
3978 files changed
+58013356
No files matched your search
@@ -0,0 +1,123 @@
|
||||
"""단면 외곽선에서 철근 중심선 꺾임점을 계산하는 모듈.
|
||||
|
||||
Rhino 없이 실행됩니다. 단위는 mm, 각도는 도(degree)입니다.
|
||||
2D 좌표는 (s, z): s = 길이방향, z = 높이 (frame.py 의 로컬 좌표와 같은 방향).
|
||||
|
||||
방법 (기존 프로젝트 rebar_pipeline/README.md §3):
|
||||
1. 단면 외곽의 각 면을 안쪽으로 피복만큼 옮긴 직선으로 바꿉니다.
|
||||
2. 이웃한 두 직선의 교점이 철근 중심선의 꺾임점입니다.
|
||||
3. 시작과 끝은 도면의 고정 길이만큼 연장합니다.
|
||||
"""
|
||||
import math
|
||||
|
||||
EPS = 1e-9
|
||||
|
||||
|
||||
def _unit_direction(p0, p1):
|
||||
dx = p1[0] - p0[0]
|
||||
dz = p1[1] - p0[1]
|
||||
length = math.hypot(dx, dz)
|
||||
if length < EPS:
|
||||
raise ValueError("길이가 0인 선분입니다: %r" % (p0,))
|
||||
return (dx / length, dz / length)
|
||||
|
||||
|
||||
def _normal(direction, side):
|
||||
"""진행 방향의 왼쪽 또는 오른쪽 단위 법선."""
|
||||
dx, dz = direction
|
||||
if side == "left":
|
||||
return (-dz, dx)
|
||||
if side == "right":
|
||||
return (dz, -dx)
|
||||
raise ValueError("side 는 'left' 또는 'right' 여야 합니다: %r" % (side,))
|
||||
|
||||
|
||||
def offset_line(p0, p1, distance, side):
|
||||
"""선분 p0->p1 을 진행 방향의 side(left/right)로 distance 만큼 옮긴 직선.
|
||||
|
||||
돌려주는 값: (직선 위의 한 점, 진행 방향 단위벡터)
|
||||
"""
|
||||
direction = _unit_direction(p0, p1)
|
||||
n = _normal(direction, side)
|
||||
point = (p0[0] + n[0] * distance, p0[1] + n[1] * distance)
|
||||
return point, direction
|
||||
|
||||
|
||||
def line_intersection(line_a, line_b):
|
||||
"""두 직선 (점, 방향) 의 교점. 평행하면 ValueError."""
|
||||
(ax, az), (adx, adz) = line_a
|
||||
(bx, bz), (bdx, bdz) = line_b
|
||||
cross = adx * bdz - adz * bdx
|
||||
if abs(cross) < EPS:
|
||||
raise ValueError("두 직선이 평행해서 교점이 없습니다")
|
||||
t = ((bx - ax) * bdz - (bz - az) * bdx) / cross
|
||||
return (ax + adx * t, az + adz * t)
|
||||
|
||||
|
||||
def centerline_points(segments, cover, side, start_len=None, end_len=None):
|
||||
"""철근 중심선의 꺾임점 목록 (s, z).
|
||||
|
||||
segments : 외곽선 선분 목록 [(시작점, 끝점), ...]. 철근이 지나가는 순서와 방향대로 적습니다.
|
||||
cover : 피복 (철근 중심에서 면까지 거리)
|
||||
side : 콘크리트 안쪽이 진행 방향의 'left' 인지 'right' 인지
|
||||
start_len: 첫 꺾임점에서 첫 선분 반대 방향으로 연장할 길이. None 이면 첫 선분의 시작점을 그대로 씁니다.
|
||||
end_len : 마지막 꺾임점에서 마지막 선분 방향으로 연장할 길이. None 이면 마지막 선분의 끝점을 그대로 씁니다.
|
||||
"""
|
||||
if not segments:
|
||||
raise ValueError("선분이 하나 이상 필요합니다")
|
||||
lines = [offset_line(p0, p1, cover, side) for (p0, p1) in segments]
|
||||
|
||||
if len(lines) == 1:
|
||||
if start_len is not None or end_len is not None:
|
||||
raise ValueError("선분이 1개이면 고정 길이를 쓸 수 없습니다")
|
||||
(p0, d) = lines[0]
|
||||
length = math.hypot(segments[0][1][0] - segments[0][0][0],
|
||||
segments[0][1][1] - segments[0][0][1])
|
||||
return [p0, (p0[0] + d[0] * length, p0[1] + d[1] * length)]
|
||||
|
||||
vertices = [line_intersection(lines[i], lines[i + 1]) for i in range(len(lines) - 1)]
|
||||
|
||||
if start_len is None:
|
||||
first = lines[0][0]
|
||||
else:
|
||||
d0 = lines[0][1]
|
||||
first = (vertices[0][0] - d0[0] * start_len, vertices[0][1] - d0[1] * start_len)
|
||||
|
||||
if end_len is None:
|
||||
seg_end = segments[-1][1]
|
||||
n = _normal(lines[-1][1], side)
|
||||
last = (seg_end[0] + n[0] * cover, seg_end[1] + n[1] * cover)
|
||||
else:
|
||||
dn = lines[-1][1]
|
||||
last = (vertices[-1][0] + dn[0] * end_len, vertices[-1][1] + dn[1] * end_len)
|
||||
|
||||
return [first] + vertices + [last]
|
||||
|
||||
|
||||
def segment_lengths(points):
|
||||
"""꺾임점 목록의 각 구간 길이."""
|
||||
return [math.hypot(b[0] - a[0], b[1] - a[1]) for a, b in zip(points, points[1:])]
|
||||
|
||||
|
||||
def polyline_length(points):
|
||||
"""꺾임점을 이은 전체 길이."""
|
||||
return sum(segment_lengths(points))
|
||||
|
||||
|
||||
def turn_angles(points):
|
||||
"""각 안쪽 꺾임점에서 진행 방향이 바뀌는 각도(도, 0~180). 직선이면 0, 직각이면 90."""
|
||||
angles = []
|
||||
for i in range(1, len(points) - 1):
|
||||
d1 = _unit_direction(points[i - 1], points[i])
|
||||
d2 = _unit_direction(points[i], points[i + 1])
|
||||
dot = max(-1.0, min(1.0, d1[0] * d2[0] + d1[1] * d2[1]))
|
||||
angles.append(math.degrees(math.acos(dot)))
|
||||
return angles
|
||||
|
||||
|
||||
def distance_to_line(point, line_p0, line_p1):
|
||||
"""점에서 (선분이 아니라) 무한 직선 line_p0-line_p1 까지의 수직거리."""
|
||||
d = _unit_direction(line_p0, line_p1)
|
||||
vx = point[0] - line_p0[0]
|
||||
vz = point[1] - line_p0[1]
|
||||
return abs(vx * d[1] - vz * d[0])
|
||||
@@ -0,0 +1,164 @@
|
||||
"""철근 중심선 간섭 검사 모듈.
|
||||
|
||||
Rhino 없이 실행됩니다. 단위는 mm. 좌표는 (x, y, z) 3D 점입니다.
|
||||
계산을 정확하게 하려면 월드 좌표(약 4.8억)보다 frame.py 로 바꾼 로컬 좌표를 쓰는 것이 좋습니다.
|
||||
|
||||
규칙 (기존 프로젝트 rebar_pipeline/rhino/08_clash_check.py):
|
||||
- 철근을 중심선 선분으로 쪼개서, 두 선분의 최단거리 d 에 대해
|
||||
관입 = rA + rB - d 가 min_penetration(기본 0.5mm)보다 크면 간섭입니다.
|
||||
- 같은 철근, 같은 그룹(겹침이음 모델링인 박스) 선분끼리는 제외합니다.
|
||||
- 철근 쌍마다 가장 깊은 관입 하나만 기록합니다.
|
||||
"""
|
||||
import math
|
||||
|
||||
EPS = 1e-12
|
||||
BBOX_MARGIN = 1.0 # 후보를 찾을 때 반지름에 더하는 여유(mm)
|
||||
|
||||
|
||||
class Seg(object):
|
||||
"""철근 중심선의 선분 하나.
|
||||
|
||||
p, q : 끝점 (x, y, z)
|
||||
radius : 철근 반지름
|
||||
bar : 철근 번호 (같은 철근끼리는 검사 제외)
|
||||
group : 그룹 번호 (같은 박스의 뚜껑·바닥 조각처럼 검사 제외할 묶음)
|
||||
mark : 철근 마크 이름 (보고용)
|
||||
"""
|
||||
|
||||
def __init__(self, p, q, radius, bar, group, mark=""):
|
||||
self.p = (float(p[0]), float(p[1]), float(p[2]))
|
||||
self.q = (float(q[0]), float(q[1]), float(q[2]))
|
||||
self.radius = float(radius)
|
||||
self.bar = bar
|
||||
self.group = group
|
||||
self.mark = mark
|
||||
|
||||
|
||||
def _sub(a, b):
|
||||
return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
|
||||
|
||||
|
||||
def _dot(a, b):
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
|
||||
|
||||
def _clamp01(x):
|
||||
if x < 0.0:
|
||||
return 0.0
|
||||
if x > 1.0:
|
||||
return 1.0
|
||||
return x
|
||||
|
||||
|
||||
def segment_distance(p1, q1, p2, q2):
|
||||
"""두 선분 p1-q1, p2-q2 사이의 최단거리 (3D, 길이 0인 선분도 가능)."""
|
||||
d1 = _sub(q1, p1)
|
||||
d2 = _sub(q2, p2)
|
||||
r = _sub(p1, p2)
|
||||
a = _dot(d1, d1)
|
||||
e = _dot(d2, d2)
|
||||
f = _dot(d2, r)
|
||||
|
||||
if a <= EPS and e <= EPS:
|
||||
s = 0.0
|
||||
t = 0.0
|
||||
elif a <= EPS:
|
||||
s = 0.0
|
||||
t = _clamp01(f / e)
|
||||
else:
|
||||
c = _dot(d1, r)
|
||||
if e <= EPS:
|
||||
t = 0.0
|
||||
s = _clamp01(-c / a)
|
||||
else:
|
||||
b = _dot(d1, d2)
|
||||
denom = a * e - b * b
|
||||
if denom > 1e-12 * a * e:
|
||||
s = _clamp01((b * f - c * e) / denom)
|
||||
else:
|
||||
s = 0.0
|
||||
t = (b * s + f) / e
|
||||
if t < 0.0:
|
||||
t = 0.0
|
||||
s = _clamp01(-c / a)
|
||||
elif t > 1.0:
|
||||
t = 1.0
|
||||
s = _clamp01((b - c) / a)
|
||||
|
||||
c1 = (p1[0] + d1[0] * s, p1[1] + d1[1] * s, p1[2] + d1[2] * s)
|
||||
c2 = (p2[0] + d2[0] * t, p2[1] + d2[1] * t, p2[2] + d2[2] * t)
|
||||
diff = _sub(c1, c2)
|
||||
return math.sqrt(_dot(diff, diff))
|
||||
|
||||
|
||||
def _box(s):
|
||||
"""선분을 반지름 + 여유만큼 부풀린 상자 (xmin, ymin, zmin, xmax, ymax, zmax)."""
|
||||
m = s.radius + BBOX_MARGIN
|
||||
return (min(s.p[0], s.q[0]) - m, min(s.p[1], s.q[1]) - m, min(s.p[2], s.q[2]) - m,
|
||||
max(s.p[0], s.q[0]) + m, max(s.p[1], s.q[1]) + m, max(s.p[2], s.q[2]) + m)
|
||||
|
||||
|
||||
def _boxes_overlap(a, b):
|
||||
return (a[0] <= b[3] and b[0] <= a[3] and
|
||||
a[1] <= b[4] and b[1] <= a[4] and
|
||||
a[2] <= b[5] and b[2] <= a[5])
|
||||
|
||||
|
||||
def find_clashes(segments, min_penetration=0.5):
|
||||
"""간섭 목록을 돌려줍니다 (관입이 깊은 것부터).
|
||||
|
||||
각 항목: {"bar_a", "bar_b", "mark_a", "mark_b", "penetration", "point"}
|
||||
bar_a < bar_b 로 정렬되고, point 는 간섭 선분 네 끝점의 평균입니다.
|
||||
"""
|
||||
boxes = [_box(s) for s in segments]
|
||||
order = sorted(range(len(segments)), key=lambda i: boxes[i][0])
|
||||
best = {}
|
||||
|
||||
for oi in range(len(order)):
|
||||
i = order[oi]
|
||||
a = segments[i]
|
||||
for oj in range(oi + 1, len(order)):
|
||||
j = order[oj]
|
||||
if boxes[j][0] > boxes[i][3]:
|
||||
break # x 방향으로 이미 떨어져 있음. 뒤쪽은 더 멀리 있음
|
||||
b = segments[j]
|
||||
if a.bar == b.bar or a.group == b.group:
|
||||
continue
|
||||
if not _boxes_overlap(boxes[i], boxes[j]):
|
||||
continue
|
||||
d = segment_distance(a.p, a.q, b.p, b.q)
|
||||
pen = a.radius + b.radius - d
|
||||
if pen <= min_penetration:
|
||||
continue
|
||||
first, second = (a, b) if a.bar <= b.bar else (b, a)
|
||||
key = (first.bar, second.bar)
|
||||
old = best.get(key)
|
||||
if old is None or pen > old["penetration"]:
|
||||
point = ((a.p[0] + a.q[0] + b.p[0] + b.q[0]) / 4.0,
|
||||
(a.p[1] + a.q[1] + b.p[1] + b.q[1]) / 4.0,
|
||||
(a.p[2] + a.q[2] + b.p[2] + b.q[2]) / 4.0)
|
||||
best[key] = {
|
||||
"bar_a": first.bar, "bar_b": second.bar,
|
||||
"mark_a": first.mark, "mark_b": second.mark,
|
||||
"penetration": pen, "point": point,
|
||||
}
|
||||
|
||||
hits = list(best.values())
|
||||
hits.sort(key=lambda h: -h["penetration"])
|
||||
return hits
|
||||
|
||||
|
||||
def summarize_by_mark(hits):
|
||||
"""마크 쌍별 간섭 개수와 최대 관입. 키는 'C1|C8' 처럼 이름순으로 정렬된 마크 쌍."""
|
||||
summary = {}
|
||||
for h in hits:
|
||||
names = sorted([h["mark_a"], h["mark_b"]])
|
||||
key = names[0] + "|" + names[1]
|
||||
item = summary.get(key)
|
||||
if item is None:
|
||||
summary[key] = {"count": 1, "max_penetration": h["penetration"]}
|
||||
else:
|
||||
item["count"] += 1
|
||||
if h["penetration"] > item["max_penetration"]:
|
||||
item["max_penetration"] = h["penetration"]
|
||||
return summary
|
||||
@@ -0,0 +1,75 @@
|
||||
"""월드 좌표와 코핑 로컬 좌표를 바꾸는 모듈.
|
||||
|
||||
Rhino 없이 실행됩니다. 단위는 mm, 각도는 라디안입니다.
|
||||
|
||||
월드 좌표 (x, y, z) <-> 로컬 좌표 (s, t, z)
|
||||
s = 길이방향 (코핑 축 방향), t = 폭방향 (s를 반시계로 90도 돌린 방향)
|
||||
z = 월드 높이를 그대로 사용
|
||||
|
||||
원점(origin_x, origin_y)과 각도(angle)는 값으로 받으므로 언제든 바꿀 수 있습니다.
|
||||
기존 프로젝트 rebar_pipeline/rhino/07_ctc_extract.py 의 OX, OY, ANG 방식과 같습니다.
|
||||
"""
|
||||
import math
|
||||
|
||||
|
||||
class Frame(object):
|
||||
"""코핑 로컬 좌표계. 원점(월드 x, y)과 s축 각도(월드 +X 기준, 반시계)로 정해집니다."""
|
||||
|
||||
def __init__(self, origin_x, origin_y, angle):
|
||||
self.origin_x = float(origin_x)
|
||||
self.origin_y = float(origin_y)
|
||||
self.angle = float(angle)
|
||||
self._cos = math.cos(self.angle)
|
||||
self._sin = math.sin(self.angle)
|
||||
|
||||
def to_local(self, x, y, z):
|
||||
"""월드 점 (x, y, z) -> 로컬 점 (s, t, z)."""
|
||||
dx = x - self.origin_x
|
||||
dy = y - self.origin_y
|
||||
return (dx * self._cos + dy * self._sin,
|
||||
-dx * self._sin + dy * self._cos,
|
||||
z)
|
||||
|
||||
def to_world(self, s, t, z):
|
||||
"""로컬 점 (s, t, z) -> 월드 점 (x, y, z)."""
|
||||
return (self.origin_x + s * self._cos - t * self._sin,
|
||||
self.origin_y + s * self._sin + t * self._cos,
|
||||
z)
|
||||
|
||||
def direction_to_local(self, dx, dy, dz):
|
||||
"""월드 방향 벡터 -> 로컬 방향 벡터 (원점 이동 없이 회전만)."""
|
||||
return (dx * self._cos + dy * self._sin,
|
||||
-dx * self._sin + dy * self._cos,
|
||||
dz)
|
||||
|
||||
def direction_to_world(self, ds, dt, dz):
|
||||
"""로컬 방향 벡터 -> 월드 방향 벡터 (원점 이동 없이 회전만)."""
|
||||
return (ds * self._cos - dt * self._sin,
|
||||
ds * self._sin + dt * self._cos,
|
||||
dz)
|
||||
|
||||
def to_local_many(self, points):
|
||||
"""월드 점 목록 -> 로컬 점 목록."""
|
||||
return [self.to_local(x, y, z) for (x, y, z) in points]
|
||||
|
||||
def to_world_many(self, points):
|
||||
"""로컬 점 목록 -> 월드 점 목록."""
|
||||
return [self.to_world(s, t, z) for (s, t, z) in points]
|
||||
|
||||
|
||||
def from_two_points(a, b):
|
||||
"""두 점 a, b (월드 (x, y))로 프레임을 만듭니다.
|
||||
|
||||
원점 = 두 점의 가운데, s축 = a에서 b로 향하는 방향.
|
||||
(예: 2주식 교각의 두 기둥 중심)
|
||||
"""
|
||||
dx = b[0] - a[0]
|
||||
dy = b[1] - a[1]
|
||||
if dx == 0 and dy == 0:
|
||||
raise ValueError("두 점이 같아서 방향을 정할 수 없습니다: %r" % (a,))
|
||||
return Frame((a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0, math.atan2(dy, dx))
|
||||
|
||||
|
||||
def convert(point, src, dst):
|
||||
"""src 프레임의 로컬 점을 dst 프레임의 로컬 점으로 바꿉니다 (월드를 거쳐서)."""
|
||||
return dst.to_local(*src.to_world(*point))
|
||||
@@ -0,0 +1,90 @@
|
||||
"""철근 구부림 반지름과 갈고리 길이 계산 모듈.
|
||||
|
||||
Rhino 없이 실행됩니다. 단위는 모두 mm입니다.
|
||||
기준값 출처: docs/design_rules.md
|
||||
"""
|
||||
|
||||
# KS D 3504 공칭 철근 직경 (mm)
|
||||
NOMINAL_DIAMETER = {
|
||||
10: 9.5,
|
||||
13: 12.7,
|
||||
16: 15.9,
|
||||
19: 19.1,
|
||||
22: 22.2,
|
||||
25: 25.4,
|
||||
29: 28.6,
|
||||
32: 31.8,
|
||||
35: 34.9,
|
||||
38: 38.1,
|
||||
}
|
||||
|
||||
# 갈고리 뒤 연장 길이 (공칭 직경의 배수). 프로젝트 적용값
|
||||
HOOK_EXTENSION_FACTOR = {90: 12.0, 135: 6.0, 180: 4.0}
|
||||
HOOK_180_MIN_EXTENSION = 60.0 # 180도 갈고리는 4db 또는 60mm 중 큰 값
|
||||
|
||||
# KDS 24 14 21: 90도~150도 표준갈고리 연장 길이 최소값 (db의 배수)
|
||||
KDS_HOOK_MIN_FACTOR = 5.0
|
||||
|
||||
|
||||
def bar_number(name):
|
||||
"""'H25', 'D25', 'h25' 같은 이름에서 호칭 번호(25)를 꺼냅니다."""
|
||||
text = str(name).strip().upper()
|
||||
if len(text) < 2 or text[0] not in ("H", "D") or not text[1:].isdigit():
|
||||
raise ValueError("알 수 없는 철근 이름입니다: %r" % (name,))
|
||||
return int(text[1:])
|
||||
|
||||
|
||||
def nominal_diameter(name):
|
||||
"""철근 이름(예: 'H25')의 공칭 직경을 mm로 돌려줍니다."""
|
||||
number = bar_number(name)
|
||||
if number not in NOMINAL_DIAMETER:
|
||||
raise ValueError("지원하지 않는 철근 규격입니다: %r" % (name,))
|
||||
return NOMINAL_DIAMETER[number]
|
||||
|
||||
|
||||
def bend_radius(name, is_stirrup=False):
|
||||
"""프로젝트 적용 구부림 최소 내면 반지름(mm).
|
||||
|
||||
D10~D25: 3db, D29~D35: 4db, D38 이상: 5db.
|
||||
스터럽/띠철근이고 D16 이하이면 2db.
|
||||
"""
|
||||
number = bar_number(name)
|
||||
db = nominal_diameter(name)
|
||||
if is_stirrup and number <= 16:
|
||||
return 2.0 * db
|
||||
if number <= 25:
|
||||
return 3.0 * db
|
||||
if number <= 35:
|
||||
return 4.0 * db
|
||||
return 5.0 * db
|
||||
|
||||
|
||||
def kds_min_inner_diameter(name):
|
||||
"""KDS 24 14 21 표 4.5-1 최소 내면 지름(mm). D16 이하 2db, D19 이상 3.5db."""
|
||||
number = bar_number(name)
|
||||
db = nominal_diameter(name)
|
||||
if number <= 16:
|
||||
return 2.0 * db
|
||||
return 3.5 * db
|
||||
|
||||
|
||||
def satisfies_kds_bend(name, radius):
|
||||
"""내면 반지름(mm)이 KDS 최소 내면 지름을 만족하는지 확인합니다."""
|
||||
return 2.0 * radius >= kds_min_inner_diameter(name)
|
||||
|
||||
|
||||
def hook_extension(name, angle):
|
||||
"""프로젝트 적용 갈고리 뒤 연장 길이(mm). angle은 90, 135, 180 중 하나."""
|
||||
if angle not in HOOK_EXTENSION_FACTOR:
|
||||
raise ValueError("지원하지 않는 갈고리 각도입니다: %r" % (angle,))
|
||||
length = HOOK_EXTENSION_FACTOR[angle] * nominal_diameter(name)
|
||||
if angle == 180:
|
||||
length = max(length, HOOK_180_MIN_EXTENSION)
|
||||
return length
|
||||
|
||||
|
||||
def kds_min_hook_extension(name, angle):
|
||||
"""KDS 24 14 21 갈고리 연장 길이 최소값(mm). 90~150도는 5db."""
|
||||
if not 90 <= angle <= 150:
|
||||
raise ValueError("KDS 5db 규정은 90~150도에만 적용됩니다: %r" % (angle,))
|
||||
return KDS_HOOK_MIN_FACTOR * nominal_diameter(name)
|
||||
@@ -0,0 +1,105 @@
|
||||
"""철근 배치(간격, 수량, 중량) 계산 모듈.
|
||||
|
||||
Rhino 없이 실행됩니다. 단위는 mm이고, 중량만 kg입니다.
|
||||
기준값 출처: docs/design_rules.md, 기존 프로젝트 rebar_pipeline/README.md
|
||||
"""
|
||||
import rebar_calc
|
||||
|
||||
# 단위중량 kg/m (기존 프로젝트 재료표 값). 아래에 없는 규격은 값을 확인한 뒤 추가하세요.
|
||||
UNIT_WEIGHT = {
|
||||
"H16": 1.560,
|
||||
"H19": 2.250,
|
||||
"H22": 3.040,
|
||||
"H25": 3.980,
|
||||
"H29": 5.040,
|
||||
}
|
||||
|
||||
DEFAULT_GMAX = 25.0 # 굵은골재 최대치수(mm). 가정값이므로 설계서의 값으로 바꿔 쓰세요.
|
||||
EPS = 1e-9
|
||||
|
||||
|
||||
def center_out_positions(count, spacing):
|
||||
"""중심(0)에서 바깥으로 spacing 간격으로 놓은 폭방향 위치 목록(작은 값부터).
|
||||
|
||||
짝수 개이면 중심에 철근이 없고(±spacing/2부터), 홀수 개이면 중심(0)에 철근이 있습니다.
|
||||
"""
|
||||
if count < 0:
|
||||
raise ValueError("철근 수는 0 이상이어야 합니다: %r" % (count,))
|
||||
if spacing <= 0:
|
||||
raise ValueError("간격은 0보다 커야 합니다: %r" % (spacing,))
|
||||
mid = (count - 1) / 2.0
|
||||
return [spacing * (i - mid) for i in range(count)]
|
||||
|
||||
|
||||
def snapped_array(count, spacing, width, cover):
|
||||
"""바깥 철근 2본은 피복에 맞추고, 나머지는 중심에서 바깥으로 배치합니다 (1CYCLE 방식).
|
||||
|
||||
바깥 철근 위치 = ±(width/2 - cover). 안쪽 철근은 중심에서 바깥으로 spacing 간격입니다.
|
||||
안쪽 철근이 바깥 철근 위치를 넘어가면 ValueError.
|
||||
"""
|
||||
if count < 2:
|
||||
raise ValueError("바깥 철근 2본이 필요하므로 count는 2 이상이어야 합니다: %r" % (count,))
|
||||
outer = width / 2.0 - cover
|
||||
inner = center_out_positions(count - 2, spacing)
|
||||
if inner and max(inner) >= outer:
|
||||
raise ValueError("폭 %r 안에 철근 %r본을 간격 %r로 놓을 수 없습니다" % (width, count, spacing))
|
||||
return [-outer] + inner + [outer]
|
||||
|
||||
|
||||
def min_horizontal_clear(name, gmax=DEFAULT_GMAX):
|
||||
"""수평 최소 순간격 = max(1.5db, 1.5Gmax, 40mm) (KDS 24 14 21 4.5.3)."""
|
||||
db = rebar_calc.nominal_diameter(name)
|
||||
return max(1.5 * db, 1.5 * gmax, 40.0)
|
||||
|
||||
|
||||
def clear_spacing(center_spacing, name):
|
||||
"""중심 간격에서 철근 직경을 뺀 순간격."""
|
||||
return center_spacing - rebar_calc.nominal_diameter(name)
|
||||
|
||||
|
||||
def horizontal_spacing_ok(positions, name, gmax=DEFAULT_GMAX):
|
||||
"""이웃한 철근 사이 순간격이 모두 수평 최소 순간격 이상이면 True."""
|
||||
ordered = sorted(positions)
|
||||
need = min_horizontal_clear(name, gmax)
|
||||
for a, b in zip(ordered, ordered[1:]):
|
||||
if clear_spacing(b - a, name) < need - EPS:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def min_vertical_clear(name):
|
||||
"""다단 배근의 연직 최소 순간격 = max(25mm, db)."""
|
||||
return max(25.0, rebar_calc.nominal_diameter(name))
|
||||
|
||||
|
||||
def vertical_pitch_ok(name, pitch):
|
||||
"""단 사이 중심 간격(pitch)의 순간격이 연직 최소 순간격 이상이면 True."""
|
||||
return clear_spacing(pitch, name) >= min_vertical_clear(name) - EPS
|
||||
|
||||
|
||||
def tier_elevations(top_z, cover, tiers, pitch):
|
||||
"""맨 위 단은 상면에서 cover만큼 아래, 다음 단부터 pitch씩 아래로 내린 높이 목록."""
|
||||
return [top_z - cover - i * pitch for i in range(tiers)]
|
||||
|
||||
|
||||
def unit_weight(name):
|
||||
"""단위중량(kg/m)."""
|
||||
key = "H%d" % rebar_calc.bar_number(name)
|
||||
if key not in UNIT_WEIGHT:
|
||||
raise ValueError("단위중량이 등록되지 않은 규격입니다: %r" % (name,))
|
||||
return UNIT_WEIGHT[key]
|
||||
|
||||
|
||||
def bar_weight(name, length_mm, count=1):
|
||||
"""철근 length_mm 길이 count본의 중량(kg)."""
|
||||
return unit_weight(name) * length_mm / 1000.0 * count
|
||||
|
||||
|
||||
def array_summary(name, length_mm, positions):
|
||||
"""한 배열의 본수, 총길이(m), 중량(kg)."""
|
||||
count = len(positions)
|
||||
return {
|
||||
"count": count,
|
||||
"total_length_m": length_mm / 1000.0 * count,
|
||||
"weight_kg": bar_weight(name, length_mm, count),
|
||||
}
|
||||
Reference in new issue
Block a user