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,226 @@
|
||||
"""Out-of-table dimension finding (stage C of the 입력 묶음 확인·확정 개편, first part, 2026-10-19).
|
||||
|
||||
Anchors: for a modelled pier (params / expected known) the app records where each numeric field was read from the drawing — every dimension / EL
|
||||
text (layer, sheet, position) whose value equals the field value. For a NEW drawing of the same series the anchors are followed: the shift between
|
||||
the two drawings is estimated per view from the texts both drawings share (R5), the candidate is the dimension text of the same layer and kind
|
||||
nearest to the predicted position (R1 EL texts, scalar dimension texts). Several instances of one dimension (front / side / plan) vote.
|
||||
Trust grades (기획서 4.2): A >= 2 instances followed and all agree, small shift spread -> candidate with evidence, may be applied
|
||||
B agreement of >= 2 instances, or a single instance -> candidate with a warning
|
||||
C instances disagree -> list only, applying needs a reason
|
||||
none: nothing found -> "근거 없음", no candidate is invented.
|
||||
R2: dimension chains `n@pitch=total` are parsed and checked (n x pitch = total).
|
||||
Validation on the real series (tools/test_anchors.py): grade A had no wrong candidate; B and C are measured and shown as such.
|
||||
"""
|
||||
import re
|
||||
import statistics
|
||||
from collections import defaultdict
|
||||
|
||||
from engine import variability
|
||||
|
||||
VERSION = "2"
|
||||
MIN_MM = 1000 # values below one metre are too common as texts to be anchored
|
||||
MAX_HITS = 6 # a value found in more places is not a specific dimension
|
||||
RADIUS = 1200.0 # search radius around the predicted position (drawing units, mm)
|
||||
K_LANDMARKS = 5
|
||||
MIN_COVERAGE = 0.70 # share of the anchors' landmark texts found again in the new drawing
|
||||
A_SPREAD = 400.0
|
||||
TABLE_RULE = "single" # "single": a candidate whose only followed instance is ONE table cell is demoted to C (2026-10-20); "none": table cells never grade; "off": old behaviour.
|
||||
# Measured on the verified Ungok P3 / P4 values: grade B wrong 10/65 (off) -> 2/39 (single) vs 3/29 (none); grade A unchanged (0 wrong)
|
||||
MARK = re.compile(r"^[A-Z]{1,2}\d{1,2}$")
|
||||
NUM = re.compile(r"^-?\d[\d,]*\.?\d*$")
|
||||
CHAIN = re.compile(r"^(\d+)\s*@\s*(\d[\d,]*(?:\.\d+)?)\s*=\s*(\d[\d,]*(?:\.\d+)?)\s*(?:\(.*\))?$")
|
||||
SKIP = {("expected", "planned_fail"), ("expected", "rebar_palette"), ("expected", "tol_mm")}
|
||||
|
||||
|
||||
def text_val(t):
|
||||
"""(value mm, kind) of a dimension / EL text: 'EL. 38.916' -> (38916, EL), '7.800' -> (7800, m), '7800' -> (7800, mm); else None."""
|
||||
s = t.strip().replace(" ", "")
|
||||
m = re.match(r"^EL\.?(\d+\.\d{3})$", s)
|
||||
if m:
|
||||
return round(float(m.group(1)) * 1000, 3), "EL"
|
||||
if NUM.match(s):
|
||||
x = s.replace(",", "")
|
||||
try:
|
||||
f = float(x)
|
||||
except ValueError:
|
||||
return None
|
||||
if re.match(r"^\d+\.\d{3}$", x):
|
||||
return round(f * 1000, 3), "m"
|
||||
if re.match(r"^\d+$", x):
|
||||
return f, "mm"
|
||||
return None
|
||||
|
||||
|
||||
def leaves(params, expected, max_list=40):
|
||||
"""(field, value) of every anchorable numeric leaf: bundles of grade V1 / V2 only, no sources / notes, no human decision records."""
|
||||
for which, doc in (("params", params), ("expected", expected)):
|
||||
for k, v in doc.items():
|
||||
m = variability.classify(which, k, v)
|
||||
if m["info"] or m["grade"] in ("F", "V3") or (which, k) in SKIP:
|
||||
continue
|
||||
stack = [((k,), v)]
|
||||
while stack:
|
||||
path, x = stack.pop()
|
||||
if isinstance(x, dict):
|
||||
for a, b in x.items():
|
||||
if a in ("src", "_note") or a.endswith("_src"):
|
||||
continue
|
||||
stack.append((path + (str(a),), b))
|
||||
elif isinstance(x, list):
|
||||
if len(x) <= max_list:
|
||||
stack.extend((path + (str(i),), b) for i, b in enumerate(x))
|
||||
elif isinstance(x, (int, float)) and not isinstance(x, bool):
|
||||
yield f"{which}:" + ".".join(path), x
|
||||
|
||||
|
||||
def _unique_labels(texts):
|
||||
out = defaultdict(list)
|
||||
for t in texts:
|
||||
s = t["text"].strip()
|
||||
if len(s) > 1 and not NUM.match(s.replace(" ", "")) and not CHAIN.match(s):
|
||||
out[(t["dwg"], s)].append(t)
|
||||
return {k: v[0] for k, v in out.items() if len(v) == 1}
|
||||
|
||||
|
||||
def learn(params, expected, texts, sources=()):
|
||||
"""Anchors of a modelled pier. `sources` = sha256 of its DWG files (to recognise a pier that follows its own drawing)."""
|
||||
by = defaultdict(list)
|
||||
for t in texts:
|
||||
v = text_val(t["text"])
|
||||
if v:
|
||||
by[round(v[0])].append({"text": t["text"].strip(), "x": t["x"], "y": t["y"], "layer": t["layer"], "dwg": t["dwg"], "kind": v[1]})
|
||||
fields = {}
|
||||
owners = defaultdict(set) # same value, same file, different bundle / leaf name -> which field the text belongs to is unknown
|
||||
cand = []
|
||||
for field, v in leaves(params, expected):
|
||||
key = round(abs(v))
|
||||
if key >= MIN_MM and 1 <= len(by.get(key, [])) <= MAX_HITS:
|
||||
which, path = field.split(":", 1)
|
||||
parts = [q for q in path.split(".") if not q.isdigit() and not MARK.match(q)] # mark rows (P3, C1 ...) are instances of one dimension kind
|
||||
owners[(which, key)].add((path.split(".")[0], re.sub(r"_(x|y|z|left|right|front|rear|top|bottom)$", "", parts[-1])))
|
||||
cand.append((field, v, key))
|
||||
for field, v, key in cand:
|
||||
which = field.split(":", 1)[0]
|
||||
fields[field] = {"v": v, "hits": by[key], "shared": len(owners[(which, key)])}
|
||||
lm = [{"text": s, "x": t["x"], "y": t["y"], "dwg": d} for (d, s), t in _unique_labels(texts).items()]
|
||||
return {"version": VERSION, "sources": list(sources), "fields": fields, "landmarks": lm}
|
||||
|
||||
|
||||
def _shift(anchor, common, dwg, x, y):
|
||||
near = sorted((c for c in common if c[4] == dwg), key=lambda c: (c[0] - x) ** 2 + (c[1] - y) ** 2)[:K_LANDMARKS]
|
||||
if not near:
|
||||
return None
|
||||
dx, dy = statistics.median(c[2] for c in near), statistics.median(c[3] for c in near)
|
||||
return dx, dy, max(abs(c[2] - dx) for c in near) + max(abs(c[3] - dy) for c in near)
|
||||
|
||||
|
||||
def follow(anchors, texts, own_sources=()):
|
||||
"""Candidates for a new drawing: {field: {value, grade, followed, agree, spread, text, x, y, dwg, layer, all, self_source}}."""
|
||||
new = _unique_labels(texts)
|
||||
common = []
|
||||
for lm in anchors.get("landmarks", []):
|
||||
t = new.get((lm["dwg"], lm["text"]))
|
||||
if t:
|
||||
common.append((lm["x"], lm["y"], t["x"] - lm["x"], t["y"] - lm["y"], lm["dwg"]))
|
||||
by_layer = defaultdict(list)
|
||||
for t in texts:
|
||||
v = text_val(t["text"])
|
||||
if v:
|
||||
by_layer[(t["dwg"], t["layer"], v[1])].append((v[0], t))
|
||||
self_source = bool(set(anchors.get("sources", [])) & set(own_sources))
|
||||
coverage = len(common) / len(anchors["landmarks"]) if anchors.get("landmarks") else 0.0
|
||||
out = {}
|
||||
for field, a in anchors.get("fields", {}).items():
|
||||
res = []
|
||||
for h in a["hits"]:
|
||||
sh = _shift(a, common, h["dwg"], h["x"], h["y"])
|
||||
if not sh:
|
||||
continue
|
||||
px, py = h["x"] + sh[0], h["y"] + sh[1]
|
||||
best = None
|
||||
for val, t in by_layer.get((h["dwg"], h["layer"], h["kind"]), []):
|
||||
d = ((t["x"] - px) ** 2 + (t["y"] - py) ** 2) ** 0.5
|
||||
if d <= RADIUS and (best is None or d < best[0]):
|
||||
best = (d, val, t)
|
||||
if best:
|
||||
res.append({"value": round(best[1]), "dist": round(best[0], 1), "spread": round(sh[2], 1), "text": best[2]["text"].strip(),
|
||||
"x": best[2]["x"], "y": best[2]["y"], "dwg": best[2]["dwg"], "layer": best[2]["layer"],
|
||||
"table_cell": "TABL" in best[2]["layer"]})
|
||||
if not res:
|
||||
continue
|
||||
# improvement #34-4 (2026-10-20): a text that sits in a schedule / table is found again by POSITION, but the rows of a table move when the pier's
|
||||
# bar list differs (Ungok P3 / P4: the length cell '3.800' of S1 was followed as the end of a rebar chain, grade B). Table cells are the job of the
|
||||
# table candidates (engine/reader, check C2). A candidate whose only followed instance is one table cell is therefore demoted to C.
|
||||
free = [r for r in res if not r["table_cell"]]
|
||||
if TABLE_RULE == "none":
|
||||
table_only = not free
|
||||
if free:
|
||||
res = free
|
||||
elif TABLE_RULE == "single":
|
||||
table_only = not free and len(res) == 1
|
||||
else:
|
||||
table_only = False
|
||||
vals = [r["value"] for r in res]
|
||||
cons = max(set(vals), key=lambda v: (vals.count(v), -abs(v)))
|
||||
agree = vals.count(cons)
|
||||
spread = max(r["spread"] for r in res)
|
||||
if table_only:
|
||||
grade = "C"
|
||||
elif len(res) >= 2 and agree == len(res) and spread < A_SPREAD and a.get("shared", 1) == 1:
|
||||
grade = "A"
|
||||
elif agree >= 2 or len(res) == 1:
|
||||
grade = "B"
|
||||
else:
|
||||
grade = "C"
|
||||
if coverage < MIN_COVERAGE and grade != "C":
|
||||
grade = "C" # the drawing frame differs from the one the anchors come from
|
||||
pos = min((r for r in res if r["value"] == cons), key=lambda r: r["dist"])
|
||||
out[field] = {"value": cons if a["v"] >= 0 else -cons, "grade": grade, "followed": len(res), "agree": agree, "spread": spread,
|
||||
"text": pos["text"], "x": pos["x"], "y": pos["y"], "dwg": pos["dwg"], "layer": pos["layer"],
|
||||
"all": sorted(set(vals)), "source_value": a["v"], "self_source": self_source, "coverage": round(coverage, 2),
|
||||
"shared": a.get("shared", 1), "table_only": table_only}
|
||||
return out
|
||||
|
||||
|
||||
def table_values(proposal_rows):
|
||||
"""{field: value} of the table candidates (engine.reader.proposals rows); list candidates are expanded per index (schedule [L, n])."""
|
||||
out = {}
|
||||
for r in proposal_rows:
|
||||
field = f"{r['file']}:" + ".".join(str(x) for x in r["path"])
|
||||
c = r["candidate"]
|
||||
if isinstance(c, (list, tuple)):
|
||||
for i, v in enumerate(c):
|
||||
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
||||
out[f"{field}.{i}"] = v
|
||||
elif isinstance(c, (int, float)) and not isinstance(c, bool):
|
||||
out[field] = c
|
||||
return out
|
||||
|
||||
|
||||
def cross_check(cand, table):
|
||||
"""Confirm / correct anchor candidates with table values of the same field: equal -> grade A (table_confirmed), different -> grade C with
|
||||
`table` = the table value. Fields without a table value keep their grade. Works on the dict returned by follow()."""
|
||||
for f, c in cand.items():
|
||||
t = table.get(f)
|
||||
if t is None:
|
||||
continue
|
||||
c["table"] = t
|
||||
if abs(abs(t) - abs(c["value"])) <= 1.5:
|
||||
c["grade"], c["table_confirmed"] = "A", True
|
||||
else:
|
||||
c["grade"], c["table_confirmed"] = "C", False
|
||||
return cand
|
||||
|
||||
|
||||
def chains(texts):
|
||||
"""R2: dimension chains `n@pitch=total` with the check n x pitch = total: [{text, dwg, x, y, n, pitch, total, ok}]."""
|
||||
def mm(s):
|
||||
s = s.replace(",", "")
|
||||
return round(float(s) * 1000) if re.fullmatch(r"\d+\.\d{3}", s) else round(float(s))
|
||||
out = []
|
||||
for t in texts:
|
||||
m = CHAIN.match(t["text"].strip())
|
||||
if m:
|
||||
n, p, tot = int(m.group(1)), mm(m.group(2)), mm(m.group(3))
|
||||
out.append({"text": t["text"].strip(), "dwg": t["dwg"], "x": t["x"], "y": t["y"], "n": n, "pitch": p, "total": tot, "ok": abs(n * p - tot) <= 1})
|
||||
return out
|
||||
@@ -0,0 +1,153 @@
|
||||
"""params <-> expected consistency (C9, 2026-10-20): values that describe the SAME physical quantity in params.json (what the model is built from) and
|
||||
expected.json (what the checkers compare against) must agree. The Rhino gates find every mismatch too, but only after ~150 s; this check finds the
|
||||
stale ones (a value that changed in one file but not in the other) before any gate is run. Origin: Ungok P3 / P4 -- the first gate runs failed almost
|
||||
entirely on values that were updated in params but not in expected (coping steps, pile pitch, hoop chain, B9 grid, tier EL ...) or the other way round.
|
||||
|
||||
Not an independent check: when both files were written from the same drawing reading they agree trivially. It only says "the two files tell one story".
|
||||
Rules apply to the Ungok layout (coping steps, footing rebar rows ...); a rule whose paths are missing in either file is skipped, so other pier types
|
||||
are not affected. Also contains the point-list rule of improvement #6: `coping_rebar.c14.hooks_up` must name existing `c14.points` (a point list keyed by
|
||||
coordinates breaks silently when the points move).
|
||||
"""
|
||||
import re
|
||||
|
||||
TOL = 1.5 # mm (drawing values are rounded to 1 mm, CAD vertices to 0.1)
|
||||
|
||||
|
||||
def _get(doc, path):
|
||||
x = doc
|
||||
for p in path.split("."):
|
||||
if isinstance(x, list):
|
||||
if not re.fullmatch(r"-?\d+", p) or int(p) >= len(x):
|
||||
return None
|
||||
x = x[int(p)]
|
||||
elif isinstance(x, dict):
|
||||
if p not in x:
|
||||
return None
|
||||
x = x[p]
|
||||
else:
|
||||
return None
|
||||
return x
|
||||
|
||||
|
||||
def _num(v):
|
||||
"""number of a value that may be a string like '11.856' (schedule cells are metres) -> None when not numeric."""
|
||||
if isinstance(v, bool) or v is None:
|
||||
return None
|
||||
if isinstance(v, (int, float)):
|
||||
return float(v)
|
||||
try:
|
||||
return float(str(v).replace(",", ""))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _rule(rid, text, p, e, tol=TOL, f=None, g=None):
|
||||
"""one pairwise rule: params path p, expected path e; f / g transform the values before comparing."""
|
||||
return dict(id=rid, text=text, p=p, e=e, tol=tol, f=f, g=g)
|
||||
|
||||
|
||||
def _steps(n=5):
|
||||
for i in range(n):
|
||||
yield i
|
||||
|
||||
|
||||
def rules(params, expected):
|
||||
R = []
|
||||
A = R.append
|
||||
# footing, lean, column, piles
|
||||
for a, b, t in (("size_x", "dx", "기초 X"), ("size_y", "dy", "기초 Y"), ("z_bottom", "z0", "기초 하면 EL"), ("thickness", "thickness", "기초 두께")):
|
||||
A(_rule("footing." + a, t, "footing." + a, "G1.footing." + b))
|
||||
A(_rule("column.height", "기둥 높이", "column.height", "G1.column.height"))
|
||||
A(_rule("column.d", "기둥 지름", "column.diameter", "G1.column.d"))
|
||||
for a, b, t in (("count_x", "nx", "말뚝 X 개수"), ("count_y", "ny", "말뚝 Y 개수"), ("length", "length", "말뚝 길이"), ("diameter", "d", "말뚝 지름"), ("edge", "edge", "말뚝 연단")):
|
||||
A(_rule("piles." + a, t, "piles." + a, "G1.piles." + b, tol=0 if a.startswith("count") else TOL))
|
||||
pp = (params.get("piles") or {})
|
||||
for ax in ("x", "y"):
|
||||
pit = f"piles.pitch_{ax}" if f"pitch_{ax}" in pp else "piles.pitch"
|
||||
A(_rule(f"piles.pitch_{ax}", f"말뚝 {ax.upper()} 피치", pit, f"G1.piles.pitch_{ax}"))
|
||||
# coping outline: steps, soffit flat
|
||||
for i in _steps():
|
||||
A(_rule(f"coping.step{i + 1}.z", f"코핑 윗면 {i + 1}단 높이", f"coping.top_steps.{i}.z", f"G1.seats.step_top.{i + 1}"))
|
||||
A(_rule(f"coping.step{i + 1}.z/G4", f"코핑 윗면 {i + 1}단 높이(G4)", f"coping.top_steps.{i}.z", f"G4.step_top.{i + 1}"))
|
||||
A(_rule(f"coping.step{i + 1}.x0", f"코핑 {i + 1}단 시작 x", f"coping.top_steps.{i}.x0", f"G1.seats.step_x.{i + 1}.0"))
|
||||
A(_rule(f"coping.step{i + 1}.x1", f"코핑 {i + 1}단 끝 x", f"coping.top_steps.{i}.x1", f"G1.seats.step_x.{i + 1}.1"))
|
||||
for nm, i in (("left", 0), ("center", 2), ("right", 4)):
|
||||
A(_rule(f"coping.z_top_{nm}", f"코핑 윗면 {nm} 높이", f"coping.top_steps.{i}.z", f"G1.coping.z_top_{nm}"))
|
||||
A(_rule("coping.dx", "코핑 길이", "coping.length_bottom_tip_to_tip", "G1.coping.dx"))
|
||||
A(_rule("coping.dy", "코핑 폭", "coping.width_y", "G1.coping.dy"))
|
||||
A(_rule("coping.flat", "코핑 하면 평평부 높이", "coping.z_bottom_flat", "G1.dxf_arc_centres.datum_el"))
|
||||
A(_rule("coping.outline.flat", "코핑 외곽 하면 높이", "coping.z_bottom_flat", "G1.coping_outline.0.1"))
|
||||
# bearings
|
||||
A(_rule("bearing.pitch", "받침 피치", "bearing_seats.pitch_x", "G1.seats.pitch_chain", tol=1.0, g=lambda v: v[1] / v[0] if isinstance(v, list) and len(v) == 2 and v[0] else None))
|
||||
A(_rule("bearing.row_y.front", "받침 앞열 y", "bearing_seats.pad_rows.front.centre_rel_girder5.1", "G1.seats.row_y.front", tol=1.5))
|
||||
A(_rule("bearing.row_y.rear", "받침 뒤열 y", "bearing_seats.pad_rows.rear.centre_rel_girder5.1", "G1.seats.row_y.rear", tol=1.5))
|
||||
# coping rebar
|
||||
for t, ms in (("1", ("C1", "C4")), ("2", ("C2", "C5")), ("3", ("C3", "C6"))):
|
||||
for m in ms:
|
||||
A(_rule(f"coping_rebar.tier{t}.{m}", f"코핑 상단 철근 {m} 높이", f"coping_rebar.top.tiers.{t}.z", f"G4.tier_z.{m}"))
|
||||
A(_rule("coping_rebar.c15", "C15 높이", "coping_rebar.c15.z", "G4.c15_top"))
|
||||
A(_rule("coping_rebar.c16", "C16 높이", "coping_rebar.c16.z", "G4.c16_top"))
|
||||
A(_rule("coping_rebar.c10_bottom", "C10 스터럽 하단 높이", "coping_rebar.bottom.z_flat", "G4.c10_bottom"))
|
||||
lv = (params.get("coping_rebar") or {}).get("loops", {}).get("levels")
|
||||
if lv:
|
||||
A(_rule("coping_rebar.loop.first", "루프 C12 시작 높이", "coping_rebar.loops.levels.0", "G4.loop_z.C12.start"))
|
||||
for k in range(3):
|
||||
A(_rule(f"coping_rebar.loop.c13.{k}", f"루프 C13 {k + 1}번째 높이", f"coping_rebar.loops.levels.{6 + k}", f"G4.loop_z.C13.{k}"))
|
||||
# column rebar chains
|
||||
A(_rule("column_rebar.hoop.radius", "후프 반경", "column_rebar.hoops.radius", "G2.r_hoop"))
|
||||
A(_rule("column_rebar.hoop.count", "후프 개수", "column_rebar.hoops.levels", "G2.hoop_chain.groups",
|
||||
tol=0, f=lambda v: len(v) if isinstance(v, list) else None, g=lambda gs: 1 + sum(int(n) for n, _ in gs) if isinstance(gs, list) else None))
|
||||
A(_rule("column_rebar.tie.count", "결속철근 단 수", "column_rebar.ties.levels", "G2.tie_chain.groups",
|
||||
tol=0, f=lambda v: len(v) if isinstance(v, list) else None, g=lambda gs: 1 + sum(int(n) for n, _ in gs) if isinstance(gs, list) else None))
|
||||
A(_rule("column_rebar.p1.top", "주철근 P1 직선 길이", "column_rebar.main_bars.p1.straight_len", "G2.p1_top_parts.2"))
|
||||
# footing rebar rows (positions include the two end bars)
|
||||
for lay, row in (("bottom_Y1", "Y_BOT"), ("bottom_X2", "X_BOT"), ("bottom_Y3", "Y_250"), ("bottom_X4", "X_250"), ("top_Y", "Y_250"), ("top_X", "X_250")):
|
||||
A(_rule(f"footing_rebar.{lay}.count", f"기초 철근 {lay} 개수", f"footing_rebar.layers.{lay}.pos", f"G3.rows.{row}.n", tol=0, f=lambda v: len(v) if isinstance(v, list) else None, g=lambda n: n + 2 if isinstance(n, (int, float)) else None))
|
||||
A(_rule(f"footing_rebar.{lay}.end", f"기초 철근 {lay} 끝", f"footing_rebar.layers.{lay}.pos.-1", f"G3.rows.{row}.ends.1"))
|
||||
A(_rule("footing_rebar.b9.x", "B9 격자 x 개수", "footing_rebar.b9.grid_x", "G3.b9_grid.x.n", tol=0, f=lambda v: len(v) if isinstance(v, list) else None))
|
||||
A(_rule("footing_rebar.b9.y", "B9 격자 y 개수", "footing_rebar.b9.grid_y", "G3.b9_grid.y.n", tol=0, f=lambda v: len(v) if isinstance(v, list) else None))
|
||||
A(_rule("footing_rebar.b9.x0", "B9 격자 x 시작", "footing_rebar.b9.grid_x.0", "G3.b9_grid.x.start"))
|
||||
A(_rule("footing_rebar.b9.y0", "B9 격자 y 시작", "footing_rebar.b9.grid_y.0", "G3.b9_grid.y.start"))
|
||||
A(_rule("footing_rebar.b10.x", "B10 x", "footing_rebar.b10.size_x", "G3.b10_size.0"))
|
||||
A(_rule("footing_rebar.b10.y", "B10 y", "footing_rebar.b10.size_y", "G3.b10_size.1"))
|
||||
# schedule copies
|
||||
for m in ("P1", "P2", "P3", "P4", "P5", "P6"):
|
||||
A(_rule(f"schedule.{m}.n", f"재료표 {m} 개수", f"column_rebar.schedule.{m}.n", f"G2.schedule_n.{m}", tol=0))
|
||||
A(_rule(f"schedule.{m}.L", f"재료표 {m} 길이", f"column_rebar.schedule.{m}.L", f"G2.schedule_L.{m}", f=lambda v: None if _num(v) is None else _num(v) * 1000))
|
||||
for i in range(1, 13):
|
||||
m = f"B{i}"
|
||||
sch = (params.get("footing_rebar") or {}).get("schedule", {}).get(m)
|
||||
if sch is None:
|
||||
continue
|
||||
# raw schedule rows: B9 has an extra count column in newer drawings; counts are compared where the row has one
|
||||
A(_rule(f"schedule.{m}.n", f"재료표 {m} 개수", f"footing_rebar.schedule.{m}.3", f"G3.schedule.{m}.1", tol=0, f=lambda v: _num(v) if _num(v) is not None and float(v) == int(float(v)) else None))
|
||||
return R
|
||||
|
||||
|
||||
def check(params, expected):
|
||||
"""-> list of {id, text, p, e, pv, ev, ok} for every rule whose two values exist, plus the point-list rule."""
|
||||
out = []
|
||||
for r in rules(params, expected):
|
||||
pv, ev = _get(params, r["p"]), _get(expected, r["e"])
|
||||
if r["f"]:
|
||||
pv = r["f"](pv)
|
||||
if r["g"]:
|
||||
ev = r["g"](ev)
|
||||
a, b = _num(pv), _num(ev)
|
||||
if a is None or b is None:
|
||||
continue
|
||||
out.append(dict(id=r["id"], text=r["text"], p=r["p"], e=r["e"], pv=a, ev=b, ok=abs(a - b) <= r["tol"]))
|
||||
# improvement #6: a list of points addressed by coordinates must name existing points
|
||||
c14 = _get(params, "coping_rebar.c14")
|
||||
if isinstance(c14, dict) and isinstance(c14.get("hooks_up"), list) and isinstance(c14.get("points"), list):
|
||||
pts = {(round(x, 1), round(z, 1)) for x, z in c14["points"]}
|
||||
for i, h in enumerate(c14["hooks_up"]):
|
||||
ok = (round(h[0], 1), round(h[1], 1)) in pts
|
||||
out.append(dict(id=f"coping_rebar.c14.hooks_up.{i}", text="c14 갈고리 위 지정 점이 c14 점 목록에 있다", p=f"coping_rebar.c14.hooks_up.{i}",
|
||||
e="coping_rebar.c14.points", pv=f"({h[0]}, {h[1]})", ev="목록에 없음" if not ok else "있음", ok=ok))
|
||||
return out
|
||||
|
||||
|
||||
def bundles_of(path):
|
||||
"""'footing.size_x' -> 'footing' (the section key of a dotted path)."""
|
||||
return path.split(".", 1)[0]
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Footing rebar layer rule of the Ungok frame (2026-10-20; was tools/infer_footing_rebar.py). The rules were read off the template pier (운곡교 P1) and
|
||||
reproduced there: bars run (footing - 200) long; the dense layers (B2/B4, B6/B8) have the two end bars, one bar 200 (X-position axis) / 175 (Y) inside, then
|
||||
a 125 grid; the sparse layers (B3, B7, B1, B5) the same with a 250 grid. The result equals the bar counts of the pier's own rebar schedule (P3, P4)."""
|
||||
|
||||
|
||||
def dense(half, first_gap):
|
||||
inner = half - first_gap
|
||||
k = int((inner - 125) // 125)
|
||||
g = [i * 125.0 for i in range(-k, k + 1)]
|
||||
return sorted({-half, half, -inner, inner, *g})
|
||||
|
||||
|
||||
def sparse(half, first_gap):
|
||||
inner = half - first_gap
|
||||
n = int(round(2 * inner / 250))
|
||||
return sorted({-half, half, *[-inner + 250.0 * i for i in range(n + 1)]})
|
||||
|
||||
|
||||
def infer(size_x, size_y):
|
||||
lx, ly = size_x - 200, size_y - 200
|
||||
hx, hy = lx / 2, ly / 2
|
||||
return {"bottom_Y1": dense(hx, 200), "bottom_X2": dense(hy, 175), "bottom_Y3": sparse(hx, 200), "bottom_X4": sparse(hy, 175),
|
||||
"top_Y": sparse(hx, 200), "top_X": sparse(hy, 175), "a_y": ly, "a_x": lx}
|
||||
@@ -0,0 +1,198 @@
|
||||
"""Gate runner: build a gate model, re-open it, check it, run negative tests and judge the gate.
|
||||
|
||||
A gate Gk passes when all of:
|
||||
checks every FAIL of checker Gk is an approved planned exception (expected.json `planned_fail`)
|
||||
negative a horizontal and a vertical move of one gate object each raise the FAIL count
|
||||
reload the re-opened file has the same object count as the built document
|
||||
unchanged objects of earlier gates are identical to the previous gate model (fast fingerprint)
|
||||
Files go to <run_dir>: Gk.3dm, Gk.json, Gk_negH.json, Gk_negV.json, Gk_fp.json, Gk_gate.json.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from engine.pipeline import (CHECKERS, GATE_PREFIXES, GATES, REBAR_GATES, RHINO_DIR, TOLERANCE_MM, build_steps,
|
||||
builder_of, check_steps, checker_of, gate_ids, scope_of)
|
||||
|
||||
APP = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(APP))
|
||||
sys.path.insert(0, str(APP / "tools"))
|
||||
from compare_fingerprints import diff_obj # noqa: E402
|
||||
from runner import run_job # noqa: E402
|
||||
|
||||
# Negative tests per gate: one horizontal and one vertical move of a gate object (mm).
|
||||
NEGATIVE = {
|
||||
"G1": ({"id": "BS08", "d": [3, 0, 0]}, {"id": "BP03", "d": [0, 0, 2]}),
|
||||
"G2": ({"mark": "P2", "d": [5, 0, 0]}, {"mark": "P3", "d": [0, 0, 5]}),
|
||||
"G3": ({"mark": "B4", "d": [5, 0, 0]}, {"mark": "B9", "d": [0, 0, 5]}),
|
||||
"G4": ({"mark": "C11", "d": [5, 0, 0]}, {"mark": "S1", "d": [0, 0, 5]}),
|
||||
"G5": ({"id": "BB05", "d": [5, 0, 0]}, {"id": "SP08", "d": [0, 0, 5]}),
|
||||
}
|
||||
|
||||
|
||||
def _load(p):
|
||||
return json.loads(Path(p).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def planned(expected, gate, item):
|
||||
for pf in expected.get("planned_fail", []):
|
||||
if pf["gate"] != gate:
|
||||
continue
|
||||
if pf.get("item") == item or (pf.get("item_prefix") and item.startswith(pf["item_prefix"])):
|
||||
return pf
|
||||
return None
|
||||
|
||||
|
||||
def _fails(res):
|
||||
return [r for r in res["results"] if r["result"] == "FAIL"]
|
||||
|
||||
|
||||
def negative_moves(X, gate):
|
||||
"""Negative-test moves of a gate: expected["negative"][gate] (pier specific) or the Ungok default.
|
||||
A default bearing id beyond this pier's bearings (BS08 / SP08 with 4 bearings) is replaced by the last bearing
|
||||
number of expected G5 el_x (OPEN #19 A7); with 10 bearings the default is unchanged."""
|
||||
ov = X.get("negative", {}).get(gate)
|
||||
if ov:
|
||||
return tuple(ov)
|
||||
nos = sorted(int(k) for k in (X.get("G5", {}).get("el_x") or {}) if str(k).isdigit())
|
||||
return tuple(dict(mv, id=f"{mv['id'][:2]}{nos[-1]:02d}") if "id" in mv and nos and int(mv["id"][2:]) not in nos
|
||||
else mv for mv in NEGATIVE[gate])
|
||||
|
||||
|
||||
def _negative_steps(gate, pid, expected, run_dir):
|
||||
"""Rhino steps: checker, then horizontal / vertical move -> checker -> undo (same document)."""
|
||||
neg_h, neg_v = negative_moves(_load(expected), gate)
|
||||
pert = RHINO_DIR / "perturb.py"
|
||||
steps = check_steps(expected, run_dir, [gate])
|
||||
for tag, mv in (("_negH", neg_h), ("_negV", neg_v)):
|
||||
glb = {"PIER_ID": pid, "MOVES": [mv]}
|
||||
steps.append({"name": f"{gate}:perturb{tag}", "script": pert, "globals": glb})
|
||||
steps += check_steps(expected, run_dir, [gate], tag)
|
||||
steps.append({"name": f"{gate}:undo{tag}", "script": pert, "globals": dict(glb, UNDO=True)})
|
||||
return steps
|
||||
|
||||
|
||||
def _judge(gate, X, run_dir):
|
||||
"""Read checker + negative-test results: (base, unplanned, planned_seen, neg)."""
|
||||
base = _load(run_dir / f"{gate}.json")
|
||||
fails = _fails(base)
|
||||
unplanned = [f["item"] for f in fails if not planned(X, gate, f["item"])]
|
||||
planned_seen = [{"item": f["item"], "reason": planned(X, gate, f["item"])["reason"]}
|
||||
for f in fails if planned(X, gate, f["item"])]
|
||||
neg = {}
|
||||
for tag, mv in zip(("negH", "negV"), negative_moves(X, gate)):
|
||||
r = _load(run_dir / f"{gate}_{tag}.json")
|
||||
new = [f["item"] for f in _fails(r) if f["item"] not in {x["item"] for x in fails}]
|
||||
neg[tag] = {"move": mv, "new_fails": new[:10], "detected": len(_fails(r)) > len(fails)}
|
||||
return base, unplanned, planned_seen, neg
|
||||
|
||||
|
||||
def run_batch_session(gates, params, expected, run_dir, log=print):
|
||||
"""Batch mode: build G1..G5 in ONE Rhino session; after each gate in `gates` is built, run its checker and
|
||||
negative tests in the same document. No per-gate save and no reload; the final model is saved once as G5.3dm.
|
||||
Writes <gate>_gate.json for each gate in `gates` (reload / unchanged = skipped) and returns {gate: report}."""
|
||||
run_dir = Path(run_dir)
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
X = _load(expected)
|
||||
pid = X["pier_id"]
|
||||
ids = gate_ids()
|
||||
for g in gates: # results / models of these gates are replaced: drop the old ones
|
||||
for suffix in ("_fp.json",) + ((".3dm",) if g != "G5" else ()):
|
||||
(run_dir / f"{g}{suffix}").unlink(missing_ok=True)
|
||||
glb = {"PARAMS_PATH": str(Path(params).resolve())}
|
||||
steps = []
|
||||
scope = scope_of(params)
|
||||
for g, _, _ in GATES:
|
||||
if g not in scope:
|
||||
continue
|
||||
script = builder_of(params, g)
|
||||
steps.append({"name": f"{g}:{script}", "script": RHINO_DIR / script, "globals": dict(glb, GATE=g)})
|
||||
if g in REBAR_GATES:
|
||||
steps.append({"name": f"{g}:rebar_colours.py", "script": RHINO_DIR / "rebar_colours.py", "globals": glb})
|
||||
if g in gates:
|
||||
steps += _negative_steps(g, pid, expected, run_dir)
|
||||
steps.append({"name": "display.py", "script": RHINO_DIR / "display.py", "globals": glb})
|
||||
t0 = time.time()
|
||||
log(f"[batch] one Rhino session: build G1..G5, check {', '.join(gates)} (no per-gate save / reload)")
|
||||
res = run_job(steps, save=run_dir / "G5.3dm", units_mm_tolerance=TOLERANCE_MM)
|
||||
secs = {}
|
||||
for st in res["steps"]:
|
||||
g = st["name"].split(":")[0]
|
||||
secs[g] = secs.get(g, 0) + st["secs"]
|
||||
out = {}
|
||||
for g in gates:
|
||||
base, unplanned, planned_seen, neg = _judge(g, X, run_dir)
|
||||
verdict = "PASS" if not unplanned and all(v["detected"] for v in neg.values()) else "FAIL"
|
||||
rep = {"gate": g, "checker": checker_of(expected, g), "verdict": verdict, "summary": base["summary"],
|
||||
"unplanned_fails": unplanned, "planned_fails": planned_seen, "negative": neg,
|
||||
"reload": {"ok": None, "skipped": "일괄 실행(단일 세션) — 저장·재로드 안 함"},
|
||||
"unchanged": {"ok": None, "compared": 0, "diffs": [], "missing": [], "against": None,
|
||||
"skipped": "일괄 실행(단일 세션) — 앞 게이트 객체를 다시 만들지 않음"},
|
||||
"model": str(run_dir / "G5.3dm") if g == "G5" else None, "mode": "batch",
|
||||
"secs": round(secs.get(g, 0), 1), "session_secs": res["wall_secs"]}
|
||||
(run_dir / f"{g}_gate.json").write_text(json.dumps(rep, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
log(f"[{g}] {verdict} checks {base['summary']['passed']}/{base['summary']['total']} "
|
||||
f"(planned FAIL {len(planned_seen)}, unplanned {len(unplanned)}) negative H/V "
|
||||
f"{neg['negH']['detected']}/{neg['negV']['detected']} (batch)")
|
||||
out[g] = rep
|
||||
log(f"[batch] done in {round(time.time() - t0, 1)}s, objects {res['objects']}, model {run_dir / 'G5.3dm'}")
|
||||
return out
|
||||
|
||||
|
||||
def run_gate(gate, params, expected, run_dir, log=print):
|
||||
run_dir = Path(run_dir)
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
X = _load(expected)
|
||||
pid = X["pier_id"]
|
||||
t0 = time.time()
|
||||
model = run_dir / f"{gate}.3dm"
|
||||
log(f"[{gate}] build G1..{gate}")
|
||||
built = run_job(build_steps(params, gate), save=model, units_mm_tolerance=TOLERANCE_MM)
|
||||
|
||||
steps = _negative_steps(gate, pid, expected, run_dir)
|
||||
steps.append({"name": "fingerprint", "script": RHINO_DIR / "fingerprint.py",
|
||||
"globals": {"MODEL_PATH": str(model), "OUT_PATH": str(run_dir / f"{gate}_fp.json"), "FAST": True}})
|
||||
log(f"[{gate}] re-open, check, negative tests, fingerprint")
|
||||
checked = run_job(steps, open_path=model)
|
||||
|
||||
base, unplanned, planned_seen, neg = _judge(gate, X, run_dir)
|
||||
|
||||
fp = _load(run_dir / f"{gate}_fp.json")
|
||||
unchanged = {"compared": 0, "diffs": [], "missing": [], "ok": True, "against": None}
|
||||
ids = scope_of(params) # previous gate = previous gate in this pier's scope
|
||||
k = ids.index(gate)
|
||||
if k > 0:
|
||||
prev_fp = run_dir / f"{ids[k - 1]}_fp.json"
|
||||
if k > 0 and not prev_fp.exists():
|
||||
unchanged.update(ok=None, skipped=f"{ids[k - 1]} 지문 없음(일괄 실행 결과) — 비교 생략")
|
||||
elif k > 0:
|
||||
prev = _load(prev_fp)
|
||||
earlier = tuple(f"{pid}-{p}" for g in ids[:k] for p in GATE_PREFIXES[g])
|
||||
keys = [oid for oid in prev["objects"] if oid.startswith(earlier)]
|
||||
unchanged["against"] = prev_fp.name
|
||||
unchanged["compared"] = len(keys)
|
||||
unchanged["missing"] = [oid for oid in keys if oid not in fp["objects"]][:10]
|
||||
for oid in keys:
|
||||
if oid in fp["objects"]:
|
||||
d = diff_obj(prev["objects"][oid], fp["objects"][oid])
|
||||
if d:
|
||||
unchanged["diffs"].append([oid, d[:3]])
|
||||
unchanged["diffs"] = unchanged["diffs"][:10]
|
||||
unchanged["ok"] = not unchanged["missing"] and not unchanged["diffs"]
|
||||
|
||||
reload_ok = built["objects"] == checked["objects"] == fp["count"]
|
||||
verdict = ("PASS" if not unplanned and all(v["detected"] for v in neg.values()) and reload_ok
|
||||
and unchanged["ok"] is not False else "FAIL")
|
||||
rep = {"gate": gate, "checker": checker_of(expected, gate), "verdict": verdict, "summary": base["summary"],
|
||||
"unplanned_fails": unplanned, "planned_fails": planned_seen,
|
||||
"negative": neg, "reload": {"built": built["objects"], "reopened": checked["objects"],
|
||||
"fingerprint": fp["count"], "ok": reload_ok},
|
||||
"unchanged": unchanged, "model": str(model), "mode": "single", "secs": round(time.time() - t0, 1),
|
||||
"build_steps": [{k_: s[k_] for k_ in ("name", "secs")} for s in built["steps"]]}
|
||||
(run_dir / f"{gate}_gate.json").write_text(json.dumps(rep, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
log(f"[{gate}] {verdict} checks {base['summary']['passed']}/{base['summary']['total']} "
|
||||
f"(planned FAIL {len(planned_seen)}, unplanned {len(unplanned)}) negative H/V "
|
||||
f"{neg['negH']['detected']}/{neg['negV']['detected']} reload {reload_ok} "
|
||||
f"unchanged {unchanged['ok']} ({unchanged['compared']}) {rep['secs']}s")
|
||||
return rep
|
||||
@@ -0,0 +1,691 @@
|
||||
"""Read the main dimensions of an Ungok-type pier straight from its drawings and propose them as params / expected values (2026-10-20, improvement #34-1).
|
||||
|
||||
Origin: the same readers were first written as separate tools (tools/extract_coping_outline.py, infer_footing_rebar.py, verify_pier_geometry.py,
|
||||
apply_pier_read.py) while the Ungok P3 / P4 piers were modelled; they cost 77 + 144 hand-made value changes. Everything is read from the drawing
|
||||
(D1 lines / circles / polylines / texts, the schedule tables) or derived by a rule that was reproduced on the finished template pier first:
|
||||
coping outline + steps + soffit flat D1 front view lines (CS-CONC-MAJR), heights from the 'EL.' label next to the highest step
|
||||
coping rebar tiers / C15 / C16 / loops D1 front view rebar lines (CS-RBAR-MAJR), C14 points = right-half circles mirrored
|
||||
footing / lean / piles / column D1 dimension texts at the sheet positions of the template frame, pile chains `n@pitch=total`
|
||||
bearings plan view pad polylines (datum = coping plan centre), pedestal outlines
|
||||
footing rebar layer rule (engine/footing_rule) + the schedule counts + mid layers from the drawn dots
|
||||
column rebar schedule rows + first hoop level = footing bottom + 1180
|
||||
Frame: all sheet positions are those of the Ungok P1 drawing set (the same frame as P2 .. P4); `frame_ok()` tells whether a new drawing has it.
|
||||
Validation: `python tools/test_geomread.py` runs the reader on P1 (must reproduce its params within 1.5 mm), on the finished P3 / P4 (no material change)
|
||||
and on the early P4 backup (P1 copy) which must come out equal to the verified P4 values.
|
||||
`read()` has no side effect: it returns changed COPIES of params / expected and the list of changes; the web screen / tools write them.
|
||||
"""
|
||||
import copy
|
||||
import glob
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import ezdxf
|
||||
|
||||
from engine import anchors, footing_rule, reader
|
||||
|
||||
FRAME_MIN_COVERAGE = 0.70
|
||||
DATUM_X, DATUM_Y = 12162.4, 26412.3 # coping plan centre in the P1 plan view (all piers of the frame)
|
||||
SEC_CENTRE_X = 30133.6 # footing centre in the section view of the P1 sheet
|
||||
ARC_X = 6951.9 # x of the soffit arc centres (about the pier axis)
|
||||
ORDER = {"XIV": "A B C R L", "XVI": "A", "VI": "A B R L", "V": "A B R L", "IX": "A B R L", "III": "A B L"}
|
||||
|
||||
GROUPS = (("coping.", "코핑"), ("coping_rebar.", "코핑 철근"), ("footing.", "기초·말뚝·기둥"), ("lean_concrete.", "기초·말뚝·기둥"), ("piles.", "기초·말뚝·기둥"),
|
||||
("column.", "기초·말뚝·기둥"), ("bearing_seats.", "받침"), ("footing_rebar.", "기초 철근"), ("column_rebar.", "기둥 철근"), ("display.", "표시"),
|
||||
("pier_id", "교각 식별자"))
|
||||
|
||||
|
||||
class FrameError(Exception):
|
||||
"""the drawing does not have the Ungok frame (or a needed element is missing) -- the message says what"""
|
||||
|
||||
|
||||
def same(a, b, tol=1.5):
|
||||
"""equal up to drawing rounding. Numbers: millimetre values (|v| >= 100) within `tol` (1.5 mm); SMALL numbers (counts, offsets, angles) must agree to
|
||||
0.15 (0.05 for angles) -- a count of 36 is not 37. Texts that are numbers (schedule cells in metres: '17.247') must be equal; texts with words
|
||||
(rule descriptions) compare by their numbers within 0.5. Lists / dicts element by element."""
|
||||
if isinstance(a, bool) or isinstance(b, bool):
|
||||
return a == b
|
||||
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
|
||||
return abs(a - b) <= (tol if max(abs(a), abs(b)) >= 100 else min(tol, 0.15))
|
||||
if isinstance(a, list) and isinstance(b, list):
|
||||
return len(a) == len(b) and all(same(x, y, tol) for x, y in zip(a, b))
|
||||
if isinstance(a, dict) and isinstance(b, dict):
|
||||
return a.keys() == b.keys() and all(same(a[k], b[k], tol) for k in a)
|
||||
if isinstance(a, str) and isinstance(b, str):
|
||||
pure = lambda t: re.fullmatch(r"\s*-?\d[\d,]*\.?\d*\s*", t) is not None # noqa: E731
|
||||
if pure(a) and pure(b):
|
||||
return abs(float(a.replace(",", "")) - float(b.replace(",", ""))) <= 0.0005
|
||||
na, nb = re.findall(r"-?\d[\d,]*\.?\d*", a), re.findall(r"-?\d[\d,]*\.?\d*", b)
|
||||
if na and len(na) == len(nb) and re.sub(r"[\d,.\-+]+", "", a) == re.sub(r"[\d,.\-+]+", "", b):
|
||||
return all(abs(float(x.replace(",", "")) - float(y.replace(",", ""))) <= 0.5 for x, y in zip(na, nb))
|
||||
return a == b
|
||||
|
||||
|
||||
def _points_key(v):
|
||||
return sorted(v, key=lambda p: (p[0], p[1]) if isinstance(p, list) else p)
|
||||
|
||||
|
||||
EXACT_PARAMS = ("footing.", "lean_concrete.", "column.", "piles.", "column_rebar.", "footing_rebar.a_", "footing_rebar.b9", "footing_rebar.b10", "footing_rebar.layers.bottom",
|
||||
"footing_rebar.layers.top", "footing_rebar.schedule", "footing_rebar.counts", "pier_id", "display.")
|
||||
EXACT_EXPECTED = ("G1.footing", "G1.column", "G1.piles", "G2.", "G3.", "pier_id")
|
||||
|
||||
|
||||
def _tol(which, path):
|
||||
"""Values taken from TEXTS (dimension texts, schedule cells) are exact numbers: a 1 mm difference is a real difference (the bar length checks have a
|
||||
1 mm tolerance). Values measured on CAD GEOMETRY (lines, circles, polylines) carry drawing rounding: 1.5 mm; angles 0.05."""
|
||||
if "rotation" in path:
|
||||
return 0.05
|
||||
if re.fullmatch(r"coping_rebar\.top\.tiers\.\d\.a", path) or path.startswith(EXACT_EXPECTED if which == "expected" else EXACT_PARAMS):
|
||||
return 0.0005
|
||||
return 1.5
|
||||
|
||||
|
||||
def _top_left(P, rng, ST, c16z):
|
||||
"""Top face of the left C9 stirrups (not drawn). P1 rule: step top - 100. When the schedule's C9 b range does not follow from that rule, the top is
|
||||
back-calculated from the range (b = top - stirrup bottom + b_offset) at the inner and the outer stirrup; accepted only when both ends agree (P4, 2026-10-20).
|
||||
-> (bands [[|x| from, z]], why)"""
|
||||
rule = [[0, c16z], [abs(ST[1][1]), ST[1][2] - 100], [abs(ST[0][1]), ST[0][2] - 100]]
|
||||
why = "소단 − 100 (P1 규칙, 추론: 좌측 스터럽은 도면에 그려져 있지 않음 — 재료표 C9 b 범위와 대조하세요)"
|
||||
cr = P["coping_rebar"]
|
||||
st, bt, xs = cr["stirrups"], cr["bottom"], cr["stirrups"].get("x_c9")
|
||||
if not (rng and rng[0] == "b_sched" and xs and cr.get("soffit_arc_centre")):
|
||||
return rule, why
|
||||
ax, cz = cr["soffit_arc_centre"]
|
||||
|
||||
def bottom(x):
|
||||
x = abs(x)
|
||||
return bt["z_flat"] if x <= bt["half_flat"] else cz + math.sqrt(bt["R_offset"] ** 2 - (x - ax) ** 2)
|
||||
|
||||
def b_of(bands, x):
|
||||
z = bands[0][1]
|
||||
for frm, zz in bands:
|
||||
if abs(x) >= frm:
|
||||
z = zz
|
||||
return z - bottom(x) + st["b_offset"]
|
||||
hi, lo = max(rng[1:3]), min(rng[1:3])
|
||||
xi, xo = min(xs, key=abs), max(xs, key=abs)
|
||||
if abs(b_of(rule, xi) - hi) <= 1.5 and abs(b_of(rule, xo) - lo) <= 1.5:
|
||||
return rule, why + " — 재료표 C9 b 범위와 일치"
|
||||
zi, zo = hi - st["b_offset"] + bottom(xi), lo - st["b_offset"] + bottom(xo)
|
||||
if abs(zi - zo) <= 1.5:
|
||||
z = (zi + zo) / 2
|
||||
t1 = cr["top"]["tiers"]["1"]["z"]
|
||||
z = t1 if abs(z - t1) <= 1.5 else round(z)
|
||||
return [[0, c16z], [abs(xi) - 50, z]], (f"재료표 C9 b 범위(최대 {hi:g}, 최소 {lo:g})로 역산한 윗면 {z:g} — 안쪽·바깥쪽 스터럽이 {abs(zi - zo):.1f} mm 안에서 일치"
|
||||
" (추론: 소단 − 100 규칙은 재료표와 어긋남)")
|
||||
return rule, why + " — 재료표 C9 b 범위와 어긋나고 역산값도 두 끝이 일치하지 않음, 눈으로 확인하세요"
|
||||
|
||||
|
||||
def _num(s):
|
||||
return float(re.search(r"\d+\.\d+|\d+", s).group())
|
||||
|
||||
|
||||
def _group(path):
|
||||
for pre, name in GROUPS:
|
||||
if path.startswith(pre) or path == pre:
|
||||
return name
|
||||
return "기타"
|
||||
|
||||
|
||||
def dxf_of(pier_dir, audit):
|
||||
"""DXF file of the front-view sheet D1: the audit entry labelled D1, else the file named (1)."""
|
||||
ent = next((a for a in audit if a.get("label") == "D1"), None)
|
||||
if ent and (pier_dir / "drawings" / "dxf" / ent["dxf"]).exists():
|
||||
return pier_dir / "drawings" / "dxf" / ent["dxf"]
|
||||
f = sorted(glob.glob(str(pier_dir / "drawings" / "dxf" / "*(1)*.dxf")))
|
||||
if not f:
|
||||
raise FrameError("D1(교각도 1) DXF가 없습니다")
|
||||
return Path(f[0])
|
||||
|
||||
|
||||
def read(params, expected, ref, texts, dxf_path, pier_name):
|
||||
"""-> (params', expected', changes, notes). changes: dicts {file, path, old, new, why, group}; notes: strings for the person."""
|
||||
P, E = copy.deepcopy(params), copy.deepcopy(expected)
|
||||
changes, notes = [], []
|
||||
|
||||
def put(doc, which, path, val, why):
|
||||
keys = path.split(".")
|
||||
x = doc
|
||||
for k in keys[:-1]:
|
||||
x = x[int(k)] if isinstance(x, list) else x[k]
|
||||
last = int(keys[-1]) if isinstance(x, list) else keys[-1]
|
||||
old = x.get(last) if isinstance(x, dict) else x[last]
|
||||
cmp_old, cmp_val = old, val
|
||||
if path.endswith(("c14.points", "c14_levels")) and isinstance(old, list) and isinstance(val, list):
|
||||
cmp_old, cmp_val = _points_key(old), _points_key(val) # the order of a point list carries no meaning
|
||||
if old is None and path.endswith(("piles.pitch_x", "piles.pitch_y")) and isinstance(x, dict) and same(x.get("pitch"), val):
|
||||
return # older layout: one `pitch` for both directions
|
||||
if old is None and path.endswith("radius_by_mark") and isinstance(x, dict) and val.get("P3") == x.get("radius"):
|
||||
return
|
||||
if not same(cmp_old, cmp_val, _tol(which, path)):
|
||||
x[last] = val
|
||||
changes.append(dict(file=which, path=path, old=old, new=val, why=why, inferred="추론" in why,
|
||||
group=_group(path) if which == "params" else "기대값(expected) " + path.split(".")[0]))
|
||||
|
||||
MS = list(ezdxf.readfile(str(dxf_path)).modelspace())
|
||||
# ================= coping outline / datum
|
||||
L = [(e.dxf.start.x, e.dxf.start.y, e.dxf.end.x, e.dxf.end.y) for e in MS if e.dxftype() == "LINE" and e.dxf.layer == "CS-CONC-MAJR"]
|
||||
tips = sorted({(round(a, 1), round(b, 1), round(c, 1), round(d, 1)) for a, b, c, d in L if 150 < abs(c - a) < 250 and 1300 < abs(d - b) < 1900 and 0 < b < 25000})
|
||||
if len(tips) != 2:
|
||||
raise FrameError(f"코핑 양 끝 경사선 2개를 찾지 못했습니다({len(tips)}개)")
|
||||
top = lambda l: (l[0], l[1]) if l[1] > l[3] else (l[2], l[3]) # noqa: E731
|
||||
bot = lambda l: (l[2], l[3]) if l[1] > l[3] else (l[0], l[1]) # noqa: E731
|
||||
left, right = sorted(tips, key=lambda l: top(l)[0])
|
||||
CX = (bot(left)[0] + bot(right)[0]) / 2
|
||||
cur, end = top(left), top(right)
|
||||
hv = [(round(a, 1), round(b, 1), round(c, 1), round(d, 1)) for a, b, c, d in L if abs(a - c) < .05 or abs(b - d) < .05]
|
||||
steps, used = [], set()
|
||||
while abs(cur[0] - end[0]) > .5 or abs(cur[1] - end[1]) > .5:
|
||||
nxt = None
|
||||
for s in hv:
|
||||
if s in used:
|
||||
continue
|
||||
for p, q in (((s[0], s[1]), (s[2], s[3])), ((s[2], s[3]), (s[0], s[1]))):
|
||||
if abs(p[0] - cur[0]) < .5 and abs(p[1] - cur[1]) < .5 and q[0] >= cur[0] - .5 and (q[1] >= cur[1] - .5 or abs(q[0] - cur[0]) > .5):
|
||||
nxt = (s, q)
|
||||
break
|
||||
if nxt:
|
||||
break
|
||||
if not nxt:
|
||||
raise FrameError(f"코핑 윗면 계단 선이 끊겼습니다 ({cur[0]:.0f}, {cur[1]:.0f})")
|
||||
used.add(nxt[0])
|
||||
if abs(nxt[1][1] - cur[1]) < .05:
|
||||
steps.append([cur[0], nxt[1][0], cur[1]])
|
||||
cur = nxt[1]
|
||||
if len(steps) != 5:
|
||||
raise FrameError(f"코핑 윗면 계단이 {len(steps)}단입니다 (5단 형식만 읽습니다)")
|
||||
y_hi = max(s[2] for s in steps)
|
||||
els = [t for t in texts if t["dwg"] == "D1" and t["text"].startswith("EL.") and abs(t["x"] - end[0]) < 1500 and 0 < t["y"] - y_hi < 1200]
|
||||
if not els:
|
||||
raise FrameError("코핑 윗면 최고 단 옆의 EL 글자를 찾지 못했습니다")
|
||||
OFF = _num(els[0]["text"]) * 1000 - y_hi # EL = y + OFF (front view)
|
||||
ST = [(round(a - CX), round(b - CX), round(y + OFF)) for a, b, y in steps]
|
||||
flat_y = sorted({round(f[1], 1) for f in hv if abs(f[1] - f[3]) < .05 and 1800 < abs(f[2] - f[0]) < 2600 and abs((f[0] + f[2]) / 2 - CX) < 600 and bot(left)[1] - 1700 < f[1] < bot(left)[1] + 100})
|
||||
if not flat_y:
|
||||
raise FrameError("코핑 아랫면 평평부를 찾지 못했습니다")
|
||||
Z_FLAT = round(flat_y[0] + OFF)
|
||||
TIP_RISE = round(top(left)[1] - bot(left)[1])
|
||||
for i, (a, b, z) in enumerate(ST):
|
||||
put(P, "params", f"coping.top_steps.{i}.x0", float(a), "D1 코핑 윗면 외곽선")
|
||||
put(P, "params", f"coping.top_steps.{i}.x1", float(b), "D1 코핑 윗면 외곽선")
|
||||
put(P, "params", f"coping.top_steps.{i}.z", z, "D1 코핑 윗면 외곽선 + EL 글자")
|
||||
put(P, "params", "coping.z_bottom_flat", Z_FLAT, "D1 코핑 아랫면 평평부")
|
||||
tip_z = Z_FLAT + TIP_RISE
|
||||
|
||||
# ================= coping rebar: lines and circles of the front view
|
||||
V = []
|
||||
for e in MS:
|
||||
if e.dxftype() == "LINE" and e.dxf.layer == "CS-RBAR-MAJR" and abs(e.dxf.start.x - e.dxf.end.x) < .05 and CX - 10 < e.dxf.start.x < 18500:
|
||||
y0, y1 = sorted((e.dxf.start.y, e.dxf.end.y))
|
||||
if 15500 < y0 and y1 < 20300 and y1 - y0 > 400:
|
||||
V.append((round(e.dxf.start.x - CX), y0 + OFF, y1 + OFF))
|
||||
pick = [v for v in V if v[0] == 2900]
|
||||
CZ_REL = round(ref["coping_rebar"]["soffit_arc_centre"][1] + (Z_FLAT - ref["coping"]["z_bottom_flat"]), 1)
|
||||
if pick:
|
||||
CZ = round(pick[0][1] - math.sqrt(12100 ** 2 - (ARC_X - 2900) ** 2), 1)
|
||||
if abs(CZ_REL - CZ) > 1.5:
|
||||
notes.append(f"아랫면 곡선 중심: 템플릿 관계 {CZ_REL}, 스터럽 하단으로 맞춘 값 {CZ} — 눈으로 확인하세요")
|
||||
else:
|
||||
CZ = CZ_REL
|
||||
put(P, "params", "coping_rebar.soffit_arc_centre", [ARC_X, CZ_REL], "템플릿 중심 + 하면 높이 차(스터럽 하단이 1.5 mm 안에서 맞음)")
|
||||
H = {}
|
||||
for e in MS:
|
||||
if e.dxftype() == "LINE" and e.dxf.layer == "CS-RBAR-MAJR" and abs(e.dxf.start.y - e.dxf.end.y) < .05 and 5500 < min(e.dxf.start.x, e.dxf.end.x) and max(e.dxf.start.x, e.dxf.end.x) < 18700 \
|
||||
and 15500 < e.dxf.start.y < 20300 and abs(e.dxf.end.x - e.dxf.start.x) > 1500:
|
||||
H[round(e.dxf.start.y + OFF)] = (round(min(e.dxf.start.x, e.dxf.end.x) - CX), round(max(e.dxf.start.x, e.dxf.end.x) - CX))
|
||||
tiers = sorted((z for z, (a, b) in H.items() if a == 0 and b > 5900 and ST[0][2] - 450 < z < ST[0][2] - 50), reverse=True)
|
||||
c15s = [(z, a, b) for z, (a, b) in H.items() if a > 3000]
|
||||
c16s = [(z, a, b) for z, (a, b) in H.items() if a == 0 and 3500 < b < 4500]
|
||||
if len(tiers) < 3 or not c15s or not c16s:
|
||||
raise FrameError("코핑 상단 철근 단·C15·C16 선을 찾지 못했습니다")
|
||||
c15, c16 = c15s[0], max(c16s)
|
||||
for t, z in zip(("1", "2", "3"), tiers[:3]):
|
||||
put(P, "params", f"coping_rebar.top.tiers.{t}.z", z, "D1 철근 수평선")
|
||||
put(P, "params", "coping_rebar.c15.z", c15[0], "D1 철근 수평선")
|
||||
put(P, "params", "coping_rebar.c15.x0", c15[1], "D1 철근 수평선")
|
||||
put(P, "params", "coping_rebar.c15.x1", c15[2], "D1 철근 수평선")
|
||||
put(P, "params", "coping_rebar.c16.z", c16[0], "D1 철근 수평선")
|
||||
put(P, "params", "coping_rebar.c16.x1", c16[2], "D1 철근 수평선")
|
||||
put(P, "params", "coping_rebar.bottom.z_flat", Z_FLAT + 100, "아랫면 평평부 + 100")
|
||||
n_total = len(P["coping_rebar"]["loops"]["levels"])
|
||||
cand = sorted((z for z, (a, b) in H.items() if a == 0 and z < tiers[-1] - 50 and z != c16[0]), reverse=True)
|
||||
chain_ = [cand[0]] if cand else []
|
||||
for z in cand[1:]:
|
||||
if abs(chain_[-1] - z - 250) <= 1.5:
|
||||
chain_.append(z)
|
||||
loops = sorted(chain_)
|
||||
lvs = None
|
||||
if len(loops) >= 4:
|
||||
lvs = [loops[0] - 250 * k for k in range(max(n_total - len(loops), 0), 0, -1)] + loops # lower levels lie below the plotted band: continue at 250
|
||||
put(P, "params", "coping_rebar.loops.levels", lvs, "D1 루프 수평선 @250(아래쪽은 이어서 계산)")
|
||||
else:
|
||||
notes.append("코핑 루프 선을 4개 미만 찾아 그대로 두었습니다")
|
||||
put(P, "params", "coping_rebar.stirrups.top_right", [[0, c16[0]], [3900, c15[0]]], "스터럽 윗면 = C16 / C15 높이")
|
||||
put(P, "params", "coping_rebar.stirrups.bottom", f"z={Z_FLAT + 100:,} for |X|<=1,073.9 else circle R 12,100 about (+-6,951.9, {CZ_REL:,})", "아랫면 규칙 문구")
|
||||
allc = set()
|
||||
for e in MS:
|
||||
if e.dxftype() == "CIRCLE" and e.dxf.layer == "CS-RBAR-MAJR" and abs(e.dxf.radius - 27) < .1 and CX - 6000 < e.dxf.center.x < 18500 and 15500 < e.dxf.center.y < 20300:
|
||||
allc.add((round(e.dxf.center.x - CX), round(e.dxf.center.y + OFF, 1)))
|
||||
circ = None # filled below, once dz (the pier's height shift) is known
|
||||
dz = ST[0][2] - ref["coping"]["top_steps"][0]["z"]
|
||||
# C14 points are READ from the drawn circles (x as drawn: the x offsets differ per pier -- 13/913/... in P1, P3, P4 but 0/900/... in P2).
|
||||
# Right-half circles are mirrored to the left; the centre point (|x| <= 20) is kept once; a circle that sits where the template has a point
|
||||
# WITHOUT a mirror image (P1: the point near x 2913 that is missing on the left because the cover would be below 40 mm) stays right-only.
|
||||
cpts = ref["coping_rebar"]["c14"]["points"]
|
||||
tset = {(round(x), round(z, 1)) for x, z in cpts}
|
||||
right_only = [(x, z + dz) for x, z in cpts if x > 20 and (-x, round(z, 1)) not in tset]
|
||||
levels = {z + dz for x, z in cpts if (x, z + dz) not in right_only}
|
||||
# keep the circles on the template's C14 heights (+ the pier's shift, 5 mm): the column hoop dots (x about +-902, every 120 / 300 -- one lies 23 mm from a
|
||||
# C14 height) and other dots are left out; in P2 a C14 circle (x = 900) is only 2 mm from a hoop dot in x, so the height has to tell them apart.
|
||||
# The right-only point of the template may sit up to 60 mm off its shifted height (P4: 18 mm).
|
||||
circ = {(x, z) for x, z in allc if x >= -20 and (any(abs(z - lz) <= 5 for lz in levels) or any(abs(x - rx) <= 20 and abs(z - rz) <= 60 for rx, rz in right_only))}
|
||||
pts = []
|
||||
for x, z in sorted(circ):
|
||||
if abs(x) <= 20 or any(abs(x - rx) <= 20 and abs(z - rz) <= 60 for rx, rz in right_only):
|
||||
pts.append([x, z])
|
||||
else:
|
||||
pts += [[x, z], [-x, z]]
|
||||
if len(pts) != len(cpts):
|
||||
notes.append(f"C14 점: 도면에서 읽은 {len(pts)}개 ≠ 템플릿 {len(cpts)}개 — 눈으로 확인하세요")
|
||||
put(P, "params", "coping_rebar.c14.points", pts, "도면 정면도의 철근 원(우측 반을 좌우 대칭, 가운데 점은 한 번)")
|
||||
hup = []
|
||||
for tx, tz in ref["coping_rebar"]["c14"]["hooks_up"]:
|
||||
near = [p for p in pts if (p[0] > 0) == (tx > 0) and abs(abs(p[0]) - abs(tx)) <= 20 and abs(p[1] - (tz + dz)) <= 60]
|
||||
if near:
|
||||
hup.append(min(near, key=lambda p: abs(p[1] - (tz + dz))))
|
||||
else:
|
||||
notes.append(f"갈고리 위 지정 점 ({tx}, {tz})에 해당하는 C14 점을 찾지 못했습니다 — 눈으로 확인하세요")
|
||||
put(P, "params", "coping_rebar.c14.hooks_up", [list(p) for p in hup], "하면 곡선을 뚫는 점만 갈고리 위로(점 좌표로 지정 — c14.points와 일치해야 함)")
|
||||
|
||||
# ================= footing / lean / piles / column
|
||||
EL_F = [_num(t["text"]) * 1000 for t in texts if t["dwg"] == "D1" and t["text"].startswith("EL. 2") and 17000 < t["x"] < 18500 and 7000 < t["y"] < 8200]
|
||||
fx = [_num(t["text"]) * 1000 for t in texts if t["dwg"] == "D1" and 11000 < t["x"] < 12500 and 3500 < t["y"] < 4000 and re.fullmatch(r"\d+\.\d{3}", t["text"])]
|
||||
fy = [_num(t["text"]) * 1000 for t in texts if t["dwg"] == "D1" and 29000 < t["x"] < 30500 and 3500 < t["y"] < 4000 and re.fullmatch(r"\d+\.\d{3}", t["text"])]
|
||||
pl = [t["text"] for t in texts if t["dwg"] == "D1" and t["text"].startswith("L=") and t["x"] < 10000 and t["y"] < 7000]
|
||||
chx = [t["text"] for t in texts if t["dwg"] == "D1" and "@1." in t["text"] and 10000 < t["x"] < 13000 and 5500 < t["y"] < 6600]
|
||||
chy = [t["text"] for t in texts if t["dwg"] == "D1" and "@1." in t["text"] and 28000 < t["x"] < 31000 and 5500 < t["y"] < 6600]
|
||||
if not (EL_F and fx and fy and pl and chx and chy):
|
||||
raise FrameError("기초 크기·하면 EL·말뚝 길이·말뚝 사슬 글자를 찾지 못했습니다")
|
||||
chx, chy = chx[0], chy[0]
|
||||
SX, SY, ZF = round(fx[0]), round(fy[0]), round(EL_F[0])
|
||||
TH = P["footing"]["thickness"]
|
||||
put(P, "params", "footing.size_x", float(SX), "D1 기초 치수")
|
||||
put(P, "params", "footing.size_y", float(SY), "D1 기초 치수")
|
||||
put(P, "params", "footing.z_bottom", ZF, "D1 EL 글자")
|
||||
put(P, "params", "lean_concrete.size_x", float(SX + 200), "기초 + 200")
|
||||
put(P, "params", "lean_concrete.size_y", float(SY + 200), "기초 + 200")
|
||||
put(P, "params", "lean_concrete.z_bottom", ZF - 100, "기초 하면 − 100")
|
||||
put(P, "params", "lean_concrete.z_top", ZF, "= 기초 하면")
|
||||
COL_H = Z_FLAT - ZF - TH
|
||||
put(P, "params", "column.height", COL_H, "코핑 하면 − (기초 하면 + 두께) (D1 높이 글자와 같음)")
|
||||
put(P, "params", "piles.length", round(_num(pl[0]) * 1000), "D1 말뚝 길이 글자")
|
||||
put(P, "params", "piles.count_x", int(chx.split("@")[0]) + 1, "D1 말뚝 사슬")
|
||||
put(P, "params", "piles.pitch_x", float(round(_num(chx.split("@")[1]) * 1000)), "D1 말뚝 사슬")
|
||||
put(P, "params", "piles.count_y", int(chy.split("@")[0]) + 1, "D1 말뚝 사슬")
|
||||
put(P, "params", "piles.pitch_y", float(round(_num(chy.split("@")[1]) * 1000)), "D1 말뚝 사슬")
|
||||
|
||||
# ================= bearings
|
||||
pads = []
|
||||
for e in MS:
|
||||
if e.dxftype() == "LWPOLYLINE" and e.dxf.layer == "CS-CONC-MAJR" and len(e) == 4:
|
||||
q = [(v[0], v[1]) for v in e.get_points()]
|
||||
if not (5000 < q[0][0] < 20000 and 23000 < q[0][1] < 29500):
|
||||
continue
|
||||
s = [math.dist(q[i], q[(i + 1) % 4]) for i in range(4)]
|
||||
if abs(s[0] - s[2]) < 1 and abs(s[1] - s[3]) < 1 and 780 < s[0] < 920 and 700 < s[1] < 860:
|
||||
pads.append((sum(v[0] for v in q) / 4 - DATUM_X, sum(v[1] for v in q) / 4 - DATUM_Y, math.degrees(math.atan2(q[1][1] - q[0][1], q[1][0] - q[0][0])) % 180))
|
||||
seen, uniq = set(), []
|
||||
for a, b, c in sorted(pads):
|
||||
k = (round(a / 3), round(b / 3))
|
||||
if k not in seen:
|
||||
seen.add(k)
|
||||
uniq.append((round(a, 1), round(b, 1), round(90 - c, 2)))
|
||||
fr = sorted(p for p in uniq if p[1] < 0)
|
||||
rr = sorted(p for p in uniq if p[1] > 0)
|
||||
if len(fr) < 2 or not rr:
|
||||
raise FrameError("평면도에서 받침 패드(앞·뒤 열)를 찾지 못했습니다")
|
||||
g5f, g5r = fr[-1], rr[-1]
|
||||
PITCH = round(g5f[0] - fr[-2][0], 1)
|
||||
put(P, "params", "bearing_seats.pitch_x", PITCH, "평면도 패드 g4 → g5")
|
||||
put(P, "params", "bearing_seats.pad_rows.front.centre_rel_girder5", [g5f[0], g5f[1]], "D1 평면도 패드 폴리라인")
|
||||
put(P, "params", "bearing_seats.pad_rows.front.rotation_deg", g5f[2], "패드 폴리라인 각도")
|
||||
put(P, "params", "bearing_seats.pad_rows.rear.centre_rel_girder5", [g5r[0], g5r[1]], "D1 평면도 패드 폴리라인")
|
||||
put(P, "params", "bearing_seats.pad_rows.rear.rotation_deg", g5r[2], "패드 폴리라인 각도")
|
||||
for i, b in enumerate(P["bearing_seats"]["bearings"]):
|
||||
ref_ = g5f if b["row"] == "front" else g5r
|
||||
put(P, "params", f"bearing_seats.bearings.{i}.centre", [round(ref_[0] - (5 - b["girder_line"]) * PITCH, 1), ref_[1]], "g4·g5는 도면, g1~g3은 피치로 계산")
|
||||
put(P, "params", f"bearing_seats.bearings.{i}.rotation_deg", ref_[2], "패드 각도")
|
||||
ped = []
|
||||
for e in MS:
|
||||
if e.dxftype() == "LINE" and e.dxf.layer == "CS-CONC-MAJR":
|
||||
s, t = e.dxf.start, e.dxf.end
|
||||
ln = math.hypot(t.x - s.x, t.y - s.y)
|
||||
if 5000 < min(s.x, t.x) and max(s.x, t.x) < 20000 and 23000 < min(s.y, t.y) and max(s.y, t.y) < 29500 and (abs(ln - 2500) < 5 or abs(ln - 1200) < 5):
|
||||
ped.append(((s.x - DATUM_X, s.y - DATUM_Y), (t.x - DATUM_X, t.y - DATUM_Y), round(ln)))
|
||||
ped5 = [p for p in ped if min(p[0][0], p[1][0]) > g5f[0] - 800 and max(p[0][0], p[1][0]) < g5f[0] + 800]
|
||||
if len(ped5) >= 5:
|
||||
longs = sorted([p for p in ped5 if p[2] == 2500], key=lambda p: p[0][0])
|
||||
shorts = sorted([p for p in ped5 if p[2] == 1200], key=lambda p: (p[0][1] + p[1][1]))
|
||||
|
||||
def lo_hi(l):
|
||||
a, b = l[0], l[1]
|
||||
return (a, b) if a[1] < b[1] else (b, a)
|
||||
(l_lo, l_hi), (r_lo, r_hi) = lo_hi(longs[0]), lo_hi(longs[-1])
|
||||
m0, m1 = sorted([shorts[1][0], shorts[1][1]])
|
||||
r1 = lambda v: [round(v[0], 1), round(v[1], 1)] # noqa: E731
|
||||
put(P, "params", "bearing_seats.pedestal_girder5.front_half", [r1(l_lo), r1(m0), r1(m1), r1(r_lo)], "D1 평면도 대좌 외곽")
|
||||
put(P, "params", "bearing_seats.pedestal_girder5.rear_half", [r1(m0), r1(l_hi), r1(r_hi), r1(m1)], "D1 평면도 대좌 외곽")
|
||||
put(P, "params", "bearing_seats.pedestal_girder5.rotation_deg", g5f[2], "대좌 각도 = 패드 각도")
|
||||
else:
|
||||
notes.append("g5 대좌 외곽(2500 × 1200)을 찾지 못해 그대로 두었습니다")
|
||||
|
||||
# ================= schedule tables of the pier itself
|
||||
rows, dia_of = {}, {}
|
||||
for c in reader.table_candidates(texts):
|
||||
if c["key"].startswith("schedule"):
|
||||
last = None # '"' in the diameter column = the row above (table order, not mark order)
|
||||
for r in c["rows"]:
|
||||
tx = [x["text"] for x in sorted(r["cells"], key=lambda z: z["x"])]
|
||||
if len(tx) > 2 and tx[2].strip() != '"' and re.fullmatch(r"[SHD]\d+", tx[2].strip()):
|
||||
last = tx[2].strip()
|
||||
if tx and re.fullmatch(r"[BPC]\d+|S\d+", tx[0].strip()):
|
||||
rows[tx[0].strip()] = tx
|
||||
if last:
|
||||
dia_of[tx[0].strip()] = int(re.search(r"\d+", last).group())
|
||||
need = [f"{p}{i}" for p, n in (("P", 6), ("B", 12)) for i in range(1, n + 1)] + ["C16"] + [f"C{i}" for i in range(1, 7)]
|
||||
miss = [m for m in need if m not in rows]
|
||||
if miss:
|
||||
raise FrameError("철근 재료표에서 다음 마크를 읽지 못했습니다: " + ", ".join(miss))
|
||||
for m in ("P1", "P2", "P3", "P4", "P5", "P6"):
|
||||
tx = rows[m]
|
||||
vals = tx[4:4 + len(ORDER[tx[1]].split())]
|
||||
new = dict(P["column_rebar"]["schedule"][m])
|
||||
new["n"] = tx[3]
|
||||
for f, v in zip(ORDER[tx[1]].split(), vals):
|
||||
new[f] = v
|
||||
if tx[1] == "XVI":
|
||||
new["L"] = vals[0]
|
||||
put(P, "params", f"column_rebar.schedule.{m}", new, "이 교각의 재료표 행")
|
||||
for m in [f"B{i}" for i in range(1, 13)]:
|
||||
put(P, "params", f"footing_rebar.schedule.{m}", rows[m], "이 교각의 재료표 행")
|
||||
n_hoop = {m: int(rows[m][3]) for m in ("P3", "P4", "P5")}
|
||||
n_tie_levels = int(rows["P6"][3]) // 4
|
||||
for t, ms in (("1", ("C1", "C4")), ("2", ("C2", "C5")), ("3", ("C3", "C6"))):
|
||||
put(P, "params", f"coping_rebar.top.tiers.{t}.a", round(_num(rows[ms[0]][4]) * 1000), "재료표 a")
|
||||
put(P, "params", "coping_rebar.c16.x0", c16[2] - round(_num(rows["C16"][4]) * 1000), "x1 − 재료표 C16 a")
|
||||
|
||||
# ================= footing rebar
|
||||
rule = footing_rule.infer(SX, SY)
|
||||
put(P, "params", "footing_rebar.a_x", rule["a_x"], "기초 − 200")
|
||||
put(P, "params", "footing_rebar.a_y", rule["a_y"], "기초 − 200")
|
||||
for k in ("bottom_Y1", "bottom_X2", "bottom_Y3", "bottom_X4", "top_Y", "top_X"):
|
||||
put(P, "params", f"footing_rebar.layers.{k}.pos", [float(v) for v in rule[k]], "철근 층 규칙")
|
||||
cnt = {"bottom_Y1": ("B2", "B4"), "bottom_X2": ("B6", "B8"), "bottom_Y3": ("B3",), "bottom_X4": ("B7",), "top_Y": ("B1",), "top_X": ("B5",)}
|
||||
for k, ms in cnt.items():
|
||||
sch = sum(int(rows[m][3]) for m in ms)
|
||||
if sch != len(rule[k]):
|
||||
notes.append(f"기초 철근 {k}: 규칙 계산 {len(rule[k])}본 ≠ 재료표 {sch}본")
|
||||
gx = [-(SX - 1100) / 2 + 250 * i for i in range(int((SX - 1100) // 250) + 1)]
|
||||
gy = [-(SY - 800) / 2 + 250 * i for i in range(int((SY - 800) // 250) + 1)]
|
||||
put(P, "params", "footing_rebar.b9.grid_x", [int(v) for v in gx], "B9 격자 규칙(x 폭 = 기초 − 1100)")
|
||||
put(P, "params", "footing_rebar.b9.grid_y", [int(v) for v in gy], "B9 격자 규칙(y 폭 = 기초 − 800)")
|
||||
B9N = int(rows["B9"][3]) if len(rows["B9"]) >= 9 else round(_num(rows["B9"][-1]) / _num(rows["B9"][-2]))
|
||||
B9G = (len(gx) * len(gy) + 1) // 2 # the model places a checkerboard of the grid
|
||||
put(P, "params", "footing_rebar.b9.count", B9G, "B9 격자의 체스판 개수(모델이 놓는 개수)")
|
||||
if B9G != B9N:
|
||||
notes.append(f"B9: 격자 {len(gx)} × {len(gy)} ÷ 2 = {B9G}본 ≠ 재료표 {B9N}본 — 작도를 채택하려면 계획된 FAIL(G3 B9 개수) 등록이 필요합니다")
|
||||
put(P, "params", "footing_rebar.b10.size_x", float(SX - 152), "B10 = 기초 − 152")
|
||||
put(P, "params", "footing_rebar.b10.size_y", float(SY - 156), "B10 = 기초 − 156")
|
||||
mid = {}
|
||||
for tag, (xmin, xmax) in (("mid_Y", (CX - 5, 18500)), ("mid_X", (23000, 37000))):
|
||||
lv = {}
|
||||
for e in MS:
|
||||
if e.dxftype() == "CIRCLE" and xmin < e.dxf.center.x < xmax:
|
||||
for l in (550, 950, 1350, 1750):
|
||||
if abs(e.dxf.center.y - (8115.6 + l)) < 1:
|
||||
lv.setdefault(float(l), []).append(round(e.dxf.center.x, 1))
|
||||
mid[tag] = lv
|
||||
|
||||
def sym(xs, centre, edge_hoop):
|
||||
r = sorted({round(abs(x - centre)) for x in xs})
|
||||
r = [v for v in r if (abs(v - edge_hoop) > 12 and v > 0) or v == 0]
|
||||
return sorted([-v for v in r if v] + [v for v in r])
|
||||
if all(len(mid[t]) == 4 for t in mid):
|
||||
put(P, "params", "footing_rebar.layers.mid_Y.levels", {str(k): [float(v) for v in sym(xs, CX, round(SX - 152) / 2)] for k, xs in sorted(mid["mid_Y"].items())}, "D1 정면도 철근 점(B12, 우측 반 → 좌우 대칭)")
|
||||
put(P, "params", "footing_rebar.layers.mid_X.levels", {str(k): [float(v) for v in sym(xs, SEC_CENTRE_X, round(SY - 156) / 2)] for k, xs in sorted(mid["mid_X"].items())}, "D1 단면도 철근 점(B11, 우측 반 → 좌우 대칭)")
|
||||
for tag, mk in (("mid_Y", "B12"), ("mid_X", "B11")):
|
||||
drawn = sum(len(v) for v in P["footing_rebar"]["layers"][tag]["levels"].values())
|
||||
if drawn != int(rows[mk][3]):
|
||||
notes.append(f"{mk}: 도면 점 {drawn}본 ≠ 재료표 {rows[mk][3]}본 (작도를 채택하려면 계획된 FAIL 등록이 필요합니다)")
|
||||
else:
|
||||
notes.append("기초 중간 층 B11·B12 점을 네 단 모두에서 찾지 못해 그대로 두었습니다")
|
||||
|
||||
cc = {"bottom_Y1": len(rule["bottom_Y1"]), "bottom_X2": len(rule["bottom_X2"]), "bottom_Y3": len(rule["bottom_Y3"]), "bottom_X4": len(rule["bottom_X4"]),
|
||||
"top_Y": len(rule["top_Y"]), "top_X": len(rule["top_X"])}
|
||||
for tag in ("mid_X", "mid_Y"):
|
||||
cc[tag] = sum(len(v) for v in P["footing_rebar"]["layers"][tag]["levels"].values())
|
||||
if isinstance(P["footing_rebar"].get("counts_check"), dict):
|
||||
put(P, "params", "footing_rebar.counts_check", {k: cc.get(k, v) for k, v in P["footing_rebar"]["counts_check"].items()}, "층별 철근 개수(위 규칙·도면 점)")
|
||||
|
||||
# ================= column rebar
|
||||
put(P, "params", "column_rebar.main_bars.p1.hook_leg_z", ZF + 300, "기초 하면 + 300")
|
||||
put(P, "params", "column_rebar.main_bars.p1.straight_from", ZF + 640, "+ 굽힘 R340")
|
||||
put(P, "params", "column_rebar.main_bars.p1.straight_len", round(_num(rows["P1"][4]) * 1000), "재료표 P1 A")
|
||||
put(P, "params", "column_rebar.main_bars.p2.z0", ZF + 300, "기초 하면 + 300")
|
||||
put(P, "params", "column_rebar.main_bars.p2.length", round(_num(rows["P2"][4]) * 1000), "재료표 P2")
|
||||
lv = [ZF + 1180 + 120 * i for i in range(n_hoop["P4"] + n_hoop["P5"])]
|
||||
lv += [lv[-1] + 300 * (i + 1) for i in range(n_hoop["P3"])]
|
||||
put(P, "params", "column_rebar.hoops.levels", lv, "첫 단 = 기초 하면 + 1180, 120 / 300 사슬(재료표 개수)")
|
||||
put(P, "params", "column_rebar.hoops.marks", ["P4"] * n_hoop["P4"] + ["P5"] * n_hoop["P5"] + ["P3"] * n_hoop["P3"], "재료표 개수")
|
||||
put(P, "params", "column_rebar.hoops.radius", round(_num(rows["P4"][6])), "재료표 P4·P5 R")
|
||||
put(P, "params", "column_rebar.hoops.radius_by_mark", {"P3": round(_num(rows["P3"][6]))}, "재료표 P3 R")
|
||||
put(P, "params", "column_rebar.ties.levels", [ZF + 1180 + 1800 * i for i in range(n_tie_levels)], "사슬 시작 = 기초 하면 + 1180, 단 수 = 재료표 P6 ÷ 4")
|
||||
|
||||
# ================= expected
|
||||
outline = [[-1100, Z_FLAT], [1100, Z_FLAT], [6200, tip_z], [6000, ST[4][2]], [ST[4][0], ST[4][2]], [ST[4][0], ST[3][2]], [ST[3][0], ST[3][2]], [ST[3][0], ST[2][2]],
|
||||
[ST[2][0], ST[2][2]], [ST[2][0], ST[1][2]], [ST[1][0], ST[1][2]], [ST[1][0], ST[0][2]], [-6000, ST[0][2]], [-6200, tip_z]]
|
||||
put(E, "expected", "G1.coping_outline", outline, "코핑 계단 + 아랫면 평평부")
|
||||
put(E, "expected", "G1.dxf_arc_centres.datum_el", Z_FLAT, "코핑 아랫면 평평부")
|
||||
put(E, "expected", "G1.footing.dx", SX, "D1")
|
||||
put(E, "expected", "G1.footing.dy", SY, "D1")
|
||||
put(E, "expected", "G1.footing.z0", ZF, "D1 EL")
|
||||
put(E, "expected", "G1.column.height", COL_H, "D1")
|
||||
put(E, "expected", "G1.coping.z_top_left", ST[0][2], "코핑 계단")
|
||||
put(E, "expected", "G1.coping.z_top_center", ST[2][2], "코핑 계단")
|
||||
put(E, "expected", "G1.coping.z_top_right", ST[4][2], "코핑 계단")
|
||||
put(E, "expected", "G1.piles.nx", int(chx.split("@")[0]) + 1, "D1")
|
||||
put(E, "expected", "G1.piles.ny", int(chy.split("@")[0]) + 1, "D1")
|
||||
put(E, "expected", "G1.piles.pitch_x", round(_num(chx.split("@")[1]) * 1000), "D1")
|
||||
put(E, "expected", "G1.piles.pitch_y", round(_num(chy.split("@")[1]) * 1000), "D1")
|
||||
put(E, "expected", "G1.piles.length", round(_num(pl[0]) * 1000), "D1")
|
||||
put(E, "expected", "G1.seats.step_top", {str(i + 1): ST[i][2] for i in range(5)}, "코핑 계단")
|
||||
put(E, "expected", "G1.seats.step_x", {str(i + 1): [ST[i][0], ST[i][1]] for i in range(5)}, "코핑 계단")
|
||||
put(E, "expected", "G1.seats.row_y", {"front": round(g5f[1]), "rear": round(g5r[1])}, "평면도 패드 폴리라인")
|
||||
chain = [t["text"] for t in texts if t["dwg"] == "D1" and re.fullmatch(r"3@2\.\d+=\d\.\d+", t["text"])]
|
||||
if chain:
|
||||
put(E, "expected", "G1.seats.pitch_chain", [3, round(_num(chain[0].split("=")[1]) * 1000)], "D1 3@피치=합계")
|
||||
put(E, "expected", "G2.hoop_chain", {"start": [150, 1030], "groups": [[n_hoop["P4"] - 1, 120], [n_hoop["P5"], 120], [n_hoop["P3"], 300]]}, "재료표 개수")
|
||||
put(E, "expected", "G2.hoop_marks_by_zone", [["P4", n_hoop["P4"]], ["P5", n_hoop["P5"]], ["P3", n_hoop["P3"]]], "재료표 개수")
|
||||
put(E, "expected", "G2.tie_chain", {"start": [150, 1030], "groups": [[n_tie_levels - 1, 1800]]}, "재료표 P6 ÷ 4 − 1 구간")
|
||||
put(E, "expected", "G2.r_hoop", round(_num(rows["P4"][6])), "재료표 P4·P5 R")
|
||||
put(E, "expected", "G2.p1_top_parts", [300, 340, round(_num(rows["P1"][4]) * 1000)], "300 + 340 + 재료표 P1 A")
|
||||
hx, hy = (SX - 200) / 2, (SY - 200) / 2
|
||||
put(E, "expected", "G3.rows", {"Y_BOT": {"ends": [-int(hx), int(hx)], "start": -int(hx - 200), "pitch": 125, "n": len([v for v in rule["bottom_Y1"] if abs(v) < hx - 200 + 1])},
|
||||
"Y_250": {"ends": [-int(hx), int(hx)], "start": -int(hx - 200), "pitch": 250, "n": len([v for v in rule["bottom_Y3"] if abs(v) < hx - 1])},
|
||||
"X_BOT": {"ends": [-int(hy), int(hy)], "start": -int(hy - 175), "pitch": 125, "n": len([v for v in rule["bottom_X2"] if abs(v) < hy - 1])},
|
||||
"X_250": {"ends": [-int(hy), int(hy)], "start": -int(hy - 175), "pitch": 250, "n": len([v for v in rule["bottom_X4"] if abs(v) < hy - 1])}}, "기초 철근 규칙")
|
||||
put(E, "expected", "G3.b9_grid", {"x": {"start": int(gx[0]), "pitch": 250, "n": len(gx)}, "y": {"start": int(gy[0]), "pitch": 250, "n": len(gy)}}, "B9 격자")
|
||||
put(E, "expected", "G3.b10_size", [SX - 152, SY - 156], "B10")
|
||||
put(E, "expected", "G4.tier_z", {"C1": tiers[0], "C4": tiers[0], "C2": tiers[1], "C5": tiers[1], "C3": tiers[2], "C6": tiers[2]}, "D1 철근 수평선")
|
||||
put(E, "expected", "G4.c10_bottom", Z_FLAT + 100, "아랫면 평평부 + 100")
|
||||
if lvs:
|
||||
put(E, "expected", "G4.loop_z", {"C12": {"start": lvs[0], "pitch": 250, "n": 6}, "C13": lvs[6:]}, "D1 루프 수평선")
|
||||
put(E, "expected", "G4.c15_top", c15[0], "D1 철근 수평선")
|
||||
put(E, "expected", "G4.c16_top", c16[0], "D1 철근 수평선")
|
||||
lev = {}
|
||||
for x, z in P["coping_rebar"]["c14"]["points"]:
|
||||
lev[z] = lev.get(z, 0) + 1
|
||||
put(E, "expected", "G4.c14_levels", [[z, n] for z, n in sorted(lev.items(), key=lambda kv: -kv[0])], "c14 점을 높이별로")
|
||||
put(E, "expected", "G4.step_top", {str(i + 1): ST[i][2] for i in range(5)}, "코핑 계단")
|
||||
# bar diameters from the schedule ('"' = same as the row above, in the order of the marks), ranges of the stirrup / loop lengths
|
||||
dia = dia_of
|
||||
for gate, marks in (("G2", [f"P{i}" for i in range(1, 7)]), ("G3", [f"B{i}" for i in range(1, 13)]), ("G4", [f"C{i}" for i in range(1, 17)] + ["S1", "S2"])):
|
||||
cur = E.get(gate, {}).get("dia")
|
||||
if isinstance(cur, dict):
|
||||
put(E, "expected", f"{gate}.dia", {m: dia.get(m, v) for m, v in cur.items()}, "이 교각의 재료표 직경")
|
||||
d2 = E.get("G2", {}).get("dia")
|
||||
if isinstance(d2, dict) and "P4" in d2 and "P6" in d2:
|
||||
put(E, "expected", "G2.tie_offset", (d2["P4"] + d2["P6"]) / 2, "크로스타이 높이 = 띠철근 높이 + (P4 직경 + P6 직경) / 2 (P1~P4 5개 값과 일치)")
|
||||
|
||||
def rng(txt):
|
||||
v = [round(_num(x) * 1000) for x in re.findall(r"\d+\.\d+", txt)]
|
||||
return v if len(v) == 2 else (v + v if len(v) == 1 else None)
|
||||
cur = E["G4"].get("range")
|
||||
if isinstance(cur, dict):
|
||||
new_r = dict(cur)
|
||||
for m, col, kind in (("C9", 5, "b_sched"), ("C10", 5, "b_sched"), ("C11", 5, "b_sched"), ("C12", 4, "a_sched"), ("C13", 4, "a_sched")):
|
||||
if m in rows and len(rows[m]) > col and rng(rows[m][col]):
|
||||
new_r[m] = [kind] + rng(rows[m][col])
|
||||
put(E, "expected", "G4.range", new_r, "재료표 C9~C13의 b·a 범위")
|
||||
tl, tl_why = _top_left(P, (E["G4"].get("range") or {}).get("C9"), ST, c16[0])
|
||||
put(P, "params", "coping_rebar.stirrups.top_left", tl, tl_why)
|
||||
pid = pier_name.replace("운곡교", "")
|
||||
old_pid = P.get("pier_id")
|
||||
if old_pid and old_pid != pid:
|
||||
put(P, "params", "display.hidden_ids", [h.replace(old_pid + "-", pid + "-", 1) for h in P["display"].get("hidden_ids", [])], "모델 객체 이름에 교각 식별자가 들어 있음")
|
||||
put(E, "expected", "pier_id", pid, "교각 이름")
|
||||
put(P, "params", "pier_id", pid, "교각 이름")
|
||||
return P, E, changes, notes
|
||||
|
||||
|
||||
TABLE_GROUP = "표 값(받침·솔플레이트·재료표)"
|
||||
|
||||
|
||||
def table_changes(P, E, texts):
|
||||
"""Changes that bring params / expected in line with the tables the app recognises (ELEVATION / 솔플레이트 / 철근재료표): one per proposal row that
|
||||
differs (engine.reader.proposals). Computed on the already-read P / E so only what the reading left different is proposed."""
|
||||
rows = reader.proposals(reader.field_candidates(reader.table_candidates(texts)), P, E) if texts else []
|
||||
return [dict(file=r["file"], path=".".join(str(x) for x in r["path"]), old=r["current"], new=r["candidate"], why=r["src"], inferred=False, group=TABLE_GROUP, row=r)
|
||||
for r in rows if not r["same"]]
|
||||
|
||||
|
||||
ANCHOR_FIELDS = ("expected:G1.seats.rear_first_from_tip",) # per-pier dimensions taken from an anchor candidate (A / B grade) when the reading left the template's value
|
||||
PENDING = "읽기 도구 제안 — 사용자 승인 필요"
|
||||
|
||||
|
||||
def _get(doc, path):
|
||||
x = doc
|
||||
for k in path.split("."):
|
||||
x = x[int(k)] if isinstance(x, list) else x.get(k) if isinstance(x, dict) else None
|
||||
if x is None:
|
||||
return None
|
||||
return x
|
||||
|
||||
|
||||
def extra_changes(P, E, texts, anchors_doc=None, own_sources=()):
|
||||
"""Proposals made on top of read() + table_changes() (2026-10-20, from the P4 rebuild):
|
||||
1. anchor candidates for the per-pier dimensions in ANCHOR_FIELDS (e.g. the rear pedestal's first distance from the tip) when they differ from the current value;
|
||||
2. planned FAIL entries the person would have to add by hand: a drawn bar count that differs from the schedule (B11 / B12, the drawing is followed),
|
||||
and a schedule length L that does not follow the (max+min)/2 convention (C9~C13). One change holding the whole list; it is never pre-selected
|
||||
(the approval of a planned FAIL stays with the person) and its entries carry `approved` = PENDING."""
|
||||
out = []
|
||||
if anchors_doc and texts:
|
||||
cand = anchors.follow(anchors_doc, texts, own_sources)
|
||||
for f in ANCHOR_FIELDS:
|
||||
c = cand.get(f)
|
||||
which, path = f.split(":", 1)
|
||||
cur = _get(E if which == "expected" else P, path)
|
||||
if c and c["grade"] in ("A", "B") and not c["self_source"] and cur is not None and not same(cur, c["value"], 0.0005):
|
||||
out.append(dict(file=which, path=path, old=cur, new=c["value"], inferred=False,
|
||||
why=f"앵커 후보 {c['grade']}등급 — 도면 글자 '{c['text']}' ({c['dwg']} {c['x']:.0f}, {c['y']:.0f})",
|
||||
group=f"기대값({which}) " + path.split(".")[0]))
|
||||
P2, E2 = copy.deepcopy(P), copy.deepcopy(E)
|
||||
if texts:
|
||||
rows = reader.proposals(reader.field_candidates(reader.table_candidates(texts)), P2, E2)
|
||||
reader.apply_proposals([r for r in rows if not r["same"]], P2, E2)
|
||||
want = [] # (gate, item, reason, numbers the reason must contain)
|
||||
layers = (P2.get("footing_rebar") or {}).get("layers") or {}
|
||||
for tag, mark in (("mid_X", "B11"), ("mid_Y", "B12")):
|
||||
lv, sch = (layers.get(tag) or {}).get("levels"), ((E2.get("G3") or {}).get("schedule") or {}).get(mark)
|
||||
if isinstance(lv, dict) and isinstance(sch, list) and len(sch) == 2:
|
||||
got, exp = sum(len(v) for v in lv.values()), sch[1]
|
||||
if got != exp:
|
||||
want.append(("G3", f"{mark} bar count vs schedule", f"작도 {got}본 채택(좌우 대칭) vs D3 {exp}본 — 설계자 확인 대기", (f"{got}본", f"{exp}본")))
|
||||
b9, b9n = (P2.get("footing_rebar") or {}).get("b9") or {}, (((E2.get("G3") or {}).get("schedule") or {}).get("B9") or [None, None])[1]
|
||||
if b9.get("count") is not None and b9n is not None and b9["count"] != b9n:
|
||||
gx, gy = b9.get("grid_x") or [], b9.get("grid_y") or []
|
||||
want.append(("G3", "B9 bar count vs schedule", f"작도 {b9['count']}본({len(gx)}×{len(gy)} 엇갈림) 채택 vs D3 {b9n}본 — 설계자 확인 대기", (f"{b9['count']}본", f"{b9n}본")))
|
||||
G4 = E2.get("G4") or {}
|
||||
conv, Ls = G4.get("range_convention"), G4.get("L") or {}
|
||||
for m, v in (G4.get("range") or {}).items():
|
||||
if conv and isinstance(Ls, dict) and Ls.get(m) is not None and v[0] in conv:
|
||||
c = conv[v[0]]
|
||||
avg = 2 * (c["a"] if v[0] == "b_sched" else c["b"]) + (v[1] + v[2]) + c["c_times"] * c["c"]
|
||||
if abs(avg - Ls[m]) > 1.0:
|
||||
want.append(("G4", f"{m} schedule L reproduced by convention (max+min)/2",
|
||||
f"재료표 {m} L {Ls[m] / 1000:.3f}이 규칙 (max+min)/2 = {avg / 1000:.3f}과 {abs(avg - Ls[m]):.0f} mm 다름 — 재료표 자체 불일치, 설계자 확인 대기", ()))
|
||||
pf, notes = copy.deepcopy(E.get("planned_fail", [])), []
|
||||
for gate, item, reason, nums in want:
|
||||
i = next((k for k, x in enumerate(pf) if x.get("gate") == gate and (x.get("item") == item or (x.get("item_prefix") and item.startswith(x["item_prefix"])))), None)
|
||||
entry = dict(gate=gate, item=item, reason=reason, approved=PENDING)
|
||||
if i is None:
|
||||
pf.append(entry)
|
||||
notes.append(item.split(" ")[0] + " 추가")
|
||||
elif not all(n in pf[i].get("reason", "") for n in nums):
|
||||
pf[i] = entry # an entry inherited from another pier: the numbers do not fit this pier
|
||||
notes.append(item.split(" ")[0] + " 숫자 갱신")
|
||||
if notes:
|
||||
out.append(dict(file="expected", path="planned_fail", old=E.get("planned_fail"), new=pf, inferred=True, group="기대값(expected) planned_fail",
|
||||
why="계획된 FAIL 제안(" + ", ".join(notes) + ") — 작도를 채택하고 재료표와 다른 항목을 등록합니다. 승인은 사람이 합니다"))
|
||||
return out
|
||||
|
||||
|
||||
def stamp_pending(E, by, day):
|
||||
"""A planned FAIL entry written from the reading screen carries PENDING; once the person has chosen and applied it, the choice is recorded in `approved`
|
||||
(the report prints this text). Returns how many entries were stamped."""
|
||||
n = 0
|
||||
for x in E.get("planned_fail", []):
|
||||
if x.get("approved") == PENDING:
|
||||
x["approved"] = f"사용자 선택 {day} ({by or '-'}) — 도면에서 읽기 화면"
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def normalize_per_bearing(P, E):
|
||||
"""A value that differs per bearing (e.g. the sole plate length L: front row 700, rear row 800) cannot stay one number in expected.G5 -- the checker
|
||||
accepts {no: value} (Ulsan 2026-10-13). Converts G5.L / G5.T when the pier's own table shows different values."""
|
||||
tab = (P.get("sole_plates") or {}).get("table")
|
||||
g5 = E.get("G5")
|
||||
if not isinstance(tab, list) or not isinstance(g5, dict):
|
||||
return
|
||||
for k in ("L", "T"):
|
||||
vals = {str(r["no"]): r[k] for r in tab if isinstance(r, dict) and k in r}
|
||||
if vals and not isinstance(g5.get(k), dict) and len(set(vals.values())) > 1:
|
||||
g5[k] = vals
|
||||
|
||||
|
||||
def apply_selected(params, expected, changes, selected):
|
||||
"""-> (params', expected') with the selected changes (indices into `changes`) written; the others stay as they are."""
|
||||
P, E = copy.deepcopy(params), copy.deepcopy(expected)
|
||||
for i in selected:
|
||||
c = changes[i]
|
||||
if c.get("row"):
|
||||
reader.apply_proposals([c["row"]], P, E)
|
||||
continue
|
||||
doc = P if c["file"] == "params" else E
|
||||
x = doc
|
||||
keys = c["path"].split(".")
|
||||
for k in keys[:-1]:
|
||||
x = x[int(k)] if isinstance(x, list) else x[k]
|
||||
x[int(keys[-1]) if isinstance(x, list) else keys[-1]] = copy.deepcopy(c["new"])
|
||||
normalize_per_bearing(P, E)
|
||||
return P, E
|
||||
|
||||
|
||||
def frame_ok(texts, anchors_template, anchors_mod):
|
||||
"""coverage of the template anchors' landmark texts in the drawing (0..1); the Ungok frame needs >= FRAME_MIN_COVERAGE."""
|
||||
if not anchors_template:
|
||||
return None
|
||||
cand = anchors_mod.follow(anchors_template, texts)
|
||||
return next(iter(cand.values()))["coverage"] if cand else 0.0
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Stage order of the T-pier app and the Rhino step list for a build up to a given gate.
|
||||
|
||||
Gates follow the required work order: G1 shape -> G2 column rebar -> G3 footing rebar -> G4 coping rebar
|
||||
-> G5 sole plates & bearings. A build "up to Gk" always rebuilds G1..Gk in a fresh document, so every
|
||||
gate model is reproducible from params alone.
|
||||
|
||||
Per-pier options (2026-10-13, Ulsan P1 = second T-pier form):
|
||||
params["scope"] gates modelled for this pier (default all); others are skipped ("범위 밖")
|
||||
params["builders"] {gate: script} replacing the default builder (e.g. G1 -> concrete_tapered.py)
|
||||
expected["checkers"] {gate: script} replacing the default checker (e.g. G1 -> g1_tapered.py)
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
RHINO_DIR = Path(__file__).resolve().parent / "rhino"
|
||||
|
||||
GATES = [
|
||||
("G1", "형상(콘크리트·말뚝·받침대좌·패드)", "concrete.py"),
|
||||
("G2", "기둥 철근", "column_rebar.py"),
|
||||
("G3", "확대기초 철근", "footing_rebar.py"),
|
||||
("G4", "코핑 철근", "coping_rebar.py"),
|
||||
("G5", "교량받침·솔플레이트", "bearings.py"),
|
||||
]
|
||||
CHECKERS = {"G1": "g1_shape.py", "G2": "g2_column_rebar.py", "G3": "g3_footing_rebar.py",
|
||||
"G4": "g4_coping_rebar.py", "G5": "g5_bearings.py"}
|
||||
# Object id prefixes created by each gate (after the pier id), used for negative tests and the
|
||||
# "earlier gates unchanged" fingerprint comparison.
|
||||
GATE_PREFIXES = {"G1": ["LC", "FT", "CL", "CP", "PL", "BS", "BP"], "G2": ["R-P", "RC-P"], "G3": ["R-B", "RC-B", "PT"],
|
||||
"G4": ["R-C", "RC-C", "R-S", "RC-S"], "G5": ["BB", "SP"]}
|
||||
# "PT": shaft cage plate (Ramp-A P1 G3, 2026-10-13). Ramp-A shaft cage bars carry "R-P1-1" etc. (marks P1-1 ...),
|
||||
# so they fall under the G2 prefixes in the unchanged check; that is harmless (all earlier gates are compared)
|
||||
REBAR_GATES = {"G2", "G3", "G4"}
|
||||
TOLERANCE_MM = 0.001
|
||||
|
||||
|
||||
def gate_ids():
|
||||
return [g for g, _, _ in GATES]
|
||||
|
||||
|
||||
def _json(path):
|
||||
return json.loads(Path(path).read_text(encoding="utf-8")) if path and Path(path).exists() else {}
|
||||
|
||||
|
||||
def scope_of(params_path):
|
||||
"""Gates modelled for this pier (params["scope"], default all), in gate order."""
|
||||
sc = _json(params_path).get("scope")
|
||||
return [g for g in gate_ids() if not sc or g in sc]
|
||||
|
||||
|
||||
def builder_of(params_path, gate):
|
||||
default = {g: s for g, _, s in GATES}[gate]
|
||||
return _json(params_path).get("builders", {}).get(gate, default)
|
||||
|
||||
|
||||
def checker_of(expected_path, gate):
|
||||
return _json(expected_path).get("checkers", {}).get(gate, CHECKERS[gate])
|
||||
|
||||
|
||||
def build_steps(params_path, upto="G5"):
|
||||
"""Rhino steps for G1..`upto`: builders, diameter colours after the last rebar stage, display state."""
|
||||
ids = gate_ids()
|
||||
if upto not in ids:
|
||||
raise ValueError(f"unknown gate {upto}")
|
||||
scope = scope_of(params_path)
|
||||
chosen = [(g, n, builder_of(params_path, g)) for g, n, _ in GATES[: ids.index(upto) + 1] if g in scope]
|
||||
glb = {"PARAMS_PATH": str(Path(params_path).resolve())}
|
||||
steps = []
|
||||
last_rebar = max((i for i, (g, _, _) in enumerate(chosen) if g in REBAR_GATES), default=None)
|
||||
for i, (g, _, script) in enumerate(chosen):
|
||||
steps.append({"name": f"{g}:{script}", "script": RHINO_DIR / script, "globals": dict(glb, GATE=g)})
|
||||
if i == last_rebar:
|
||||
steps.append({"name": f"{g}:rebar_colours.py", "script": RHINO_DIR / "rebar_colours.py", "globals": glb})
|
||||
steps.append({"name": "display.py", "script": RHINO_DIR / "display.py", "globals": glb})
|
||||
return steps
|
||||
|
||||
|
||||
def check_steps(expected_path, out_dir, gates=None, tag=""):
|
||||
"""Rhino steps running the gate checkers; each writes <out_dir>/<gate><tag>.json."""
|
||||
steps = []
|
||||
for g in gates or gate_ids():
|
||||
chk = checker_of(expected_path, g)
|
||||
steps.append({"name": f"{g}:{chk}", "script": RHINO_DIR / "checks" / chk,
|
||||
"globals": {"EXPECTED_PATH": str(Path(expected_path).resolve()),
|
||||
"RESULT_PATH": str(Path(out_dir).resolve() / f"{g}{tag}.json")}})
|
||||
return steps
|
||||
@@ -0,0 +1,460 @@
|
||||
"""Drawing reader (semi-automatic): DWG -> DXF conversion, text index, text search and table candidates.
|
||||
|
||||
Values are never written into params/expected here; the reader only proposes candidates with their source
|
||||
(drawing, layer, position) and the user confirms them in the web form.
|
||||
Validated on one drawing set (Ungok P1 D1-D3): dimensions there are TEXT on dimension layers, not DIMENSION.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import ezdxf
|
||||
|
||||
ODA = Path(r"C:\Program Files\ODA\ODAFileConverter 27.1.0\ODAFileConverter.exe")
|
||||
EL_RE = re.compile(r"EL\.?\s*([0-9]{1,3}\.[0-9]{2,3})\b")
|
||||
|
||||
|
||||
def sha256(path):
|
||||
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def convert(dwg_paths, out_dir):
|
||||
"""Convert DWG files to DXF in out_dir (sources are copied first and never touched)."""
|
||||
out_dir = Path(out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
audit = []
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tin, tout = Path(tmp, "in"), Path(tmp, "out")
|
||||
tin.mkdir(), tout.mkdir()
|
||||
for p in dwg_paths:
|
||||
shutil.copy2(p, tin / Path(p).name)
|
||||
subprocess.run([str(ODA), str(tin), str(tout), "ACAD2018", "DXF", "0", "1", "*.DWG"], check=True, timeout=600)
|
||||
for p in dwg_paths:
|
||||
produced = tout / (Path(p).stem + ".dxf")
|
||||
if not produced.is_file():
|
||||
raise RuntimeError(f"conversion failed: {Path(p).name}")
|
||||
target = out_dir / produced.name
|
||||
shutil.copy2(produced, target)
|
||||
audit.append({"source": Path(p).name, "source_sha256": sha256(p), "dxf": target.name,
|
||||
"dxf_sha256": sha256(target), "engine": "ODA File Converter 27.1.0"})
|
||||
return audit
|
||||
|
||||
|
||||
def _text(e):
|
||||
return (e.plain_text() if e.dxftype() == "MTEXT" else e.dxf.text).strip()
|
||||
|
||||
|
||||
def index_dxf(path, label):
|
||||
"""All TEXT/MTEXT (incl. those inside block inserts) of one drawing with layer and position."""
|
||||
doc = ezdxf.readfile(path)
|
||||
msp = doc.modelspace()
|
||||
out = []
|
||||
for e in msp.query("TEXT MTEXT"):
|
||||
p = e.dxf.insert
|
||||
h = e.dxf.char_height if e.dxftype() == "MTEXT" else e.dxf.height
|
||||
out.append({"dwg": label, "layer": e.dxf.layer, "x": round(p.x, 1), "y": round(p.y, 1),
|
||||
"h": round(h or 0, 1), "text": _text(e), "handle": e.dxf.handle})
|
||||
for ins in msp.query("INSERT"):
|
||||
try:
|
||||
for v in ins.virtual_entities():
|
||||
if v.dxftype() in ("TEXT", "MTEXT"):
|
||||
p = v.dxf.insert
|
||||
out.append({"dwg": label, "layer": v.dxf.layer, "x": round(p.x, 1), "y": round(p.y, 1),
|
||||
"h": 0, "text": _text(v), "handle": ins.dxf.handle, "block": ins.dxf.name})
|
||||
except Exception: # unresolvable block (proxy etc.): its texts are simply not indexed
|
||||
pass
|
||||
return [t for t in out if t["text"]]
|
||||
|
||||
|
||||
def search(texts, query, regex=False, limit=200):
|
||||
if regex:
|
||||
rx = re.compile(query, re.I)
|
||||
hit = [t for t in texts if rx.search(t["text"])]
|
||||
else:
|
||||
q = query.lower()
|
||||
hit = [t for t in texts if q in t["text"].lower()]
|
||||
return hit[:limit]
|
||||
|
||||
|
||||
def el_candidates(texts):
|
||||
"""'EL. 26.208' style texts -> value in mm (Z = EL x 1000)."""
|
||||
out = []
|
||||
for t in texts:
|
||||
m = EL_RE.search(t["text"].replace(" ", ""))
|
||||
if m:
|
||||
out.append(dict(t, value_mm=round(float(m.group(1)) * 1000, 1)))
|
||||
return out
|
||||
|
||||
|
||||
def table_below(texts, anchor, align="center", width=25000.0, depth=20000.0, stops=(), row_tol=0.6, stops_below=()):
|
||||
# window scales with the title text height (25,000 x 20,000 at the Ungok title height 420; Ulsan 1:100 sheet
|
||||
# has 700 and wider tables, 2026-10-13)
|
||||
k = max(1.0, (anchor["h"] or 0) / 420.0)
|
||||
width, depth = width * k, depth * k
|
||||
"""Texts in a window below an anchor (table title) of the same drawing, grouped into rows/columns.
|
||||
|
||||
Window: `width` wide, centred on the title (align="center") or starting at it (align="left"), cut at the
|
||||
nearest other table title on the right (`stops`), `depth` deep. Columns are then limited to the x range of
|
||||
the header row (first row with >= 4 cells) plus one column gap, which drops neighbouring drawing texts.
|
||||
Returns [{"y": .., "cells": [{"x", "text", "layer"}...]}], top row first.
|
||||
"""
|
||||
h = anchor["h"] or 150
|
||||
x0, x1 = ((anchor["x"] - width / 2, anchor["x"] + width / 2) if align == "center"
|
||||
else (anchor["x"] - 2 * h, anchor["x"] + width))
|
||||
for s in stops:
|
||||
if s["dwg"] == anchor["dwg"] and s is not anchor and abs(s["y"] - anchor["y"]) < 2 * h and anchor["x"] < s["x"] < x1:
|
||||
x1 = s["x"] - 2 * h
|
||||
y1, y0 = anchor["y"], anchor["y"] - depth
|
||||
for s in list(stops) + list(stops_below): # another table title below in the same column ends this table (Ulsan SD400
|
||||
if (s["dwg"] == anchor["dwg"] and s is not anchor and y0 < s["y"] < y1 - 2 * h # above SD500, 2026-10-13)
|
||||
and abs(s["x"] - anchor["x"]) < 2 * h):
|
||||
y0 = s["y"] + h
|
||||
cells = [t for t in texts if t["dwg"] == anchor["dwg"] and x0 <= t["x"] <= x1 and y0 <= t["y"] < y1
|
||||
and t is not anchor]
|
||||
if not cells:
|
||||
return []
|
||||
tol = row_tol * (sorted(t["h"] for t in cells)[len(cells) // 2] or 150)
|
||||
rows = []
|
||||
for t in sorted(cells, key=lambda t: -t["y"]):
|
||||
if rows and abs(rows[-1]["y"] - t["y"]) <= tol:
|
||||
rows[-1]["cells"].append(t)
|
||||
else:
|
||||
rows.append({"y": t["y"], "cells": [t]})
|
||||
for r in rows:
|
||||
r["cells"] = sorted(r["cells"], key=lambda c: c["x"])
|
||||
# header = first row with >= 4 cells whose first following row with a plain number cell (text-only rows =
|
||||
# further header lines, Ulsan three-line header, are skipped) has at least half as many cells. On Ramp-A P1
|
||||
# the labels of the detail drawing between the title and the table (C1 C3 C2 S1 S2, then '2,200' alone) were
|
||||
# taken as the header (2026-10-13)
|
||||
def numeric(r):
|
||||
return any(re.fullmatch(r"-?[\d.,]+", c["text"].strip()) for c in r["cells"])
|
||||
|
||||
def next_data_ok(i):
|
||||
# rows lying wholly outside the header's x span belong to a neighbouring table and are skipped (Okseong P1:
|
||||
# ELEVATION TABLE rows interleave in the 솔플레이트 TABLE window, 2026-10-14)
|
||||
hx = [c["x"] for c in rows[i]["cells"]]
|
||||
pad = max([b - a for a, b in zip(sorted(hx), sorted(hx)[1:])] or [0])
|
||||
inside = lambda r: any(min(hx) - pad <= c["x"] <= max(hx) + pad for c in r["cells"])
|
||||
nxt = next((r for r in rows[i + 1:] if numeric(r) and inside(r)), None)
|
||||
return nxt is not None and 2 * len(nxt["cells"]) >= len(rows[i]["cells"])
|
||||
|
||||
cand = [i for i, r in enumerate(rows) if len(r["cells"]) >= 4]
|
||||
good = [i for i in cand if next_data_ok(i)]
|
||||
head = rows[(good or cand)[0]] if cand else None
|
||||
if head:
|
||||
# multi-line header (Ulsan schedule, 2026-10-13: '길이'/'총길이' one line above '번호 형식 직경 개수 A..J',
|
||||
# units '(M)' one line below): merge the text-only lines within 2 text heights below the first header line
|
||||
# into it; unit lines (all cells in parentheses) and cells on top of an existing header cell are dropped
|
||||
hh = sorted(t["h"] for t in cells)[len(cells) // 2] or 150
|
||||
i = rows.index(head)
|
||||
while i + 1 < len(rows) and head["y"] - rows[i + 1]["y"] <= 2 * hh:
|
||||
nxt = rows[i + 1]
|
||||
if any(re.fullmatch(r"[\d.,~%()°˚-]+", c["text"].strip()) for c in nxt["cells"]):
|
||||
break
|
||||
if not all(c["text"].strip().startswith("(") for c in nxt["cells"]):
|
||||
for c in nxt["cells"]:
|
||||
if all(abs(c["x"] - o["x"]) > hh for o in head["cells"]):
|
||||
head["cells"].append(c)
|
||||
head["cells"].sort(key=lambda c: c["x"])
|
||||
rows.pop(i + 1)
|
||||
xs = [c["x"] for c in head["cells"]]
|
||||
# the header row may also hold the header of a neighbouring table on the same line (Ulsan: ELEVATION and
|
||||
# 솔플레이트 TABLE side by side, 2026-10-13): split it at gaps > 3 x the median gap and keep the cluster
|
||||
# nearest to the title
|
||||
gaps = sorted(b - a for a, b in zip(xs, xs[1:]))
|
||||
med = gaps[len(gaps) // 2] if gaps else 0
|
||||
clusters, cur = [], [xs[0]]
|
||||
for a_, b_ in zip(xs, xs[1:]):
|
||||
if med and b_ - a_ > 3 * med:
|
||||
clusters.append(cur)
|
||||
cur = []
|
||||
cur.append(b_)
|
||||
clusters.append(cur)
|
||||
xs = min(clusters, key=lambda cl: 0 if cl[0] <= anchor["x"] <= cl[-1] else min(abs(cl[0] - anchor["x"]), abs(cl[-1] - anchor["x"])))
|
||||
gap = max([b - a for a, b in zip(xs, xs[1:])] or [med or 1000])
|
||||
lo, hi = xs[0] - gap, xs[-1] + gap
|
||||
rows = rows[rows.index(head):]
|
||||
for r in rows:
|
||||
r["cells"] = [c for c in r["cells"] if lo <= c["x"] <= hi]
|
||||
rows = [r for r in rows if r["cells"]]
|
||||
return [{"y": r["y"], "cells": [{"x": c["x"], "text": c["text"], "layer": c["layer"]} for c in r["cells"]]}
|
||||
for r in rows]
|
||||
|
||||
|
||||
TABLE_ANCHORS = [ # (key, title pattern, title alignment over the table)
|
||||
("elevation", r"ELEVATION\s*TABLE", "center"),
|
||||
("sole_plate", r"솔플레이트.*TABLE", "center"),
|
||||
("schedule_SD400", r"^\(SD[0-9]00\)$", "left"),
|
||||
]
|
||||
|
||||
|
||||
def table_candidates(texts):
|
||||
out = []
|
||||
titles = {key: search(texts, pat, regex=True) for key, pat, _ in TABLE_ANCHORS}
|
||||
for key, pat, align in TABLE_ANCHORS:
|
||||
anchors = titles[key]
|
||||
# a table title of another kind below in the same column also ends the window (Ramp-A P1: the ELEVATION
|
||||
# TABLE window ran into the 솔플레이트 TABLE below it, 2026-10-13); on the right only titles of the same kind
|
||||
below = [t for k_, ts in titles.items() if k_ != key for t in ts]
|
||||
for a in anchors:
|
||||
k = f"schedule_{a['text'].strip('()')}" if key.startswith("schedule") else key
|
||||
out.append({"key": k, "anchor": a, "rows": table_below(texts, a, align, stops=anchors, stops_below=below)})
|
||||
return out
|
||||
|
||||
|
||||
def build_index(dxf_paths, labels, out_json):
|
||||
texts = []
|
||||
for p, lab in zip(dxf_paths, labels):
|
||||
texts += index_dxf(p, lab)
|
||||
Path(out_json).write_text(json.dumps(texts, ensure_ascii=False), encoding="utf-8")
|
||||
return texts
|
||||
|
||||
|
||||
def table_records(rows, key_pred):
|
||||
"""Map cells of data rows to header columns (nearest header x within 0.6 x the gap to the neighbouring header
|
||||
on the cell's side; blank cells stay absent). Data rows = rows whose first cell satisfies key_pred; that cell
|
||||
is stored as "_key". Returns [(row y, {header: text})].
|
||||
The tolerance is per column side, not the smallest gap of the table: a long range text such as '12.228~3.151'
|
||||
in a wide column (Ungok P2 C12, 562 from the 'A' header, smallest gap 900) was dropped (OPEN #19 A3)."""
|
||||
head = [(c["x"], re.sub(r"\s+", "", c["text"])) for c in rows[0]["cells"]]
|
||||
xs = [x for x, _ in head]
|
||||
gaps = [b - a for a, b in zip(xs, xs[1:])]
|
||||
out = []
|
||||
for r in rows[1:]:
|
||||
if not r["cells"] or not key_pred(r["cells"][0]["text"]):
|
||||
continue
|
||||
rec = {"_key": r["cells"][0]["text"]}
|
||||
for c in r["cells"][1:]:
|
||||
i = min(range(len(head)), key=lambda j: abs(head[j][0] - c["x"]))
|
||||
x, name = head[i]
|
||||
side = gaps[i - 1] if c["x"] < x and i > 0 else gaps[min(i, len(gaps) - 1)] if gaps else 0
|
||||
if abs(x - c["x"]) <= 0.6 * side and name not in rec:
|
||||
rec[name] = c["text"]
|
||||
out.append((r["y"], rec))
|
||||
return out
|
||||
|
||||
|
||||
def _num(s):
|
||||
return float(s.replace(",", ""))
|
||||
|
||||
|
||||
def _int(s):
|
||||
"""Table integer; thousands separators allowed ('1,000' in the Ramp-A sole plate table, 2026-10-13)."""
|
||||
return int(round(_num(s)))
|
||||
|
||||
|
||||
def _m_to_mm(s):
|
||||
v = _num(s)
|
||||
return round(v * 1000, 1) if "." in s and v < 100 else v
|
||||
|
||||
|
||||
MARK_RE = re.compile(r"^[A-Z]{1,2}\d{1,2}$")
|
||||
EL_X = "EL.'X'"
|
||||
|
||||
|
||||
def _get(d, path):
|
||||
for k in path:
|
||||
if isinstance(d, list) and isinstance(k, int) and 0 <= k < len(d): # list index (params table rows)
|
||||
d = d[k]
|
||||
continue
|
||||
if not isinstance(d, dict) or k not in d:
|
||||
return None
|
||||
d = d[k]
|
||||
return d
|
||||
|
||||
|
||||
def proposals(cands, params, expected):
|
||||
"""Candidate vs current value per field. Schedule rows are routed to the expected/params entries that
|
||||
already list that mark (G2 P*, G3 B*, G4 C*/S*, params rebar_marks). Returns rows for the review table:
|
||||
{"file": "expected"|"params", "path": [...], "current", "candidate", "src", "same"}."""
|
||||
docs = {"expected": expected, "params": params}
|
||||
rows = []
|
||||
|
||||
def row(file, path, cand, src):
|
||||
cur = _get(docs[file], path)
|
||||
if cur is None and file == "expected" and path[0] in ("G1", "G2", "G3", "G4", "G5"):
|
||||
return # field not used by this pier's checks
|
||||
if cur is None and file == "params":
|
||||
return # only fields this pier's params already have
|
||||
same = cur == cand or (isinstance(cur, (int, float)) and isinstance(cand, (int, float))
|
||||
and abs(cur - cand) < 1e-6)
|
||||
rows.append({"file": file, "path": path, "current": cur, "candidate": cand, "src": src, "same": same})
|
||||
|
||||
def params_rows(t, c):
|
||||
"""The same bearing / sole plate table value as a separate params row (OPEN #19 A4): the user selects
|
||||
it on its own, so a misread value does not change the model and its check value in one click."""
|
||||
no = c.get("no") if c.get("no") is not None else (t[-1] if t[-1].isdigit() else None)
|
||||
if no is None:
|
||||
return
|
||||
|
||||
def index(path):
|
||||
lst = _get(params, path)
|
||||
return next((i for i, e in enumerate(lst) if isinstance(e, dict) and str(e.get("no")) == str(no)),
|
||||
None) if isinstance(lst, list) else None
|
||||
|
||||
bi, si = index(["bearing_seats", "bearings"]), index(["sole_plates", "table"])
|
||||
bp, sp = ["bearing_seats", "bearings", bi], ["sole_plates", "table", si]
|
||||
if t[1:3] == ["G5", "el_x"] and bi is not None:
|
||||
row("params", bp + ["el_x"], c["value"], c["src"])
|
||||
elif t[1:4] == ["G1", "seats", "table"] and bi is not None:
|
||||
for k, v in zip(("h1", "h2", "A", "B"), c["value"][1:]):
|
||||
row("params", bp + [k], v, c["src"])
|
||||
elif t[1] == "G5" and t[2] in ("S", "W", "L", "T", "h3") and si is not None:
|
||||
if t[2] == "h3" and "h3" not in params["sole_plates"]["table"][si]:
|
||||
if not any(r["file"] == "params" and r["path"] == ["sole_plates", "h3"]
|
||||
and r["candidate"] == c["value"] for r in rows): # common h3 (Ungok): once per value
|
||||
row("params", ["sole_plates", "h3"], c["value"], c["src"])
|
||||
else:
|
||||
row("params", sp + [t[2]], c["value"], c["src"])
|
||||
|
||||
for c in cands:
|
||||
t = c["target"].split(".")
|
||||
if t[0] == "expected":
|
||||
path = t[1:]
|
||||
if c.get("no") is not None and isinstance(_get(expected, path), dict):
|
||||
path = path + [str(c["no"])] # per-bearing values (Ulsan G5 h3 / L / T, 2026-10-13)
|
||||
row("expected", path, c["value"], c["src"])
|
||||
params_rows(t, c)
|
||||
elif t[0] == "schedule":
|
||||
m, v = t[1], c["value"]
|
||||
if m in (_get(expected, ["G2", "schedule_L"]) or {}):
|
||||
row("expected", ["G2", "schedule_L", m], v["L"], c["src"])
|
||||
row("expected", ["G2", "schedule_n", m], v["n"], c["src"])
|
||||
if m in (_get(expected, ["G3", "schedule"]) or {}):
|
||||
row("expected", ["G3", "schedule", m], [v["L"], v["n"]], c["src"])
|
||||
if m in (_get(expected, ["G4", "count"]) or {}):
|
||||
row("expected", ["G4", "count", m], v["n"], c["src"])
|
||||
row("expected", ["G4", "L", m], v["L"], c["src"])
|
||||
if m in (params.get("rebar_marks") or {}):
|
||||
# a schedule without a shape column (Ramp-A D4, 2026-10-13) proposes the diameter only
|
||||
shape = v["shape"] if v["shape"] is not None else (params["rebar_marks"][m] or {}).get("shape")
|
||||
row("params", ["rebar_marks", m], {"dia": v["dia"], "shape": shape}, c["src"])
|
||||
return rows
|
||||
|
||||
|
||||
def apply_proposals(rows, params, expected):
|
||||
"""Write the candidate value of each given proposal row into params / expected (in place)."""
|
||||
docs = {"expected": expected, "params": params}
|
||||
for r in rows:
|
||||
d = docs[r["file"]]
|
||||
for k in r["path"][:-1]:
|
||||
d = d[k]
|
||||
d[r["path"][-1]] = r["candidate"]
|
||||
|
||||
|
||||
def field_candidates(tables):
|
||||
"""Candidate values for params / expected fields from the recognised tables.
|
||||
|
||||
Returns [{"target": "expected.G5.el_x.1", "value": .., "src": "D2 ELEVATION TABLE NO.1 EL.'X' 43.625", "no": ..}].
|
||||
`no` (bearing number) routes h3 / L / T to the per-bearing entry when expected holds them per bearing.
|
||||
Only the tables validated on the Ungok P1 drawings are mapped; everything else stays manual.
|
||||
"""
|
||||
out = []
|
||||
|
||||
def add(target, value, src, no=None):
|
||||
out.append({"target": target, "value": value, "src": src, "no": no})
|
||||
|
||||
for t in tables:
|
||||
dwg = t["anchor"]["dwg"]
|
||||
if not t["rows"]:
|
||||
continue
|
||||
if t["key"] == "elevation":
|
||||
for _, r in table_records(t["rows"], lambda s: s.isdigit()):
|
||||
no = r["_key"]
|
||||
src = f"{dwg} ELEVATION TABLE NO.{no}"
|
||||
if EL_X in r:
|
||||
elx = round(_num(r[EL_X]) * 1000)
|
||||
add(f"expected.G5.el_x.{no}", elx, f"{src} EL.'X' {r[EL_X]}")
|
||||
if all(k in r for k in ("h1", "h2", "A", "B")):
|
||||
add(f"expected.G1.seats.table.{no}", [elx, _int(r["h1"]), _int(r["h2"]), _int(r["A"]), _int(r["B"])],
|
||||
f"{src} EL.'X', h1, h2, A, B")
|
||||
if "h3" in r:
|
||||
add("expected.G5.h3", _int(r["h3"]), f"{src} h3", no)
|
||||
elif t["key"] == "sole_plate":
|
||||
for _, r in table_records(t["rows"], lambda s: s.isdigit()):
|
||||
no = r["_key"]
|
||||
src = f"{dwg} 솔플레이트 상세 TABLE NO.{no}"
|
||||
if "S(%)" in r:
|
||||
add(f"expected.G5.S.{no}", _num(r["S(%)"]), f"{src} S(%)")
|
||||
if "W" in r:
|
||||
add(f"expected.G5.W.{no}", _int(r["W"]), f"{src} W")
|
||||
for k in ("L", "T"):
|
||||
if k in r:
|
||||
add(f"expected.G5.{k}", _int(r[k]), f"{src} {k}", no)
|
||||
elif t["key"].startswith("schedule"):
|
||||
last_dia = None
|
||||
for _, r in table_records(t["rows"], lambda s: bool(MARK_RE.match(s))):
|
||||
m = r["_key"]
|
||||
dia = r.get("직경", "")
|
||||
dia = last_dia if dia in ('"', "") else dia
|
||||
last_dia = dia
|
||||
src = f"{dwg} 철근재료표 {t['anchor']['text']} {m}"
|
||||
try:
|
||||
L = _m_to_mm(r["길이"]) if "길이" in r else None
|
||||
n = int(r["개수"]) if r.get("개수", "").isdigit() else None
|
||||
if n is None and L and "총길이" in r:
|
||||
n = round(_num(r["총길이"]) * 1000 / L) # quantity blank -> total / L (derived)
|
||||
src += " (개수 = 총길이/길이 [역산])"
|
||||
except ValueError:
|
||||
# cells landed in the wrong column (e.g. a two-row header, Ulsan 2026-10-13): no candidate
|
||||
continue
|
||||
add(f"schedule.{m}", {"shape": r.get("형식"), "dia": dia, "n": n, "L": L}, src)
|
||||
return out
|
||||
|
||||
|
||||
def _pt(p):
|
||||
return [round(p[0], 1), round(p[1], 1)]
|
||||
|
||||
|
||||
def entity_info(dxf_path, handle):
|
||||
"""One drawing object by handle (OPEN #19 A6: D1 outline values were read by handle with ad-hoc scripts).
|
||||
Returns {"type", "layer", "handle", "values": {name: value}} or None. Coordinates are drawing (DXF) units."""
|
||||
doc = ezdxf.readfile(dxf_path)
|
||||
e = doc.entitydb.get(handle.strip().upper())
|
||||
if e is None or not hasattr(e, "dxf"):
|
||||
return None
|
||||
t, d = e.dxftype(), e.dxf
|
||||
v = {}
|
||||
if t == "LINE":
|
||||
v = {"start": _pt(d.start), "end": _pt(d.end), "length": round((d.end - d.start).magnitude, 1),
|
||||
"dx": round(d.end.x - d.start.x, 1), "dy": round(d.end.y - d.start.y, 1)}
|
||||
elif t in ("LWPOLYLINE", "POLYLINE"):
|
||||
pts = [_pt(p) for p in (e.get_points("xy") if t == "LWPOLYLINE" else e.points())]
|
||||
segs = [((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2) ** 0.5 for a, b in zip(pts, pts[1:] + pts[:1] if e.is_closed else pts[1:])]
|
||||
v = {"closed": e.is_closed, "vertices": len(pts), "points": pts[:50], "segment_lengths": [round(s, 1) for s in segs[:50]],
|
||||
"length (straight segments)": round(sum(segs), 1)}
|
||||
elif t in ("ARC", "CIRCLE"):
|
||||
v = {"centre": _pt(d.center), "radius": round(d.radius, 1)}
|
||||
if t == "ARC":
|
||||
sweep = (d.end_angle - d.start_angle) % 360
|
||||
v.update(start_angle=round(d.start_angle, 3), end_angle=round(d.end_angle, 3),
|
||||
arc_length=round(d.radius * sweep * 3.141592653589793 / 180, 1),
|
||||
start_point=_pt(e.start_point), end_point=_pt(e.end_point))
|
||||
elif t in ("TEXT", "MTEXT"):
|
||||
v = {"insert": _pt(d.insert), "text": _text(e)}
|
||||
elif t == "INSERT":
|
||||
v = {"block": d.name, "insert": _pt(d.insert), "rotation": d.get("rotation", 0),
|
||||
"scale": [d.get("xscale", 1), d.get("yscale", 1)]}
|
||||
elif t == "DIMENSION":
|
||||
v = {"measurement": round(e.get_measurement(), 1) if hasattr(e, "get_measurement") else None,
|
||||
"text": d.get("text", "")}
|
||||
try:
|
||||
from ezdxf import bbox
|
||||
b = bbox.extents([e])
|
||||
if b.has_data:
|
||||
v["bbox"] = [_pt(b.extmin), _pt(b.extmax)]
|
||||
v["bbox_size"] = [round(b.size.x, 1), round(b.size.y, 1)]
|
||||
except Exception: # objects without geometry (e.g. an empty block) have no extents
|
||||
pass
|
||||
return {"type": t, "layer": d.get("layer", ""), "handle": d.handle, "values": v}
|
||||
|
||||
|
||||
def el_list(texts):
|
||||
"""EL texts for manual reference (no automatic field mapping: position decides the meaning)."""
|
||||
return [{"dwg": t["dwg"], "layer": t["layer"], "x": t["x"], "y": t["y"], "text": t["text"],
|
||||
"value_mm": t["value_mm"]} for t in el_candidates(texts)]
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Template recommendation from a drawing set (S02, 2026-10-15): estimate the fit questions from the drawing texts and
|
||||
rank the protected templates by the number of matching answers.
|
||||
|
||||
Only questions the texts can answer are estimated; the rest stay None ("판단 불가") and are answered by the user.
|
||||
Estimates are proposals with their evidence, never confirmations. Rules were checked on 9 drawing sets only
|
||||
(Ungok P1-P4, Ulsan P1-P2, Ramp-A P1, Okseong P1, Saejae-1 P1); weak ones are marked.
|
||||
"""
|
||||
import re
|
||||
|
||||
Q_PILE, Q_SOFFIT, Q_BEARING = "foundation", "soffit", "bearings"
|
||||
QUESTIONS = [
|
||||
(Q_PILE, "기초 형식(말뚝 유무)"),
|
||||
(Q_SOFFIT, "코핑 하면 형상"),
|
||||
(Q_BEARING, "받침 개수"),
|
||||
]
|
||||
R_RE = re.compile(r"(?:^|[\s(])R\s*=?\s*(\d[\d,]*(?:\.\d+)?)\s*$")
|
||||
|
||||
|
||||
def profile(params):
|
||||
"""Answers a template's params.json stands for."""
|
||||
piles = "piles" in params or "pile" in params
|
||||
cp = params.get("coping", {})
|
||||
if "soffit_radius" in cp:
|
||||
soffit = "곡선"
|
||||
elif cp.get("form") == "y_shaft":
|
||||
soffit = "나팔형"
|
||||
else:
|
||||
soffit = "직선 경사"
|
||||
found = "말뚝만" if ("footing" not in params and piles) else ("기초판+말뚝" if piles else "기초판만")
|
||||
return {Q_PILE: found, Q_SOFFIT: soffit, Q_BEARING: len(params.get("bearing_seats", {}).get("bearings", []))}
|
||||
|
||||
|
||||
def _r_mm(text):
|
||||
m = R_RE.search(text.strip())
|
||||
if not m:
|
||||
return None
|
||||
s = m.group(1)
|
||||
return round(float(s) * 1000) if re.fullmatch(r"\d+\.\d{3}", s) else round(float(s.replace(",", "")))
|
||||
|
||||
|
||||
def _where(t):
|
||||
return f"{t.get('dwg', '?')} {t.get('layer', '')} ({t['x']:.0f}, {t['y']:.0f}) '{t['text'][:40]}'"
|
||||
|
||||
|
||||
def count_bearings(rows):
|
||||
"""Largest run 1..n of integer first cells under the ELEVATION TABLE title."""
|
||||
n = 0
|
||||
for r in rows:
|
||||
c = r["cells"][0]["text"].strip() if r.get("cells") else ""
|
||||
if c.isdigit() and int(c) == n + 1:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def estimate(texts, table_rows=None):
|
||||
"""{question: (answer or None, evidence str, confidence)}; table_rows = rows of the ELEVATION TABLE (reader.table_candidates)."""
|
||||
out = {}
|
||||
piles = [t for t in texts if "말뚝" in t["text"]]
|
||||
cast = [t for t in piles if "현장타설말뚝" in t["text"]]
|
||||
if cast:
|
||||
out[Q_PILE] = ("말뚝만", "현장타설말뚝 문자: " + _where(cast[0]), "약함")
|
||||
elif piles:
|
||||
out[Q_PILE] = ("기초판+말뚝", f"'말뚝' 문자 {len(piles)}개: " + _where(piles[0]), "보통")
|
||||
else:
|
||||
out[Q_PILE] = ("기초판만", "'말뚝' 문자 없음", "보통")
|
||||
rs = [(v, t) for t in texts if len(t["text"]) < 30 and (v := _r_mm(t["text"])) is not None]
|
||||
big = [(v, t) for v, t in rs if v >= 10000]
|
||||
mid = [(v, t) for v, t in rs if 1000 < v < 10000]
|
||||
if big:
|
||||
out[Q_SOFFIT] = ("곡선", f"R {big[0][0]} mm: " + _where(big[0][1]), "보통")
|
||||
elif mid:
|
||||
out[Q_SOFFIT] = ("나팔형", f"R {mid[0][0]} mm: " + _where(mid[0][1]), "약함")
|
||||
else:
|
||||
small = f"(R {rs[0][0]} mm는 평면 모서리 등으로 보고 제외)" if rs else ""
|
||||
out[Q_SOFFIT] = ("직선 경사", "정면도 R 문자 없음 " + small, "약함")
|
||||
n = count_bearings(table_rows) if table_rows else 0
|
||||
out[Q_BEARING] = (n, f"ELEVATION TABLE 행 {n}개", "보통") if n else (None, "ELEVATION TABLE을 찾지 못함", "")
|
||||
return out
|
||||
|
||||
|
||||
def rank(est, profiles):
|
||||
"""Templates ordered by the number of estimated answers they match: [{name, matched, asked, hits{q: bool}}]."""
|
||||
asked = [q for q, _ in QUESTIONS if est.get(q, (None,))[0] is not None]
|
||||
rows = []
|
||||
for name, prof in profiles.items():
|
||||
hits = {q: prof[q] == est[q][0] for q in asked}
|
||||
rows.append({"name": name, "matched": sum(hits.values()), "asked": len(asked), "hits": hits, "profile": prof})
|
||||
rows.sort(key=lambda r: -r["matched"])
|
||||
return rows
|
||||
|
||||
|
||||
def mismatches(answers, prof):
|
||||
"""Questions whose answer differs from the template (strings compared; 'rebar_*' = '아니오' counts); empty = same form."""
|
||||
out = [label for q, label in QUESTIONS if str(prof[q]) != str(answers.get(q, "")).strip()]
|
||||
out += [label for key, label in (("rebar_shape", "철근 형상 종류"), ("rebar_method", "철근 생성 방식")) if answers.get(key) == "아니오"]
|
||||
return out
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Region image of a converted drawing (DXF) for the "도면 보기" links of the candidate tables (stage C, 2026-10-19).
|
||||
|
||||
The same drawing code as the reading aid Code/울산P1판독/dxftools.render, parameterised by a DXF path. Entities that touch the region are drawn
|
||||
black on white; Korean text shows as boxes (no font mapping), numbers are readable.
|
||||
"""
|
||||
import ezdxf
|
||||
from ezdxf import bbox
|
||||
|
||||
|
||||
def _in_region(e, x0, y0, x1, y1):
|
||||
try:
|
||||
b = bbox.extents([e], fast=True)
|
||||
except Exception:
|
||||
return False
|
||||
return b.has_data and b.extmax.x >= x0 and b.extmin.x <= x1 and b.extmax.y >= y0 and b.extmin.y <= y1
|
||||
|
||||
|
||||
def render(dxf_path, cx, cy, half, out_png, px=1400, mark=True):
|
||||
"""PNG of the square [cx-half, cx+half] x [cy-half, cy+half]; a ring marks the point (cx, cy)."""
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
from ezdxf.addons.drawing import Frontend, RenderContext
|
||||
from ezdxf.addons.drawing.config import BackgroundPolicy, ColorPolicy, Configuration
|
||||
from ezdxf.addons.drawing.matplotlib import MatplotlibBackend
|
||||
doc = ezdxf.readfile(dxf_path)
|
||||
x0, x1, y0, y1 = cx - half, cx + half, cy - half, cy + half
|
||||
keep = [e for e in doc.modelspace() if _in_region(e, x0, y0, x1, y1)]
|
||||
fig = plt.figure(figsize=(px / 100, px / 100), dpi=100)
|
||||
ax = fig.add_axes([0, 0, 1, 1])
|
||||
cfg = Configuration(background_policy=BackgroundPolicy.WHITE, color_policy=ColorPolicy.BLACK)
|
||||
Frontend(RenderContext(doc), MatplotlibBackend(ax), config=cfg).draw_entities(keep)
|
||||
if mark:
|
||||
ax.add_patch(plt.Circle((cx, cy), half * 0.04, fill=False, color="red", linewidth=2))
|
||||
ax.set_xlim(x0, x1)
|
||||
ax.set_ylim(y0, y1)
|
||||
ax.set_aspect("equal")
|
||||
ax.axis("off")
|
||||
fig.savefig(out_png, facecolor="white")
|
||||
plt.close(fig)
|
||||
return out_png
|
||||
@@ -0,0 +1,217 @@
|
||||
"""Review report draft (Korean Markdown) from gate results, drawing tables and approval records.
|
||||
|
||||
Chapters follow the requirements review (기획 v0.1.01 §3.7): 개요, 오류·불일치, 개선방안, 추가 사항, 누락,
|
||||
철근재료표(원본 vs 모델), 철근상세, plus 검증 방법. Verdict vocabulary is fixed (오류·불일치·누락·확인 필요);
|
||||
automatic entries use only 불일치 (approved planned FAIL) and 확인 필요 (everything else). Final verdicts are
|
||||
confirmed by a person, so the report is always issued as a draft.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from engine import reader
|
||||
from engine.pipeline import GATES, gate_ids
|
||||
|
||||
VOCAB = ("오류", "불일치", "누락", "확인 필요")
|
||||
REBAR_GATES = {"G2": "기둥", "G3": "확대기초", "G4": "코핑"}
|
||||
|
||||
|
||||
def _rj(p, default=None):
|
||||
p = Path(p)
|
||||
return json.loads(p.read_text(encoding="utf-8")) if p.exists() else default
|
||||
|
||||
|
||||
def _cell(v):
|
||||
return str(v).replace("|", "/").replace("\n", " ")
|
||||
|
||||
|
||||
def _table(head, rows):
|
||||
out = ["| " + " | ".join(head) + " |", "|" + "|".join(" :--- " for _ in head) + "|"]
|
||||
out += ["| " + " | ".join(_cell(c) for c in r) + " |" for r in rows]
|
||||
return out
|
||||
|
||||
|
||||
def collect(pier_dir, run_dir):
|
||||
pier_dir, run_dir = Path(pier_dir), Path(run_dir)
|
||||
d = {"info": _rj(pier_dir / "pier.json", {"name": pier_dir.name, "type": "T형"}),
|
||||
"audit": _rj(pier_dir / "drawings" / "audit.json", []),
|
||||
"approvals": _rj(run_dir / "approvals.json", {}),
|
||||
"expected": _rj(pier_dir / "expected.json", {}), "params": _rj(pier_dir / "params.json", {}),
|
||||
"gates": {}, "results": {}, "autocheck": _rj(pier_dir / "autocheck.json", {}), "confirm": _rj(pier_dir / "confirm.json", {})}
|
||||
for g in gate_ids():
|
||||
d["gates"][g] = _rj(run_dir / f"{g}_gate.json")
|
||||
d["results"][g] = _rj(run_dir / f"{g}.json")
|
||||
texts = _rj(pier_dir / "drawings" / "texts.json", [])
|
||||
tables = reader.table_candidates(texts) if texts else []
|
||||
cands = reader.field_candidates(tables)
|
||||
d["schedule"] = [c for c in cands if c["target"].startswith("schedule.")]
|
||||
d["diffs"] = [r for r in reader.proposals(cands, d["params"], d["expected"]) if not r["same"]] if cands else []
|
||||
return d
|
||||
|
||||
|
||||
def _n(v):
|
||||
"""Number for a table cell: '-' when missing, no trailing .0 for integral values."""
|
||||
if v is None:
|
||||
return "-"
|
||||
return int(v) if isinstance(v, float) and v.is_integer() else v
|
||||
|
||||
|
||||
def _bar_counts(results):
|
||||
"""Model bar count per mark from the checker results (items '<mark> bar count...')."""
|
||||
out = {}
|
||||
for g in REBAR_GATES:
|
||||
for r in (results.get(g) or {}).get("results", []):
|
||||
m = re.match(r"^(\S+) bar count", r["item"])
|
||||
if m and "actual" in r:
|
||||
out[m.group(1)] = (int(r["actual"]), int(r["expected"]), r["result"], g)
|
||||
return out
|
||||
|
||||
|
||||
def build(pier_dir, run_dir, author="", today=None, stale=(), out_of_scope=()):
|
||||
"""stale: gates whose result predates the current inputs (shown as void, not counted as done)."""
|
||||
d = collect(pier_dir, run_dir)
|
||||
for g in stale:
|
||||
d["gates"][g] = None
|
||||
d["results"][g] = None
|
||||
today = today or time.strftime("%Y-%m-%d")
|
||||
name, gates, appr = d["info"]["name"], d["gates"], d["approvals"]
|
||||
done = [g for g in gate_ids() if gates[g]]
|
||||
all_ok = all(gates[g] and gates[g]["verdict"] == "PASS" and appr.get(g, {}).get("decision") == "승인"
|
||||
for g in gate_ids() if g not in out_of_scope)
|
||||
L = [f"# {name} 교각 모델링 검토보고서 (초안)", "",
|
||||
f"> 앱이 게이트 결과·도면 표·승인 기록으로 자동 작성한 **초안**입니다. '오류'·'불일치' 판정과 3~5장의 서술은 "
|
||||
f"담당자가 확인·확정해야 합니다. 작성일 {today}{' · 작성 ' + author if author else ''}.", ""]
|
||||
|
||||
# 1 overview
|
||||
L += ["## 1. 개요", "", f"- 교각: {name} ({d['info'].get('type', 'T형')}) · 좌표: 로컬(교각 중심 원점, X 교축직각, Y 교축, "
|
||||
f"Z = EL×1000, mm) · 산출: .3dm", f"- 게이트 진행: {len(done)}/{5 - len(out_of_scope)} 실행{(' (범위 밖: ' + ', '.join(out_of_scope) + ')') if out_of_scope else ''}, "
|
||||
+ ("**전 게이트 PASS·승인**" if all_ok else "**미완료 — 실행·승인되지 않은 게이트가 있음**"), ""]
|
||||
if d["audit"]:
|
||||
L += ["### 1.1 입력 도면", ""] + _table(["구분", "DWG", "SHA-256(앞 16자)"],
|
||||
[[a.get("label", ""), a["source"], a["source_sha256"][:16]] for a in d["audit"]]) + [""]
|
||||
rows = []
|
||||
for g, gname, _ in GATES:
|
||||
r, a = gates[g], appr.get(g, {})
|
||||
if not r:
|
||||
rows.append([g, gname, "범위 밖" if g in out_of_scope else
|
||||
("입력 변경 — 무효(재실행 필요)" if g in stale else "미실행"), "-", "-", "-"])
|
||||
continue
|
||||
s = r["summary"]
|
||||
rows.append([g, gname, r["verdict"], f"{s['passed']}/{s['total']}",
|
||||
f"{a.get('decision', '-')} {a.get('by', '')} {a.get('at', '')}".strip(), a.get("note", "") or "-"])
|
||||
L += ["### 1.2 게이트 결과", ""] + _table(["게이트", "내용", "판정", "검사 PASS", "승인", "의견"], rows) + [""]
|
||||
|
||||
# 2 errors / mismatches
|
||||
finds = []
|
||||
for g in done:
|
||||
for pf in gates[g]["planned_fails"]:
|
||||
ex = next((x for x in d["expected"].get("planned_fail", []) if x["gate"] == g and
|
||||
(x.get("item") == pf["item"] or (x.get("item_prefix") and pf["item"].startswith(x["item_prefix"])))), {})
|
||||
finds.append(["불일치", g, pf["item"], pf["reason"], f"예외 승인: {ex.get('approved', '-')}"])
|
||||
for item in gates[g]["unplanned_fails"]:
|
||||
finds.append(["확인 필요", g, item, "계획되지 않은 FAIL", "게이트 미승인 사유"])
|
||||
for r in d["diffs"]:
|
||||
finds.append(["확인 필요", "-", ".".join([r["file"]] + r["path"]),
|
||||
f"도면 표 값 {json.dumps(r['candidate'], ensure_ascii=False)} ≠ 입력값 {json.dumps(r['current'], ensure_ascii=False)}",
|
||||
r["src"]])
|
||||
L += ["## 2. 도면 오류·불일치", "",
|
||||
"자동 등재 기준: 승인된 계획 FAIL → '불일치', 계획되지 않은 FAIL과 도면 표 값·입력값 차이 → '확인 필요'.", ""]
|
||||
L += (_table(["판정", "게이트", "항목", "내용", "근거"], finds) if finds else ["자동으로 찾은 항목이 없습니다."]) + [""]
|
||||
|
||||
# 3-5 human chapters with automatic hints
|
||||
L += ["## 3. 개선방안", "", "[담당자 작성] 2장 항목별로 도면 수정·설계자 확인 방안을 적습니다.", "",
|
||||
"## 4. 추가되어야 할 사항", "", "[담당자 작성] 도면에 없어 추정·가정으로 모델링한 사항(입력 묶음의 `[추론]` 표기)을 정리합니다.", ""]
|
||||
inferred = sorted({k for k, v in d["params"].items() if "[추론]" in json.dumps(v, ensure_ascii=False)})
|
||||
if inferred:
|
||||
L += [f"- 자동 확인: `[추론]` 표기가 있는 입력 묶음 — {', '.join(inferred)}", ""]
|
||||
L += ["## 5. 누락사항", "", "[담당자 작성] 도면에서 빠진 치수·표·상세를 적습니다.", ""]
|
||||
nomodel = [c["target"][9:] for c in d["schedule"] if c["target"][9:] not in d["params"].get("rebar_marks", {})]
|
||||
if nomodel:
|
||||
L += [f"- 자동 확인: 재료표에 있으나 모델 대상이 아닌 마크 — {', '.join(sorted(set(nomodel)))} (범위 결정 확인)", ""]
|
||||
|
||||
# 6 schedule vs model
|
||||
bc = _bar_counts(d["results"])
|
||||
rows, seen = [], set()
|
||||
for c in d["schedule"]:
|
||||
m, v = c["target"][9:], c["value"]
|
||||
if m in seen:
|
||||
continue
|
||||
seen.add(m)
|
||||
mc = bc.get(m)
|
||||
if m not in d["params"].get("rebar_marks", {}):
|
||||
model, judge = "모델 외", "-"
|
||||
elif not mc:
|
||||
model, judge = "미검사", "-" # its rebar gate has not run (or is void)
|
||||
else:
|
||||
model, judge = mc[0], ("일치" if mc[0] == v["n"] else "불일치")
|
||||
rows.append([m, v["shape"] or "-", v["dia"] or "-", _n(v["n"]), _n(v["L"]), model, judge, c["src"]])
|
||||
L += ["## 6. 철근재료표 (원본 vs 모델)", "",
|
||||
"원본 = 도면 재료표 판독값(동일 기호 \" 해석 포함), 모델 = 게이트 검사의 본수 실측값.", ""]
|
||||
L += (_table(["마크", "형식", "직경", "재료표 수량", "재료표 L(mm)", "모델 본수", "대조", "근거"], rows)
|
||||
if rows else ["도면 재료표를 인식하지 못했습니다(도면 미등록 또는 표 형식 상이)."]) + [""]
|
||||
|
||||
# 7 rebar detail (records)
|
||||
L += ["## 7. 철근상세 (기록)", "", "원길이·곡선길이·피복은 판정이 아닌 기록 값입니다(요구사항 §6.2).", ""]
|
||||
for g, part in REBAR_GATES.items():
|
||||
res = d["results"].get(g)
|
||||
if not res:
|
||||
continue
|
||||
recs = [[r["item"], r.get("detail", "")] for r in res["results"]
|
||||
if r["result"] == "INFO" and ("length" in r["item"] or "cover" in r["item"])]
|
||||
L += [f"### 7.{list(REBAR_GATES).index(g) + 1} {part} ({g})", ""] + _table(["항목", "값"], recs) + [""]
|
||||
|
||||
# 8 verification method
|
||||
rows = []
|
||||
for g in done:
|
||||
r = gates[g]
|
||||
n = r["negative"]
|
||||
ok = lambda v, yes, no: "생략(일괄 실행)" if v is None else (yes if v else no)
|
||||
rows.append([g, ok(r["reload"]["ok"], "일치", "불일치"),
|
||||
" / ".join(f"{'수평' if k == 'negH' else '수직'} {'검출' if v['detected'] else '미검출'}" for k, v in n.items()),
|
||||
ok(r["unchanged"]["ok"], f"불변 ({r['unchanged']['compared']})", f"변경 ({r['unchanged']['compared']})")])
|
||||
L += ["## 8. 검증 방법", "",
|
||||
"- 검사기는 생성 입력(params)이 아니라 도면 문자·표에서 옮긴 독립 기대값(expected)만 읽습니다.",
|
||||
"- 게이트마다 G1부터 새로 생성 → 저장 파일을 다시 열어 검사(재로드) → 객체를 수평·수직으로 옮겨 FAIL이 나는지 확인(음성 시험) → 앞 게이트 객체 지문 비교.",
|
||||
"- 일괄 실행(단일 Rhino 세션)으로 검사한 게이트는 중간 저장·재로드와 앞 게이트 비교를 하지 않으며, 표에 '생략(일괄 실행)'으로 적습니다. 모델은 마지막 G5만 저장합니다.",
|
||||
""] + _table(["게이트", "재로드", "음성 시험", "앞 게이트 객체"], rows) + [""]
|
||||
ac = (d.get("autocheck") or {}).get("sections") or {}
|
||||
if ac:
|
||||
names = {"g": "일치", "w": "주의", "b": "불일치", "n": "근거 없음", "x": "해당 없음"}
|
||||
rows = []
|
||||
for b, r in sorted(ac.items(), key=lambda kv: ("gwbnx".index(kv[1]["st"]) if kv[1]["st"] in "gwbnx" else 9, kv[0])):
|
||||
if r["st"] == "x" and b.split(":")[1].startswith("_"):
|
||||
continue
|
||||
c = d["confirm"].get(b) or {}
|
||||
human = f"{c['by']} {c['at']}" + (f" (앱 확인 {names.get(c.get('app_st'), '-')}, 사유: {c['why']})" if c.get("why") else "") if c else "미확정"
|
||||
rows.append([b, names.get(r["st"], r["st"]), r["summary"][:90], human])
|
||||
n = {k: sum(1 for r in ac.values() if r["st"] == k) for k in "gwbnx"}
|
||||
L += ["### 8.1 입력 묶음 앱 확인 결과 요약", "",
|
||||
f"- 앱 확인(검사 버전 {d['autocheck'].get('version', '-')}, {d['autocheck'].get('at', '-')}): 일치 {n['g']} · 주의 {n['w']} · 불일치 {n['b']} · 근거 없음 {n['n']} · 해당 없음 {n['x']} (전체 묶음 {len(ac)}개).",
|
||||
"- 앱 확인은 도면 표·재료표 수량·고정 항목을 앱이 대조한 결과입니다. '일치'는 독립 근거와 값이 같다는 뜻이며 도면이 맞다는 보증이 아닙니다. 확정은 사람이 했고, 앱 확인이 일치가 아닌 묶음은 사유가 기록되어 있습니다.",
|
||||
""] + _table(["묶음", "앱 확인", "근거", "사람 확정"], rows) + [""]
|
||||
L += ["## 변경 이력", "", "| 버전 | 일자 | 내용 |", "| :--- | :--- | :--- |", f"| v0.1.00 | {today} | 앱 자동 생성 초안 |", ""]
|
||||
return "\n".join(L), d
|
||||
|
||||
|
||||
def check(md, d):
|
||||
"""Report self-check (pattern of check_report.py): chapters, vocabulary, schedule rows, gate table."""
|
||||
need = ["## 1. 개요", "## 2. 도면 오류·불일치", "## 3. 개선방안", "## 4. 추가되어야 할 사항", "## 5. 누락사항",
|
||||
"## 6. 철근재료표", "## 7. 철근상세", "## 8. 검증 방법"]
|
||||
res = {"chapters_missing": [h for h in need if h not in md]}
|
||||
sec2 = md.split("## 2.")[1].split("## 3.")[0]
|
||||
words = re.findall(r"^\| ([^|]+?) \| (?:G\d|-) \|", sec2, re.M)
|
||||
res["vocab_bad"] = [w for w in words if w not in VOCAB]
|
||||
res["finding_rows"] = len(words)
|
||||
res["finding_rows_without_source"] = len([l for l in sec2.splitlines() if re.match(r"^\| (오류|불일치|누락|확인 필요) \|", l)
|
||||
and l.rstrip().endswith("| |")])
|
||||
marks = {c["target"][9:] for c in d["schedule"]}
|
||||
sec6 = md.split("## 6.")[1].split("## 7.")[0]
|
||||
res["schedule_rows"] = len([l for l in sec6.splitlines() if re.match(r"^\| [A-Z]{1,2}\d", l)])
|
||||
res["schedule_marks"] = len(marks)
|
||||
res["gate_rows"] = len(re.findall(r"^\| G\d \|", md.split("### 1.2")[1].split("## 2.")[0], re.M)) if "### 1.2" in md else 0
|
||||
# a pier with rebar marks must have its schedule read (Ulsan 2026-10-13: 0 of 0 rows passed silently)
|
||||
res["schedule_unread"] = bool(d["params"].get("rebar_marks")) and not marks
|
||||
res["ok"] = (not res["chapters_missing"] and not res["vocab_bad"] and res["schedule_rows"] == res["schedule_marks"]
|
||||
and not res["schedule_unread"] and res["gate_rows"] == 5 and res["finding_rows_without_source"] == 0)
|
||||
return res
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Write the review report draft (.md canonical + .pdf rendition + self-check json) into a run folder.
|
||||
|
||||
The PDF renderer is the one used for the Ungok P1 review report (Code/교각모델링/md_to_pdf.py: Malgun Gothic,
|
||||
U+2212 replaced, column min width = longest token); it is imported, not copied.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from engine import report
|
||||
|
||||
LEGACY = Path(__file__).resolve().parents[2] / "교각모델링"
|
||||
|
||||
|
||||
def write(pier_dir, run_dir, author="", stale=(), out_of_scope=()):
|
||||
sys.path.insert(0, str(LEGACY))
|
||||
import md_to_pdf # noqa: E402 (legacy renderer)
|
||||
today = time.strftime("%Y-%m-%d")
|
||||
md, d = report.build(pier_dir, run_dir, author, today, stale, out_of_scope)
|
||||
stem = f"보고서_{d['info']['name']}교각모델링검토_{today}_v0.1.00"
|
||||
run_dir = Path(run_dir)
|
||||
md_path, pdf_path = run_dir / f"{stem}.md", run_dir / f"{stem}.pdf"
|
||||
md_path.write_text(md, encoding="utf-8")
|
||||
md_to_pdf.build(md, pdf_path, f"{d['info']['name']} 교각 모델링 검토보고서 (초안)")
|
||||
chk = report.check(md, d)
|
||||
(run_dir / "report_check.json").write_text(json.dumps(chk, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
return {"md": md_path, "pdf": pdf_path, "check": chk}
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Review summary for the one-screen confirmation (2026-10-20): what changed in a bundle, where its values came from, and the reason text the app
|
||||
writes into the confirmation record. Pure functions (no I/O); the web layer (web/app.py `review`) supplies the data.
|
||||
|
||||
Why: confirming 20+ bundles one by one took too long, the reasons written under time pressure were empty ('12', 'asf'), and it was hard to see what
|
||||
changed. The confirmation stays a HUMAN act (nothing is confirmed without the person's ticks and name); the app only prepares what the person looks at
|
||||
and writes an informative reason from facts it knows (기획서 4.3 / 6.9).
|
||||
"""
|
||||
import json
|
||||
|
||||
from engine import verify
|
||||
|
||||
MAX_EXAMPLES = 3
|
||||
|
||||
|
||||
def _short(v):
|
||||
s = json.dumps(v, ensure_ascii=False) if not isinstance(v, (int, float, str)) else str(v)
|
||||
return s if len(s) <= 24 else s[:21] + "…"
|
||||
|
||||
|
||||
def bundle_change(value, origin_value):
|
||||
"""Change of a bundle against the pier's creation snapshot: {mode, n_changed, n_total, examples:[(path, old, new)], text}.
|
||||
mode 'none' when there is no snapshot (older piers), 'same' when nothing changed."""
|
||||
if origin_value is None:
|
||||
return {"mode": "none", "n_changed": None, "n_total": None, "examples": [], "text": "생성 시점 값 기록이 없어 무엇이 바뀌었는지 비교하지 못함"}
|
||||
old, new = dict(verify._walk(origin_value)), dict(verify._walk(value))
|
||||
paths = sorted(set(old) | set(new))
|
||||
diff = [p for p in paths if old.get(p) != new.get(p)]
|
||||
if not diff:
|
||||
return {"mode": "same", "n_changed": 0, "n_total": len(paths), "examples": [], "text": f"생성 시점 값 그대로({len(paths)}개 값)"}
|
||||
ex = [(".".join(p), old.get(p), new.get(p)) for p in diff[:MAX_EXAMPLES]]
|
||||
txt = f"생성 시점 대비 {len(diff)}/{len(paths)}개 값이 바뀜 — " + "; ".join(f"{p}: {_short(o)} → {_short(n)}" for p, o, n in ex) + (" …" if len(diff) > MAX_EXAMPLES else "")
|
||||
return {"mode": "changed", "n_changed": len(diff), "n_total": len(paths), "examples": ex, "text": txt}
|
||||
|
||||
|
||||
def provenance(which, key, read_log=None, anchor_log=None, candidates_log=None):
|
||||
"""Facts about where values of the bundle were written from (tools of the app) -> list of short sentences."""
|
||||
out = []
|
||||
pre = f"{which}:{key}."
|
||||
for e in read_log or []:
|
||||
n = sum(1 for f in e.get("fields", []) if f.startswith(pre))
|
||||
if n:
|
||||
out.append(f"도면에서 읽기 {n}건 반영 ({e.get('at', '')[:16]} {e.get('by') or '-'})")
|
||||
for e in anchor_log or []:
|
||||
n = sum(1 for f in e.get("fields", []) if isinstance(f, dict) and f.get("field", "").startswith(pre))
|
||||
if n:
|
||||
out.append(f"표 밖 치수 후보 {n}건 반영 ({e.get('at', '')[:16]})")
|
||||
return out
|
||||
|
||||
|
||||
def not_checked(checks, st=None):
|
||||
"""What the app did NOT verify, one sentence each: the checks that are 근거 없음 / 주의 / 불일치 (id + text); when none of the checks says so
|
||||
(a status that comes from 'counts only' or from the lack of any evidence) the general statement."""
|
||||
out = [f"{c['id']} {c['text']}" for c in checks if c["st"] in ("n", "w", "b")]
|
||||
if not out and st == "w":
|
||||
out = ["수량만 확인했고 위치·좌표·치수는 도면과 직접 대조하지 못함"]
|
||||
if not out and st == "n":
|
||||
out = ["앱이 독립적으로 대조할 근거가 없음 — 도면과 직접 대조"]
|
||||
return out
|
||||
|
||||
|
||||
def flags(item):
|
||||
"""Things the person should know before ticking a bundle in bulk (shown as chips, written into the reason): the bundle is still the template's
|
||||
original, or it carries a provisional registration that waits for the designer."""
|
||||
out = []
|
||||
if item["change"]["mode"] == "same" and item["st"] in ("n", "w"):
|
||||
out.append("템플릿 원본 그대로(새 도면 근거 없음)")
|
||||
if any("설계자 확인" in d for d in item.get("detail") or []):
|
||||
out.append("설계자 확인 전 임시 등록 포함")
|
||||
return out
|
||||
|
||||
|
||||
def group_of(st):
|
||||
"""review group: A 일치, B 주의·근거 없음, D 사람 결정 기록 / 참고, C 불일치 (cannot be confirmed)"""
|
||||
return {"g": "A", "w": "B", "n": "B", "x": "D", "b": "C"}.get(st, "B")
|
||||
|
||||
|
||||
def auto_reason(item, note=""):
|
||||
"""The reason text written into the confirmation record: facts the app knows + the person's note. Never empty."""
|
||||
st = item["st"]
|
||||
head = "[검토 요약 일괄 확정] "
|
||||
if st == "g":
|
||||
why = head + f"앱 확인 일치 — {item['summary']}"
|
||||
else:
|
||||
why = head + f"사용자가 도면과 대조했다고 표시함. 앱 확인 {verify.ST_NAME.get(st, st)} — {item['summary']}"
|
||||
if item["change"]["mode"] in ("changed", "same"):
|
||||
why += f" | 변경: {item['change']['text']}"
|
||||
if item["prov"]:
|
||||
why += " | 출처: " + "; ".join(item["prov"])
|
||||
if item.get("flags"):
|
||||
why += " | 주의 표시: " + "; ".join(item["flags"])
|
||||
if note.strip():
|
||||
why += f" | 의견: {note.strip()}"
|
||||
return why[:900]
|
||||
@@ -0,0 +1,110 @@
|
||||
"""G5 stage: bridge bearings (black boxes) and sole plates (wedges) on each h2 pad.
|
||||
|
||||
Run inside Rhino 8 via runner (globals PARAMS_PATH) after concrete.py.
|
||||
Ported from Code/교각모델링/build_bearing_plates_rhino.py (Ungok P1, model v0.1.09). Rules: 설계서 v0.1.10 §8. Reads params.json `bearing_seats`,
|
||||
`sole_plates`, `coping`. Bottom of the bearing = modelled pad top (= coping step + h1 + h2 = EL.'X' within 1 mm).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
import System
|
||||
from Rhino.DocObjects import ObjectAttributes, ObjectColorSource
|
||||
from System.Drawing import Color
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
TOL = 0.01
|
||||
PID = P["pier_id"]
|
||||
H3 = P["sole_plates"].get("h3") # D2 ELEVATION TABLE h3 (common value; a table row may override)
|
||||
|
||||
|
||||
def layer(path, color):
|
||||
parent = -1
|
||||
parts = path.split("::")
|
||||
for i in range(1, len(parts) + 1):
|
||||
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
|
||||
if idx < 0:
|
||||
lay = Rhino.DocObjects.Layer()
|
||||
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
|
||||
if parent >= 0:
|
||||
lay.ParentLayerId = doc.Layers[parent].Id
|
||||
idx = doc.Layers.Add(lay)
|
||||
parent = idx
|
||||
return parent
|
||||
|
||||
|
||||
def corners(cx, cy, along, across, rot_deg):
|
||||
"""Plan corners, `along` on +Y rotated clockwise by rot_deg; order: (-a,-c), (+a,-c), (+a,+c), (-a,+c)."""
|
||||
t = math.radians(rot_deg)
|
||||
ua, uc = (math.sin(t), math.cos(t)), (math.cos(t), -math.sin(t))
|
||||
out = []
|
||||
for sa, sc in ((-1, -1), (1, -1), (1, 1), (-1, 1)):
|
||||
out.append((cx + sa * ua[0] * along / 2 + sc * uc[0] * across / 2,
|
||||
cy + sa * ua[1] * along / 2 + sc * uc[1] * across / 2, sa * along / 2))
|
||||
return out
|
||||
|
||||
|
||||
def hexa(bottom, top):
|
||||
pts = [rg.Point3d(*p) for p in bottom + top]
|
||||
b = rg.Brep.CreateFromBox(pts)
|
||||
if b is None or not b.IsSolid:
|
||||
raise RuntimeError("hexahedron failed")
|
||||
if b.SolidOrientation == rg.BrepSolidOrientation.Inward:
|
||||
b.Flip()
|
||||
return b
|
||||
|
||||
|
||||
def add(brep, key, oid, color, props):
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = layer(key, color)
|
||||
attr.Name = oid
|
||||
attr.ObjectColor = Color.FromArgb(*color)
|
||||
attr.ColorSource = ObjectColorSource.ColorFromObject
|
||||
attr.SetUserString("id", oid)
|
||||
for k, v in props.items():
|
||||
attr.SetUserString(k, str(v))
|
||||
if doc.Objects.AddBrep(brep, attr) == System.Guid.Empty:
|
||||
raise RuntimeError(f"{oid}: AddBrep failed")
|
||||
|
||||
|
||||
def main():
|
||||
for o in list(doc.Objects):
|
||||
if (o.Attributes.GetUserString("id") or "").startswith((f"{PID}-BB", f"{PID}-SP")):
|
||||
doc.Objects.Delete(o, True)
|
||||
sp = P["sole_plates"]
|
||||
if not sp["table"]: # 2026-10-13: an empty table silently produced no bearings
|
||||
raise RuntimeError("sole_plates.table is empty")
|
||||
steps = P["coping"].get("top_steps")
|
||||
bear = {b["no"]: b for b in P["bearing_seats"]["bearings"]}
|
||||
for r in sp["table"]:
|
||||
b = bear[r["no"]]
|
||||
cx, cy = b["centre"]
|
||||
rot = b["rotation_deg"]
|
||||
seat = b["seat_z"] if "seat_z" in b else steps[b["girder_line"] - 1]["z"] # seat_z: tapered form (Ulsan)
|
||||
z0 = seat + b["h1"] + b["h2"] # modelled pad top
|
||||
h3 = r.get("h3", H3) # per bearing when tabled
|
||||
if sp.get("bearing_size") == "AB": # bearing body A (along) x B (across) (Ulsan detail drawing)
|
||||
along, across, rule = b["A"], b["B"], "A x B (교량받침 상세)"
|
||||
else: # (L-100) x (W-100) per user 2026-10-08 (Ungok)
|
||||
along, across, rule = r["L"] - sp["bearing_shrink"], r["W"] - sp["bearing_shrink"], None
|
||||
cb = corners(cx, cy, along, across, rot)
|
||||
add(hexa([(x, y, z0) for x, y, _ in cb], [(x, y, z0 + h3) for x, y, _ in cb]),
|
||||
f"{PID}::교량받침", f"{PID}-BB{r['no']:02d}", tuple(sp["colours"]["bearing"]),
|
||||
dict(role="bridge bearing", no=r["no"], size=f"{along}x{across}x{h3}", el_x=b["el_x"],
|
||||
src=f"ELEVATION TABLE h3, {rule}" if rule else "D2 ELEVATION TABLE h3, (L-100)x(W-100) per user 2026-10-08"))
|
||||
# sole plate: bottom flat, top sloped S% along the pad long axis, centre thickness T (sign of S sets thick end)
|
||||
zb = z0 + h3
|
||||
cs = corners(cx, cy, r["L"], r["W"], rot)
|
||||
top = [(x, y, zb + r["T"] + r["S"] / 100 * s) for x, y, s in cs]
|
||||
add(hexa([(x, y, zb) for x, y, _ in cs], top), f"{PID}::솔플레이트", f"{PID}-SP{r['no']:02d}",
|
||||
tuple(sp["colours"]["sole_plate"]),
|
||||
dict(role="sole plate", no=r["no"], S=r["S"], L=r["L"], W=r["W"], T=r["T"],
|
||||
end_thick=f"{r['T'] - r['S'] / 100 * r['L'] / 2:.2f}/{r['T'] + r['S'] / 100 * r['L'] / 2:.2f}",
|
||||
src=sp.get("sole_plate_src", "D2 솔플레이트 상세 TABLE; slope along pad axis, thick end -Y [HITL]")))
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"bearings": len(sp["table"]), "sole_plates": len(sp["table"])}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Shared helpers for the gate checkers (imported inside Rhino 8).
|
||||
|
||||
Checkers read only EXPECTED_PATH (independent drawing values) and never the generator params.
|
||||
"""
|
||||
import json
|
||||
|
||||
from Rhino.DocObjects import ObjectEnumeratorSettings
|
||||
|
||||
|
||||
def load_expected(path):
|
||||
return json.load(open(path, encoding="utf-8"))
|
||||
|
||||
|
||||
def all_objects(doc):
|
||||
"""All live objects incl. hidden/locked (doc.Objects alone skips hidden ones)."""
|
||||
s = ObjectEnumeratorSettings()
|
||||
s.HiddenObjects = True
|
||||
s.LockedObjects = True
|
||||
return list(doc.Objects.GetObjectList(s))
|
||||
|
||||
|
||||
def chain(spec, base=0.0):
|
||||
"""Expand a dimension chain: start offsets (cumulative) then groups [[n, pitch], ...] from the last point."""
|
||||
pts, z = [], base
|
||||
for s in spec["start"]:
|
||||
z += s
|
||||
pts.append(z)
|
||||
for n, p in spec["groups"]:
|
||||
for _ in range(n):
|
||||
z += p
|
||||
pts.append(z)
|
||||
return pts
|
||||
|
||||
|
||||
def series(spec):
|
||||
"""Expand {start, pitch, n} (or a literal list) into a list of values."""
|
||||
if isinstance(spec, list):
|
||||
return list(spec)
|
||||
return [spec["start"] + spec["pitch"] * i for i in range(spec["n"])]
|
||||
|
||||
|
||||
def row(spec):
|
||||
"""Bar positions: two end bars plus a regular series."""
|
||||
return sorted(list(spec["ends"]) + series(spec))
|
||||
|
||||
|
||||
def ychain(spec, half):
|
||||
"""Section chain measured from the -Y face: listed first bars (absolute), then n x pitch from the last of
|
||||
them, then the last bar at `end`; returned relative to the section centre (minus `half`)."""
|
||||
pts = list(spec["start"])
|
||||
pts += [pts[-1] + spec["pitch"] * k for k in range(1, spec["n"] + 1)] + [spec["end"]]
|
||||
return [y - half for y in sorted(pts)]
|
||||
|
||||
|
||||
def write_result(path, results, extra=None):
|
||||
summ = dict(total=len([r for r in results if r["result"] != "INFO"]),
|
||||
passed=sum(r["result"] == "PASS" for r in results),
|
||||
failed=sum(r["result"] == "FAIL" for r in results))
|
||||
summ.update(extra or {})
|
||||
json.dump(dict(summary=summ, results=results), open(path, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
||||
print(json.dumps(summ, ensure_ascii=False))
|
||||
return summ
|
||||
@@ -0,0 +1,217 @@
|
||||
"""G1 gate checker, form "tapered T with shared pedestals": piles and plan skew optional (unified 2026-10-15 from g1_skew.py,
|
||||
Okseong P1, and this file, Saejae-1 P1): lean, footing, column, coping (straight soffit, stepped top, side taper), shared pedestals
|
||||
with a sloped top, pads. G1 expected "piles" absent = spread footing (no pile checks); coping.skew_deg 0 = rectangular plan.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G1) come from dimension / EL texts and
|
||||
the bearing tables; the skew angle and the pedestal outlines are drawn values ([작도]). Generator params are not read.
|
||||
Checks: C5 population, C2 closed solids, C1 extents (lean, footing, column, piles), C4 coping (front section at Y=0,
|
||||
side width at X=0, skewed end faces), C3 joints / overlaps, C6 volumes, C7 pedestals / pads.
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, TOL, G = X["pier_id"], X["tol_mm"], X["G1"]
|
||||
F, LN, CO, C, PI = G["footing"], G["lean"], G["column"], G["coping"], G.get("piles")
|
||||
FT_TOP = F["z0"] + F["thickness"]
|
||||
ZB = FT_TOP + CO["height"] # column top = coping bottom
|
||||
ZT = ZB + C["soffit_rise"]
|
||||
FLAT, TIP, W, WB = C["bottom_flat_half"], C["tip_half"], C["width_y"] / 2, C["side_bottom_width"] / 2
|
||||
K = math.tan(math.radians(C["skew_deg"])) # [작도]
|
||||
EDGES = [-TIP] + C["step_bounds"] + [TIP] # top step boundaries at Y = 0 (front dimension chain)
|
||||
STEPS = C["step_z"] # step levels (EL.'X' - h1 - h2 of the bearing rows)
|
||||
results = []
|
||||
|
||||
|
||||
def objs(prefix):
|
||||
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)]
|
||||
|
||||
|
||||
def brep(o):
|
||||
return o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry)
|
||||
|
||||
|
||||
def vol(b):
|
||||
return rg.VolumeMassProperties.Compute(b).Volume
|
||||
|
||||
|
||||
def bbox(b):
|
||||
return b.GetBoundingBox(True)
|
||||
|
||||
|
||||
def check(cid, name, expected, actual, tol=TOL):
|
||||
results.append(dict(check=cid, item=name, expected=round(expected, 3), actual=round(actual, 3),
|
||||
diff=round(actual - expected, 3), tol=tol, result="PASS" if abs(actual - expected) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(cid, name, ok, detail=""):
|
||||
results.append(dict(check=cid, item=name, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def top_z(x):
|
||||
for a, b, z in zip(EDGES, EDGES[1:], STEPS):
|
||||
if a <= x <= b:
|
||||
return z
|
||||
return None
|
||||
|
||||
|
||||
def bottom_z(x):
|
||||
ax = abs(x)
|
||||
return ZB if ax <= FLAT else ZB + C["soffit_rise"] * (ax - FLAT) / (TIP - FLAT)
|
||||
|
||||
|
||||
def coping_volume_expected():
|
||||
"""Shear keeps the volume: integrate the unsheared plan area (front x extent x side width) over z."""
|
||||
total, n = 0.0, 20000
|
||||
z1 = max(STEPS)
|
||||
dz = (z1 - ZB) / n
|
||||
for i in range(n):
|
||||
z = ZB + (i + 0.5) * dz
|
||||
if z < ZT:
|
||||
xh = FLAT + (TIP - FLAT) * (z - ZB) / (ZT - ZB)
|
||||
yh = WB + (W - WB) * (z - ZB) / (ZT - ZB)
|
||||
total += 2 * xh * 2 * yh * dz
|
||||
else:
|
||||
length = sum(b - a for a, b, s in zip(EDGES, EDGES[1:], STEPS) if s >= z)
|
||||
total += length * 2 * W * dz
|
||||
return total
|
||||
|
||||
|
||||
def inter_vol(a, b):
|
||||
r = rg.Brep.CreateBooleanIntersection(a, b, 0.01)
|
||||
return sum(vol(x) for x in r) if r else 0.0
|
||||
|
||||
|
||||
def main():
|
||||
ids = {k: f"{PID}-{k}" for k in ("LC", "FT", "CL", "CP", "PL")}
|
||||
counts = {k: len(objs(v)) for k, v in ids.items()}
|
||||
flag("C5", "object counts", counts == {"LC": 1, "FT": 1, "CL": 1, "CP": 1, "PL": PI["nx"] * PI["ny"] if PI else 0}, str(counts))
|
||||
lc, ft, cl, cp = (brep(objs(ids[k])[0]) for k in ("LC", "FT", "CL", "CP"))
|
||||
piles = [brep(o) for o in objs(ids["PL"])]
|
||||
allb = [lc, ft, cl, cp] + piles
|
||||
flag("C2", f"IsSolid all {len(allb)}", all(b.IsSolid for b in allb), f"{sum(b.IsSolid for b in allb)}/{len(allb)}")
|
||||
# C1 extents
|
||||
for key, b, dx, dy, z0, z1 in (("footing", ft, F["dx"], F["dy"], F["z0"], FT_TOP),
|
||||
("lean", lc, LN["dx"], LN["dy"], F["z0"] - LN["thickness"], F["z0"])):
|
||||
bb = bbox(b)
|
||||
check("C1", f"{key} X", dx, bb.Max.X - bb.Min.X)
|
||||
check("C1", f"{key} Y", dy, bb.Max.Y - bb.Min.Y)
|
||||
check("C1", f"{key} Zbot", z0, bb.Min.Z)
|
||||
check("C1", f"{key} Ztop", z1, bb.Max.Z)
|
||||
check("C1", f"{key} centre X", 0, (bb.Max.X + bb.Min.X) / 2)
|
||||
check("C1", f"{key} centre Y", 0, (bb.Max.Y + bb.Min.Y) / 2)
|
||||
bb = bbox(cl)
|
||||
check("C1", "column diameter X", CO["d"], bb.Max.X - bb.Min.X)
|
||||
check("C1", "column diameter Y", CO["d"], bb.Max.Y - bb.Min.Y)
|
||||
check("C1", "column Zbot", FT_TOP, bb.Min.Z)
|
||||
check("C1", "column Ztop", ZB, bb.Max.Z)
|
||||
xs = sorted({round((bbox(b).Min.X + bbox(b).Max.X) / 2, 1) for b in piles})
|
||||
ys = sorted({round((bbox(b).Min.Y + bbox(b).Max.Y) / 2, 1) for b in piles})
|
||||
if PI:
|
||||
flag("C1", f"pile grid {PI['nx']} x {PI['ny']}", len(xs) == PI["nx"] and len(ys) == PI["ny"], f"x={xs} y={ys}")
|
||||
if PI and xs and ys:
|
||||
check("C1", "pile edge X", PI["edge"], xs[0] + F["dx"] / 2)
|
||||
check("C1", "pile edge Y", PI["edge"], ys[0] + F["dy"] / 2)
|
||||
check("C1", "pile pitch X (worst)", 0, max(abs(b - a - PI["pitch"]) for a, b in zip(xs, xs[1:])))
|
||||
check("C1", "pile pitch Y (worst)", 0, max(abs(b - a - PI["pitch"]) for a, b in zip(ys, ys[1:])))
|
||||
pb = bbox(piles[0])
|
||||
check("C1", "pile diameter", PI["d"], pb.Max.X - pb.Min.X)
|
||||
check("C1", "pile length", PI["length"], pb.Max.Z - pb.Min.Z)
|
||||
check("C1", "pile head embedment", PI["head_embedment"], pb.Max.Z - F["z0"])
|
||||
# C4 coping: front section at Y = 0
|
||||
ok, curves, pts = rg.Intersect.Intersection.BrepPlane(cp, rg.Plane(rg.Point3d(0, 0, 0), rg.Vector3d.YAxis), 0.01)
|
||||
sec = rg.Curve.JoinCurves(curves, 0.1)
|
||||
flag("C4", "front section (Y=0) closed single loop", len(sec) == 1 and sec[0].IsClosed, f"{len(sec)} loop(s)")
|
||||
|
||||
def z_at(x, upper):
|
||||
line = rg.LineCurve(rg.Point3d(x, 0, 0), rg.Point3d(x, 0, 200000))
|
||||
zs = sorted(ev.PointA.Z for ev in rg.Intersect.Intersection.CurveCurve(sec[0], line, 0.01, 0.01))
|
||||
return zs[-1] if upper else zs[0]
|
||||
|
||||
for x in (0, FLAT - 1, -(FLAT - 1), 3500, -3500, TIP - 5, -(TIP - 5)):
|
||||
check("C4", f"bottom at X={x:+.0f}", bottom_z(x), z_at(x, False))
|
||||
for a, b in zip(EDGES, EDGES[1:]):
|
||||
x = (a + b) / 2
|
||||
check("C4", f"top step at X={x:+.0f}", top_z(x), z_at(x, True))
|
||||
bb = bbox(cp)
|
||||
check("C4", "coping Zbot", ZB, bb.Min.Z)
|
||||
check("C4", "coping Ztop (highest step)", max(STEPS), bb.Max.Z)
|
||||
# side width at X = 0 (side taper)
|
||||
ok, curves, pts = rg.Intersect.Intersection.BrepPlane(cp, rg.Plane(rg.Point3d(0, 0, 0), rg.Vector3d.XAxis), 0.01)
|
||||
ss = rg.Curve.JoinCurves(curves, 0.1)
|
||||
for zz in (ZB + 1, (ZB + ZT) / 2, ZT - 1, ZT + 500):
|
||||
hits = sorted(ev.PointA.Y for c in ss for ev in rg.Intersect.Intersection.CurveCurve(
|
||||
c, rg.LineCurve(rg.Point3d(0, -9000, zz), rg.Point3d(0, 9000, zz)), 0.01, 0.01))
|
||||
w = WB + (W - WB) * min(zz - ZB, ZT - ZB) / (ZT - ZB)
|
||||
check("C4", f"side width at Z={zz:.0f}", 2 * w, (hits[-1] - hits[0]) if hits else 0)
|
||||
# skewed end faces: points 3 mm inside / outside the sheared ends at y = +-(W - 50), z mid of the end face
|
||||
for sx in (-1, 1):
|
||||
for y in (-(W - 50), W - 50):
|
||||
zz = (ZT + top_z(sx * (TIP - 10))) / 2
|
||||
xe = sx * TIP - K * y
|
||||
ins = cp.IsPointInside(rg.Point3d(xe - sx * 3, y, zz), 0.01, False)
|
||||
out = not cp.IsPointInside(rg.Point3d(xe + sx * 3, y, zz), 0.01, False)
|
||||
flag("C4", f"skewed end {'left' if sx < 0 else 'right'} at y={y:+.0f} [작도 skew {C['skew_deg']}]", ins and out,
|
||||
f"x_end {xe:.1f}")
|
||||
# C3 joints / overlaps
|
||||
check("C3", "LC top = FT bottom", bbox(lc).Max.Z, bbox(ft).Min.Z, tol=0.01)
|
||||
check("C3", "FT top = CL bottom", bbox(ft).Max.Z, bbox(cl).Min.Z, tol=0.01)
|
||||
check("C3", "CL top = CP bottom", bbox(cl).Max.Z, bbox(cp).Min.Z, tol=0.01)
|
||||
check("C3", "overlap FT-CL (mm3)", 0, inter_vol(ft, cl), tol=1.0)
|
||||
check("C3", "overlap CL-CP (mm3)", 0, inter_vol(cl, cp), tol=1.0)
|
||||
check("C3", "overlap LC-piles (mm3)", 0, sum(inter_vol(lc, b) for b in piles), tol=1.0)
|
||||
if PI:
|
||||
emb = len(piles) * math.pi * (PI["d"] / 2) ** 2 * PI["head_embedment"]
|
||||
check("C3", "intended overlap FT-piles (mm3)", emb, sum(inter_vol(ft, b) for b in piles), tol=emb * 0.001)
|
||||
# C6 volumes
|
||||
fv = F["dx"] * F["dy"] * F["thickness"] / 1e9
|
||||
check("C6", "footing volume m3", fv, vol(ft) / 1e9, tol=fv * 0.001)
|
||||
lv = (LN["dx"] * LN["dy"] - (len(piles) * math.pi * (PI["d"] / 2) ** 2 if PI else 0)) * LN["thickness"] / 1e9
|
||||
check("C6", "lean volume m3 (minus pile holes)", lv, vol(lc) / 1e9, tol=lv * 0.001)
|
||||
cv_ = math.pi * (CO["d"] / 2000) ** 2 * CO["height"] / 1000
|
||||
check("C6", "column volume m3", cv_, vol(cl) / 1e9, tol=cv_ * 0.001)
|
||||
cpv = coping_volume_expected() / 1e9
|
||||
check("C6", "coping volume m3 (dimension outline, shear keeps volume)", cpv, vol(cp) / 1e9, tol=cpv * 0.001)
|
||||
# C7 shared pedestals and pads
|
||||
S = G["seats"]
|
||||
n0 = len(PID) + 3
|
||||
seats = {int(o.Attributes.GetUserString("id")[n0:]): brep(o) for o in objs(f"{PID}-BS")}
|
||||
pads = {int(o.Attributes.GetUserString("id")[n0:]): brep(o) for o in objs(f"{PID}-BP")}
|
||||
flag("C7", "pedestal / pad counts", len(seats) == S["n_pedestals"] and sorted(pads) == sorted(int(k) for k in S["table"]),
|
||||
f"pedestals={len(seats)} pads={len(pads)}")
|
||||
flag("C7", "IsSolid pedestals+pads", all(b.IsSolid for b in list(seats.values()) + list(pads.values())))
|
||||
for no_s, row in S["table"].items():
|
||||
no = int(no_s)
|
||||
elx, h1, h2, a, b = row
|
||||
x, y = S["pos"][no_s]
|
||||
p = pads[no]
|
||||
top = top_z(x + K * y) # back to the Y = 0 step of this bearing line
|
||||
check("C7", f"NO.{no} table EL.'X' - h1 - h2 = coping step", top, elx - h1 - h2)
|
||||
check("C7", f"NO.{no} pad top = EL.'X'", elx, bbox(p).Max.Z)
|
||||
pb = bbox(p) # plan centre: the sloped pad bottom shifts the volume centroid
|
||||
check("C7", f"NO.{no} pad centre X (bbox)", x, (pb.Min.X + pb.Max.X) / 2)
|
||||
check("C7", f"NO.{no} pad centre Y (bbox)", y, (pb.Min.Y + pb.Max.Y) / 2)
|
||||
ped = [s for s in seats.values() if s.IsPointInside(rg.Point3d(x, y, top + 1), 0.01, False)]
|
||||
flag("C7", f"NO.{no} on one pedestal", len(ped) == 1, f"{len(ped)}")
|
||||
if ped:
|
||||
line = rg.LineCurve(rg.Point3d(x, y, top - 10), rg.Point3d(x, y, top + 1000))
|
||||
_ok, _crv, hits = rg.Intersect.Intersection.CurveBrep(line, ped[0], 0.01)
|
||||
zs = [pt.Z for pt in hits]
|
||||
check("C7", f"NO.{no} pedestal top at the bearing = step + h1", top + h1, max(zs) if zs else 0)
|
||||
check("C7", f"NO.{no} pedestal bottom at the bearing = step", top, min(zs) if zs else 0)
|
||||
check("C7", f"NO.{no} overlap pad-pedestal (mm3)", 0, inter_vol(p, ped[0]), tol=1.0)
|
||||
for no, s in seats.items():
|
||||
check("C7", f"pedestal {no} overlap with coping (mm3)", 0, inter_vol(s, cp), tol=1.0)
|
||||
write_result(RESULT_PATH, results, dict(volumes_m3={"lean": round(vol(lc) / 1e9, 3), "footing": round(vol(ft) / 1e9, 3),
|
||||
"column": round(vol(cl) / 1e9, 3), "coping": round(vol(cp) / 1e9, 3),
|
||||
"pile_each": round(vol(piles[0]) / 1e9, 3) if piles else 0}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,264 @@
|
||||
"""G1 gate checker for the "single shaft" form (Ramp-A P1, 2026-10-13): drilled shaft, column, Y coping,
|
||||
pedestals, pads.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G1 of EXPECTED_PATH) come from
|
||||
drawing dimension texts / EL texts / tables; generator params are not read. Two inputs have no dimension text
|
||||
and are drawn values (marked "[작도]" in the item names): the plan end arc corner and the pedestal rotations.
|
||||
Checks: C5 population, C2 closed solids, C1 extents, C4 coping sections (front Y=0, side X=0, plan end),
|
||||
C3 joints and overlaps, C6 volumes (coping integrated from the dimension outline), C7 pedestals / pads.
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, TOL, G = X["pier_id"], X["tol_mm"], X["G1"]
|
||||
PI, CO, C = G["pile"], G["column"], G["coping"]
|
||||
results = []
|
||||
|
||||
# expected outline from the dimensions
|
||||
ZB = CO["z0"] + CO["length"] # column top = coping bottom
|
||||
ZT = ZB + C["soffit_rise"] # tip level (end face bottom)
|
||||
ZTL, ZTR = C["z_top_left"], C["z_top_right"]
|
||||
HB, HT = C["column_face_half"], C["bottom_length"] / 2
|
||||
TOPH, R = C["top_length"] / 2, C["soffit_radius"]
|
||||
WT, WB = C["width_y"] / 2, C["side_bottom_width"] / 2
|
||||
PCORNER = C["plan_end_corner_x"] # [작도]
|
||||
|
||||
|
||||
def soffit_centre():
|
||||
"""Centre of the R arc through (-HB, ZB) and (-HT, ZT), on the outer (lower-left) side."""
|
||||
ax, az, bx, bz = -HB, ZB, -HT, ZT
|
||||
mx, mz = (ax + bx) / 2, (az + bz) / 2
|
||||
d = math.hypot(bx - ax, bz - az)
|
||||
h = math.sqrt(R * R - d * d / 4)
|
||||
ux, uz = (bz - az) / d, -(bx - ax) / d # unit normal of the chord
|
||||
c1, c2 = (mx + h * ux, mz + h * uz), (mx - h * ux, mz - h * uz)
|
||||
return min(c1, c2, key=lambda c: c[1]) # the centre below the chord
|
||||
|
||||
|
||||
CX, CZ = soffit_centre()
|
||||
|
||||
|
||||
def soffit_x(z):
|
||||
"""|x| of the soffit at height z (ZB..ZT)."""
|
||||
return abs(CX + math.sqrt(R * R - (z - CZ) ** 2))
|
||||
|
||||
|
||||
def plan_arc(y):
|
||||
sag = HT - PCORNER
|
||||
rr = (WT * WT + sag * sag) / (2 * sag)
|
||||
return HT - (rr - math.sqrt(rr * rr - y * y))
|
||||
|
||||
|
||||
def end_x(y, z, zt):
|
||||
"""|x| of the ruled end face at (y, z): plan arc at ZT, straight top edge TOPH at zt."""
|
||||
t = (z - ZT) / (zt - ZT)
|
||||
return plan_arc(y) + t * (TOPH - plan_arc(y))
|
||||
|
||||
|
||||
def objs(prefix):
|
||||
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)]
|
||||
|
||||
|
||||
def brep(o):
|
||||
return o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry)
|
||||
|
||||
|
||||
def vol(b):
|
||||
return rg.VolumeMassProperties.Compute(b).Volume
|
||||
|
||||
|
||||
def bbox(b):
|
||||
return b.GetBoundingBox(True)
|
||||
|
||||
|
||||
def check(cid, name, expected, actual, tol=TOL):
|
||||
results.append(dict(check=cid, item=name, expected=round(expected, 3), actual=round(actual, 3),
|
||||
diff=round(actual - expected, 3), tol=tol, result="PASS" if abs(actual - expected) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(cid, name, ok, detail=""):
|
||||
results.append(dict(check=cid, item=name, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def coping_volume_expected():
|
||||
"""Sum over z slices and y strips of the x extent: soffit (below ZT) limited by the end face continued
|
||||
below ZT, end face above ZT; left half up to ZTL, right half up to ZTR."""
|
||||
total, nz, ny = 0.0, 3000, 200
|
||||
z1 = max(ZTL, ZTR)
|
||||
dz = (z1 - ZB) / nz
|
||||
for i in range(nz):
|
||||
z = ZB + (i + 0.5) * dz
|
||||
w = WB + (WT - WB) * (z - ZB) / (ZT - ZB) if z < ZT else WT
|
||||
dy = 2 * w / ny
|
||||
area = 0.0
|
||||
for zt in (ZTL, ZTR):
|
||||
if z > zt:
|
||||
continue
|
||||
for j in range(ny):
|
||||
y = -w + (j + 0.5) * dy
|
||||
xe = end_x(y, z, zt)
|
||||
area += (min(soffit_x(z), xe) if z < ZT else xe) * dy
|
||||
total += area * dz
|
||||
return total
|
||||
|
||||
|
||||
def main():
|
||||
pl = sorted(objs(f"{PID}-PL"), key=lambda o: o.Attributes.GetUserString("id"))
|
||||
counts = {k: len(objs(f"{PID}-{k}")) for k in ("LC", "FT", "CL", "CP", "PL")}
|
||||
flag("C5", "object counts", counts == {"LC": 0, "FT": 0, "CL": 1, "CP": 1, "PL": len(PI["segments"])}, str(counts))
|
||||
cl, cp = brep(objs(f"{PID}-CL")[0]), brep(objs(f"{PID}-CP")[0])
|
||||
pls = [brep(o) for o in pl]
|
||||
allb = pls + [cl, cp]
|
||||
flag("C2", f"IsSolid all {len(allb)}", all(b.IsSolid for b in allb), f"{sum(b.IsSolid for b in allb)}/{len(allb)}")
|
||||
|
||||
# C1 pile segments (bottom to top), column, coping extents
|
||||
z = PI["z_bottom"]
|
||||
for k, (s, b) in enumerate(zip(PI["segments"], pls), 1):
|
||||
bb = bbox(b)
|
||||
check("C1", f"pile segment {k} diameter X", s["d"], bb.Max.X - bb.Min.X)
|
||||
check("C1", f"pile segment {k} diameter Y", s["d"], bb.Max.Y - bb.Min.Y)
|
||||
check("C1", f"pile segment {k} Zbot", z, bb.Min.Z)
|
||||
check("C1", f"pile segment {k} Ztop", z + s["length"], bb.Max.Z)
|
||||
z += s["length"]
|
||||
check("C1", "pile top = EL text", CO["z0"], z)
|
||||
bb = bbox(cl)
|
||||
check("C1", "column diameter X", CO["d"], bb.Max.X - bb.Min.X)
|
||||
check("C1", "column diameter Y", CO["d"], bb.Max.Y - bb.Min.Y)
|
||||
check("C1", "column Zbot", CO["z0"], bb.Min.Z)
|
||||
check("C1", "column Ztop", ZB, bb.Max.Z)
|
||||
bb = bbox(cp)
|
||||
check("C1", "coping length X (bottom tips)", 2 * HT, bb.Max.X - bb.Min.X)
|
||||
check("C1", "coping width Y", 2 * WT, bb.Max.Y - bb.Min.Y)
|
||||
check("C1", "coping Zbot", ZB, bb.Min.Z)
|
||||
check("C1", "coping Ztop", max(ZTL, ZTR), bb.Max.Z)
|
||||
check("C1", "coping centre X", 0, (bb.Max.X + bb.Min.X) / 2)
|
||||
check("C1", "total height (pile bottom - coping top left)", G["total_height"], ZTL - PI["z_bottom"])
|
||||
|
||||
# C4 front section at Y = 0
|
||||
def section(plane):
|
||||
ok, curves, pts = rg.Intersect.Intersection.BrepPlane(cp, plane, 0.01)
|
||||
return rg.Curve.JoinCurves(curves, 0.1)
|
||||
|
||||
sec = section(rg.Plane(rg.Point3d(0, 0, 0), rg.Vector3d.YAxis))
|
||||
flag("C4", "front section closed single loop", len(sec) == 1 and sec[0].IsClosed, f"{len(sec)} loop(s)")
|
||||
|
||||
def hits(crv, a, b):
|
||||
return sorted(ev.PointA for ev in rg.Intersect.Intersection.CurveCurve(crv, rg.LineCurve(a, b), 0.01, 0.01))
|
||||
|
||||
def z_at(x, upper):
|
||||
zs = sorted(p.Z for p in hits(sec[0], rg.Point3d(x, 0, 0), rg.Point3d(x, 0, 200000)))
|
||||
return zs[-1] if upper else zs[0]
|
||||
|
||||
for x in (0, HB - 1, -(HB - 1)):
|
||||
check("C4", f"bottom at X={x:+.0f} (column top)", ZB, z_at(x, False))
|
||||
for x in (1500, -1500, 2500, -2500, 3500, -3500, HT - 5, -(HT - 5)):
|
||||
check("C4", f"soffit R{R:.0f} at X={x:+.0f}", CZ + math.sqrt(R * R - (abs(x) + CX) ** 2), z_at(x, False))
|
||||
for x in (-3000, -1000, 1000, 3000):
|
||||
check("C4", f"top at X={x:+.0f}", ZTL if x < 0 else ZTR, z_at(x, True))
|
||||
for x in (-(TOPH + HT) / 2, (TOPH + HT) / 2):
|
||||
zt = ZTL if x < 0 else ZTR
|
||||
check("C4", f"end face at X={x:+.0f} (top {TOPH:.0f} -> bottom {HT:.0f})",
|
||||
ZT + (zt - ZT) * (HT - abs(x)) / (HT - TOPH), z_at(x, True))
|
||||
check("C4", "top step at the pier centre (right - left)", C["top_step"], z_at(1, True) - z_at(-1, True))
|
||||
|
||||
# C4 side section at X = 1 (X = 0 lies on the top step face, 2026-10-13): width 2,000 at the column top ->
|
||||
# 2,200 at the tip level, constant above
|
||||
sec_s = section(rg.Plane(rg.Point3d(1, 0, 0), rg.Vector3d.XAxis))
|
||||
flag("C4", "side section closed single loop", len(sec_s) == 1 and sec_s[0].IsClosed, f"{len(sec_s)} loop(s)")
|
||||
for zz in (ZB + 1, ZB + 750, ZT - 1, ZT + 500):
|
||||
ys = [p.Y for p in hits(sec_s[0], rg.Point3d(1, -5000, zz), rg.Point3d(1, 5000, zz))]
|
||||
w = WB + (WT - WB) * min(zz - ZB, ZT - ZB) / (ZT - ZB)
|
||||
check("C4", f"side width at Z={zz:.0f}", 2 * w, max(ys) - min(ys) if ys else 0)
|
||||
|
||||
# C4 plan end (ruled face: convex arc at the tip level, straight top edge)
|
||||
for sx in (-1, 1):
|
||||
zt = ZTL if sx < 0 else ZTR
|
||||
for zz, y in ((ZT + 5, 0), (ZT + 5, WT - 10), ((ZT + zt) / 2, 0), ((ZT + zt) / 2, WT - 10), (zt - 5, WT - 10)):
|
||||
xe = end_x(y, zz, zt)
|
||||
ins = cp.IsPointInside(rg.Point3d(sx * (xe - 3), y, zz), 0.01, False)
|
||||
out = not cp.IsPointInside(rg.Point3d(sx * (xe + 3), y, zz), 0.01, False)
|
||||
flag("C4", f"end face {'left' if sx < 0 else 'right'} at y={y:.0f} z={zz:.0f} [작도 corner {PCORNER:.0f}]",
|
||||
ins and out, f"x_end {sx * xe:.1f}: inside 3 mm in {ins}, outside 3 mm out {out}")
|
||||
|
||||
# C3 joints and overlaps
|
||||
def inter_vol(a, b):
|
||||
r = rg.Brep.CreateBooleanIntersection(a, b, 0.01)
|
||||
return sum(vol(x) for x in r) if r else 0.0
|
||||
chain = pls + [cl, cp]
|
||||
names = [f"PL{k:02d}" for k in range(1, len(pls) + 1)] + ["CL", "CP"]
|
||||
for (na, a), (nb, b) in zip(zip(names, chain), zip(names[1:], chain[1:])):
|
||||
check("C3", f"{na} top = {nb} bottom", bbox(a).Max.Z, bbox(b).Min.Z, tol=0.01)
|
||||
check("C3", f"overlap {na}-{nb} (mm3)", 0, inter_vol(a, b), tol=1.0)
|
||||
|
||||
# C6 volumes
|
||||
for k, (s, b) in enumerate(zip(PI["segments"], pls), 1):
|
||||
v = math.pi * (s["d"] / 2000) ** 2 * s["length"] / 1000
|
||||
check("C6", f"pile segment {k} volume m3", v, vol(b) / 1e9, tol=v * 0.001)
|
||||
cv_ = math.pi * (CO["d"] / 2000) ** 2 * CO["length"] / 1000
|
||||
check("C6", "column volume m3", cv_, vol(cl) / 1e9, tol=cv_ * 0.001)
|
||||
cpv = coping_volume_expected() / 1e9
|
||||
check("C6", "coping volume m3 (dimension outline)", cpv, vol(cp) / 1e9, tol=cpv * 0.001)
|
||||
|
||||
# C7 pedestals and pads
|
||||
S = G["seats"]
|
||||
nos = sorted(int(k) for k in S["table"])
|
||||
n0 = len(PID) + 3
|
||||
seats = {int(o.Attributes.GetUserString("id")[n0:]): brep(o) for o in objs(f"{PID}-BS")}
|
||||
pads = {int(o.Attributes.GetUserString("id")[n0:]): brep(o) for o in objs(f"{PID}-BP")}
|
||||
flag("C7", "pedestal / pad counts", sorted(seats) == nos and sorted(pads) == nos, f"seats={len(seats)} pads={len(pads)}")
|
||||
allb = list(seats.values()) + list(pads.values())
|
||||
flag("C7", "IsSolid pedestals+pads", all(b.IsSolid for b in allb), f"{sum(b.IsSolid for b in allb)}/{len(allb)}")
|
||||
p_along, p_across = S["pedestal_along"], S["pedestal_across"]
|
||||
m0, m1 = S["pad_margin"]
|
||||
|
||||
def bottom_edges(b):
|
||||
zmin = min(v.Location.Z for v in b.Vertices)
|
||||
return [e.PointAtEnd - e.PointAtStart for e in b.Edges
|
||||
if abs(e.PointAtStart.Z - zmin) < 0.01 and abs(e.PointAtEnd.Z - zmin) < 0.01
|
||||
and e.PointAtStart.DistanceTo(e.PointAtEnd) > 1]
|
||||
|
||||
for no in nos:
|
||||
elx, h1, h2, a, b = S["table"][str(no)]
|
||||
x, y = S["pos"][str(no)]
|
||||
top = ZTL if x < 0 else ZTR
|
||||
s, p = seats[no], pads[no]
|
||||
sb, pb = bbox(s), bbox(p)
|
||||
check("C7", f"NO.{no} table EL.'X' - h1 - h2 = coping top ({'left' if x < 0 else 'right'})", top, elx - h1 - h2)
|
||||
check("C7", f"NO.{no} pedestal bottom = coping top", top, sb.Min.Z)
|
||||
check("C7", f"NO.{no} pedestal top = top + h1", top + h1, sb.Max.Z)
|
||||
check("C7", f"NO.{no} pad top = EL.'X'", elx, pb.Max.Z)
|
||||
ed = bottom_edges(s)
|
||||
al = [v for v in ed if abs(v.Y) > abs(v.X)]
|
||||
ac = [v for v in ed if abs(v.Y) <= abs(v.X)]
|
||||
check("C7", f"NO.{no} pedestal along (C3)", p_along, al[0].Length if al else 0)
|
||||
check("C7", f"NO.{no} pedestal across (C6)", p_across, ac[0].Length if ac else 0)
|
||||
if al:
|
||||
v = al[0] if al[0].Y > 0 else -al[0]
|
||||
check("C7", f"NO.{no} pedestal rotation deg [작도]", S["rotation_deg"][str(no)],
|
||||
math.degrees(math.atan2(v.X, v.Y)), tol=0.05)
|
||||
check("C7", f"NO.{no} pedestal centre X", x, (sb.Max.X + sb.Min.X) / 2)
|
||||
check("C7", f"NO.{no} pedestal centre Y", y, (sb.Max.Y + sb.Min.Y) / 2)
|
||||
c = rg.VolumeMassProperties.Compute(p).Centroid
|
||||
check("C7", f"NO.{no} pad centre X", x, c.X)
|
||||
check("C7", f"NO.{no} pad centre Y", y, c.Y)
|
||||
mm_ = (m0 + m1) / 2
|
||||
pv = h2 / 6 * ((a + m0) * (b + m0) + (a + m1) * (b + m1) + 4 * (a + mm_) * (b + mm_))
|
||||
check("C7", f"NO.{no} pad volume (prismatoid A,B)", pv, vol(p), tol=pv * 0.001)
|
||||
check("C7", f"NO.{no} overlap pedestal-coping (mm3)", 0, inter_vol(s, cp), tol=1.0)
|
||||
check("C7", f"NO.{no} overlap pad-pedestal (mm3)", 0, inter_vol(p, s), tol=1.0)
|
||||
corners = [rg.Point3d(q.X, q.Y, sb.Min.Z - 1) for q in [v.Location for v in s.Vertices] if abs(q.Z - sb.Min.Z) < 0.01]
|
||||
flag("C7", f"NO.{no} pedestal footprint on the coping top", all(cp.IsPointInside(q, 0.01, False) for q in corners),
|
||||
f"{len(corners)} corners tested 1 mm below")
|
||||
write_result(RESULT_PATH, results, dict(volumes_m3={**{f"pile{k}": round(vol(b) / 1e9, 3) for k, b in enumerate(pls, 1)},
|
||||
"column": round(vol(cl) / 1e9, 3), "coping": round(vol(cp) / 1e9, 3)}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,242 @@
|
||||
"""G1 gate checker: concrete, piles, pedestals and pads (checks C1-C7) inside Rhino 8.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Independent expected values from EXPECTED_PATH
|
||||
section G1 (drawing texts / DXF); generator params are not read.
|
||||
Ported from Code/교각모델링/verify_rhino.py (same checks and order; fingerprint part moved to the gate runner).
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, TOL_MM, G = X["pier_id"], X["tol_mm"], X["G1"]
|
||||
results = []
|
||||
|
||||
f, c_, cl_, pl_ = G["footing"], G["coping"], G["column"], G["piles"]
|
||||
FT_TOP = f["z0"] + f["thickness"]
|
||||
EXPECTED = {
|
||||
"footing": dict(dx=f["dx"], dy=f["dy"], z0=f["z0"], z1=FT_TOP),
|
||||
"lean": dict(dx=f["dx"] + 2 * G["lean"]["margin"], dy=f["dy"] + 2 * G["lean"]["margin"],
|
||||
z0=f["z0"] - G["lean"]["thickness"], z1=f["z0"]),
|
||||
"column": dict(d=cl_["d"], z0=FT_TOP, z1=FT_TOP + cl_["height"]),
|
||||
"coping": dict(dx=c_["dx"], dy=c_["dy"], z_bottom=c_["z_top_left"] - c_["rise_upper"] - c_["rise_lower"],
|
||||
z_tip_bottom=c_["z_top_left"] - c_["rise_upper"], z_top_left=c_["z_top_left"],
|
||||
z_top_right=c_["z_top_right"], z_top_center=c_["z_top_center"],
|
||||
flat_half=c_["dx"] / 2 - c_["flat_from_tip"], top_half=c_["dx"] / 2 - c_["end_inset"],
|
||||
radius=c_["radius"]),
|
||||
"piles": dict(nx=pl_["nx"], ny=pl_["ny"], pitch_x=pl_.get("pitch_x", pl_.get("pitch")),
|
||||
pitch_y=pl_.get("pitch_y", pl_.get("pitch")), edge=pl_["edge"], d=pl_["d"], length=pl_["length"]),
|
||||
}
|
||||
_a = G["dxf_arc_centres"]
|
||||
DXF_LEFT_ARC_CENTER = (_a["left"][0] - _a["datum_x"], _a["datum_el"] + (_a["left"][1] - _a["datum_y"]))
|
||||
DXF_RIGHT_ARC_CENTER = (_a["right"][0] - _a["datum_x"], _a["datum_el"] + (_a["right"][1] - _a["datum_y"]))
|
||||
|
||||
S = G["seats"]
|
||||
TABLE = {int(k): tuple(v) for k, v in S["table"].items()}
|
||||
NG = S["girders"]
|
||||
STEP_TOP = {int(k): v for k, v in S["step_top"].items()}
|
||||
STEP_X = {int(k): tuple(v) for k, v in S["step_x"].items()}
|
||||
ROW_Y = S["row_y"]
|
||||
PITCH = S["pitch_chain"][1] / S["pitch_chain"][0]
|
||||
|
||||
|
||||
def objs(prefix):
|
||||
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)]
|
||||
|
||||
|
||||
def brep(o):
|
||||
return o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry)
|
||||
|
||||
|
||||
def vol(b):
|
||||
return rg.VolumeMassProperties.Compute(b).Volume
|
||||
|
||||
|
||||
def bbox(b):
|
||||
return b.GetBoundingBox(True)
|
||||
|
||||
|
||||
def check(cid, name, expected, actual, tol=TOL_MM):
|
||||
ok = abs(expected - actual) <= tol
|
||||
results.append(dict(check=cid, item=name, expected=round(expected, 3), actual=round(actual, 3),
|
||||
diff=round(actual - expected, 3), tol=tol, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def flag(cid, name, ok, detail):
|
||||
results.append(dict(check=cid, item=name, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def check_seats(cp):
|
||||
n0 = len(PID) + 3
|
||||
seats = {int(o.Attributes.GetUserString("id")[n0:]): brep(o) for o in objs(f"{PID}-BS")}
|
||||
pads = {int(o.Attributes.GetUserString("id")[n0:]): brep(o) for o in objs(f"{PID}-BP")}
|
||||
nos = sorted(TABLE)
|
||||
flag("C7", "pedestal / pad counts", sorted(seats) == nos and sorted(pads) == nos,
|
||||
f"seats={len(seats)} pads={len(pads)}")
|
||||
allb = list(seats.values()) + list(pads.values())
|
||||
flag("C7", "IsSolid pedestals+pads", all(b.IsSolid for b in allb), f"{sum(b.IsSolid for b in allb)}/{2 * len(nos)}")
|
||||
m0, m1 = S["pad_margin"]
|
||||
pa, pc = S["pedestal_plan"]
|
||||
half_w = c_["dy"] / 2
|
||||
pad_x = {}
|
||||
for no, (elx, h1, h2, a, b) in TABLE.items():
|
||||
g = (no - 1) % NG + 1
|
||||
row = "front" if no <= NG else "rear"
|
||||
s, p = seats[no], pads[no]
|
||||
sb, pb = bbox(s), bbox(p)
|
||||
check("C7", f"NO.{no} pedestal bottom = step {g}", STEP_TOP[g], sb.Min.Z)
|
||||
check("C7", f"NO.{no} pedestal top = step + h1", STEP_TOP[g] + h1, sb.Max.Z)
|
||||
check("C7", f"NO.{no} pad top = EL.'X'", elx, pb.Max.Z)
|
||||
flag("C7", f"NO.{no} pedestal within step {g} / coping width",
|
||||
STEP_X[g][0] - TOL_MM <= sb.Min.X and sb.Max.X <= STEP_X[g][1] + TOL_MM
|
||||
and -half_w - TOL_MM <= sb.Min.Y and sb.Max.Y <= half_w + TOL_MM,
|
||||
f"x {sb.Min.X:.1f}..{sb.Max.X:.1f} y {sb.Min.Y:.1f}..{sb.Max.Y:.1f}")
|
||||
area = pa / 1000 * pc / 1000
|
||||
check("C7", f"NO.{no} pedestal half area (m2)", area, vol(s) / h1 / 1e6, tol=area * 0.002)
|
||||
mm_ = (m0 + m1) / 2
|
||||
pv = h2 / 6 * ((a + m0) * (b + m0) + (a + m1) * (b + m1) + 4 * (a + mm_) * (b + mm_))
|
||||
check("C7", f"NO.{no} pad volume (prismatoid A,B)", pv, vol(p), tol=pv * 0.001)
|
||||
c = rg.VolumeMassProperties.Compute(p).Centroid
|
||||
check("C7", f"NO.{no} pad centre Y (S1)", ROW_Y[row], c.Y, tol=2.0)
|
||||
pad_x.setdefault(row, {})[g] = c.X
|
||||
check("C7", f"NO.{no} overlap pedestal-coping (mm3)", 0,
|
||||
sum(vol(x) for x in (rg.Brep.CreateBooleanIntersection(s, cp, 0.01) or [])), tol=1.0)
|
||||
check("C7", f"NO.{no} overlap pad-pedestal (mm3)", 0,
|
||||
sum(vol(x) for x in (rg.Brep.CreateBooleanIntersection(p, s, 0.01) or [])), tol=1.0)
|
||||
for row, xs in pad_x.items():
|
||||
for g in range(1, NG):
|
||||
check("C7", f"{row} pad pitch girder {g}-{g + 1}", PITCH, xs[g + 1] - xs[g], tol=1.0)
|
||||
first = S["rear_first_from_tip"]
|
||||
for g in range(1, NG + 1):
|
||||
check("C7", f"rear pad centre X girder {g} (dim {first / 1000:.3f}+{PITCH / 1000:.3f}n)",
|
||||
-c_["dx"] / 2 + first + (g - 1) * PITCH, pad_x["rear"][g], tol=2.0)
|
||||
for row, rng in (("front", range(1, NG + 1)), ("rear", range(NG + 1, 2 * NG + 1))):
|
||||
cxs = [rg.VolumeMassProperties.Compute(seats[n]).Centroid.X for n in rng]
|
||||
for g in range(NG - 1):
|
||||
check("C7", f"{row} pedestal pitch girder {g + 1}-{g + 2}", PITCH, cxs[g + 1] - cxs[g], tol=1.0)
|
||||
|
||||
|
||||
def main():
|
||||
ids = {k: f"{PID}-{k}" for k in ("LC", "FT", "CL", "CP", "PL")}
|
||||
lc, ft, cl, cp = (brep(objs(ids[k])[0]) for k in ("LC", "FT", "CL", "CP"))
|
||||
piles = [brep(o) for o in objs(ids["PL"])]
|
||||
e = EXPECTED
|
||||
p = e["piles"]
|
||||
|
||||
counts = {ids[k]: len(objs(ids[k])) for k in ("LC", "FT", "CL", "CP", "PL")}
|
||||
exp_counts = {ids["LC"]: 1, ids["FT"]: 1, ids["CL"]: 1, ids["CP"]: 1, ids["PL"]: p["nx"] * p["ny"]}
|
||||
flag("C5", "object counts", counts == exp_counts, str(counts))
|
||||
|
||||
all_b = [lc, ft, cl, cp] + piles
|
||||
flag("C2", f"IsSolid all {len(all_b)}", all(b.IsSolid for b in all_b), f"{sum(b.IsSolid for b in all_b)}/{len(all_b)}")
|
||||
|
||||
for key, b in (("footing", ft), ("lean", lc)):
|
||||
bb = bbox(b)
|
||||
check("C1", f"{key} X", e[key]["dx"], bb.Max.X - bb.Min.X)
|
||||
check("C1", f"{key} Y", e[key]["dy"], bb.Max.Y - bb.Min.Y)
|
||||
check("C1", f"{key} Zbot", e[key]["z0"], bb.Min.Z)
|
||||
check("C1", f"{key} Ztop", e[key]["z1"], bb.Max.Z)
|
||||
check("C1", f"{key} centre X", 0, (bb.Max.X + bb.Min.X) / 2)
|
||||
check("C1", f"{key} centre Y", 0, (bb.Max.Y + bb.Min.Y) / 2)
|
||||
bb = bbox(cl)
|
||||
check("C1", "column diameter X", e["column"]["d"], bb.Max.X - bb.Min.X)
|
||||
check("C1", "column diameter Y", e["column"]["d"], bb.Max.Y - bb.Min.Y)
|
||||
check("C1", "column Zbot", e["column"]["z0"], bb.Min.Z)
|
||||
check("C1", "column Ztop", e["column"]["z1"], bb.Max.Z)
|
||||
bb = bbox(cp)
|
||||
check("C1", "coping length X", e["coping"]["dx"], bb.Max.X - bb.Min.X)
|
||||
check("C1", "coping width Y", e["coping"]["dy"], bb.Max.Y - bb.Min.Y)
|
||||
check("C1", "coping Zbot", e["coping"]["z_bottom"], bb.Min.Z)
|
||||
check("C1", "coping Ztop (right end)", e["coping"]["z_top_right"], bb.Max.Z)
|
||||
xs = sorted({round((bbox(b).Min.X + bbox(b).Max.X) / 2, 1) for b in piles})
|
||||
ys = sorted({round((bbox(b).Min.Y + bbox(b).Max.Y) / 2, 1) for b in piles})
|
||||
flag("C1", "pile grid", len(xs) == p["nx"] and len(ys) == p["ny"], f"x={xs} y={ys}")
|
||||
check("C1", "pile edge X", p["edge"], xs[0] - (-e["footing"]["dx"] / 2))
|
||||
check("C1", "pile edge Y", p["edge"], ys[0] - (-e["footing"]["dy"] / 2))
|
||||
for a, b2 in zip(xs, xs[1:]):
|
||||
check("C1", "pile pitch X", p["pitch_x"], b2 - a)
|
||||
for a, b2 in zip(ys, ys[1:]):
|
||||
check("C1", "pile pitch Y", p["pitch_y"], b2 - a)
|
||||
pb = bbox(piles[0])
|
||||
check("C1", "pile diameter", p["d"], pb.Max.X - pb.Min.X)
|
||||
check("C1", "pile length", p["length"], pb.Max.Z - pb.Min.Z)
|
||||
check("C1", "pile head embedment", pl_["head_embedment"], pb.Max.Z - e["footing"]["z0"])
|
||||
|
||||
# C4 coping front section at Y=0 (re-projection to drawing)
|
||||
plane = rg.Plane(rg.Point3d(0, 0, 0), rg.Vector3d.YAxis)
|
||||
ok, curves, pts = rg.Intersect.Intersection.BrepPlane(cp, plane, 0.01)
|
||||
sec = rg.Curve.JoinCurves(curves, 0.1)
|
||||
flag("C4", "section closed single loop", len(sec) == 1 and sec[0].IsClosed, f"{len(sec)} loop(s)")
|
||||
c = e["coping"]
|
||||
|
||||
def z_at(x, upper):
|
||||
line = rg.LineCurve(rg.Point3d(x, 0, 0), rg.Point3d(x, 0, 60000))
|
||||
zs = sorted(ev.PointA.Z for ev in rg.Intersect.Intersection.CurveCurve(sec[0], line, 0.01, 0.01))
|
||||
return zs[-1] if upper else zs[0]
|
||||
|
||||
sx = {k: (v[0] + v[1]) / 2 for k, v in STEP_X.items()} # sample each top step at its middle
|
||||
check("C4", "bottom flat at X=0", c["z_bottom"], z_at(0, False))
|
||||
check("C4", f"bottom flat at X=+{c['flat_half'] - 1:.0f}", c["z_bottom"], z_at(c["flat_half"] - 1, False))
|
||||
check("C4", f"bottom at X=-{c['top_half'] + 199:.0f} (tip)", c["z_tip_bottom"], z_at(-c["top_half"] - 199, False), tol=2.0)
|
||||
check("C4", f"top left step (X={sx[1]:.0f})", c["z_top_left"], z_at(sx[1], True))
|
||||
check("C4", "top centre step (X=0)", c["z_top_center"], z_at(0, True))
|
||||
check("C4", f"top right step (X={sx[NG]:.0f})", c["z_top_right"], z_at(sx[NG], True))
|
||||
worst = 0.0
|
||||
step = (c["dx"] / 2 - c["flat_half"] - 200) / 50
|
||||
for side, (cx, cz) in ((1, DXF_RIGHT_ARC_CENTER), (-1, DXF_LEFT_ARC_CENTER)):
|
||||
for k in range(1, 50):
|
||||
x = side * (c["flat_half"] + 100 + k * step)
|
||||
zb = z_at(x, False)
|
||||
worst = max(worst, abs(math.hypot(x - cx, zb - cz) - c["radius"]))
|
||||
check("C4", "soffit radius max deviation vs DXF ARC centre (98 samples)", 0, worst)
|
||||
|
||||
# C3 joints and overlaps
|
||||
def inter_vol(a, b):
|
||||
r = rg.Brep.CreateBooleanIntersection(a, b, 0.01)
|
||||
return sum(vol(x) for x in r) if r else 0.0
|
||||
|
||||
check("C3", "LC top = FT bottom", bbox(lc).Max.Z, bbox(ft).Min.Z, tol=0.01)
|
||||
check("C3", "FT top = CL bottom", bbox(ft).Max.Z, bbox(cl).Min.Z, tol=0.01)
|
||||
check("C3", "CL top = CP bottom", bbox(cl).Max.Z, bbox(cp).Min.Z, tol=0.01)
|
||||
check("C3", "overlap LC-FT (mm3)", 0, inter_vol(lc, ft), tol=1.0)
|
||||
check("C3", "overlap FT-CL (mm3)", 0, inter_vol(ft, cl), tol=1.0)
|
||||
check("C3", "overlap CL-CP (mm3)", 0, inter_vol(cl, cp), tol=1.0)
|
||||
check("C3", "overlap LC-piles (mm3)", 0, sum(inter_vol(lc, b) for b in piles), tol=1.0)
|
||||
emb = len(piles) * math.pi * (p["d"] / 2) ** 2 * pl_["head_embedment"]
|
||||
check("C3", "intended overlap FT-piles (mm3)", emb, sum(inter_vol(ft, b) for b in piles), tol=emb * 0.001)
|
||||
flag("C3", "column within coping flat zone", e["column"]["d"] / 2 <= c["flat_half"]
|
||||
and e["column"]["d"] / 2 <= c["dy"] / 2,
|
||||
f"r={e['column']['d'] / 2:.0f} <= {c['flat_half']:.0f} (X), {c['dy'] / 2:.0f} (Y)")
|
||||
|
||||
# C6 volumes vs analytic formulas
|
||||
fv = f["dx"] / 1000 * f["dy"] / 1000 * f["thickness"] / 1000
|
||||
check("C6", "footing volume m3", fv, vol(ft) / 1e9, tol=fv * 0.001)
|
||||
cv_ = math.pi * (cl_["d"] / 2000) ** 2 * cl_["height"] / 1000
|
||||
check("C6", "column volume m3", cv_, vol(cl) / 1e9, tol=round(cv_, 2) * 0.001)
|
||||
lv = (e["lean"]["dx"] / 1000 * e["lean"]["dy"] / 1000 * G["lean"]["thickness"] / 1000
|
||||
- len(piles) * math.pi * (p["d"] / 2000) ** 2 * G["lean"]["thickness"] / 1000)
|
||||
check("C6", "lean volume m3", lv, vol(lc) / 1e9, tol=lv * 0.001)
|
||||
poly = [tuple(q) for q in G["coping_outline"]]
|
||||
area = 0.5 * abs(sum(x1 * y2 - x2 * y1 for (x1, y1), (x2, y2) in zip(poly, poly[1:] + poly[:1])))
|
||||
(x1, z1), (x2, z2) = poly[1], poly[2]
|
||||
chord = math.hypot(x2 - x1, z2 - z1)
|
||||
theta = 2 * math.asin(chord / 2 / c["radius"])
|
||||
seg = c["radius"] ** 2 / 2 * (theta - math.sin(theta))
|
||||
cv = (area - 2 * seg) * c["dy"] / 1e9
|
||||
check("C6", "coping volume m3", cv, vol(cp) / 1e9, tol=cv * 0.001)
|
||||
|
||||
if objs(f"{PID}-BS") or objs(f"{PID}-BP"):
|
||||
check_seats(cp)
|
||||
|
||||
write_result(RESULT_PATH, results, dict(
|
||||
volumes_m3={k: round(vol(b) / 1e9, 3) for k, b in (("lean", lc), ("footing", ft), ("column", cl), ("coping", cp))},
|
||||
pile_volume_each_m3=round(vol(piles[0]) / 1e9, 3)))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,257 @@
|
||||
"""G1 gate checker for the "tapered T" form (Ulsan P1, 2026-10-13): footing, lean, column, coping, pedestals, pads.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G1 of EXPECTED_PATH) come from
|
||||
drawing dimension texts / EL texts / tables; generator params are not read.
|
||||
Optional (Ulsan P2, 2026-10-15; absent -> the checks are unchanged): G1.footing.rot_deg (foundation drawn in its own axes, rotated
|
||||
counter-clockwise about the pier centre; lean / footing / piles are checked after rotating them back and their rotated plan extents are
|
||||
compared with the closed form), G1.piles {nx, ny, pitch_x, pitch_y, edge_x, edge_y, d, length, head_embedment}.
|
||||
Checks: C5 population, C2 closed solids, C1 extents, C4 coping section at Y=0 and plan corner rounding,
|
||||
C3 joints and overlaps, C6 volumes (coping: plan section areas integrated over Z from the
|
||||
dimensions, R corner fillets in closed form), C7 pedestals / pads per bearing.
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, TOL, G = X["pier_id"], X["tol_mm"], X["G1"]
|
||||
F, LN, CO, C, PI = G["footing"], G["lean"], G["column"], G["coping"], G.get("piles")
|
||||
ROT = F.get("rot_deg", 0.0)
|
||||
FT_TOP = F["z0"] + F["thickness"]
|
||||
Z_CB = FT_TOP + CO["height"] # column top = coping bottom
|
||||
results = []
|
||||
|
||||
|
||||
def objs(prefix):
|
||||
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)]
|
||||
|
||||
|
||||
def brep(o):
|
||||
return o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry)
|
||||
|
||||
|
||||
def vol(b):
|
||||
return rg.VolumeMassProperties.Compute(b).Volume
|
||||
|
||||
|
||||
def bbox(b):
|
||||
return b.GetBoundingBox(True)
|
||||
|
||||
|
||||
def unrot(b):
|
||||
"""Copy of b rotated back by the foundation rotation (identity when the foundation is not rotated)."""
|
||||
b = b.DuplicateBrep()
|
||||
if ROT:
|
||||
b.Transform(rg.Transform.Rotation(-math.radians(ROT), rg.Vector3d.ZAxis, rg.Point3d.Origin))
|
||||
return b
|
||||
|
||||
|
||||
def check(cid, name, expected, actual, tol=TOL):
|
||||
results.append(dict(check=cid, item=name, expected=round(expected, 3), actual=round(actual, 3),
|
||||
diff=round(actual - expected, 3), tol=tol, result="PASS" if abs(actual - expected) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(cid, name, ok, detail=""):
|
||||
results.append(dict(check=cid, item=name, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
# expected coping outline from the dimensions (front view, Y = 0)
|
||||
FLAT, TIP, TOPH, RISE = C["bottom_flat_half"], C["tip_half"], C["top_half"], C["soffit_rise"]
|
||||
ZTL, ZTR = C["z_top_left"], C["z_top_right"]
|
||||
Z_TIP = Z_CB + RISE
|
||||
|
||||
|
||||
def bottom_z(x):
|
||||
ax = abs(x)
|
||||
return Z_CB if ax <= FLAT else Z_CB + RISE * (ax - FLAT) / (TIP - FLAT)
|
||||
|
||||
|
||||
def top_z(x):
|
||||
"""Upper outline: top level of the side, then the battered end face from top_half down to the tip."""
|
||||
zt = ZTL if x < 0 else ZTR
|
||||
ax = abs(x)
|
||||
if ax <= TOPH:
|
||||
return zt
|
||||
return zt - (zt - Z_TIP) * (ax - TOPH) / (TIP - TOPH)
|
||||
|
||||
|
||||
def coping_volume_expected():
|
||||
"""Integrate the plan section area over Z. At height z each side reaches x_end(z): the soffit line below the
|
||||
tip level (plan outline: tip length, corner radius R), the battered end face above it (rounded outline
|
||||
shrinking from the tip length to the top length). Corner cuts use the closed form of the R fillet area."""
|
||||
W, R = C["width_y"], C["plan_corner_radius"]
|
||||
|
||||
def cut_area(e): # area cut by one R fillet over 0..e beyond its start
|
||||
e = min(max(e, 0.0), R)
|
||||
return R * e - (e / 2 * math.sqrt(R * R - e * e) + R * R / 2 * math.asin(e / R))
|
||||
|
||||
total, n = 0.0, 40000
|
||||
z0, z1 = Z_CB, max(ZTL, ZTR)
|
||||
dz = (z1 - z0) / n
|
||||
for i in range(n):
|
||||
z = z0 + (i + 0.5) * dz
|
||||
area = 0.0
|
||||
for zt in (ZTL, ZTR):
|
||||
if z > zt:
|
||||
continue
|
||||
if z < Z_TIP:
|
||||
x_end = FLAT + (TIP - FLAT) * (z - Z_CB) / RISE # soffit line
|
||||
rect_end = TIP
|
||||
else:
|
||||
x_end = TIP - (TIP - TOPH) * (z - Z_TIP) / (zt - Z_TIP)
|
||||
rect_end = x_end
|
||||
area += W * x_end - 2 * cut_area(x_end - (rect_end - R))
|
||||
total += area * dz
|
||||
return total
|
||||
|
||||
|
||||
def main():
|
||||
ids = {k: f"{PID}-{k}" for k in ("LC", "FT", "CL", "CP", "PL")}
|
||||
S = G["seats"]
|
||||
nos = sorted(int(k) for k in S["table"])
|
||||
counts = {k: len(objs(v)) for k, v in ids.items()}
|
||||
flag("C5", "object counts", counts == {"LC": 1, "FT": 1, "CL": 1, "CP": 1, "PL": PI["nx"] * PI["ny"] if PI else 0}, str(counts))
|
||||
lc, ft, cl, cp = (brep(objs(ids[k])[0]) for k in ("LC", "FT", "CL", "CP"))
|
||||
piles = [brep(o) for o in objs(ids["PL"])]
|
||||
allb = [lc, ft, cl, cp] + piles
|
||||
flag("C2", f"IsSolid all {len(allb)}", all(b.IsSolid for b in allb), f"{sum(b.IsSolid for b in allb)}/{len(allb)}")
|
||||
|
||||
for key, b, dx, dy, z0, z1 in (("footing", ft, F["dx"], F["dy"], F["z0"], FT_TOP),
|
||||
("lean", lc, F["dx"] + 2 * LN["margin"], F["dy"] + 2 * LN["margin"],
|
||||
F["z0"] - LN["thickness"], F["z0"])):
|
||||
if ROT: # raw plan extent of the rotated rectangle (closed form)
|
||||
t = math.radians(abs(ROT))
|
||||
rb = bbox(b)
|
||||
check("C1", f"{key} rotated extent X", dx * math.cos(t) + dy * math.sin(t), rb.Max.X - rb.Min.X)
|
||||
check("C1", f"{key} rotated extent Y", dx * math.sin(t) + dy * math.cos(t), rb.Max.Y - rb.Min.Y)
|
||||
bb = bbox(unrot(b))
|
||||
check("C1", f"{key} X", dx, bb.Max.X - bb.Min.X)
|
||||
check("C1", f"{key} Y", dy, bb.Max.Y - bb.Min.Y)
|
||||
check("C1", f"{key} Zbot", z0, bb.Min.Z)
|
||||
check("C1", f"{key} Ztop", z1, bb.Max.Z)
|
||||
check("C1", f"{key} centre X", 0, (bb.Max.X + bb.Min.X) / 2)
|
||||
check("C1", f"{key} centre Y", 0, (bb.Max.Y + bb.Min.Y) / 2)
|
||||
bb = bbox(cl)
|
||||
check("C1", "column diameter X", CO["d"], bb.Max.X - bb.Min.X)
|
||||
check("C1", "column diameter Y", CO["d"], bb.Max.Y - bb.Min.Y)
|
||||
check("C1", "column Zbot", FT_TOP, bb.Min.Z)
|
||||
check("C1", "column Ztop", Z_CB, bb.Max.Z)
|
||||
if PI and piles:
|
||||
pr = [bbox(unrot(b)) for b in piles]
|
||||
xs = sorted({round((q.Min.X + q.Max.X) / 2, 1) for q in pr})
|
||||
ys = sorted({round((q.Min.Y + q.Max.Y) / 2, 1) for q in pr})
|
||||
flag("C1", f"pile grid {PI['nx']} x {PI['ny']}", len(xs) == PI["nx"] and len(ys) == PI["ny"], f"x={xs} y={ys}")
|
||||
if len(xs) == PI["nx"] and len(ys) == PI["ny"]:
|
||||
check("C1", "pile edge X", PI["edge_x"], xs[0] + F["dx"] / 2)
|
||||
check("C1", "pile edge Y", PI["edge_y"], ys[0] + F["dy"] / 2)
|
||||
check("C1", "pile pitch X (worst)", 0, max(abs(b - a - PI["pitch_x"]) for a, b in zip(xs, xs[1:])))
|
||||
check("C1", "pile pitch Y (worst)", 0, max(abs(b - a - PI["pitch_y"]) for a, b in zip(ys, ys[1:])))
|
||||
check("C1", "pile diameter", PI["d"], pr[0].Max.X - pr[0].Min.X)
|
||||
check("C1", "pile length", PI["length"], pr[0].Max.Z - pr[0].Min.Z)
|
||||
check("C1", "pile head embedment", PI["head_embedment"], pr[0].Max.Z - F["z0"])
|
||||
bb = bbox(cp)
|
||||
check("C1", "coping length X (tip to tip)", 2 * TIP, bb.Max.X - bb.Min.X)
|
||||
check("C1", "coping width Y", C["width_y"], bb.Max.Y - bb.Min.Y)
|
||||
check("C1", "coping Zbot", Z_CB, bb.Min.Z)
|
||||
check("C1", "coping Ztop", max(ZTL, ZTR), bb.Max.Z)
|
||||
check("C1", "coping centre X", 0, (bb.Max.X + bb.Min.X) / 2)
|
||||
|
||||
# C4 section at Y = 0 against the dimension outline
|
||||
ok, curves, pts = rg.Intersect.Intersection.BrepPlane(cp, rg.Plane(rg.Point3d(0, 0, 0), rg.Vector3d.YAxis), 0.01)
|
||||
sec = rg.Curve.JoinCurves(curves, 0.1)
|
||||
flag("C4", "section closed single loop", len(sec) == 1 and sec[0].IsClosed, f"{len(sec)} loop(s)")
|
||||
|
||||
def z_at(x, upper):
|
||||
line = rg.LineCurve(rg.Point3d(x, 0, 0), rg.Point3d(x, 0, 200000))
|
||||
zs = sorted(ev.PointA.Z for ev in rg.Intersect.Intersection.CurveCurve(sec[0], line, 0.01, 0.01))
|
||||
return zs[-1] if upper else zs[0]
|
||||
|
||||
for x in (0, FLAT - 1, -(FLAT - 1), 3000, -3000, TIP - 5, -(TIP - 5)):
|
||||
check("C4", f"bottom at X={x:+.0f}", bottom_z(x), z_at(x, False))
|
||||
for x in (-2000, 2000, -(TOPH - 10), TOPH - 10, -(TOPH + TIP) / 2, (TOPH + TIP) / 2):
|
||||
check("C4", f"top at X={x:+.0f}", top_z(x), z_at(x, True))
|
||||
check("C4", "top step at the pier centre (left - right)", ZTL - ZTR, z_at(-1, True) - z_at(1, True))
|
||||
# plan corner rounding (R) just above the tips
|
||||
R, W = C["plan_corner_radius"], C["width_y"]
|
||||
zc = Z_TIP + 50
|
||||
inside = lambda x, y: cp.IsPointInside(rg.Point3d(x, y, zc), 0.01, False)
|
||||
# test point on the 45 deg diagonal from the fillet centre, half way between the arc (R) and the sharp corner
|
||||
d = (R + R * math.sqrt(2)) / 2 / math.sqrt(2)
|
||||
corner_x, corner_y = TIP - R + d, W / 2 - R + d
|
||||
flag("C4", f"plan corners rounded R{R} (4 corners outside, edges inside)",
|
||||
all(not inside(sx * corner_x, sy * corner_y) for sx in (1, -1) for sy in (1, -1))
|
||||
and all(inside(sx * (TIP - R - 50), sy * (W / 2 - 10)) for sx in (1, -1) for sy in (1, -1)),
|
||||
f"z {zc}, test point ({corner_x:.0f}, {corner_y:.0f})")
|
||||
|
||||
# C3 joints and overlaps
|
||||
def inter_vol(a, b):
|
||||
r = rg.Brep.CreateBooleanIntersection(a, b, 0.01)
|
||||
return sum(vol(x) for x in r) if r else 0.0
|
||||
check("C3", "LC top = FT bottom", bbox(lc).Max.Z, bbox(ft).Min.Z, tol=0.01)
|
||||
check("C3", "FT top = CL bottom", bbox(ft).Max.Z, bbox(cl).Min.Z, tol=0.01)
|
||||
check("C3", "CL top = CP bottom", bbox(cl).Max.Z, bbox(cp).Min.Z, tol=0.01)
|
||||
check("C3", "overlap LC-FT (mm3)", 0, inter_vol(lc, ft), tol=1.0)
|
||||
check("C3", "overlap FT-CL (mm3)", 0, inter_vol(ft, cl), tol=1.0)
|
||||
check("C3", "overlap CL-CP (mm3)", 0, inter_vol(cl, cp), tol=1.0)
|
||||
if PI:
|
||||
check("C3", "overlap LC-piles (mm3)", 0, sum(inter_vol(lc, b) for b in piles), tol=1.0)
|
||||
emb = len(piles) * math.pi * (PI["d"] / 2) ** 2 * PI["head_embedment"]
|
||||
check("C3", "intended overlap FT-piles (mm3)", emb, sum(inter_vol(ft, b) for b in piles), tol=emb * 0.001)
|
||||
flag("C3", "column within coping bottom flat zone", CO["d"] / 2 <= FLAT and CO["d"] / 2 <= W / 2,
|
||||
f"r={CO['d'] / 2:.0f} <= {FLAT:.0f} (X), {W / 2:.0f} (Y)")
|
||||
|
||||
# C6 volumes
|
||||
fv = F["dx"] * F["dy"] * F["thickness"] / 1e9
|
||||
check("C6", "footing volume m3", fv, vol(ft) / 1e9, tol=fv * 0.001)
|
||||
cv_ = math.pi * (CO["d"] / 2000) ** 2 * CO["height"] / 1000
|
||||
check("C6", "column volume m3", cv_, vol(cl) / 1e9, tol=cv_ * 0.001)
|
||||
holes = len(piles) * math.pi * (PI["d"] / 2) ** 2 if PI else 0
|
||||
lv = ((F["dx"] + 2 * LN["margin"]) * (F["dy"] + 2 * LN["margin"]) - holes) * LN["thickness"] / 1e9
|
||||
check("C6", "lean volume m3 (minus pile holes)", lv, vol(lc) / 1e9, tol=lv * 0.001)
|
||||
cpv = coping_volume_expected() / 1e9
|
||||
check("C6", "coping volume m3 (dimension outline, R corners)", cpv, vol(cp) / 1e9, tol=cpv * 0.001)
|
||||
|
||||
# C7 pedestals and pads
|
||||
n0 = len(PID) + 3
|
||||
seats = {int(o.Attributes.GetUserString("id")[n0:]): brep(o) for o in objs(f"{PID}-BS")}
|
||||
pads = {int(o.Attributes.GetUserString("id")[n0:]): brep(o) for o in objs(f"{PID}-BP")}
|
||||
flag("C7", "pedestal / pad counts", sorted(seats) == nos and sorted(pads) == nos, f"seats={len(seats)} pads={len(pads)}")
|
||||
allb = list(seats.values()) + list(pads.values())
|
||||
flag("C7", "IsSolid pedestals+pads", all(b.IsSolid for b in allb), f"{sum(b.IsSolid for b in allb)}/{len(allb)}")
|
||||
px, py = S["pedestal"]
|
||||
m0, m1 = S["pad_margin"]
|
||||
for no in nos:
|
||||
elx, h1, h2, a, b = S["table"][str(no)]
|
||||
x, y = S["pos"][str(no)]
|
||||
top = ZTL if x < 0 else ZTR
|
||||
s, p = seats[no], pads[no]
|
||||
sb, pb = bbox(s), bbox(p)
|
||||
check("C7", f"NO.{no} table EL.'X' - h1 - h2 = coping top ({'left' if x < 0 else 'right'})", top, elx - h1 - h2)
|
||||
check("C7", f"NO.{no} pedestal bottom = coping top", top, sb.Min.Z)
|
||||
check("C7", f"NO.{no} pedestal top = top + h1", top + h1, sb.Max.Z)
|
||||
check("C7", f"NO.{no} pad top = EL.'X'", elx, pb.Max.Z)
|
||||
check("C7", f"NO.{no} pedestal size X", px, sb.Max.X - sb.Min.X)
|
||||
check("C7", f"NO.{no} pedestal size Y", py, sb.Max.Y - sb.Min.Y)
|
||||
check("C7", f"NO.{no} pedestal centre X", x, (sb.Max.X + sb.Min.X) / 2)
|
||||
check("C7", f"NO.{no} pedestal centre Y", y, (sb.Max.Y + sb.Min.Y) / 2)
|
||||
c = rg.VolumeMassProperties.Compute(p).Centroid
|
||||
check("C7", f"NO.{no} pad centre X", x, c.X)
|
||||
check("C7", f"NO.{no} pad centre Y", y, c.Y)
|
||||
mm_ = (m0 + m1) / 2
|
||||
pv = h2 / 6 * ((a + m0) * (b + m0) + (a + m1) * (b + m1) + 4 * (a + mm_) * (b + mm_))
|
||||
check("C7", f"NO.{no} pad volume (prismatoid A,B)", pv, vol(p), tol=pv * 0.001)
|
||||
check("C7", f"NO.{no} overlap pedestal-coping (mm3)", 0,
|
||||
sum(vol(q) for q in (rg.Brep.CreateBooleanIntersection(s, cp, 0.01) or [])), tol=1.0)
|
||||
check("C7", f"NO.{no} overlap pad-pedestal (mm3)", 0,
|
||||
sum(vol(q) for q in (rg.Brep.CreateBooleanIntersection(p, s, 0.01) or [])), tol=1.0)
|
||||
write_result(RESULT_PATH, results, dict(volumes_m3={k: round(vol(b) / 1e9, 3) for k, b in
|
||||
(("lean", lc), ("footing", ft), ("column", cl), ("coping", cp))}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,143 @@
|
||||
"""G2 gate checker: column rebar P1-P6 (check C8) inside Rhino 8.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values from EXPECTED_PATH sections G1 (footing
|
||||
bottom) and G2 (D3 schedule L / counts, D1 dimension chains, section radii); generator params are not read.
|
||||
Ported from Code/교각모델링/verify_rebar.py (same checks and order).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, chain, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, TOL, G = X["pier_id"], X["tol_mm"], X["G2"]
|
||||
SCHED_L, SCHED_N, DIA = G["schedule_L"], G["schedule_n"], G["dia"]
|
||||
FB = X["G1"]["footing"]["z0"]
|
||||
LEVELS = chain(G["hoop_chain"], FB)
|
||||
TIE_LEVELS = chain(G["tie_chain"], FB)
|
||||
R_OUT, R_IN, R_HOOP = G["r_outer"], G["r_inner"], G["r_hoop"]
|
||||
TOP = FB + sum(G["p1_top_parts"])
|
||||
NDIV = G["main_divisions"]
|
||||
results = []
|
||||
|
||||
|
||||
def check(item, exp, act, tol=TOL):
|
||||
results.append(dict(check="C8", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail):
|
||||
results.append(dict(check="C8", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def ids(prefix):
|
||||
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)]
|
||||
|
||||
|
||||
def concrete():
|
||||
out = []
|
||||
for oid in (f"{PID}-CL", f"{PID}-FT", f"{PID}-CP"):
|
||||
o = [x for x in all_objects(doc) if x.Attributes.GetUserString("id") == oid][0]
|
||||
out.append(o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
cl = {m: [o for o in ids(f"{PID}-RC-") if o.Attributes.GetUserString("mark") == m] for m in SCHED_L}
|
||||
pipes = {m: [o for o in ids(f"{PID}-R-") if o.Attributes.GetUserString("mark") == m] for m in SCHED_L}
|
||||
for m in SCHED_L:
|
||||
occ = {o.Attributes.GetUserString("occurrence").rstrip("ab") for o in cl[m]}
|
||||
check(f"{m} bar count", SCHED_N[m], len(occ), tol=0)
|
||||
flag(f"{m} pipes closed & paired with centrelines",
|
||||
len(pipes[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pipes[m]),
|
||||
f"pipes {len(pipes[m])} / centrelines {len(cl[m])}")
|
||||
for m in SCHED_L:
|
||||
per = {}
|
||||
for o in cl[m]:
|
||||
k = o.Attributes.GetUserString("occurrence").rstrip("ab")
|
||||
per[k] = per.get(k, 0) + float(o.Attributes.GetUserString("design_length")) + float(
|
||||
o.Attributes.GetUserString("coupler_length") or 0) # schedule L includes coupler b (D3 V, IX)
|
||||
worst = max(abs(v - SCHED_L[m]) for v in per.values())
|
||||
check(f"{m} original length (+coupler b) vs schedule L (worst of {len(per)})", 0, worst)
|
||||
curve = {}
|
||||
for o in cl[m]:
|
||||
k = o.Attributes.GetUserString("occurrence").rstrip("ab")
|
||||
curve[k] = curve.get(k, 0) + o.Geometry.GetLength() + float(
|
||||
o.Attributes.GetUserString("coupler_length") or 0)
|
||||
dev = [v - SCHED_L[m] for v in curve.values()]
|
||||
results.append(dict(check="C8", item=f"{m} curve length - L (record, fillets shorten hooks)",
|
||||
detail=f"min {min(dev):.1f} / max {max(dev):.1f} mm", result="INFO"))
|
||||
if m in G["no_fillet_marks"]:
|
||||
check(f"{m} curve length (+coupler b) vs L (no fillet)", 0, max(abs(d) for d in dev))
|
||||
for m, r in (("P1", R_OUT), ("P2", R_IN)):
|
||||
angs, worst_r = [], 0
|
||||
for o in cl[m]:
|
||||
c = o.Geometry
|
||||
p = c.PointAtEnd if c.PointAtEnd.Z > c.PointAtStart.Z else c.PointAtStart
|
||||
angs.append(math.degrees(math.atan2(p.Y, p.X)) % 360)
|
||||
worst_r = max(worst_r, abs(math.hypot(p.X, p.Y) - r))
|
||||
check(f"{m} top EL", TOP, p.Z) if o is cl[m][0] else None
|
||||
angs.sort()
|
||||
worst_a = max(abs(a - i * 360 / NDIV) for i, a in enumerate(angs))
|
||||
check(f"{m} radius (worst)", 0, worst_r, tol=0.5)
|
||||
check(f"{m} angle vs i*360/{NDIV} (worst, deg)", 0, worst_a, tol=0.01)
|
||||
p1b = min(min(o.Geometry.PointAtStart.Z, o.Geometry.PointAtEnd.Z) for o in cl["P1"])
|
||||
p2b = min(min(o.Geometry.PointAtStart.Z, o.Geometry.PointAtEnd.Z) for o in cl["P2"])
|
||||
dz = G["bottom_above_footing"]
|
||||
check(f"P1 hook leg EL (footing bottom + {dz})", FB + dz, p1b)
|
||||
check("P2 bottom EL", FB + dz, p2b)
|
||||
hz = {}
|
||||
for m in ("P3", "P4", "P5"):
|
||||
for o in cl[m]:
|
||||
bb = o.Geometry.GetBoundingBox(True)
|
||||
hz.setdefault(round(bb.Max.Z, 1), set()).add(m)
|
||||
if m != "P3":
|
||||
p = o.Geometry.PointAtStart
|
||||
check(f"{m} radius", R_HOOP, math.hypot(p.X, p.Y), tol=0.5) if o is cl[m][0] else None
|
||||
zs = sorted(hz)
|
||||
flag(f"hoop level count {len(LEVELS)}", len(zs) == len(LEVELS), f"{len(zs)}")
|
||||
check("hoop levels vs dim chain (worst)", 0, max(abs(a - b) for a, b in zip(zs, LEVELS)))
|
||||
marks_by_level = [list(hz[z])[0] for z in zs]
|
||||
zones = G["hoop_marks_by_zone"]
|
||||
flag("hoop marks by zone (" + ", ".join(f"{m} x{n}" for m, n in zones) + ")",
|
||||
marks_by_level == [m for m, n in zones for _ in range(n)], "")
|
||||
tz = sorted({round(o.Geometry.GetBoundingBox(True).Max.Z, 1) for o in cl["P6"]})
|
||||
off = G["tie_offset"]
|
||||
check(f"tie levels vs chain + {off} (worst)", 0, max(abs(a - (b + off)) for a, b in zip(tz, TIE_LEVELS)))
|
||||
solids = concrete()
|
||||
union = rg.Brep.CreateBooleanUnion(solids, 0.01)
|
||||
shell = union[0] if union and len(union) == 1 else None # outer surface only (no internal joint faces)
|
||||
results.append(dict(check="C8", item="concrete union for cover", detail=f"{len(union) if union else 0} brep",
|
||||
result="INFO"))
|
||||
min_cover = {}
|
||||
for m in SCHED_L:
|
||||
bad, cov = 0, 1e9
|
||||
for o in cl[m]:
|
||||
c = o.Geometry
|
||||
n = max(4, int(c.GetLength() / 100))
|
||||
for t in c.DivideByCount(n, True):
|
||||
p = c.PointAt(t)
|
||||
radial = rg.Vector3d(p.X, p.Y, 0)
|
||||
radial.Unitize() if radial.Length > 1e-6 else None
|
||||
for q in (p, p + radial * (DIA[m] / 2), p - radial * (DIA[m] / 2),
|
||||
p + rg.Vector3d(0, 0, DIA[m] / 2), p - rg.Vector3d(0, 0, DIA[m] / 2)):
|
||||
if not any(s.IsPointInside(q, 0.01, False) for s in solids):
|
||||
bad += 1
|
||||
d = p.DistanceTo(shell.ClosestPoint(p)) if shell and shell.IsPointInside(p, 0.01, False) else -1
|
||||
cov = min(cov, d - DIA[m] / 2)
|
||||
min_cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside concrete", 0, bad, tol=0)
|
||||
results.append(dict(check="C8", item="min cover to concrete surface per mark (record)",
|
||||
detail=json.dumps(min_cover), result="INFO"))
|
||||
results.append(dict(check="C8", item="clearance P1 outer surface vs hoop inner surface (record)",
|
||||
detail=f"{R_HOOP - DIA['P4'] / 2 - (R_OUT + DIA['P1'] / 2):.1f} mm", result="INFO"))
|
||||
write_result(RESULT_PATH, results, dict(min_cover=min_cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,156 @@
|
||||
"""G2 gate checker, form "ring" (Ulsan P1, 2026-10-13): column rebar with one main ring, hoop zones, cross ties.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G2 + G1 footing / column) come
|
||||
from the schedule (L, counts, R) and the dimension chains of the rebar drawing; generator params are not read.
|
||||
Checks (C8): counts, original length (+coupler) vs schedule L, curve length for bars without fillets, main bar
|
||||
radius / division / leg and top level, hoop level chain / zones / radius, tie levels / count per level,
|
||||
containment in concrete and minimum cover (record).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, chain, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, TOL, G = X["pier_id"], X["tol_mm"], X["G2"]
|
||||
SCHED_L, SCHED_N, DIA = G["schedule_L"], G["schedule_n"], G["dia"]
|
||||
FB = X["G1"]["footing"]["z0"]
|
||||
M = G["main"]
|
||||
LEVELS = chain(G["hoop_chain"], FB)
|
||||
TIE = G["tie"]
|
||||
TIE_LEVELS = chain(TIE["chain"], FB)
|
||||
results = []
|
||||
|
||||
|
||||
def check(item, exp, act, tol=TOL):
|
||||
results.append(dict(check="C8", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C8", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def info(item, detail):
|
||||
results.append(dict(check="C8", item=item, detail=detail, result="INFO"))
|
||||
|
||||
|
||||
def ids(prefix):
|
||||
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)]
|
||||
|
||||
|
||||
def main():
|
||||
cl = {m: [o for o in ids(f"{PID}-RC-") if o.Attributes.GetUserString("mark") == m] for m in SCHED_L}
|
||||
pipes = {m: [o for o in ids(f"{PID}-R-") if o.Attributes.GetUserString("mark") == m] for m in SCHED_L}
|
||||
occ = lambda o: o.Attributes.GetUserString("occurrence").rstrip("ab")
|
||||
tie_mark = TIE["mark"]
|
||||
for m in SCHED_L:
|
||||
n_occ = len(cl[m]) if m == tie_mark else len({occ(o) for o in cl[m]})
|
||||
check(f"{m} bar count", SCHED_N[m], n_occ, tol=0)
|
||||
flag(f"{m} pipes closed & paired with centrelines",
|
||||
len(pipes[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pipes[m]), f"{len(pipes[m])}/{len(cl[m])}")
|
||||
for m in SCHED_L:
|
||||
per, curve = {}, {}
|
||||
for o in cl[m]:
|
||||
k = o.Attributes.GetUserString("occurrence") if m == tie_mark else occ(o)
|
||||
cp = float(o.Attributes.GetUserString("coupler_length") or 0)
|
||||
per[k] = per.get(k, 0) + float(o.Attributes.GetUserString("design_length")) + cp
|
||||
curve[k] = curve.get(k, 0) + o.Geometry.GetLength() + cp
|
||||
if not per:
|
||||
continue
|
||||
check(f"{m} original length (+coupler) vs schedule L (worst of {len(per)})", 0,
|
||||
max(abs(v - SCHED_L[m]) for v in per.values()))
|
||||
dev = [v - SCHED_L[m] for v in curve.values()]
|
||||
info(f"{m} curve length - L (record)", f"min {min(dev):.1f} / max {max(dev):.1f} mm")
|
||||
if m in G["no_fillet_marks"]:
|
||||
check(f"{m} curve length (+coupler) vs L (no fillet)", 0, max(abs(d) for d in dev))
|
||||
# main bars
|
||||
mm_, R0, ND = M["mark"], M["radius"], M["divisions"]
|
||||
angs, worst_r, tops, legs = [], 0, [], []
|
||||
for o in cl[mm_]:
|
||||
c = o.Geometry
|
||||
p = c.PointAtEnd if c.PointAtEnd.Z > c.PointAtStart.Z else c.PointAtStart
|
||||
angs.append(math.degrees(math.atan2(p.Y, p.X)) % 360)
|
||||
worst_r = max(worst_r, abs(math.hypot(p.X, p.Y) - R0))
|
||||
tops.append(p.Z)
|
||||
legs.append(min(c.PointAtStart.Z, c.PointAtEnd.Z))
|
||||
angs.sort()
|
||||
check(f"{mm_} radius (worst)", 0, worst_r, tol=0.5)
|
||||
check(f"{mm_} angle vs start + i*360/{ND} (worst, deg)", 0,
|
||||
max(abs(a - (M.get("start_deg", 0) + i * 360 / ND)) for i, a in enumerate(angs)), tol=0.01)
|
||||
check(f"{mm_} top EL (worst)", 0, max(abs(t - (FB + sum(M["top_parts"]))) for t in tops))
|
||||
check(f"{mm_} hook leg EL (footing bottom + {M['bottom_above_footing']}) (worst)", 0,
|
||||
max(abs(z - (FB + M["bottom_above_footing"])) for z in legs))
|
||||
# hoops
|
||||
hz = {}
|
||||
for m, _ in G["hoop_zones"]:
|
||||
for o in cl[m]:
|
||||
hz.setdefault(round(o.Geometry.GetBoundingBox(True).Max.Z, 1), set()).add(m)
|
||||
zs = sorted(hz)
|
||||
flag(f"hoop level count {len(LEVELS)}", len(zs) == len(LEVELS), f"{len(zs)}")
|
||||
check("hoop levels vs dim chain (worst)", 0, max(abs(a - b) for a, b in zip(zs, LEVELS)) if zs else 1e9)
|
||||
flag("hoop marks by zone (" + ", ".join(f"{m} x{n}" for m, n in G["hoop_zones"]) + ")",
|
||||
[list(hz[z])[0] for z in zs] == [m for m, n in G["hoop_zones"] for _ in range(n)])
|
||||
for m, _ in G["hoop_zones"]:
|
||||
o = cl[m][0]
|
||||
p = o.Geometry.PointAt(o.Geometry.Domain.Mid)
|
||||
check(f"{m} radius (mid point)", G.get("r_hoop_by_mark", {}).get(m, G["r_hoop"]), math.hypot(p.X, p.Y), tol=0.5)
|
||||
if "inner" in M: # Okseong P1: inner ring (2026-10-14)
|
||||
im = M["inner"]
|
||||
tops = [max((o.Geometry.PointAtStart, o.Geometry.PointAtEnd), key=lambda q: q.Z) for o in cl[im["mark"]]]
|
||||
lows = [min((o.Geometry.PointAtStart, o.Geometry.PointAtEnd), key=lambda q: q.Z) for o in cl[im["mark"]]]
|
||||
check(f"{im['mark']} inner ring radius (worst)", 0, max(abs(math.hypot(q.X, q.Y) - im["radius"]) for q in tops) if tops else 1e9, tol=0.5)
|
||||
flag(f"{im['mark']} hooks bent inwards", bool(lows) and all(math.hypot(q.X, q.Y) < im["radius"] for q in lows))
|
||||
# ties
|
||||
if TIE.get("y_lift"): # Okseong P1: X tie at the level, Y tie lifted
|
||||
occ_ = lambda o: o.Attributes.GetUserString("occurrence")
|
||||
lx = sorted(round(o.Geometry.GetBoundingBox(True).Max.Z, 1) for o in cl[tie_mark] if occ_(o).endswith("X"))
|
||||
ly = sorted(round(o.Geometry.GetBoundingBox(True).Max.Z, 1) for o in cl[tie_mark] if occ_(o).endswith("Y"))
|
||||
flag(f"tie X / Y levels {len(TIE_LEVELS)} each", len(lx) == len(ly) == len(TIE_LEVELS), f"X {len(lx)} Y {len(ly)}")
|
||||
check("tie X levels vs chain (worst)", 0, max(abs(a - b - TIE["offset"]) for a, b in zip(lx, TIE_LEVELS)) if lx else 1e9)
|
||||
check(f"tie Y = X + {TIE['y_lift']} (worst)", 0, max(abs(b - a - TIE["y_lift"]) for a, b in zip(lx, ly)) if ly else 1e9)
|
||||
else:
|
||||
tz = {}
|
||||
for o in cl[tie_mark]:
|
||||
tz.setdefault(round(o.Geometry.GetBoundingBox(True).Max.Z, 1), []).append(o)
|
||||
lv = sorted(tz)
|
||||
flag(f"tie levels {len(TIE_LEVELS)} x {TIE['per_level']}", len(lv) == len(TIE_LEVELS)
|
||||
and all(len(v) == TIE["per_level"] for v in tz.values()), f"{len(lv)} levels")
|
||||
check(f"tie levels vs chain + {TIE['offset']} (worst)", 0,
|
||||
max(abs(a - (b + TIE["offset"])) for a, b in zip(lv, TIE_LEVELS)) if lv else 1e9)
|
||||
# containment and cover
|
||||
solids = []
|
||||
for oid in (f"{PID}-CL", f"{PID}-FT", f"{PID}-CP"):
|
||||
o = [x for x in all_objects(doc) if x.Attributes.GetUserString("id") == oid][0]
|
||||
solids.append(o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry))
|
||||
union = rg.Brep.CreateBooleanUnion(solids, 0.01)
|
||||
shell = union[0] if union and len(union) == 1 else None
|
||||
info("concrete union for cover", f"{len(union) if union else 0} brep")
|
||||
min_cover = {}
|
||||
for m in SCHED_L:
|
||||
bad, cov = 0, 1e9
|
||||
r = DIA[m] / 2
|
||||
for o in cl[m]:
|
||||
c = o.Geometry
|
||||
for t in c.DivideByCount(max(4, int(c.GetLength() / 150)), True):
|
||||
p = c.PointAt(t)
|
||||
radial = rg.Vector3d(p.X, p.Y, 0)
|
||||
radial.Unitize() if radial.Length > 1e-6 else None
|
||||
for q in (p, p + radial * r, p - radial * r, p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
||||
if not any(s.IsPointInside(q, 0.01, False) for s in solids):
|
||||
bad += 1
|
||||
d = p.DistanceTo(shell.ClosestPoint(p)) if shell and shell.IsPointInside(p, 0.01, False) else -1
|
||||
cov = min(cov, d - r)
|
||||
min_cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside concrete", 0, bad, tol=0)
|
||||
info("min cover to concrete surface per mark (record)", json.dumps(min_cover))
|
||||
write_result(RESULT_PATH, results, dict(min_cover=min_cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,163 @@
|
||||
"""G2 gate checker, form "straight" (Ramp-A P1, 2026-10-13): straight main ring with a bottom coupler, lapped
|
||||
half-ring hoops, X / Y cross ties; column on a drilled shaft (no footing).
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G2) come from the schedule (L, N, r)
|
||||
and the D2 dimension chains (anchor EL 45.492: '2,000' below, 14@150 / 78@150 hoops, 7@300 / 39@300 ties) with
|
||||
the user decisions K1-K3; generator params are not read.
|
||||
Checks (C8): counts, original length (+coupler) vs schedule L, main radius / division / bottom / top, hoop and tie
|
||||
level grids and radius, X / Y ties apart, containment in concrete (pile segments, column, coping), min cover (record).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, TOL, G = X["pier_id"], X["tol_mm"], X["G2"]
|
||||
SCHED_L, SCHED_N, DIA = G["schedule_L"], G["schedule_n"], G["dia"]
|
||||
M, HP, TI = G["main"], G["hoops"], G["ties"]
|
||||
A = G["chain_anchor"]
|
||||
|
||||
|
||||
def grid(g):
|
||||
return [A - g["below"] * g["pitch"] + k * g["pitch"] for k in range(SCHED_N[g["mark"]] // g.get("per_level", 1))]
|
||||
|
||||
|
||||
HOOP_LEVELS, TIE_LEVELS = grid(HP), grid(TI)
|
||||
CO = X["G1"]["column"]
|
||||
R_COL, Z_CL0, Z_CL1 = CO["d"] / 2, CO["z0"], CO["z0"] + CO["length"]
|
||||
results = []
|
||||
|
||||
|
||||
def check(item, exp, act, tol=TOL):
|
||||
results.append(dict(check="C8", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C8", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def info(item, detail):
|
||||
results.append(dict(check="C8", item=item, detail=detail, result="INFO"))
|
||||
|
||||
|
||||
def ids(prefix):
|
||||
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)]
|
||||
|
||||
|
||||
def main():
|
||||
cl = {m: [o for o in ids(f"{PID}-RC-") if o.Attributes.GetUserString("mark") == m] for m in SCHED_L}
|
||||
pipes = {m: [o for o in ids(f"{PID}-R-") if o.Attributes.GetUserString("mark") == m] for m in SCHED_L}
|
||||
occ = lambda o: o.Attributes.GetUserString("occurrence").rstrip("ab")
|
||||
tie = TI["mark"]
|
||||
for m in SCHED_L:
|
||||
n_occ = len(cl[m]) if m == tie else len({occ(o) for o in cl[m]})
|
||||
check(f"{m} bar count", SCHED_N[m], n_occ, tol=0)
|
||||
flag(f"{m} pipes closed & paired with centrelines",
|
||||
len(pipes[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pipes[m]), f"{len(pipes[m])}/{len(cl[m])}")
|
||||
for m in SCHED_L:
|
||||
per, curve = {}, {}
|
||||
for o in cl[m]:
|
||||
k = o.Attributes.GetUserString("occurrence") if m == tie else occ(o)
|
||||
cp = float(o.Attributes.GetUserString("coupler_length") or 0)
|
||||
per[k] = per.get(k, 0) + float(o.Attributes.GetUserString("design_length")) + cp
|
||||
curve[k] = curve.get(k, 0) + o.Geometry.GetLength() + cp
|
||||
if per:
|
||||
check(f"{m} original length (+coupler) vs schedule L (worst of {len(per)})", 0,
|
||||
max(abs(v - SCHED_L[m]) for v in per.values()))
|
||||
dev = [v - SCHED_L[m] for v in curve.values()]
|
||||
info(f"{m} curve length (+coupler) - L (record)", f"min {min(dev):.1f} / max {max(dev):.1f} mm")
|
||||
# main bars: straight, chain bottom (anchor - 2,000), top = bottom + L - coupler (K1)
|
||||
mm_ = M["mark"]
|
||||
bottom = A - M["below_anchor"]
|
||||
top = bottom + SCHED_L[mm_] - M["coupler"]
|
||||
angs, worst_r, zb, zt = [], 0, [], []
|
||||
for o in cl[mm_]:
|
||||
c = o.Geometry
|
||||
lo, hi = sorted((c.PointAtStart, c.PointAtEnd), key=lambda p: p.Z)
|
||||
angs.append(math.degrees(math.atan2(hi.Y, hi.X)) % 360)
|
||||
worst_r = max(worst_r, abs(math.hypot(lo.X, lo.Y) - M["radius"]), abs(math.hypot(hi.X, hi.Y) - M["radius"]))
|
||||
zb.append(lo.Z)
|
||||
zt.append(hi.Z)
|
||||
angs.sort()
|
||||
check(f"{mm_} radius (worst)", 0, worst_r, tol=0.5)
|
||||
check(f"{mm_} angle vs {M['start_deg']} + i*360/{M['divisions']} (worst, deg)", 0,
|
||||
max(abs(a - (M["start_deg"] + i * 360 / M["divisions"])) for i, a in enumerate(angs)), tol=0.01)
|
||||
check(f"{mm_} bottom EL = chain {A / 1000:.3f} - {M['below_anchor']} (worst)", 0, max(abs(z - bottom) for z in zb))
|
||||
check(f"{mm_} top EL = bottom + L - coupler {M['coupler']} (worst)", 0, max(abs(z - top) for z in zt))
|
||||
# hoops: levels on the chain grid, two pieces per level, radius
|
||||
hz = {}
|
||||
for o in cl[HP["mark"]]:
|
||||
bb = o.Geometry.GetBoundingBox(True)
|
||||
hz.setdefault(round((bb.Max.Z + bb.Min.Z) / 2, 1), []).append(o)
|
||||
zs = sorted(hz)
|
||||
flag(f"{HP['mark']} levels {len(HOOP_LEVELS)} x 2 pieces", len(zs) == len(HOOP_LEVELS)
|
||||
and all(len(v) == 2 for v in hz.values()), f"{len(zs)} levels")
|
||||
check(f"{HP['mark']} levels vs chain grid {HP['pitch']} (worst)", 0,
|
||||
max(abs(a - b) for a, b in zip(zs, HOOP_LEVELS)) if len(zs) == len(HOOP_LEVELS) else 1e9)
|
||||
for o in cl[HP["mark"]][:4]:
|
||||
c = o.Geometry
|
||||
p = c.PointAt(c.Domain.Mid)
|
||||
check(f"{HP['mark']} radius at mid point ({o.Attributes.GetUserString('occurrence')})", HP["radius"],
|
||||
math.hypot(p.X, p.Y), tol=0.5)
|
||||
# ties: X at the grid level, Y lifted (K3); levels = top of the straight part
|
||||
by = lambda s: [o.Geometry for o in cl[tie] if o.Attributes.GetUserString("occurrence").endswith(s)]
|
||||
xs, ys = by("X"), by("Y")
|
||||
txm = sorted(round(g.GetBoundingBox(True).Max.Z, 1) for g in xs)
|
||||
tym = sorted(round(g.GetBoundingBox(True).Max.Z, 1) for g in ys)
|
||||
flag(f"{tie} X / Y per level, {len(TIE_LEVELS)} levels", len(txm) == len(tym) == len(TIE_LEVELS), f"X {len(txm)} Y {len(tym)}")
|
||||
if len(txm) == len(TIE_LEVELS):
|
||||
check(f"{tie} X levels vs chain grid {TI['pitch']} (worst)", 0, max(abs(a - b) for a, b in zip(txm, TIE_LEVELS)))
|
||||
if len(tym) == len(TIE_LEVELS):
|
||||
check(f"{tie} Y levels = X + {TI['y_lift']} (worst)", 0, max(abs(a - b - TI["y_lift"]) for a, b in zip(tym, TIE_LEVELS)))
|
||||
xs.sort(key=lambda g: g.GetBoundingBox(True).Max.Z)
|
||||
ys.sort(key=lambda g: g.GetBoundingBox(True).Max.Z)
|
||||
gaps = []
|
||||
for a, b in zip(xs, ys):
|
||||
ok, pa, pb = a.ClosestPoints(b)
|
||||
gaps.append(pa.DistanceTo(pb) if ok else 0.0)
|
||||
gap = min(gaps) if gaps else 0.0
|
||||
check(f"{tie} X / Y centrelines apart >= bar diameter {DIA[tie]} (min, capped)", DIA[tie], min(gap, DIA[tie]), tol=0.01)
|
||||
info(f"{tie} X / Y centreline distance (min, record)", f"{gap:.2f} mm")
|
||||
# containment and cover
|
||||
solids = []
|
||||
for o in all_objects(doc):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if oid in (f"{PID}-CL", f"{PID}-CP") or oid.startswith(f"{PID}-PL"):
|
||||
solids.append(o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry))
|
||||
union = rg.Brep.CreateBooleanUnion(solids, 0.01)
|
||||
shell = union[0] if union and len(union) == 1 else None
|
||||
info("concrete union for cover", f"{len(union) if union else 0} brep from {len(solids)} solids"
|
||||
+ ("" if shell else " -> min cover = column side cover in the column zone (R - radial distance - bar radius)"))
|
||||
min_cover = {}
|
||||
for m in SCHED_L:
|
||||
bad, cov = 0, 1e9
|
||||
r = DIA[m] / 2
|
||||
for o in cl[m]:
|
||||
c = o.Geometry
|
||||
for t in c.DivideByCount(max(4, int(c.GetLength() / 150)), True):
|
||||
p = c.PointAt(t)
|
||||
radial = rg.Vector3d(p.X, p.Y, 0)
|
||||
radial.Unitize() if radial.Length > 1e-6 else None
|
||||
for q in (p, p + radial * r, p - radial * r, p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
||||
if not any(s.IsPointInside(q, 0.01, False) for s in solids):
|
||||
bad += 1
|
||||
if shell:
|
||||
d = p.DistanceTo(shell.ClosestPoint(p)) if shell.IsPointInside(p, 0.01, False) else -1
|
||||
cov = min(cov, d - r)
|
||||
elif Z_CL0 <= p.Z <= Z_CL1: # no union (coplanar contact faces): side cover in the column zone
|
||||
cov = min(cov, R_COL - math.hypot(p.X, p.Y) - r)
|
||||
min_cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside concrete", 0, bad, tol=0)
|
||||
info("min cover to concrete surface per mark (record)", json.dumps(min_cover))
|
||||
write_result(RESULT_PATH, results, dict(min_cover=min_cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,146 @@
|
||||
"""G3 gate checker: footing rebar B1-B12 (check C9) and rebar colours inside Rhino 8.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values from EXPECTED_PATH sections G1 (footing
|
||||
bottom) and G3 (D3 schedule, D1/D2 dimension chains, F1 layer heights); mid-layer positions are drawn dots
|
||||
(derived, not independent). Generator params are not read.
|
||||
Ported from Code/교각모델링/verify_footing_rebar.py (same checks and order).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectColorSource
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, row, series, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, G = X["pier_id"], X["G3"]
|
||||
FB = X["G1"]["footing"]["z0"]
|
||||
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
|
||||
DIA = G["dia"]
|
||||
Y_BOT, Y_250, X_BOT, X_250 = (row(G["rows"][k]) for k in ("Y_BOT", "Y_250", "X_BOT", "X_250"))
|
||||
Z = G["layer_z"]
|
||||
B10_Z = series(G["b10_levels"])
|
||||
B9_X, B9_Y = series(G["b9_grid"]["x"]), series(G["b9_grid"]["y"])
|
||||
PALETTE = {int(k): tuple(v) for k, v in X["rebar_palette"].items()}
|
||||
results = []
|
||||
|
||||
|
||||
def check(item, exp, act, tol=1.0):
|
||||
results.append(dict(check="C9", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C9", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def info(item, detail):
|
||||
results.append(dict(check="C9", item=item, detail=detail, result="INFO"))
|
||||
|
||||
|
||||
def objs(prefix, mark):
|
||||
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)
|
||||
and o.Attributes.GetUserString("mark") == mark]
|
||||
|
||||
|
||||
def main():
|
||||
cl = {m: objs(f"{PID}-RC-", m) for m in SCHED}
|
||||
pp = {m: objs(f"{PID}-R-", m) for m in SCHED}
|
||||
for m, (L, n) in SCHED.items():
|
||||
check(f"{m} bar count vs schedule", n, len(cl[m]), tol=0)
|
||||
flag(f"{m} pipes closed & paired", len(pp[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pp[m]),
|
||||
f"{len(pp[m])}/{len(cl[m])}")
|
||||
if cl[m]:
|
||||
worst = max(abs(float(o.Attributes.GetUserString("design_length")) - L) for o in cl[m])
|
||||
check(f"{m} original length vs L (worst)", 0, worst)
|
||||
dev = [o.Geometry.GetLength() - L for o in cl[m]]
|
||||
info(f"{m} curve length - L (bend fillets)", f"min {min(dev):.1f} / max {max(dev):.1f}")
|
||||
|
||||
def mids(m):
|
||||
return [o.Geometry.PointAt(o.Geometry.Domain.Mid) for o in cl[m]]
|
||||
for m in ("B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8"):
|
||||
ps = mids(m)
|
||||
check(f"{m} layer z (worst)", 0, max(abs(p.Z - (FB + Z[m])) for p in ps))
|
||||
|
||||
def pos_check(marks, axis, expected, name):
|
||||
got = sorted(round(getattr(p, axis), 1) for m in marks for p in mids(m))
|
||||
ok = len(got) == len(expected)
|
||||
worst = max(abs(a - b) for a, b in zip(got, expected)) if ok else 1e9
|
||||
check(f"{name} positions vs chain (worst, n={len(got)}/{len(expected)})", 0, worst)
|
||||
pos_check(("B2", "B4"), "X", Y_BOT, "bottom Y-bars B2+B4")
|
||||
pos_check(("B6", "B8"), "Y", X_BOT, "bottom X-bars B6+B8")
|
||||
pos_check(("B3",), "X", Y_250, "B3")
|
||||
pos_check(("B1",), "X", Y_250, "B1")
|
||||
pos_check(("B7",), "Y", X_250, "B7")
|
||||
pos_check(("B5",), "Y", X_250, "B5")
|
||||
ye, xe = G["rows"]["Y_BOT"], G["rows"]["X_BOT"]
|
||||
b2x = sorted(round(p.X) for p in mids("B2"))
|
||||
flag(f"B2 at ends and alternate from {ye['start']}", b2x == sorted(list(ye["ends"]) + Y_BOT[1:-1][0::2]), "")
|
||||
b6y = sorted(round(p.Y) for p in mids("B6"))
|
||||
flag(f"B6 at ends and alternate from {xe['start']}", b6y == sorted(list(xe["ends"]) + X_BOT[1:-1][0::2]), "")
|
||||
mid = G["mid_levels"]
|
||||
for m in ("B11", "B12"):
|
||||
zs = sorted({round(p.Z - FB) for p in mids(m)})
|
||||
flag(f"{m} mid levels {'/'.join(str(v) for v in mid)} [derived]", zs == mid, str(zs))
|
||||
zs = sorted(round(o.Geometry.GetBoundingBox(True).Min.Z - FB, 1) for o in cl["B10"])
|
||||
check("B10 levels vs chain (worst)", 0, max(abs(a - b) for a, b in zip(zs, B10_Z)) if len(zs) == len(B10_Z) else 1e9)
|
||||
bb = cl["B10"][0].Geometry.GetBoundingBox(True)
|
||||
check("B10 centreline size X", G["b10_size"][0], bb.Max.X - bb.Min.X)
|
||||
check("B10 centreline size Y", G["b10_size"][1], bb.Max.Y - bb.Min.Y)
|
||||
# B9: vertical leg x = bbox min X (hooks point +X); y constant
|
||||
pts = {(round(o.Geometry.GetBoundingBox(True).Min.X), round(o.Geometry.GetBoundingBox(True).Center.Y))
|
||||
for o in cl["B9"]}
|
||||
exp = {(x, y) for i, x in enumerate(B9_X) for j, y in enumerate(B9_Y) if (i + j) % 2 == 0}
|
||||
flag(f"B9 positions = checkerboard of {len(B9_X)}x{len(B9_Y)} grid", pts == exp, f"{len(pts)} / {len(exp)}")
|
||||
zr = [o.Geometry.GetBoundingBox(True) for o in cl["B9"]]
|
||||
e0, e1 = G["b9_erec"]
|
||||
a9, c9 = G["b9_a"], G["b9_c"]
|
||||
check("B9 bottom z (worst of all)", 0, max(abs(b.Min.Z - (FB + (e0 + e1) / 2 - a9 / 2)) for b in zr))
|
||||
check("B9 vertical extent a (bottom leg z to top of straight)", a9,
|
||||
max(b.Max.Z for b in zr[:1]) - min(b.Min.Z for b in zr[:1]) - c9 / math.pi, tol=1.0)
|
||||
solids = []
|
||||
for oid in (f"{PID}-FT", f"{PID}-CL", f"{PID}-CP"):
|
||||
o = [x for x in all_objects(doc) if x.Attributes.GetUserString("id") == oid][0]
|
||||
solids.append(o.Geometry)
|
||||
ft = solids[0]
|
||||
shell = None
|
||||
for order in ([solids[1], solids[0], solids[2]], solids): # union is order-sensitive in Rhino 8
|
||||
u = rg.Brep.CreateBooleanUnion(order, 0.01)
|
||||
if u and len(u) == 1:
|
||||
shell = u[0]
|
||||
break
|
||||
if shell is None:
|
||||
raise RuntimeError("concrete union failed")
|
||||
cover = {}
|
||||
for m in SCHED:
|
||||
bad, cov = 0, 1e9
|
||||
for o in cl[m]:
|
||||
c = o.Geometry
|
||||
for t in c.DivideByCount(max(4, int(c.GetLength() / 200)), True):
|
||||
p = c.PointAt(t)
|
||||
r = DIA[m] / 2
|
||||
for q in (p, p + rg.Vector3d(r, 0, 0), p - rg.Vector3d(r, 0, 0), p + rg.Vector3d(0, r, 0),
|
||||
p - rg.Vector3d(0, r, 0), p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
||||
if not ft.IsPointInside(q, 0.01, False):
|
||||
bad += 1
|
||||
cov = min(cov, p.DistanceTo(shell.ClosestPoint(p)) - r)
|
||||
cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside footing", 0, bad, tol=0)
|
||||
info("min cover to concrete exterior per mark", json.dumps(cover))
|
||||
wrong = 0
|
||||
for o in all_objects(doc):
|
||||
if (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-R-"):
|
||||
d = int(o.Attributes.GetUserString("dia")[1:])
|
||||
c = o.Attributes.ObjectColor
|
||||
if o.Attributes.ColorSource != ObjectColorSource.ColorFromObject or (c.R, c.G, c.B) != PALETTE[d]:
|
||||
wrong += 1
|
||||
check("rebar pipe colour by diameter mismatches (all rebar)", 0, wrong, tol=0)
|
||||
write_result(RESULT_PATH, results, dict(cover=cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,158 @@
|
||||
"""G3 gate checker, form "mats" (Ulsan P1, 2026-10-13): footing rebar groups, perimeter hoops, verticals.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G3) from the schedule and the
|
||||
position chains of the rebar drawing (layer heights are drawn values, marked [derived]); params are not read.
|
||||
expected.G3: schedule {mark: [L, n]}, dia {mark: d}, rows {name: {first, start, pitch, n, last}},
|
||||
layers {mark: {axis, z, row | pos, subset}} (subset "on"/"off": positions on / off the `of` row)
|
||||
hoops {mark, half, chain {start, groups}}, verticals {mark, grid_x row, grid_y row, parity, z0, a}
|
||||
Optional (Ulsan P2, 2026-10-15; absent -> unchanged): G1.footing.rot_deg (bars are checked after turning their centrelines back by the
|
||||
foundation rotation), hoops.half_x / half_y, verticals {exclude {x, y} (grid points with |x| <= x and |y| <= y are absent), legs {bottom, top}}.
|
||||
Checks (C9): counts, original length, layer z, positions, hoop levels / size, vertical grid, containment in the
|
||||
footing, minimum cover (record), rebar colours by diameter.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectColorSource
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, chain, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, G = X["pier_id"], X["G3"]
|
||||
FB = X["G1"]["footing"]["z0"]
|
||||
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
|
||||
DIA = G["dia"]
|
||||
PALETTE = {int(k): tuple(v) for k, v in X["rebar_palette"].items()}
|
||||
ROT = X["G1"]["footing"].get("rot_deg", 0.0)
|
||||
results = []
|
||||
|
||||
|
||||
def bbx(o):
|
||||
"""Bounding box of the bar centreline in the foundation's own axes."""
|
||||
g = o.Geometry
|
||||
if ROT:
|
||||
g = g.DuplicateCurve()
|
||||
g.Transform(rg.Transform.Rotation(-math.radians(ROT), rg.Vector3d.ZAxis, rg.Point3d.Origin))
|
||||
return g.GetBoundingBox(True)
|
||||
|
||||
|
||||
def check(item, exp, act, tol=1.0):
|
||||
results.append(dict(check="C9", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C9", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def info(item, detail):
|
||||
results.append(dict(check="C9", item=item, detail=detail, result="INFO"))
|
||||
|
||||
|
||||
def row(spec):
|
||||
return sorted(list(spec.get("first", [])) + [spec["start"] + spec["pitch"] * i for i in range(spec["n"])]
|
||||
+ list(spec.get("last", [])))
|
||||
|
||||
|
||||
def positions(lay):
|
||||
if "pos" in lay:
|
||||
return sorted(lay["pos"])
|
||||
base = row(G["rows"][lay["row"]])
|
||||
if "subset" in lay:
|
||||
ref = set(row(G["rows"][lay["of"]]))
|
||||
base = [v for v in base if (v in ref) == (lay["subset"] == "on")]
|
||||
return base
|
||||
|
||||
|
||||
def objs(prefix, mark):
|
||||
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)
|
||||
and o.Attributes.GetUserString("mark") == mark]
|
||||
|
||||
|
||||
def main():
|
||||
cl = {m: objs(f"{PID}-RC-", m) for m in SCHED}
|
||||
pp = {m: objs(f"{PID}-R-", m) for m in SCHED}
|
||||
for m, (L, n) in SCHED.items():
|
||||
check(f"{m} bar count vs schedule", n, len(cl[m]), tol=0)
|
||||
flag(f"{m} pipes closed & paired", len(pp[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pp[m]),
|
||||
f"{len(pp[m])}/{len(cl[m])}")
|
||||
if cl[m]:
|
||||
check(f"{m} original length vs L (worst)", 0,
|
||||
max(abs(float(o.Attributes.GetUserString("design_length")) - L) for o in cl[m]))
|
||||
dev = [o.Geometry.GetLength() - L for o in cl[m]]
|
||||
info(f"{m} curve length - L (bend fillets / laps)", f"min {min(dev):.1f} / max {max(dev):.1f}")
|
||||
for m, lay in G["layers"].items():
|
||||
if not cl[m]:
|
||||
continue
|
||||
# horizontal part: the lowest (legs up / none) or highest (legs down) straight segment -> bbox mid level
|
||||
zs, ps = [], []
|
||||
for o in cl[m]:
|
||||
bb = bbx(o)
|
||||
leg_down = lay.get("leg", 0) < 0
|
||||
zs.append((bb.Max.Z if leg_down else bb.Min.Z) - FB)
|
||||
ps.append(round((bb.Min.X + bb.Max.X) / 2 if lay["axis"] == "Y" else (bb.Min.Y + bb.Max.Y) / 2, 1))
|
||||
check(f"{m} layer z (worst) [derived]", 0, max(abs(z - lay["z"]) for z in zs))
|
||||
exp = positions(lay)
|
||||
got = sorted(ps)
|
||||
ok = len(got) == len(exp)
|
||||
check(f"{m} positions vs chain (worst, n={len(got)}/{len(exp)})", 0,
|
||||
max(abs(a - b) for a, b in zip(got, exp)) if ok else 1e9)
|
||||
hp = G["hoops"]
|
||||
lv = chain(hp["chain"], 0)
|
||||
hz = sorted(round(bbx(o).Min.Z - FB, 1) for o in cl[hp["mark"]])
|
||||
check(f"{hp['mark']} levels vs chain (worst)", 0, max(abs(a - b) for a, b in zip(hz, lv)) if len(hz) == len(lv) else 1e9)
|
||||
if cl[hp["mark"]]:
|
||||
bb = bbx(cl[hp["mark"]][0])
|
||||
check(f"{hp['mark']} centreline size X [derived]", 2 * hp.get("half_x", hp.get("half")), bb.Max.X - bb.Min.X)
|
||||
check(f"{hp['mark']} centreline size Y [derived]", 2 * hp.get("half_y", hp.get("half")), bb.Max.Y - bb.Min.Y)
|
||||
vt = G["verticals"]
|
||||
gx, gy = row(G["rows"][vt["grid_x"]]), row(G["rows"][vt["grid_y"]])
|
||||
pts = {(round(bbx(o).Min.X), round(bbx(o).Min.Y)) for o in cl[vt["mark"]]}
|
||||
if "exclude" in vt: # Ulsan P2: grid minus the central rectangle
|
||||
ex = vt["exclude"]
|
||||
exp = {(round(x), round(y)) for x in gx for y in gy if not (abs(x) <= ex["x"] and abs(y) <= ex["y"])}
|
||||
flag(f"{vt['mark']} positions = {len(gx)}x{len(gy)} grid minus |x|<={ex['x']} & |y|<={ex['y']} [추론]", pts == exp, f"{len(pts)} / {len(exp)}")
|
||||
else:
|
||||
exp = {(round(x), round(y)) for i, x in enumerate(gx) for j, y in enumerate(gy) if (i + j) % 2 == vt["parity"]}
|
||||
flag(f"{vt['mark']} positions = checkerboard of {len(gx)}x{len(gy)} grid [추론]", pts == exp, f"{len(pts)} / {len(exp)}")
|
||||
if cl[vt["mark"]]:
|
||||
check(f"{vt['mark']} bottom z (worst)", 0,
|
||||
max(abs(bbx(o).Min.Z - FB - vt["z0"]) for o in cl[vt["mark"]]))
|
||||
bb = bbx(cl[vt["mark"]][0])
|
||||
check(f"{vt['mark']} vertical extent a", vt["a"], bb.Max.Z - bb.Min.Z)
|
||||
if "legs" in vt: # both legs run towards +Y from the same point: Y extent = the longer leg
|
||||
check(f"{vt['mark']} leg extent along Y (longer leg)", max(vt["legs"].values()), bb.Max.Y - bb.Min.Y)
|
||||
ft = [o for o in all_objects(doc) if o.Attributes.GetUserString("id") == f"{PID}-FT"][0].Geometry
|
||||
cover = {}
|
||||
for m in SCHED:
|
||||
bad, cov = 0, 1e9
|
||||
r = DIA[m] / 2
|
||||
for o in cl[m]:
|
||||
c = o.Geometry
|
||||
for t in c.DivideByCount(max(4, int(c.GetLength() / 200)), True):
|
||||
p = c.PointAt(t)
|
||||
for q in (p, p + rg.Vector3d(r, 0, 0), p - rg.Vector3d(r, 0, 0), p + rg.Vector3d(0, r, 0),
|
||||
p - rg.Vector3d(0, r, 0), p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
||||
if not ft.IsPointInside(q, 0.01, False):
|
||||
bad += 1
|
||||
cov = min(cov, p.DistanceTo(ft.ClosestPoint(p)) - r)
|
||||
cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside footing", 0, bad, tol=0)
|
||||
info("min cover to footing surface per mark (record; top face under the column counted)", json.dumps(cover))
|
||||
wrong = 0
|
||||
for o in all_objects(doc):
|
||||
if (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-R-"):
|
||||
d = int(o.Attributes.GetUserString("dia")[1:])
|
||||
c = o.Attributes.ObjectColor
|
||||
if o.Attributes.ColorSource != ObjectColorSource.ColorFromObject or (c.R, c.G, c.B) != PALETTE.get(d):
|
||||
wrong += 1
|
||||
check("rebar pipe colour by diameter mismatches (all rebar)", 0, wrong, tol=0)
|
||||
write_result(RESULT_PATH, results, dict(cover=cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,115 @@
|
||||
"""G3 gate checker, footing rebar as centreline polylines (Okseong P1, 2026-10-14): mats B1~B8, mid layers B11 / B12,
|
||||
perimeter loops B10, vertical ties B9.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G3) come from the schedule (L, N), the
|
||||
front / side dimension chains (bar positions), the leg lengths b of the schedule, the drawn mat / loop levels and
|
||||
the B9 grid rule with the user decisions F1-F4; generator params are not read.
|
||||
Checks (C9): counts, original length vs L, per mat: positions vs chain, straight level, span, leg length and
|
||||
direction; mid layers; B10 levels / size; B9 grid / height; containment in the footing; min cover (record).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, chain, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, G = X["pier_id"], X["G3"]
|
||||
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
|
||||
results = []
|
||||
|
||||
|
||||
def check(item, exp, act, tol=1.0):
|
||||
results.append(dict(check="C9", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C9", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def info(item, detail):
|
||||
results.append(dict(check="C9", item=item, detail=detail, result="INFO"))
|
||||
|
||||
|
||||
def bb(o):
|
||||
return o.Geometry.GetBoundingBox(True)
|
||||
|
||||
|
||||
def worst(a, b):
|
||||
return max(abs(x - y) for x, y in zip(a, b)) if len(a) == len(b) and a else 1e9
|
||||
|
||||
|
||||
def main():
|
||||
cl = {m: [] for m in SCHED}
|
||||
pp = {m: [] for m in SCHED}
|
||||
for o in all_objects(doc):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
m = o.Attributes.GetUserString("mark")
|
||||
if m in SCHED and oid.startswith(f"{PID}-RC-"):
|
||||
cl[m].append(o)
|
||||
elif m in SCHED and oid.startswith(f"{PID}-R-"):
|
||||
pp[m].append(o)
|
||||
for m, (L, n) in SCHED.items():
|
||||
check(f"{m} bar count vs schedule", n, len(cl[m]), tol=0)
|
||||
flag(f"{m} pipes closed & paired", len(pp[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pp[m]), f"{len(pp[m])}/{len(cl[m])}")
|
||||
if cl[m]:
|
||||
check(f"{m} original length vs L (worst)", 0, max(abs(float(o.Attributes.GetUserString("design_length")) - L) for o in cl[m]))
|
||||
half = G["half_a"]
|
||||
for name, mt in G["mats"].items():
|
||||
objs = [o for m in mt["marks"] for o in cl[m]]
|
||||
axis = mt["axis"]
|
||||
pos = sorted(round((bb(o).Min.X + bb(o).Max.X) / 2 if axis == "Y" else (bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in objs)
|
||||
want = chain(mt["chain"])
|
||||
check(f"{name} positions vs chain ({len(pos)}/{len(want)}, worst)", 0, worst(pos, want))
|
||||
leg = mt["leg"]
|
||||
straight = [bb(o).Min.Z if leg > 0 else bb(o).Max.Z for o in objs]
|
||||
check(f"{name} straight part level {mt['z']} (worst)", 0, max(abs(z - mt["z"]) for z in straight) if straight else 1e9)
|
||||
span = [(bb(o).Max.Y - bb(o).Min.Y) if axis == "Y" else (bb(o).Max.X - bb(o).Min.X) for o in objs]
|
||||
check(f"{name} span 2 x {half} (worst)", 0, max(abs(s - 2 * half) for s in span) if span else 1e9)
|
||||
legs = [(bb(o).Max.Z - bb(o).Min.Z) for o in objs]
|
||||
check(f"{name} leg {abs(leg)} {'up' if leg > 0 else 'down'} (worst)", 0, max(abs(h - abs(leg)) for h in legs) if legs else 1e9)
|
||||
a, b = mt["marks"]
|
||||
ia = sorted(round((bb(o).Min.X + bb(o).Max.X) / 2 if axis == "Y" else (bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in cl[a])
|
||||
flag(f"{name} {a}/{b} alternate from the edge (F3)", ia == want[0::2], f"{a} {len(ia)}")
|
||||
for m, md in G["mid"].items():
|
||||
got = sorted((round(bb(o).Max.Z, 1), round((bb(o).Min.X + bb(o).Max.X) / 2 if md["axis"] == "Y" else (bb(o).Min.Y + bb(o).Max.Y) / 2, 1)) for o in cl[m])
|
||||
want = sorted((md["z_hi"] if round(p) % 1000 == 0 else md["z_lo"], round(p, 1)) for p in chain(md["chain"]))
|
||||
flag(f"{m} levels / positions ({len(want)})", len(got) == len(want) and all(abs(g[0] - w[0]) <= 1 and abs(g[1] - w[1]) <= 1 for g, w in zip(got, want)),
|
||||
f"{got[:3]} vs {want[:3]}")
|
||||
b10 = G["b10"]
|
||||
zs = sorted(round(bb(o).Min.Z, 1) for o in cl["B10"])
|
||||
check("B10 levels (worst)", 0, worst(zs, sorted(b10["levels"])))
|
||||
check("B10 loop size X (worst)", 0, max(abs(bb(o).Max.X - bb(o).Min.X - b10["size"]) for o in cl["B10"]) if cl["B10"] else 1e9)
|
||||
b9 = G["b9"]
|
||||
cols = sorted({round((bb(o).Min.X + bb(o).Max.X) / 2, 1) for o in cl["B9"]})
|
||||
flag(f"B9 columns {b9['n_cols']} @{b9['pitch_x']}", len(cols) == b9["n_cols"] and all(abs(b - a - b9["pitch_x"]) < 1 for a, b in zip(cols, cols[1:])), f"{len(cols)}")
|
||||
check("B9 height top-bottom (worst)", b9["height"], max((bb(o).Max.Z - bb(o).Min.Z for o in cl["B9"]), key=lambda h: abs(h - b9["height"])) if cl["B9"] else 0)
|
||||
flag("B9 hooks the top and bottom Y bars", all(bb(o).Max.Z > G["mats"]["y_top"]["z"] and bb(o).Min.Z < G["mats"]["y_bottom"]["z"] for o in cl["B9"]))
|
||||
# containment and cover
|
||||
ft = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-FT"][0]
|
||||
cover = {}
|
||||
for m in SCHED:
|
||||
bad, cov = 0, 1e9
|
||||
r = G["dia"][m] / 2
|
||||
for o in cl[m]:
|
||||
cu = o.Geometry
|
||||
for t in cu.DivideByCount(max(6, int(cu.GetLength() / 300)), True):
|
||||
p = cu.PointAt(t)
|
||||
for q in (p, p + rg.Vector3d(r, 0, 0), p - rg.Vector3d(r, 0, 0), p + rg.Vector3d(0, r, 0),
|
||||
p - rg.Vector3d(0, r, 0), p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
||||
if not ft.IsPointInside(q, 0.01, False):
|
||||
bad += 1
|
||||
cov = min(cov, p.DistanceTo(ft.ClosestPoint(p)) - r)
|
||||
cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside the footing", 0, bad, tol=0)
|
||||
info("min cover to the footing surface per mark (record)", json.dumps(cover))
|
||||
write_result(RESULT_PATH, results, dict(cover=cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,181 @@
|
||||
"""G3 gate checker, form "shaft cage" (Ramp-A P1, 2026-10-13): drilled-shaft cage bars and the anti-uplift plate.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G3) come from the D4 schedule (L, N),
|
||||
the 구조물 치수표 chains measured from the shaft top EL (D1), the section radii and the user decisions S1-S6;
|
||||
generator params are not read.
|
||||
Checks (C9): counts, original length (+coupler) vs schedule L, main radius / division / piece levels, hoop and
|
||||
tie level grids, hoop radius, X / Y ties apart, U bars (count, bottom level, width, planes), plate (size,
|
||||
thickness, centre, cover), containment in the pile segments, min side cover (record).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, TOL, G = X["pier_id"], X["tol_mm"], X["G3"]
|
||||
SCHED_L, SCHED_N, DIA = G["schedule_L"], G["schedule_n"], G["dia"]
|
||||
M, HP, TI, U, PL = G["main"], G["hoops"], G["ties"], G["u_bars"], G["plate"]
|
||||
TOP = G["shaft_top"]
|
||||
PILE = X["G1"]["pile"]
|
||||
Z_BOT = PILE["z_bottom"]
|
||||
SEG = [] # (z0, z1, radius) bottom to top
|
||||
z = Z_BOT
|
||||
for s in PILE["segments"]:
|
||||
SEG.append((z, z + s["length"], s["d"] / 2))
|
||||
z += s["length"]
|
||||
|
||||
|
||||
def chain(c):
|
||||
"""Levels from the shaft top: first offset, then (count, pitch) groups downwards; returned bottom to top."""
|
||||
out = [TOP - c["first"]]
|
||||
for n, p in c["groups"]:
|
||||
out += [out[-1] - p * k for k in range(1, n + 1)]
|
||||
return sorted(out)
|
||||
|
||||
|
||||
HOOP_LEVELS, TIE_LEVELS = chain(HP["chain"]), chain(TI["chain"])
|
||||
results = []
|
||||
|
||||
|
||||
def check(item, exp, act, tol=TOL):
|
||||
results.append(dict(check="C9", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C9", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def info(item, detail):
|
||||
results.append(dict(check="C9", item=item, detail=detail, result="INFO"))
|
||||
|
||||
|
||||
def ids(prefix):
|
||||
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)]
|
||||
|
||||
|
||||
def main():
|
||||
cl = {m: [o for o in ids(f"{PID}-RC-") if o.Attributes.GetUserString("mark") == m] for m in SCHED_L}
|
||||
pipes = {m: [o for o in ids(f"{PID}-R-") if o.Attributes.GetUserString("mark") == m] for m in SCHED_L}
|
||||
occ = lambda o: o.Attributes.GetUserString("occurrence").rstrip("ab")
|
||||
hm, tm = HP["mark"], TI["mark"]
|
||||
for m in SCHED_L:
|
||||
n_occ = len({occ(o) for o in cl[m]}) if m == hm else len(cl[m])
|
||||
check(f"{m} bar count", SCHED_N[m], n_occ, tol=0)
|
||||
flag(f"{m} pipes closed & paired with centrelines",
|
||||
len(pipes[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pipes[m]), f"{len(pipes[m])}/{len(cl[m])}")
|
||||
for m in SCHED_L:
|
||||
per, curve = {}, {}
|
||||
for o in cl[m]:
|
||||
k = occ(o) if m == hm else o.Attributes.GetUserString("occurrence")
|
||||
cp = float(o.Attributes.GetUserString("coupler_length") or 0)
|
||||
per[k] = per.get(k, 0) + float(o.Attributes.GetUserString("design_length")) + cp
|
||||
curve[k] = curve.get(k, 0) + o.Geometry.GetLength() + cp
|
||||
if per:
|
||||
check(f"{m} original length (+coupler) vs schedule L (worst of {len(per)})", 0,
|
||||
max(abs(v - SCHED_L[m]) for v in per.values()))
|
||||
dev = [v - SCHED_L[m] for v in curve.values()]
|
||||
info(f"{m} curve length (+coupler) - L (record)", f"min {min(dev):.1f} / max {max(dev):.1f} mm")
|
||||
# main bars: pieces stacked between the first and the last hoop level (S6: coupler length 0)
|
||||
zb, zt = HOOP_LEVELS[0], HOOP_LEVELS[-1]
|
||||
check("main span (last - first hoop level) = sum of piece L", zt - zb, sum(SCHED_L[mk] for mk in M["pieces"]))
|
||||
z = zb
|
||||
for mk in M["pieces"]: # bottom to top
|
||||
lo = [min(o.Geometry.PointAtStart.Z, o.Geometry.PointAtEnd.Z) for o in cl[mk]]
|
||||
hi = [max(o.Geometry.PointAtStart.Z, o.Geometry.PointAtEnd.Z) for o in cl[mk]]
|
||||
check(f"{mk} bottom EL (worst)", 0, max(abs(v - z) for v in lo))
|
||||
check(f"{mk} top EL = bottom + L (worst)", 0, max(abs(v - (z + SCHED_L[mk])) for v in hi))
|
||||
z += SCHED_L[mk]
|
||||
rr = [math.hypot(o.Geometry.PointAtStart.X, o.Geometry.PointAtStart.Y) for o in cl[mk]]
|
||||
check(f"{mk} radius (worst)", 0, max(abs(r - M["radius"]) for r in rr), tol=0.5)
|
||||
a = sorted(math.degrees(math.atan2(o.Geometry.PointAtStart.Y, o.Geometry.PointAtStart.X)) % 360 for o in cl[mk])
|
||||
check(f"{mk} angle vs {M['start_deg']} + i*360/{M['divisions']} (worst, deg)", 0,
|
||||
max(abs(x - (M["start_deg"] + i * 360 / M["divisions"])) for i, x in enumerate(a)), tol=0.01)
|
||||
# hoops
|
||||
hz = {}
|
||||
for o in cl[hm]:
|
||||
bb = o.Geometry.GetBoundingBox(True)
|
||||
hz.setdefault(round((bb.Max.Z + bb.Min.Z) / 2, 1), []).append(o)
|
||||
zs = sorted(hz)
|
||||
flag(f"{hm} levels {len(HOOP_LEVELS)} x 2 pieces", len(zs) == len(HOOP_LEVELS) and all(len(v) == 2 for v in hz.values()),
|
||||
f"{len(zs)} levels")
|
||||
check(f"{hm} levels vs 구조물 치수표 chain (worst)", 0,
|
||||
max(abs(a - b) for a, b in zip(zs, HOOP_LEVELS)) if len(zs) == len(HOOP_LEVELS) else 1e9)
|
||||
for o in cl[hm][:4]:
|
||||
c = o.Geometry
|
||||
p = c.PointAt(c.Domain.Mid)
|
||||
check(f"{hm} radius at mid point ({o.Attributes.GetUserString('occurrence')})", HP["radius"], math.hypot(p.X, p.Y), tol=0.5)
|
||||
# ties
|
||||
by = lambda s: sorted((o.Geometry for o in cl[tm] if o.Attributes.GetUserString("occurrence").endswith(s)),
|
||||
key=lambda g: g.GetBoundingBox(True).Max.Z)
|
||||
xs, ys = by("X"), by("Y")
|
||||
txm = [g.GetBoundingBox(True).Max.Z for g in xs]
|
||||
tym = [g.GetBoundingBox(True).Max.Z for g in ys]
|
||||
flag(f"{tm} X / Y per level, {len(TIE_LEVELS)} levels", len(txm) == len(tym) == len(TIE_LEVELS), f"X {len(txm)} Y {len(tym)}")
|
||||
if len(txm) == len(TIE_LEVELS) == len(tym):
|
||||
check(f"{tm} X levels vs chain (worst)", 0, max(abs(a - b) for a, b in zip(txm, TIE_LEVELS)))
|
||||
check(f"{tm} Y levels = X + {TI['y_lift']} (worst)", 0, max(abs(a - b - TI["y_lift"]) for a, b in zip(tym, TIE_LEVELS)))
|
||||
gaps = []
|
||||
for a, b in zip(xs, ys):
|
||||
ok, pa, pb = a.ClosestPoints(b)
|
||||
gaps.append(pa.DistanceTo(pb) if ok else 0.0)
|
||||
gap = min(gaps) if gaps else 0.0
|
||||
check(f"{tm} X / Y centrelines apart >= bar diameter {DIA[tm]} (min, capped)", DIA[tm], min(gap, DIA[tm]), tol=0.01)
|
||||
# U bars: bottom level, overall width (vertical legs on the hoop circle), one plane each through the centre
|
||||
um = U["mark"]
|
||||
bots = [o.Geometry.GetBoundingBox(True).Min.Z for o in cl[um]]
|
||||
check(f"{um} bottom EL = shaft bottom + {U['bottom_above']} (worst, centreline)", 0,
|
||||
max(abs(b - (Z_BOT + U["bottom_above"])) for b in bots) if bots else 1e9)
|
||||
w, planar = [], 0
|
||||
for o in cl[um]:
|
||||
c = o.Geometry
|
||||
a, b = c.PointAtStart, c.PointAtEnd
|
||||
w.append(math.hypot(a.X - b.X, a.Y - b.Y))
|
||||
ok, pl = c.TryGetPlane(0.01)
|
||||
if ok and abs(pl.Normal.Z) < 1e-6 and abs(pl.DistanceTo(rg.Point3d(0, 0, pl.Origin.Z))) < 0.5:
|
||||
planar += 1
|
||||
check(f"{um} leg spacing (worst) = 2 x hoop radius {2 * HP['radius']:.0f}", 0, max(abs(x - 2 * HP["radius"]) for x in w) if w else 1e9)
|
||||
check(f"{um} vertical planes through the centre", len(cl[um]), planar, tol=0)
|
||||
# plate
|
||||
pts = ids(f"{PID}-PT")
|
||||
flag("plate count 1", len(pts) == 1, str(len(pts)))
|
||||
if pts:
|
||||
bb = pts[0].Geometry.GetBoundingBox(True)
|
||||
check("plate size X", PL["size"], bb.Max.X - bb.Min.X)
|
||||
check("plate size Y", PL["size"], bb.Max.Y - bb.Min.Y)
|
||||
check("plate thickness", PL["thickness"], bb.Max.Z - bb.Min.Z, tol=0.01)
|
||||
check("plate centre X / Y offset", 0, math.hypot((bb.Max.X + bb.Min.X) / 2, (bb.Max.Y + bb.Min.Y) / 2))
|
||||
check(f"plate top = U bottom - {DIA[um] / 2:.0f} (S4)", Z_BOT + U["bottom_above"] - DIA[um] / 2, bb.Max.Z, tol=0.01)
|
||||
info("plate bottom cover (record)", f"{bb.Min.Z - Z_BOT:.1f} mm")
|
||||
# containment and side cover in the pile segments
|
||||
solids = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-PL")]
|
||||
min_cover = {}
|
||||
for m in SCHED_L:
|
||||
bad, cov = 0, 1e9
|
||||
r = DIA[m] / 2
|
||||
for o in cl[m]:
|
||||
c = o.Geometry
|
||||
for t in c.DivideByCount(max(4, int(c.GetLength() / 150)), True):
|
||||
p = c.PointAt(t)
|
||||
radial = rg.Vector3d(p.X, p.Y, 0)
|
||||
radial.Unitize() if radial.Length > 1e-6 else None
|
||||
for q in (p, p + radial * r, p - radial * r, p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
||||
if not any(s.IsPointInside(q, 0.01, False) for s in solids):
|
||||
bad += 1
|
||||
R = next((rad for z0, z1, rad in SEG if z0 <= p.Z <= z1), None)
|
||||
if R is not None:
|
||||
cov = min(cov, R - math.hypot(p.X, p.Y) - r, p.Z - Z_BOT - r)
|
||||
min_cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside the pile concrete", 0, bad, tol=0)
|
||||
info("min cover per mark (side or bottom, record)", json.dumps(min_cover))
|
||||
write_result(RESULT_PATH, results, dict(min_cover=min_cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,124 @@
|
||||
"""G3 gate checker, footing rebar as centreline polylines with drawn position lists (Saejae-1 P1, 2026-10-15; adapted from g3_poly.py,
|
||||
Okseong P1): mats B1~B8, mid layers B11 / B12, perimeter loops B10, vertical ties B9 (6 rows, 8 / 7 per row alternating).
|
||||
Positions are explicit lists (expected G3 "pos" / "levels" / "b9") typed from the drawn points, not dimension chains.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G3) come from the schedule (L, N), the
|
||||
front / side dimension chains (bar positions), the leg lengths b of the schedule, the drawn mat / loop levels and
|
||||
the B9 grid rule with the user decisions F1-F4; generator params are not read.
|
||||
Checks (C9): counts, original length vs L, per mat: positions vs chain, straight level, span, leg length and
|
||||
direction; mid layers; B10 levels / size; B9 grid / height; containment in the footing; min cover (record).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, G = X["pier_id"], X["G3"]
|
||||
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
|
||||
results = []
|
||||
|
||||
|
||||
def check(item, exp, act, tol=1.0):
|
||||
results.append(dict(check="C9", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C9", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def info(item, detail):
|
||||
results.append(dict(check="C9", item=item, detail=detail, result="INFO"))
|
||||
|
||||
|
||||
def bb(o):
|
||||
return o.Geometry.GetBoundingBox(True)
|
||||
|
||||
|
||||
def mid_(o):
|
||||
return (bb(o).Min.X + bb(o).Max.X) / 2, (bb(o).Min.Y + bb(o).Max.Y) / 2
|
||||
|
||||
|
||||
def worst(a, b):
|
||||
return max(abs(x - y) for x, y in zip(a, b)) if len(a) == len(b) and a else 1e9
|
||||
|
||||
|
||||
def main():
|
||||
cl = {m: [] for m in SCHED}
|
||||
pp = {m: [] for m in SCHED}
|
||||
for o in all_objects(doc):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
m = o.Attributes.GetUserString("mark")
|
||||
if m in SCHED and oid.startswith(f"{PID}-RC-"):
|
||||
cl[m].append(o)
|
||||
elif m in SCHED and oid.startswith(f"{PID}-R-"):
|
||||
pp[m].append(o)
|
||||
for m, (L, n) in SCHED.items():
|
||||
check(f"{m} bar count vs schedule", n, len(cl[m]), tol=0)
|
||||
flag(f"{m} pipes closed & paired", len(pp[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pp[m]), f"{len(pp[m])}/{len(cl[m])}")
|
||||
if cl[m]:
|
||||
check(f"{m} original length vs L (worst)", 0, max(abs(float(o.Attributes.GetUserString("design_length")) - L) for o in cl[m]))
|
||||
half = G["half_a"]
|
||||
for name, mt in G["mats"].items():
|
||||
objs = [o for m in mt["marks"] for o in cl[m]]
|
||||
axis = mt["axis"]
|
||||
pos = sorted(round((bb(o).Min.X + bb(o).Max.X) / 2 if axis == "Y" else (bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in objs)
|
||||
want = sorted(mt["pos"])
|
||||
check(f"{name} positions vs chain ({len(pos)}/{len(want)}, worst)", 0, worst(pos, want))
|
||||
leg = mt["leg"]
|
||||
straight = [bb(o).Min.Z if leg > 0 else bb(o).Max.Z for o in objs]
|
||||
check(f"{name} straight part level {mt['z']} (worst)", 0, max(abs(z - mt["z"]) for z in straight) if straight else 1e9)
|
||||
span = [(bb(o).Max.Y - bb(o).Min.Y) if axis == "Y" else (bb(o).Max.X - bb(o).Min.X) for o in objs]
|
||||
check(f"{name} span 2 x {half} (worst)", 0, max(abs(s - 2 * half) for s in span) if span else 1e9)
|
||||
legs = [(bb(o).Max.Z - bb(o).Min.Z) for o in objs]
|
||||
check(f"{name} leg {abs(leg)} {'up' if leg > 0 else 'down'} (worst)", 0, max(abs(h - abs(leg)) for h in legs) if legs else 1e9)
|
||||
a, b = mt["marks"]
|
||||
ia = sorted(round((bb(o).Min.X + bb(o).Max.X) / 2 if axis == "Y" else (bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in cl[a])
|
||||
flag(f"{name} {a}/{b} alternate from the edge (F3)", ia == want[0::2], f"{a} {len(ia)}")
|
||||
for m, md in G["mid"].items():
|
||||
lift = md.get("lift", 0)
|
||||
got = sorted((round(bb(o).Max.Z, 1), round((bb(o).Min.X + bb(o).Max.X) / 2 if md["axis"] == "Y" else (bb(o).Min.Y + bb(o).Max.Y) / 2, 1)) for o in cl[m])
|
||||
want = sorted((float(z) + lift, p) for z, ps in md["levels"].items() for p in ps)
|
||||
flag(f"{m} levels / positions ({len(want)})", len(got) == len(want) and all(abs(g[0] - w[0]) <= 1 and abs(g[1] - w[1]) <= 1 for g, w in zip(got, want)),
|
||||
f"{got[:3]} vs {want[:3]}")
|
||||
b10 = G["b10"]
|
||||
zs = sorted(round(bb(o).Min.Z, 1) for o in cl["B10"])
|
||||
check("B10 levels (worst)", 0, worst(zs, sorted(b10["levels"])))
|
||||
check("B10 loop size X (worst)", 0, max(abs(bb(o).Max.X - bb(o).Min.X - b10["size"]) for o in cl["B10"]) if cl["B10"] else 1e9)
|
||||
b9 = G["b9"]
|
||||
got9 = sorted((round(mid_(o)[0], 1), round(mid_(o)[1], 1)) for o in cl["B9"])
|
||||
want9 = sorted((x, y) for k, y in enumerate(b9["rows"]) for x in (b9["cols_even"] if k % 2 == 0 else b9["cols_odd"]))
|
||||
flag(f"B9 grid {len(want9)} (rows {len(b9['rows'])}, 8 / 7 per row)", len(got9) == len(want9) and all(abs(g[0] - w[0]) <= 1 and abs(g[1] - w[1]) <= 1 for g, w in zip(got9, want9)),
|
||||
f"{len(got9)}")
|
||||
check("B9 height top-bottom (worst)", b9["height"], max((bb(o).Max.Z - bb(o).Min.Z for o in cl["B9"]), key=lambda h: abs(h - b9["height"])) if cl["B9"] else 0)
|
||||
check("B9 span along Y a (worst)", b9["a"], max((bb(o).Max.Y - bb(o).Min.Y for o in cl["B9"]), key=lambda h: abs(h - b9["a"])) if cl["B9"] else 0)
|
||||
flag("B9 hooks the top and bottom Y bars", all(bb(o).Max.Z > G["mats"]["y_top"]["z"] and bb(o).Min.Z < G["mats"]["y_bottom"]["z"] for o in cl["B9"]))
|
||||
# containment and cover
|
||||
ft = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-FT"][0]
|
||||
cover = {}
|
||||
for m in SCHED:
|
||||
bad, cov = 0, 1e9
|
||||
r = G["dia"][m] / 2
|
||||
for o in cl[m]:
|
||||
cu = o.Geometry
|
||||
for t in cu.DivideByCount(max(6, int(cu.GetLength() / 300)), True):
|
||||
p = cu.PointAt(t)
|
||||
for q in (p, p + rg.Vector3d(r, 0, 0), p - rg.Vector3d(r, 0, 0), p + rg.Vector3d(0, r, 0),
|
||||
p - rg.Vector3d(0, r, 0), p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
||||
if not ft.IsPointInside(q, 0.01, False):
|
||||
bad += 1
|
||||
cov = min(cov, p.DistanceTo(ft.ClosestPoint(p)) - r)
|
||||
cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside the footing", 0, bad, tol=0)
|
||||
info("min cover to the footing surface per mark (record)", json.dumps(cover))
|
||||
write_result(RESULT_PATH, results, dict(cover=cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,138 @@
|
||||
"""G4 gate checker: coping rebar C1-C16, S1, S2 (check C10) inside Rhino 8.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values from EXPECTED_PATH section G4 (D3 counts, L,
|
||||
a/b ranges; D1/D2 dimension chains). Width decisions (K1) make the original-length checks of the range marks
|
||||
fail by design; they are reported as FAIL and listed in `planned_fail`. Generator params are not read.
|
||||
Ported from Code/교각모델링/verify_coping_rebar.py (same checks and order).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectColorSource
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, series, write_result, ychain # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, G = X["pier_id"], X["G4"]
|
||||
COUNT, L, DIA = G["count"], G["L"], G["dia"]
|
||||
RANGE = {m: tuple(v) for m, v in G["range"].items()}
|
||||
Y24 = ychain(G["y24_chain"], G["half_width"])
|
||||
Y13 = ychain(G["y13_chain"], G["half_width"])
|
||||
TIER = G["tier_z"]
|
||||
STIR_X = {m: sorted(series(v)) for m, v in G["stirrup_x"].items()}
|
||||
LOOP_Z = {m: series(v) for m, v in G["loop_z"].items()}
|
||||
PALETTE = {int(k): tuple(v) for k, v in X["rebar_palette"].items()}
|
||||
results = []
|
||||
|
||||
|
||||
def check(item, exp, act, tol=1.0):
|
||||
results.append(dict(check="C10", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C10", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def info(item, detail):
|
||||
results.append(dict(check="C10", item=item, detail=detail, result="INFO"))
|
||||
|
||||
|
||||
def main():
|
||||
objs = all_objects(doc)
|
||||
cl = {m: [o for o in objs if (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-RC-")
|
||||
and o.Attributes.GetUserString("mark") == m] for m in COUNT}
|
||||
pp = {m: [o for o in objs if (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-R-")
|
||||
and o.Attributes.GetUserString("mark") == m] for m in COUNT}
|
||||
for m in COUNT:
|
||||
check(f"{m} bar count", COUNT[m], len(cl[m]), tol=0)
|
||||
flag(f"{m} pipes closed & paired", len(pp[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pp[m]))
|
||||
if not cl[m]:
|
||||
continue
|
||||
lens = [float(o.Attributes.GetUserString("design_length")) for o in cl[m]]
|
||||
if m in RANGE:
|
||||
key, hi, lo = RANGE[m]
|
||||
vals = [float(o.Attributes.GetUserString(key)) for o in cl[m]]
|
||||
check(f"{m} {key} max vs schedule", hi, max(vals))
|
||||
check(f"{m} {key} min vs schedule", lo, min(vals))
|
||||
cv = G["range_convention"][key]
|
||||
fixed = cv["a"] if key == "b_sched" else cv["b"]
|
||||
avg_conv = 2 * fixed + (hi + lo) + cv["c_times"] * cv["c"] # schedule convention: (max+min)/2
|
||||
check(f"{m} schedule L reproduced by convention (max+min)/2", L[m], avg_conv)
|
||||
ws = sorted({o.Attributes.GetUserString("w_model") for o in cl[m]})
|
||||
results.append(dict(check="C10", item=f"{m} original length vs L (drawn widths {ws}, K1 decision)",
|
||||
detail=f"model mean {sum(lens) / len(lens):.1f} vs L {L[m]}", result="FAIL"))
|
||||
else:
|
||||
check(f"{m} original length vs L (worst)", 0, max(abs(v - L[m]) for v in lens))
|
||||
dev = [o.Geometry.GetLength() - float(o.Attributes.GetUserString("design_length")) for o in cl[m]]
|
||||
info(f"{m} curve - original length", f"{min(dev):.1f} / {max(dev):.1f}")
|
||||
for m, z in TIER.items():
|
||||
zs = [o.Geometry.PointAt(o.Geometry.Domain.Mid).Z for o in cl[m]]
|
||||
check(f"{m} tier EL (worst)", 0, max(abs(v - z) for v in zs))
|
||||
for pair in G["pairs"]:
|
||||
ys = sorted((round(o.Geometry.PointAt(o.Geometry.Domain.Mid).Y), m) for m in pair for o in cl[m])
|
||||
flag(f"{'/'.join(pair)} Y = A-A chain, alternate from {pair[0]} at {Y24[0]:.0f}",
|
||||
[y for y, _ in ys] == Y24 and [m for _, m in ys] == [pair[i % 2] for i in range(len(Y24))])
|
||||
for m, xs in STIR_X.items():
|
||||
got = sorted({round(o.Geometry.GetBoundingBox(True).Center.X) for o in cl[m]})
|
||||
flag(f"{m} stirrup X positions", got == xs, f"{len(got)} positions")
|
||||
per = {}
|
||||
for o in cl[m]:
|
||||
k = round(o.Geometry.GetBoundingBox(True).Center.X)
|
||||
per[k] = per.get(k, 0) + 1
|
||||
flag(f"{m} {G['stirrups_per_x']} stirrups per position", set(per.values()) == {G["stirrups_per_x"]})
|
||||
zb = [round(o.Geometry.GetBoundingBox(True).Min.Z) for o in cl["C10"]]
|
||||
check(f"C10 stirrup bottom EL {G['c10_bottom'] / 1000:.3f} (worst)", 0, max(abs(v - G["c10_bottom"]) for v in zb))
|
||||
for m, zs in LOOP_Z.items():
|
||||
got = sorted({round(o.Geometry.GetBoundingBox(True).Center.Z) for o in cl[m]})
|
||||
flag(f"{m} loop levels", got == zs, str(got))
|
||||
for m in ("C15", "C16"):
|
||||
got = sorted(round(o.Geometry.GetBoundingBox(True).Center.Y) for o in cl[m])
|
||||
flag(f"{m} Y = B-B chain", got == Y13, f"{len(got)}")
|
||||
check(f"C15 EL {G['c15_top'] / 1000:.3f}", G["c15_top"], max(o.Geometry.GetBoundingBox(True).Max.Z for o in cl["C15"]))
|
||||
check(f"C16 EL {G['c16_top'] / 1000:.3f}", G["c16_top"], max(o.Geometry.GetBoundingBox(True).Max.Z for o in cl["C16"]))
|
||||
# horizontal-segment height (bbox max picks up upward hooks)
|
||||
c14z = sorted(round(o.Geometry.PointAt(o.Geometry.Domain.Mid).Z, 1) for o in cl["C14"])
|
||||
exp14 = sorted(z for z, n in G["c14_levels"] for _ in range(n))
|
||||
flag("C14 levels/count per level", c14z == exp14, "")
|
||||
step = {int(k): v for k, v in G["step_top"].items()}
|
||||
for m, (n_per, dz) in G["s_per_girder"].items():
|
||||
by = {}
|
||||
for o in cl[m]:
|
||||
g = int(o.Attributes.GetUserString("occurrence")[1])
|
||||
by.setdefault(g, []).append(o.Geometry.GetBoundingBox(True).Max.Z)
|
||||
flag(f"{m} per girder line = {n_per}", sorted(len(v) for v in by.values()) == [n_per] * len(step))
|
||||
check(f"{m} EL = step top - {dz} (worst)", 0, max(abs(z - (step[g] - dz)) for g, v in by.items() for z in v))
|
||||
cp = [o for o in objs if o.Attributes.GetUserString("id") == f"{PID}-CP"][0].Geometry
|
||||
cover = {}
|
||||
for m in COUNT:
|
||||
bad, cov = 0, 1e9
|
||||
r = DIA[m] / 2
|
||||
for o in cl[m]:
|
||||
c = o.Geometry
|
||||
for t in c.DivideByCount(max(4, int(c.GetLength() / 200)), True):
|
||||
p = c.PointAt(t)
|
||||
for q in (p, p + rg.Vector3d(r, 0, 0), p - rg.Vector3d(r, 0, 0), p + rg.Vector3d(0, r, 0),
|
||||
p - rg.Vector3d(0, r, 0), p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
||||
if not cp.IsPointInside(q, 0.01, False):
|
||||
bad += 1
|
||||
cov = min(cov, p.DistanceTo(cp.ClosestPoint(p)) - r)
|
||||
cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside coping", 0, bad, tol=0)
|
||||
info("min cover to coping surface per mark (top face under pedestals counted)", json.dumps(cover))
|
||||
wrong = 0
|
||||
for o in objs:
|
||||
if (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-R-"):
|
||||
d = int(o.Attributes.GetUserString("dia")[1:])
|
||||
c = o.Attributes.ObjectColor
|
||||
if o.Attributes.ColorSource != ObjectColorSource.ColorFromObject or (c.R, c.G, c.B) != PALETTE[d]:
|
||||
wrong += 1
|
||||
check("rebar colour mismatches (all rebar)", 0, wrong, tol=0)
|
||||
write_result(RESULT_PATH, results, dict(cover=cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,222 @@
|
||||
"""G4 gate checker, Okseong P1 coping + bearing-seat rebar (2026-10-14): C1~C20 and S1~S8 as centreline polylines (plan skew 18 deg).
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G4, plus the G1 pedestal table) come from the schedule
|
||||
(L, N, polyline length = L - lap), the drawn levels / positions of the rebar drawing, the section dimension chains and the user
|
||||
decisions H1-H6; generator params are not read. Coping bars are sheared like the coping: x_model = x - tan(skew) * y, so the
|
||||
section x of a bar is bbox mid X + tan * bbox mid Y. Seat bars are checked in the pedestal frame.
|
||||
Checks (C10): counts / pipes / polyline length, top layers (rows, ends, legs), C19 / C20, C7, stirrups (x, top, bottom, y), side loops
|
||||
and ties, C18, seat bars per pedestal, containment in the coping + pedestals, minimum cover (record).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import statistics
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, G = X["pier_id"], X["G4"]
|
||||
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
|
||||
T = math.tan(math.radians(G["skew_deg"]))
|
||||
COS = math.cos(math.radians(G["skew_deg"]))
|
||||
results = []
|
||||
|
||||
|
||||
def check(item, exp, act, tol=1.0):
|
||||
results.append(dict(check="C10", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C10", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def info(item, detail):
|
||||
results.append(dict(check="C10", item=item, detail=detail, result="INFO"))
|
||||
|
||||
|
||||
def bb(o):
|
||||
return o.Geometry.GetBoundingBox(True)
|
||||
|
||||
|
||||
def mid(o):
|
||||
b = bb(o)
|
||||
return (b.Min.X + b.Max.X) / 2, (b.Min.Y + b.Max.Y) / 2
|
||||
|
||||
|
||||
def xs(o):
|
||||
"""Section x of a coping bar (shear removed)."""
|
||||
mx, my = mid(o)
|
||||
return mx + T * my
|
||||
|
||||
|
||||
def worst(got, want):
|
||||
got, want = sorted(got), sorted(want)
|
||||
return max((abs(a - b) for a, b in zip(got, want)), default=1e9) if len(got) == len(want) else 1e9
|
||||
|
||||
|
||||
def main():
|
||||
cl = {m: [] for m in SCHED}
|
||||
pp = {m: [] for m in SCHED}
|
||||
for o in all_objects(doc):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
m = o.Attributes.GetUserString("mark")
|
||||
if m in SCHED and oid.startswith(f"{PID}-RC-"):
|
||||
cl[m].append(o)
|
||||
elif m in SCHED and oid.startswith(f"{PID}-R-"):
|
||||
pp[m].append(o)
|
||||
for m, (L, n) in SCHED.items():
|
||||
check(f"{m} bar count vs schedule", n, len(cl[m]), tol=0)
|
||||
flag(f"{m} pipes closed & paired", len(pp[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pp[m]), f"{len(pp[m])}/{len(cl[m])}")
|
||||
dl = [float(o.Attributes.GetUserString("design_length")) for o in cl[m]]
|
||||
if m in G["fixed_length"]:
|
||||
check(f"{m} polyline length vs L - lap (worst)", 0, max(abs(v - G["fixed_length"][m]) for v in dl) if dl else 1e9)
|
||||
elif dl:
|
||||
info(f"{m} mean polyline length vs L (variable mark, record)",
|
||||
f"mean {statistics.mean(dl):.1f} / L {L} (diff {statistics.mean(dl) - L:+.1f}); min {min(dl):.0f} max {max(dl):.0f}")
|
||||
# top layers C1~C6
|
||||
tp, y25 = G["top"], G["y25"]
|
||||
for m in ("C1", "C2", "C3", "C4", "C5", "C6"):
|
||||
s = tp[m]
|
||||
want = [y for i, y in enumerate(y25) if i % 2 == s["parity"]]
|
||||
got = [mid(o)[1] for o in cl[m]]
|
||||
check(f"{m} y rows vs chain (worst, n={len(got)}/{len(want)})", 0, worst(got, want))
|
||||
check(f"{m} level of the straight part (worst)", 0, max(abs(bb(o).Max.Z - s["z"]) for o in cl[m]))
|
||||
check(f"{m} leg {s['leg']} (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - s["leg"]) for o in cl[m]))
|
||||
check(f"{m} x ends +-{tp['x_end']} sheared (worst)", 0,
|
||||
max(max(abs(bb(o).Min.X + T * mid(o)[1] + tp["x_end"]), abs(bb(o).Max.X + T * mid(o)[1] - tp["x_end"])) for o in cl[m]))
|
||||
# C19, C20
|
||||
c19 = G["c19"]
|
||||
check("C19 y rows vs chain (worst)", 0, worst([mid(o)[1] for o in cl["C19"]], y25))
|
||||
check("C19 level (worst)", 0, max(abs(bb(o).Max.Z - c19["z"]) for o in cl["C19"]))
|
||||
check("C19 x ends (worst)", 0, max(max(abs(bb(o).Min.X + T * mid(o)[1] - c19["x"][0]), abs(bb(o).Max.X + T * mid(o)[1] - c19["x"][1])) for o in cl["C19"]))
|
||||
check("C19 leg 370 (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - G["c20_leg"]) for o in cl["C19"]))
|
||||
for k, pc in enumerate(G["c20"]):
|
||||
grp = [o for o in cl["C20"] if abs(bb(o).Max.Z - pc["z"]) < 0.5 and abs(bb(o).Min.X + T * mid(o)[1] - pc["x"][0]) < 3]
|
||||
flag(f"C20 piece {k + 1}: 25 bars at EL {pc['z']}, x {pc['x']}", len(grp) == 25, f"{len(grp)}")
|
||||
if grp:
|
||||
check(f"C20 piece {k + 1} x ends (worst)", 0, max(max(abs(bb(o).Min.X + T * mid(o)[1] - pc["x"][0]), abs(bb(o).Max.X + T * mid(o)[1] - pc["x"][1])) for o in grp))
|
||||
check(f"C20 piece {k + 1} leg 370 (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - G["c20_leg"]) for o in grp))
|
||||
check(f"C20 piece {k + 1} y rows (worst)", 0, worst([mid(o)[1] for o in grp], y25))
|
||||
# C7
|
||||
c7 = G["c7"]
|
||||
check("C7 lowest level per bar vs drawn (worst)", 0, worst([bb(o).Min.Z for o in cl["C7"]], c7["flat_z"]))
|
||||
normal = [o for o in cl["C7"] if abs(mid(o)[1]) < 1400]
|
||||
check("C7 top end EL 20.3672 (bars not on the side face, worst)", 0,
|
||||
max(abs(bb(o).Max.Z - (c7["top_end_z"] + (0 if abs(mid(o)[1]) <= 1000 else 365.2))) for o in normal))
|
||||
check("C7 x ends +-5875 (worst)", 0, max(max(abs(bb(o).Min.X + T * mid(o)[1] + c7["x_end"]), abs(bb(o).Max.X + T * mid(o)[1] - c7["x_end"])) for o in normal))
|
||||
edge = [o for o in cl["C7"] if abs(mid(o)[1]) >= 1400]
|
||||
check("C7 side-face bars end where the slope meets the coping top (x end)", 0,
|
||||
max(max(abs(bb(o).Min.X + T * mid(o)[1] + c7["capped"]["x_end"]), abs(bb(o).Max.X + T * mid(o)[1] - c7["capped"]["x_end"])) for o in edge) if edge else 1e9, tol=8)
|
||||
got = sorted(round(mid(o)[1]) for o in cl["C7"])
|
||||
flag("C7 y rows: 11 @250 + 2 on the side face", got == sorted([-1250 + 250 * k for k in range(11)] + [-1525, 1525]), f"{got}")
|
||||
# stirrups
|
||||
st = G["stirrups"]
|
||||
|
||||
def top_of(x):
|
||||
return next(z for a, b, z in st["top"] if a <= x < b)
|
||||
|
||||
def bottom_of(x):
|
||||
s = st["bottom"]
|
||||
return s["flat_z"] if abs(x) <= s["flat_half"] else s["flat_z"] + s["slope"] * (abs(x) - s["flat_half"])
|
||||
|
||||
for zone, (rect, pent) in st["marks"].items():
|
||||
want = st["x"][zone]
|
||||
got_r = [xs(o) for o in cl[rect]]
|
||||
check(f"{rect} positions = drawn x ({len(want)}, worst)", 0, worst(got_r, want))
|
||||
got_p = [xs(o) - st["pent_dx"] for o in cl[pent]]
|
||||
check(f"{pent} positions = drawn x + {st['pent_dx']}, 2 per x (worst)", 0, worst(got_p, [x for x in want for _ in (0, 1)]))
|
||||
dt = max(abs(bb(o).Max.Z - top_of(xs(o))) for o in cl[rect] + cl[pent]) if cl[rect] else 1e9
|
||||
db = max(abs(bb(o).Min.Z - bottom_of(xs(o) - (st["pent_dx"] if o in cl[pent] else 0))) for o in cl[rect] + cl[pent]) if cl[rect] else 1e9
|
||||
check(f"{rect}/{pent} top = coping top - 97.5 (drawn zones, worst)", 0, dt)
|
||||
check(f"{rect}/{pent} bottom = soffit + 120 - 22.5 (worst)", 0, db, tol=2.5)
|
||||
check(f"{rect} legs y +-{st['rect_y']} (worst)", 0, max(max(abs(bb(o).Max.Y - st["rect_y"] - 0), abs(bb(o).Min.Y + st["rect_y"])) for o in cl[rect]) if cl[rect] else 1e9, tol=2)
|
||||
lo, hi = st["pent_y"]
|
||||
okp = all((abs(bb(o).Min.Y - lo) < 2 and abs(bb(o).Max.Y - hi) < 2) or (abs(bb(o).Max.Y + lo) < 2 and abs(bb(o).Min.Y + hi) < 2) for o in cl[pent])
|
||||
flag(f"{pent} y between +-{lo} and +-{hi} (A-A)", okp)
|
||||
# side loops and ties
|
||||
lp = G["loops"]
|
||||
for key in ("upper", "lower"):
|
||||
sec = lp[key]
|
||||
mloop, mtie = sec["marks"]
|
||||
gl = sorted((round(bb(o).Min.Z, 1), round(bb(o).Max.X - bb(o).Min.X - 2 * T * (bb(o).Max.Y - bb(o).Min.Y) / 2, 1), round((bb(o).Max.Y - bb(o).Min.Y) / 2, 1)) for o in cl[mloop])
|
||||
wl = sorted((z, round(2 * x, 1), y) for z, x, y in zip(sec["z"], sec["x"], sec["y"]))
|
||||
flag(f"{mloop} loops: level / x length (corner fillets <= 50) / half width ({len(wl)})", len(gl) == len(wl) and all(abs(a[0] - b[0]) <= 1 and abs(a[1] - b[1]) <= 50 and abs(a[2] - b[2]) <= 1 for a, b in zip(gl, wl)), f"{gl[:2]}")
|
||||
flat = max((bb(o).Max.Z - bb(o).Min.Z) for o in cl[mloop] + cl[mtie])
|
||||
check(f"{mloop}/{mtie} horizontal (worst dz)", 0, flat, tol=0.01)
|
||||
gt = sorted((round(bb(o).Min.Z, 1), round(mid(o)[1], 1), round(bb(o).Max.X - bb(o).Min.X, 1)) for o in cl[mtie])
|
||||
wt = sorted((z, y, round(2 * x, 1)) for z, x in zip(sec["z"], sec["x"]) for y in lp["tie_y"])
|
||||
flag(f"{mtie} ties: level / y / x length ({len(wt)})", len(gt) == len(wt) and all(abs(a[0] - b[0]) <= 1 and abs(a[1] - b[1]) <= 1 and abs(a[2] - b[2]) <= 1 for a, b in zip(gt, wt)), f"{len(gt)}")
|
||||
# C18
|
||||
c18 = G["c18"]
|
||||
got = {}
|
||||
for o in cl["C18"]: # lower rows have the legs UP (the straight part is the lowest level)
|
||||
z = bb(o).Max.Z if bb(o).Max.Z - bb(o).Min.Z < 1 or abs((bb(o).Max.Z - bb(o).Min.Z) - 370) < 1 and bb(o).Min.Z > 20000 else bb(o).Min.Z
|
||||
got.setdefault(f"{z:.1f}", []).append(o)
|
||||
flag("C18 levels and counts", {k: len(v) for k, v in got.items()} == c18["levels"], f"{ {k: len(v) for k, v in got.items()} }")
|
||||
check("C18 widths per level (worst; fillets on the slanted legs <= 30)", 0, max(abs((bb(o).Max.Y - bb(o).Min.Y) - c18["width"][k]) for k, v in got.items() if k in c18["width"] for o in v), tol=30)
|
||||
# seats
|
||||
se = G["seats"]
|
||||
ped = {}
|
||||
for o in all_objects(doc):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if oid.startswith(f"{PID}-BS"):
|
||||
ped[int(oid[len(PID) + 3:])] = o.Geometry.GetBoundingBox(True)
|
||||
flag("5 pedestals found", sorted(ped) == [1, 2, 3, 4, 5], f"{sorted(ped)}")
|
||||
for k, pb in sorted(ped.items()):
|
||||
cx, cy = (pb.Min.X + pb.Max.X) / 2, (pb.Min.Y + pb.Max.Y) / 2
|
||||
yf, yr = se["y"]["front"], se["y"]["rear"]
|
||||
zp = pb.Min.Z + se["h1"]["front"] + (se["h1"]["rear"] - se["h1"]["front"]) * (cy - yf) / (yr - yf)
|
||||
ml, ms, mll, mss = se["marks_heavy"] if k == se["heavy"] else se["marks_light"]
|
||||
dl_, ds_ = G["dia"][ml], G["dia"][ms]
|
||||
grp = lambda m: [o for o in cl[m] if o.Attributes.GetUserString("occurrence").startswith(f"P{k}-")]
|
||||
gl, gs, gll, gss = grp(ml), grp(ms), grp(mll), grp(mss)
|
||||
flag(f"pedestal {k}: {ml} {ms} {mll} {mss} counts 11 / 21 / 22 / 42", (len(gl), len(gs), len(gll), len(gss)) == (11, 21, 22, 42), f"{len(gl)} {len(gs)} {len(gll)} {len(gss)}")
|
||||
check(f"pedestal {k} {ml} level {se['top_below']} below the pedestal top (worst)", 0, max(abs(bb(o).Max.Z - (zp - se["top_below"])) for o in gl), tol=1.5)
|
||||
check(f"pedestal {k} {ml} legs {se['leg']} (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - se["leg"]) for o in gl + gs))
|
||||
check(f"pedestal {k} {ml} lateral positions (worst)", 0, worst([(mid(o)[0] - cx) * COS for o in gl], se["long_t"]), tol=1.5)
|
||||
check(f"pedestal {k} {ml} length along the end face (worst)", 0, max(abs((bb(o).Max.Y - bb(o).Min.Y) - se["long_len"] * COS) for o in gl), tol=1.5)
|
||||
check(f"pedestal {k} {ms} level {ds_ if False else dl_} under {ml} (worst)", 0, max(abs(bb(o).Max.Z - (zp - se["top_below"] - dl_)) for o in gs), tol=1.5)
|
||||
check(f"pedestal {k} {ms} y positions (worst)", 0, worst([mid(o)[1] - cy for o in gs], se["short_o"]), tol=1.5)
|
||||
check(f"pedestal {k} {ms} x length (worst)", 0, max(abs((bb(o).Max.X - bb(o).Min.X) - se["short_len"]) for o in gs), tol=1.5)
|
||||
for layer, dz in enumerate(se["lower_below"]):
|
||||
l1 = [o for o in gll if abs(bb(o).Min.Z - (zp - dz)) < 1.5]
|
||||
l2 = [o for o in gss if abs(bb(o).Min.Z - (zp - dz + (G["dia"][mll] + G["dia"][mss]) / 2)) < 1.5]
|
||||
flag(f"pedestal {k} layer {layer + 1} EL pedestal top - {dz}: {mll} 11 / {mss} 21", (len(l1), len(l2)) == (11, 21), f"{len(l1)} {len(l2)}")
|
||||
if l1:
|
||||
check(f"pedestal {k} {mll} layer {layer + 1} positions (worst)", 0, worst([(mid(o)[0] - cx) * COS for o in l1], se["low_long_t"]), tol=1.5)
|
||||
check(f"pedestal {k} {mll} layer {layer + 1} length (worst)", 0, max(abs((bb(o).Max.Y - bb(o).Min.Y) - se["low_long_len"] * COS) for o in l1), tol=1.5)
|
||||
if l2:
|
||||
check(f"pedestal {k} {mss} layer {layer + 1} y positions (worst)", 0, worst([mid(o)[1] - cy for o in l2], se["low_short_o"]), tol=1.5)
|
||||
# containment and cover
|
||||
solids = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CP"
|
||||
or (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-BS")]
|
||||
solids = [s if isinstance(s, rg.Brep) else rg.Brep.TryConvertBrep(s) for s in solids]
|
||||
cp = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CP"][0]
|
||||
info("concrete solids for containment", f"{len(solids)} (coping + pedestals)")
|
||||
cover = {}
|
||||
for m in SCHED:
|
||||
bad, cov = 0, 1e9
|
||||
r = G["dia"][m] / 2
|
||||
for o in cl[m]:
|
||||
cu = o.Geometry
|
||||
for t in cu.DivideByCount(max(6, int(cu.GetLength() / 200)), True):
|
||||
p = cu.PointAt(t)
|
||||
for q in (p, p + rg.Vector3d(r, 0, 0), p - rg.Vector3d(r, 0, 0), p + rg.Vector3d(0, r, 0),
|
||||
p - rg.Vector3d(0, r, 0), p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
||||
if not any(s.IsPointInside(q, 0.01, False) for s in solids):
|
||||
bad += 1
|
||||
if cp.IsPointInside(p, 0.01, False):
|
||||
cov = min(cov, p.DistanceTo(cp.ClosestPoint(p)) - r)
|
||||
cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside concrete", 0, bad, tol=0)
|
||||
info("min cover to the coping surface per mark (record; seat bars measured to the coping only)", json.dumps(cover))
|
||||
write_result(RESULT_PATH, results, dict(cover=cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,207 @@
|
||||
"""G4 gate checker, form "polylines" (Ulsan P1, 2026-10-13): coping rebar C1~C15 and bearing-seat S1~S4.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G4) are the schedule (L, n, ranges)
|
||||
and drawn coordinates of the rebar drawing (front / side views, sections, seat details); params are not read.
|
||||
Checks (C10): counts, fixed-length marks vs L, variable dimension ranges vs schedule (record mean length),
|
||||
top / bottom layer level, y rows and x extents, stirrup positions / pairs / heights, side-bar and hoop levels
|
||||
and ends, C14 positions, seat bars, containment in the coping + pedestals, minimum cover (record).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import statistics
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, G = X["pier_id"], X["G4"]
|
||||
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
|
||||
results = []
|
||||
|
||||
|
||||
def check(item, exp, act, tol=1.0):
|
||||
results.append(dict(check="C10", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C10", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def info(item, detail):
|
||||
results.append(dict(check="C10", item=item, detail=detail, result="INFO"))
|
||||
|
||||
|
||||
def row(spec):
|
||||
return [spec["start"] + spec["pitch"] * i for i in range(spec["n"])]
|
||||
|
||||
|
||||
def bb(o):
|
||||
return o.Geometry.GetBoundingBox(True)
|
||||
|
||||
|
||||
def worst(pairs):
|
||||
return max((abs(a - b) for a, b in pairs), default=1e9)
|
||||
|
||||
|
||||
def match(got, exp, tol=1.0):
|
||||
"""Every expected tuple has one got tuple within tol (and same count)."""
|
||||
got = list(got)
|
||||
if len(got) != len(exp):
|
||||
return False
|
||||
for e in exp:
|
||||
k = next((i for i, g in enumerate(got) if all(abs(a - b) <= tol for a, b in zip(g, e))), None)
|
||||
if k is None:
|
||||
return False
|
||||
got.pop(k)
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
cl = {m: [] for m in SCHED}
|
||||
pp = {m: [] for m in SCHED}
|
||||
for o in all_objects(doc):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
m = o.Attributes.GetUserString("mark")
|
||||
if m in SCHED and oid.startswith(f"{PID}-RC-"):
|
||||
cl[m].append(o)
|
||||
elif m in SCHED and oid.startswith(f"{PID}-R-"):
|
||||
pp[m].append(o)
|
||||
dl = {m: [float(o.Attributes.GetUserString("design_length")) for o in cl[m]] for m in SCHED}
|
||||
for m, (L, n) in SCHED.items():
|
||||
check(f"{m} bar count vs schedule", n, len(cl[m]), tol=0)
|
||||
flag(f"{m} pipes closed & paired", len(pp[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pp[m]),
|
||||
f"{len(pp[m])}/{len(cl[m])}")
|
||||
if not dl[m]:
|
||||
continue
|
||||
if m in G["fixed_length"]:
|
||||
check(f"{m} original length vs L (worst)", 0, max(abs(v - L) for v in dl[m]))
|
||||
else:
|
||||
info(f"{m} mean original length vs L (variable mark, record)",
|
||||
f"mean {statistics.mean(dl[m]):.1f} / L {L} (diff {statistics.mean(dl[m]) - L:+.1f}); min {min(dl[m]):.0f} max {max(dl[m]):.0f}")
|
||||
# top / bottom layers
|
||||
rows = {k: row(v) for k, v in G["rows"].items()}
|
||||
cy_abs = G["corner"]["y_abs"]
|
||||
clipped_x = set(G["corner"]["x_clipped"])
|
||||
for m, lay in G["layers"].items():
|
||||
if not cl[m]:
|
||||
continue
|
||||
ys = rows[lay["y"]]
|
||||
if lay["parity"] is not None:
|
||||
ys = [y for i, y in enumerate(ys) if i % 2 == lay["parity"]]
|
||||
got = sorted(round((bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in cl[m])
|
||||
check(f"{m} y rows vs chain (worst, n={len(got)}/{len(ys)})", 0, worst(zip(got, ys)) if len(got) == len(ys) else 1e9)
|
||||
if "z" in lay:
|
||||
check(f"{m} level (top straight, worst)", 0, max(abs(bb(o).Max.Z - lay["z"]) for o in cl[m]))
|
||||
else:
|
||||
check(f"{m} bottom level (worst)", 0, max(abs(bb(o).Min.Z - lay["z_min"]) for o in cl[m]))
|
||||
if m in G["x_extent"]:
|
||||
x0, x1 = G["x_extent"][m]
|
||||
inner = [o for o in cl[m] if abs((bb(o).Min.Y + bb(o).Max.Y) / 2) <= cy_abs]
|
||||
check(f"{m} drawn x ends, |y| <= {cy_abs} (worst; corner fillets)", 0,
|
||||
max(max(abs(bb(o).Min.X - x0), abs(bb(o).Max.X - x1)) for o in inner), tol=12)
|
||||
info(f"{m} bars shortened at the R500 plan corners [추론]",
|
||||
f"{sum(1 for o in cl[m] if o not in inner and bb(o).Max.X - bb(o).Min.X < x1 - x0 - 30)}"
|
||||
f" of {len(cl[m]) - len(inner)} outer bars")
|
||||
# stirrups
|
||||
st = G["stirrups"]
|
||||
for m, xs in st["x"].items():
|
||||
got = {}
|
||||
for o in cl[m]:
|
||||
b = bb(o)
|
||||
got.setdefault(round((b.Min.X + b.Max.X) / 2), []).append(o)
|
||||
flag(f"{m} stirrup positions = drawn x ({len(xs)}) x 3", sorted(got) == sorted(round(x) for x in xs)
|
||||
and all(len(v) == 3 for v in got.values()), f"{len(got)} positions")
|
||||
pairs_ok, hw = True, 0
|
||||
for x, os_ in got.items():
|
||||
if x in clipped_x:
|
||||
continue
|
||||
pairs_ok &= match([(bb(o).Min.Y, bb(o).Max.Y) for o in os_], [tuple(p) for p in st["y_pairs"]])
|
||||
exp_h = st["height"].get(f"{x:.0f}")
|
||||
hw = max([hw] + [abs((bb(o).Max.Z - bb(o).Min.Z) - exp_h) for o in os_]) if exp_h else 1e9
|
||||
flag(f"{m} legs at y pairs {st['y_pairs']} (A-A; corner positions {sorted(clipped_x)} excluded)", pairs_ok)
|
||||
check(f"{m} centreline height vs drawn + 54 (worst)", 0, hw)
|
||||
# variable ranges (schedule)
|
||||
for m, (dim, lo, hi) in G["ranges"].items():
|
||||
if not cl[m]:
|
||||
continue
|
||||
if dim == "B":
|
||||
vals = [bb(o).Max.Z - bb(o).Min.Z for o in cl[m]]
|
||||
check(f"{m} B min vs schedule '{lo}~{hi}'", lo, min(vals), tol=2)
|
||||
check(f"{m} B max vs schedule '{lo}~{hi}'", hi, max(vals), tol=2)
|
||||
else:
|
||||
vals = []
|
||||
for o in cl[m]:
|
||||
b = bb(o)
|
||||
a = b.Max.X - b.Min.X + (44 if m in ("C11", "C13") else (54 if m == "C10" else 44 - 790))
|
||||
vals.append(a)
|
||||
rng = f"model {min(vals):.0f}~{max(vals):.0f} / schedule {lo}~{hi}"
|
||||
ok = abs(min(vals) - lo) <= 2 and abs(max(vals) - hi) <= 2
|
||||
results.append(dict(check="C10", item=f"{m} A range vs schedule", detail=rng,
|
||||
result="PASS" if ok else "INFO"))
|
||||
# side bars and hoops per level
|
||||
lv = G["levels"]
|
||||
for key, yset in (("side", G["side"]["y"]), ("hoops", None)):
|
||||
k0 = 0
|
||||
for m, nlev in G[key]["marks"]:
|
||||
exp = lv[k0:k0 + nlev]
|
||||
k0 += nlev
|
||||
got = []
|
||||
for o in cl[m]:
|
||||
b = bb(o)
|
||||
c = o.Geometry.PointAt(o.Geometry.Domain.Mid)
|
||||
got.append((round(b.Min.Z, 1), b.Min.X, b.Max.X, c.Y if yset else b.Max.Y - b.Min.Y))
|
||||
want = [(zz, xl, xr, y) for zz, xl, xr in exp for y in (yset or [2 * G["hoops"]["y"]])]
|
||||
flag(f"{m} levels / drawn ends / y ({len(want)})", match(got, want), f"{len(got)} bars")
|
||||
# C14
|
||||
c = G["c14"]
|
||||
got = [((bb(o).Min.X + bb(o).Max.X) / 2, bb(o).Min.Z, bb(o).Min.Y, bb(o).Max.Y) for o in cl["C14"]]
|
||||
n_corner = sum(1 for p in c["pts"] if round(abs(p[0]) + 19) in clipped_x)
|
||||
full = [g for g in got if abs(g[2] - c["y"][0]) <= 1 and abs(g[3] - c["y"][1]) <= 1]
|
||||
flag("C14 levels (drawn section marks)", match([(g[1],) for g in got], [(p[1],) for p in c["pts"]]), f"{len(got)}")
|
||||
flag(f"C14 y span {c['y']} except {n_corner} corner bars", len(full) == len(got) - n_corner
|
||||
and all(g[2] >= c["y"][0] - 1 and g[3] <= c["y"][1] + 1 for g in got), f"{len(full)} full")
|
||||
# seat bars
|
||||
se = G["seats"]
|
||||
for no, s in se["bearings"].items():
|
||||
cx, cy = s["centre"]
|
||||
my, mx = s["marks"]
|
||||
oy = [o for o in cl[my] if o.Attributes.GetUserString("occurrence").startswith(f"B{no}")]
|
||||
ox = [o for o in cl[mx] if o.Attributes.GetUserString("occurrence").startswith(f"B{no}")]
|
||||
flag(f"bearing {no} {my} x positions / y span", match([((bb(o).Min.X + bb(o).Max.X) / 2, bb(o).Min.Y, bb(o).Max.Y) for o in oy],
|
||||
[(cx + d, cy - se["half"], cy + se["half"]) for d in se["offsets"]]), f"{len(oy)}")
|
||||
flag(f"bearing {no} {mx} y positions / x span", match([((bb(o).Min.Y + bb(o).Max.Y) / 2, bb(o).Min.X, bb(o).Max.X) for o in ox],
|
||||
[(cy + d, cx - se["half"], cx + se["half"]) for d in se["offsets"]]), f"{len(ox)}")
|
||||
check(f"bearing {no} {my} level 50 below pedestal top (worst)", 0, max(abs(bb(o).Max.Z - s["z_top"]) for o in oy))
|
||||
check(f"bearing {no} legs {se['leg']} (worst)", se["leg"], min(bb(o).Max.Z - bb(o).Min.Z for o in oy + ox))
|
||||
# containment and cover
|
||||
solids = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CP"
|
||||
or (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-BS")]
|
||||
solids = [s if isinstance(s, rg.Brep) else rg.Brep.TryConvertBrep(s) for s in solids]
|
||||
cp = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CP"][0]
|
||||
info("concrete solids for containment", f"{len(solids)} (coping + pedestals)")
|
||||
cover = {}
|
||||
for m in SCHED:
|
||||
bad, cov = 0, 1e9
|
||||
r = G["dia"][m] / 2
|
||||
for o in cl[m]:
|
||||
cu = o.Geometry
|
||||
for t in cu.DivideByCount(max(6, int(cu.GetLength() / 150)), True):
|
||||
p = cu.PointAt(t)
|
||||
for q in (p, p + rg.Vector3d(r, 0, 0), p - rg.Vector3d(r, 0, 0), p + rg.Vector3d(0, r, 0),
|
||||
p - rg.Vector3d(0, r, 0), p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
||||
if not any(s.IsPointInside(q, 0.01, False) for s in solids):
|
||||
bad += 1
|
||||
if cp.IsPointInside(p, 0.01, False):
|
||||
cov = min(cov, p.DistanceTo(cp.ClosestPoint(p)) - r)
|
||||
cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside concrete", 0, bad, tol=0)
|
||||
info("min cover to coping surface per mark (record; seat bars measured to the coping only)", json.dumps(cover))
|
||||
write_result(RESULT_PATH, results, dict(cover=cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,185 @@
|
||||
"""G4 gate checker, Ramp-A P1 coping rebar (2026-10-14): C1~C16 and bearing-seat S1~S4 as centreline polylines.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G4, plus the G1 coping dimensions) come
|
||||
from the D3 schedule (L, N), the D2 / D3 drawn positions and section dimensions, and the user decisions T1-T6;
|
||||
generator params are not read.
|
||||
Checks (C10): counts, fixed-length marks vs L (variable marks recorded), top layers (level, y rows, x ends),
|
||||
stirrup positions per zone, stirrup top / bottom cover along the soffit, inner stirrup offset, side loops per level,
|
||||
C6 bottom level, C15 / C16, seat bars per bearing, containment in the coping + pedestals, min cover (record).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import statistics
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, G, C, CO = X["pier_id"], X["G4"], X["G1"]["coping"], X["G1"]["column"]
|
||||
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
|
||||
ZB = CO["z0"] + CO["length"] # column top = coping bottom
|
||||
ZT = ZB + C["soffit_rise"]
|
||||
ZTL, ZTR = C["z_top_left"], C["z_top_right"]
|
||||
R, HB, HT = C["soffit_radius"], C["column_face_half"], C["bottom_length"] / 2
|
||||
results = []
|
||||
|
||||
|
||||
def soffit_centre():
|
||||
ax, az, bx, bz = -HB, ZB, -HT, ZT # R arc through (-HB, ZB) and (-HT, ZT), centre below the chord
|
||||
mx, mz = (ax + bx) / 2, (az + bz) / 2
|
||||
d = math.hypot(bx - ax, bz - az)
|
||||
h = math.sqrt(R * R - d * d / 4)
|
||||
ux, uz = (bz - az) / d, -(bx - ax) / d
|
||||
return min(((mx + h * ux, mz + h * uz), (mx - h * ux, mz - h * uz)), key=lambda c: c[1])
|
||||
|
||||
|
||||
SCX, SCZ = soffit_centre()
|
||||
SCX = abs(SCX)
|
||||
|
||||
|
||||
def soffit_z(x):
|
||||
return ZB if abs(x) <= HB else SCZ + math.sqrt(R * R - (SCX - abs(x)) ** 2)
|
||||
|
||||
|
||||
def check(item, exp, act, tol=1.0):
|
||||
results.append(dict(check="C10", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C10", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def info(item, detail):
|
||||
results.append(dict(check="C10", item=item, detail=detail, result="INFO"))
|
||||
|
||||
|
||||
def row(s):
|
||||
return [s["start"] + s["pitch"] * i for i in range(s["n"])]
|
||||
|
||||
|
||||
def bb(o):
|
||||
return o.Geometry.GetBoundingBox(True)
|
||||
|
||||
|
||||
def worst(pairs):
|
||||
pairs = list(pairs)
|
||||
return max((abs(a - b) for a, b in pairs), default=1e9)
|
||||
|
||||
|
||||
def main():
|
||||
cl = {m: [] for m in SCHED}
|
||||
pp = {m: [] for m in SCHED}
|
||||
for o in all_objects(doc):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
m = o.Attributes.GetUserString("mark")
|
||||
if m in SCHED and oid.startswith(f"{PID}-RC-"):
|
||||
cl[m].append(o)
|
||||
elif m in SCHED and oid.startswith(f"{PID}-R-"):
|
||||
pp[m].append(o)
|
||||
for m, (L, n) in SCHED.items():
|
||||
check(f"{m} bar count vs schedule", n, len(cl[m]), tol=0)
|
||||
flag(f"{m} pipes closed & paired", len(pp[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pp[m]),
|
||||
f"{len(pp[m])}/{len(cl[m])}")
|
||||
dl = [float(o.Attributes.GetUserString("design_length")) for o in cl[m]]
|
||||
if not dl:
|
||||
continue
|
||||
if m in G["fixed_length"]:
|
||||
check(f"{m} original length vs L (worst)", 0, max(abs(v - L) for v in dl))
|
||||
else:
|
||||
info(f"{m} mean original length vs L (variable mark, record)",
|
||||
f"mean {statistics.mean(dl):.1f} / L {L} (diff {statistics.mean(dl) - L:+.1f}); min {min(dl):.0f} max {max(dl):.0f}")
|
||||
# top layers (straight part = top of the bar)
|
||||
for m, s in G["layers"].items():
|
||||
got = sorted(round((bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in cl[m])
|
||||
want = row(s["y"])
|
||||
check(f"{m} y rows (worst, {len(got)}/{len(want)})", 0, worst(zip(got, want)) if len(got) == len(want) else 1e9)
|
||||
check(f"{m} level of the straight part (worst)", 0, max(abs(bb(o).Max.Z - s["z"]) for o in cl[m]))
|
||||
if "x_top" in s:
|
||||
x0, x1 = s["x_top"]
|
||||
ends = []
|
||||
for o in cl[m]:
|
||||
c = o.Geometry
|
||||
top_pts = [p for p in (c.PointAt(t) for t in c.DivideByLength(5, True)) if abs(p.Z - s["z"]) < 0.5]
|
||||
ends.append(max(abs(min(p.X for p in top_pts) - x0), abs(max(p.X for p in top_pts) - x1)))
|
||||
check(f"{m} straight part x ends vs drawn (worst; fillet tangent points)", 0, max(ends), tol=s.get("x_tol", 70))
|
||||
# stirrups
|
||||
st = G["stirrups"]
|
||||
xs = row(st["x"])
|
||||
for zone, (m_out, m_in, x0, x1) in st["zones"].items():
|
||||
want = [x for x in xs if x0 <= x <= x1]
|
||||
got_o = sorted(round(bb(o).Min.X, 1) for o in cl[m_out])
|
||||
got_i = sorted(round(bb(o).Min.X, 1) for o in cl[m_in])
|
||||
check(f"{m_out} positions = drawn x ({len(want)})", 0, worst(zip(got_o, want)) if len(got_o) == len(want) else 1e9)
|
||||
check(f"{m_in} positions = drawn x + {st['inner_dx']} (2 per x)", 0,
|
||||
worst(zip(got_i, sorted(x + st["inner_dx"] for x in want for _ in (0, 1)))) if len(got_i) == 2 * len(want) else 1e9)
|
||||
for m in (m_out, m_in):
|
||||
dt, dbm = [], []
|
||||
for o in cl[m]:
|
||||
b = bb(o)
|
||||
x = b.Min.X - (st["inner_dx"] if m == m_in else 0)
|
||||
t = ZTL if x <= 0 else ZTR
|
||||
dt.append(abs((t - b.Max.Z) - st["top_cover"]))
|
||||
dbm.append(abs((b.Min.Z - soffit_z(x)) - st["bottom_cover"] * (1 if abs(x) < HB else
|
||||
# on the arc the 77.5 cover is radial: vertical gap = R' - R projected
|
||||
(SCZ + math.sqrt((R + st["bottom_cover"]) ** 2 - (SCX - abs(x)) ** 2) - soffit_z(x)) / st["bottom_cover"])))
|
||||
check(f"{m} top cover {st['top_cover']} (worst)", 0, max(dt))
|
||||
check(f"{m} bottom cover {st['bottom_cover']} to the soffit (worst)", 0, max(dbm))
|
||||
ys = [(bb(o).Min.Y, bb(o).Max.Y) for o in cl[m_out]]
|
||||
check(f"{m_out} legs y {st['outer_y']} (worst)", 0, worst([(a, st["outer_y"][0]) for a, _ in ys] + [(b, st["outer_y"][1]) for _, b in ys]))
|
||||
# side loops
|
||||
lp = G["loops"]
|
||||
for m, levels in lp["levels"].items():
|
||||
zs = {}
|
||||
for o in cl[m]:
|
||||
zs.setdefault(round(bb(o).Min.Z, 1), []).append(o)
|
||||
flag(f"{m} levels {levels} x {lp['per_level']}", sorted(zs) == sorted(levels)
|
||||
and all(len(v) == lp["per_level"] for v in zs.values()), f"{sorted(zs)}")
|
||||
flat = [abs(bb(o).Max.Z - bb(o).Min.Z) for o in cl[m]]
|
||||
check(f"{m} loops horizontal (worst dz)", 0, max(flat) if flat else 1e9, tol=0.01)
|
||||
# C6, C15, C16
|
||||
c6 = G["c6"]
|
||||
check("C6 bottom straight level (worst)", 0, max(abs(bb(o).Min.Z - c6["z_bottom"]) for o in cl["C6"]) if cl["C6"] else 1e9)
|
||||
got = sorted(round(o.Geometry.PointAt(o.Geometry.Domain.Mid).Y, 1) for o in cl["C6"])
|
||||
check("C6 y rows at mid-span (worst)", 0, worst(zip(got, c6["y"])) if len(got) == len(c6["y"]) else 1e9)
|
||||
for m in ("C15",):
|
||||
w = [bb(o).Max.Y - bb(o).Min.Y for o in cl[m]]
|
||||
check(f"{m} width a (worst)", G["c15"]["a"], max(w, key=lambda v: abs(v - G["c15"]["a"])) if w else 0)
|
||||
# seats
|
||||
for no, want in G["seats"].items():
|
||||
for m, n in want.items():
|
||||
k = sum(1 for o in cl[m] if o.Attributes.GetUserString("occurrence").startswith(f"B{no}-"))
|
||||
check(f"bearing {no} {m} count", n, k, tol=0)
|
||||
# containment and cover
|
||||
solids = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CP"
|
||||
or (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-BS")]
|
||||
solids = [s if isinstance(s, rg.Brep) else rg.Brep.TryConvertBrep(s) for s in solids]
|
||||
cp = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CP"][0]
|
||||
col = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CL"]
|
||||
info("concrete solids for containment", f"{len(solids)} (coping + pedestals); bars below the coping bottom checked in the column")
|
||||
cover = {}
|
||||
for m in SCHED:
|
||||
bad, cov = 0, 1e9
|
||||
r = G["dia"][m] / 2
|
||||
for o in cl[m]:
|
||||
cu = o.Geometry
|
||||
for t in cu.DivideByCount(max(6, int(cu.GetLength() / 150)), True):
|
||||
p = cu.PointAt(t)
|
||||
for q in (p, p + rg.Vector3d(r, 0, 0), p - rg.Vector3d(r, 0, 0), p + rg.Vector3d(0, r, 0),
|
||||
p - rg.Vector3d(0, r, 0), p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
||||
if not any(s.IsPointInside(q, 0.01, False) for s in solids + col):
|
||||
bad += 1
|
||||
if cp.IsPointInside(p, 0.01, False):
|
||||
cov = min(cov, p.DistanceTo(cp.ClosestPoint(p)) - r)
|
||||
cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside concrete", 0, bad, tol=0)
|
||||
info("min cover to the coping surface per mark (record; seat bars measured to the coping only)", json.dumps(cover))
|
||||
write_result(RESULT_PATH, results, dict(cover=cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,207 @@
|
||||
"""G4 gate checker, Saejae-1 P1 coping + bearing-seat rebar (2026-10-15): C1~C20 and S1~S2 as centreline polylines (rectangular plan, no skew).
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G4) come from the schedule (L, N, polyline length = a + legs),
|
||||
the drawn levels / positions of the rebar drawing, the section dimension chains and the Okseong rules H1-H6 applied again; generator params
|
||||
are not read. Checks (C10): counts / pipes / polyline length, top layers, C19 / C20, C7, stirrups (x, top, bottom, y), side loops and ties,
|
||||
C18, seat bars per pedestal, containment in the coping + pedestals, minimum cover (record).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import statistics
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected, write_result # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, G = X["pier_id"], X["G4"]
|
||||
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
|
||||
results = []
|
||||
|
||||
|
||||
def check(item, exp, act, tol=1.0):
|
||||
results.append(dict(check="C10", item=item, expected=round(exp, 3), actual=round(act, 3),
|
||||
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C10", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def info(item, detail):
|
||||
results.append(dict(check="C10", item=item, detail=detail, result="INFO"))
|
||||
|
||||
|
||||
def bb(o):
|
||||
return o.Geometry.GetBoundingBox(True)
|
||||
|
||||
|
||||
def mid(o):
|
||||
b = bb(o)
|
||||
return (b.Min.X + b.Max.X) / 2, (b.Min.Y + b.Max.Y) / 2
|
||||
|
||||
|
||||
def worst(got, want):
|
||||
got, want = sorted(got), sorted(want)
|
||||
return max((abs(a - b) for a, b in zip(got, want)), default=1e9) if len(got) == len(want) else 1e9
|
||||
|
||||
|
||||
def occ(o):
|
||||
return o.Attributes.GetUserString("occurrence") or ""
|
||||
|
||||
|
||||
def main():
|
||||
cl = {m: [] for m in SCHED}
|
||||
pp = {m: [] for m in SCHED}
|
||||
for o in all_objects(doc):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
m = o.Attributes.GetUserString("mark")
|
||||
if m in SCHED and oid.startswith(f"{PID}-RC-"):
|
||||
cl[m].append(o)
|
||||
elif m in SCHED and oid.startswith(f"{PID}-R-"):
|
||||
pp[m].append(o)
|
||||
for m, (L, n) in SCHED.items():
|
||||
check(f"{m} bar count vs schedule", n, len(cl[m]), tol=0)
|
||||
flag(f"{m} pipes closed & paired", len(pp[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pp[m]), f"{len(pp[m])}/{len(cl[m])}")
|
||||
dl = [float(o.Attributes.GetUserString("design_length")) for o in cl[m]]
|
||||
if m in G["fixed_length"]:
|
||||
check(f"{m} polyline length vs a + legs (worst)", 0, max(abs(v - G["fixed_length"][m]) for v in dl) if dl else 1e9)
|
||||
elif dl:
|
||||
info(f"{m} mean polyline length vs L (variable mark, record)",
|
||||
f"mean {statistics.mean(dl):.1f} / L {L} (diff {statistics.mean(dl) - L:+.1f}); min {min(dl):.0f} max {max(dl):.0f}")
|
||||
# top layers C1~C6
|
||||
tp, y23 = G["top"], G["y23"]
|
||||
for m in ("C1", "C2", "C3", "C4", "C5", "C6"):
|
||||
s = tp[m]
|
||||
want = [y for i, y in enumerate(y23) if i % 2 == s["parity"]]
|
||||
check(f"{m} y rows vs chain (worst, n={len(cl[m])}/{len(want)})", 0, worst([mid(o)[1] for o in cl[m]], want))
|
||||
check(f"{m} level of the straight part (worst)", 0, max(abs(bb(o).Max.Z - s["z"]) for o in cl[m]))
|
||||
check(f"{m} leg {s['leg']} (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - s["leg"]) for o in cl[m]))
|
||||
check(f"{m} x ends +-{tp['x_end']} (worst)", 0, max(max(abs(bb(o).Min.X + tp["x_end"]), abs(bb(o).Max.X - tp["x_end"])) for o in cl[m]))
|
||||
# C19, C20
|
||||
c19 = G["c19"]
|
||||
check("C19 y rows vs chain (worst)", 0, worst([mid(o)[1] for o in cl["C19"]], y23))
|
||||
check("C19 level (worst)", 0, max(abs(bb(o).Max.Z - c19["z"]) for o in cl["C19"]))
|
||||
check("C19 x ends (worst)", 0, max(max(abs(bb(o).Min.X - c19["x"][0]), abs(bb(o).Max.X - c19["x"][1])) for o in cl["C19"]))
|
||||
check("C19 legs 370 (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - G["c20_leg"]) for o in cl["C19"]))
|
||||
for k, pc in enumerate(G["c20"]):
|
||||
grp = [o for o in cl["C20"] if abs(bb(o).Max.Z - pc["z"]) < 0.5 and abs(bb(o).Min.X - pc["x"][0]) < 3]
|
||||
flag(f"C20 piece {k + 1}: 23 bars at EL {pc['z']}, x {pc['x']}", len(grp) == 23, f"{len(grp)}")
|
||||
if grp:
|
||||
check(f"C20 piece {k + 1} x ends (worst)", 0, max(max(abs(bb(o).Min.X - pc["x"][0]), abs(bb(o).Max.X - pc["x"][1])) for o in grp))
|
||||
check(f"C20 piece {k + 1} leg 370 (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - G["c20_leg"]) for o in grp))
|
||||
check(f"C20 piece {k + 1} y rows (worst)", 0, worst([mid(o)[1] for o in grp], y23))
|
||||
# C7
|
||||
c7 = G["c7"]
|
||||
check("C7 lowest level per bar vs drawn (worst)", 0, worst([bb(o).Min.Z for o in cl["C7"]], c7["flat_z"]))
|
||||
flat = [o for o in cl["C7"] if abs(mid(o)[1]) <= 875]
|
||||
check("C7 top end EL 31.8453 (flat bars, worst)", 0, max(abs(bb(o).Max.Z - c7["top_end_z"]) for o in flat))
|
||||
check("C7 x ends +-5875 (flat bars, worst)", 0, max(max(abs(bb(o).Min.X + c7["x_end"]), abs(bb(o).Max.X - c7["x_end"])) for o in flat))
|
||||
raised = [o for o in cl["C7"] if 875 < abs(mid(o)[1]) <= 1125]
|
||||
check("C7 raised bars (y +-1125) top end (worst)", 0, max(abs(bb(o).Max.Z - c7["raised_top_z"]) for o in raised))
|
||||
edge = [o for o in cl["C7"] if abs(mid(o)[1]) > 1125]
|
||||
check("C7 side-face bars capped at the cap level (worst)", 0, max(abs(bb(o).Max.Z - c7["cap_z"]) for o in edge), tol=1.5)
|
||||
flag("C7 y rows: 12 @250", sorted(round(mid(o)[1]) for o in cl["C7"]) == [-1375 + 250 * k for k in range(12)])
|
||||
# stirrups
|
||||
st = G["stirrups"]
|
||||
|
||||
def zone(x):
|
||||
return "left" if x < st["zone_cut"][0] else ("centre" if x <= st["zone_cut"][1] else "right")
|
||||
|
||||
def top_of(x):
|
||||
return st["step_z"][sum(1 for b in st["step_b"] if x >= b)] - st["top_below"]
|
||||
|
||||
def bottom_of(x):
|
||||
if abs(x) <= st["flat_half"]:
|
||||
return st["flat_bottom"]
|
||||
return st["flat_bottom"] - st["cover_n"] + st["soffit_slope"] * (abs(x) - st["flat_half"]) + st["cover_n"] * (1 + st["soffit_slope"] ** 2) ** 0.5
|
||||
|
||||
for zn, (rect, pent) in st["marks"].items():
|
||||
sx = [x for x in st["x"] if zone(x) == zn]
|
||||
want_r = [x + o for x in sx for o in st["off"]["R"]]
|
||||
want_p = [x + o for x in sx for o in st["off"]["P"]]
|
||||
check(f"{rect} positions = stations + x offset ({len(want_r)}, worst)", 0, worst([mid(o)[0] for o in cl[rect]], want_r), tol=0.5)
|
||||
check(f"{pent} positions = stations + x offset ({len(want_p)}, worst)", 0, worst([mid(o)[0] for o in cl[pent]], want_p), tol=0.5)
|
||||
dt = db = 0.0
|
||||
for o in cl[rect] + cl[pent]:
|
||||
xb = mid(o)[0]
|
||||
dt = max(dt, abs(bb(o).Max.Z - top_of(xb)))
|
||||
db = max(db, abs(bb(o).Min.Z - bottom_of(xb)))
|
||||
check(f"{rect}/{pent} top = step top - {st['top_below']:.0f} (worst)", 0, dt, tol=1.5)
|
||||
check(f"{rect}/{pent} bottom = soffit + cover along the normal (worst, sloped part <= 4)", 0, db, tol=4.0)
|
||||
okr = all(any(abs(bb(o).Min.Y - a) < 2 and abs(bb(o).Max.Y - b) < 2 for a, b in st["rect_y"]) for o in cl[rect])
|
||||
flag(f"{rect} y legs {st['rect_y']} (B-B)", okr)
|
||||
pin, pout = st["pent_in"], st["pent_out"]
|
||||
okp = all((abs(bb(o).Min.Y + pin) < 2 and abs(bb(o).Max.Y - pout) < 2) or (abs(bb(o).Max.Y - pin) < 2 and abs(bb(o).Min.Y + pout) < 2) for o in cl[pent])
|
||||
flag(f"{pent} y between {pin:.0f} and {pout:.0f} (B-B)", okp)
|
||||
# side loops and ties
|
||||
lp = G["loops"]
|
||||
for key in ("upper", "lower"):
|
||||
sec = lp[key]
|
||||
mloop, mtie = sec["marks"]
|
||||
gl = sorted((round(bb(o).Min.Z, 1), round(bb(o).Max.X - bb(o).Min.X, 1), round((bb(o).Max.Y - bb(o).Min.Y) / 2, 1)) for o in cl[mloop])
|
||||
wl = sorted((z, round(2 * x, 1), y) for z, x, y in zip(sec["z"], sec["x"], sec["y"]))
|
||||
flag(f"{mloop} loops: level / x length / half width ({len(wl)})", len(gl) == len(wl) and all(abs(a[0] - b[0]) <= 1 and abs(a[1] - b[1]) <= 1 and abs(a[2] - b[2]) <= 1 for a, b in zip(gl, wl)), f"{gl[:2]}")
|
||||
check(f"{mloop}/{mtie} horizontal (worst dz)", 0, max((bb(o).Max.Z - bb(o).Min.Z) for o in cl[mloop] + cl[mtie]), tol=0.01)
|
||||
gt = sorted((round(bb(o).Min.Z, 1), round(mid(o)[1], 1), round(bb(o).Max.X - bb(o).Min.X, 1)) for o in cl[mtie])
|
||||
wt = sorted((z, y, round(2 * x, 1)) for z, x in zip(sec["z"], sec["x"]) for y in lp["tie_y"])
|
||||
flag(f"{mtie} ties: level / y / x length ({len(wt)})", len(gt) == len(wt) and all(abs(a[0] - b[0]) <= 1 and abs(a[1] - b[1]) <= 1 and abs(a[2] - b[2]) <= 1 for a, b in zip(gt, wt)), f"{len(gt)}")
|
||||
# C18
|
||||
c18 = G["c18"]
|
||||
got = {}
|
||||
for o in cl["C18"]: # upper rows: legs DOWN (straight part highest); lower rows: legs UP (lowest)
|
||||
z = bb(o).Max.Z if bb(o).Max.Z > 32100 else bb(o).Min.Z
|
||||
got.setdefault(f"{z:.1f}", []).append(o)
|
||||
flag("C18 levels and counts", {k: len(v) for k, v in got.items()} == c18["levels"], f"{ {k: len(v) for k, v in got.items()} }")
|
||||
check("C18 widths per level (worst; fillets on the slanted legs <= 30)", 0, max(abs((bb(o).Max.Y - bb(o).Min.Y) - c18["width"][k]) for k, v in got.items() if k in c18["width"] for o in v), tol=30)
|
||||
# seats
|
||||
se = G["seats"]
|
||||
ped = {}
|
||||
for o in all_objects(doc):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if oid.startswith(f"{PID}-BS"):
|
||||
ped[int(oid[len(PID) + 3:])] = o.Geometry.GetBoundingBox(True)
|
||||
flag("5 pedestals found", sorted(ped) == [1, 2, 3, 4, 5], f"{sorted(ped)}")
|
||||
for k, pb in sorted(ped.items()):
|
||||
cx, cy = (pb.Min.X + pb.Max.X) / 2, (pb.Min.Y + pb.Max.Y) / 2
|
||||
g1 = [o for o in cl["S1"] if occ(o).startswith(f"P{k}-")]
|
||||
g2 = [o for o in cl["S2"] if occ(o).startswith(f"P{k}-")]
|
||||
flag(f"pedestal {k}: S1 11 / S2 20", (len(g1), len(g2)) == (11, 20), f"{len(g1)} {len(g2)}")
|
||||
zt = pb.Max.Z - se["top_below"]
|
||||
check(f"pedestal {k} S1 level {se['top_below']} below the pedestal top (worst, plane slope <= 20)", 0, max(abs(bb(o).Max.Z - zt) for o in g1), tol=20)
|
||||
check(f"pedestal {k} S1 legs {se['leg']} (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - se["leg"]) for o in g1 + g2))
|
||||
check(f"pedestal {k} S2 level 13 under S1 (worst)", 0, max(abs(bb(o).Max.Z - (min(bb(p).Max.Z for p in g1) - 13)) for o in g2), tol=1.5)
|
||||
check(f"pedestal {k} S1 x positions (worst)", 0, worst([mid(o)[0] - cx for o in g1], se["s1_t"]), tol=1.5)
|
||||
check(f"pedestal {k} S1 length along Y (worst)", 0, max(abs((bb(o).Max.Y - bb(o).Min.Y) - se["s1_len"]) for o in g1), tol=1.5)
|
||||
check(f"pedestal {k} S2 y positions (worst)", 0, worst([mid(o)[1] - cy for o in g2], se["s2_o"]), tol=1.5)
|
||||
check(f"pedestal {k} S2 length along X (worst)", 0, max(abs((bb(o).Max.X - bb(o).Min.X) - se["s2_len"]) for o in g2), tol=1.5)
|
||||
# containment and cover
|
||||
solids = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CP"
|
||||
or (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-BS")]
|
||||
solids = [s if isinstance(s, rg.Brep) else rg.Brep.TryConvertBrep(s) for s in solids]
|
||||
cp = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CP"][0]
|
||||
info("concrete solids for containment", f"{len(solids)} (coping + pedestals)")
|
||||
cover = {}
|
||||
for m in SCHED:
|
||||
bad, cov = 0, 1e9
|
||||
r = G["dia"][m] / 2
|
||||
for o in cl[m]:
|
||||
cu = o.Geometry
|
||||
for t in cu.DivideByCount(max(6, int(cu.GetLength() / 200)), True):
|
||||
p = cu.PointAt(t)
|
||||
for q in (p, p + rg.Vector3d(r, 0, 0), p - rg.Vector3d(r, 0, 0), p + rg.Vector3d(0, r, 0),
|
||||
p - rg.Vector3d(0, r, 0), p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
||||
if not any(s.IsPointInside(q, 0.01, False) for s in solids):
|
||||
bad += 1
|
||||
if cp.IsPointInside(p, 0.01, False):
|
||||
cov = min(cov, p.DistanceTo(cp.ClosestPoint(p)) - r)
|
||||
cover[m] = round(cov, 1)
|
||||
check(f"{m} sample points outside concrete", 0, bad, tol=0)
|
||||
info("min cover to the coping surface per mark (record; seat bars measured to the coping only)", json.dumps(cover))
|
||||
write_result(RESULT_PATH, results, dict(cover=cover))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,135 @@
|
||||
"""G5 gate checker: bridge bearings (BB) and sole plates (SP), check C11, inside Rhino 8.
|
||||
|
||||
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values from EXPECTED_PATH section G5 (D2 ELEVATION
|
||||
TABLE, 솔플레이트 상세 TABLE, user rules); orientation is compared with the modelled h2 pads (BP).
|
||||
Generator params are not read. Ported from Code/교각모델링/verify_bearing_plates.py (same checks and order).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectColorSource
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _common import all_objects, load_expected # noqa: E402
|
||||
|
||||
doc = __rhino_doc__
|
||||
X = load_expected(EXPECTED_PATH)
|
||||
PID, G = X["pier_id"], X["G5"]
|
||||
ELX = {int(k): v for k, v in G["el_x"].items()}
|
||||
S = {int(k): v for k, v in G["S"].items()}
|
||||
W = {int(k): v for k, v in G["W"].items()}
|
||||
L, T, H3, SHRINK = G["L"], G["T"], G["h3"], G.get("bearing_shrink")
|
||||
# per-bearing values (Ulsan 2026-10-13): L / T / h3 may be {no: value}; bearing_size {no: [along, across]} replaces
|
||||
# the (L-shrink) x (W-shrink) rule; thick_end "-Y" (Ungok, default) or "+Y"
|
||||
per = lambda v, no: v[str(no)] if isinstance(v, dict) else v
|
||||
BSIZE = {int(k): v for k, v in G.get("bearing_size", {}).items()}
|
||||
THICK = G.get("thick_end", "-Y")
|
||||
COL_B, COL_S = tuple(G["colours"]["bearing"]), tuple(G["colours"]["sole_plate"])
|
||||
results = []
|
||||
|
||||
|
||||
def check(item, exp, act, tol=1.0):
|
||||
results.append(dict(check="C11", item=item, expected=round(exp, 4), actual=round(act, 4),
|
||||
diff=round(act - exp, 4), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
||||
|
||||
|
||||
def flag(item, ok, detail=""):
|
||||
results.append(dict(check="C11", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
||||
|
||||
|
||||
def edges_xy(brep, top=False):
|
||||
"""Return horizontal edge vectors of the lowest (or highest) face (4 edges). Pads use the top face: their bottom
|
||||
may follow a sloped pedestal top (Okseong P1, 2026-10-14)."""
|
||||
zref = (max if top else min)(v.Location.Z for v in brep.Vertices)
|
||||
out = []
|
||||
for e in brep.Edges:
|
||||
a, b = e.PointAtStart, e.PointAtEnd
|
||||
if abs(a.Z - zref) < 0.01 and abs(b.Z - zref) < 0.01 and a.DistanceTo(b) > 1:
|
||||
out.append(b - a)
|
||||
return out
|
||||
|
||||
|
||||
def split_axes(vecs):
|
||||
"""Bridge-axis (along) edge = within 45 deg of +Y; the other is across. Not by length (W > L occurs)."""
|
||||
along = [v for v in vecs if abs(v.Y) > abs(v.X)]
|
||||
across = [v for v in vecs if abs(v.Y) <= abs(v.X)]
|
||||
return along[0], across[0]
|
||||
|
||||
|
||||
def axis_deg(vec):
|
||||
"""Angle of a plan vector from +Y, clockwise positive, folded to [-90, 90)."""
|
||||
a = math.degrees(math.atan2(vec.X, vec.Y))
|
||||
while a >= 90:
|
||||
a -= 180
|
||||
while a < -90:
|
||||
a += 180
|
||||
return a
|
||||
|
||||
|
||||
def main():
|
||||
objs = all_objects(doc)
|
||||
by = lambda p: {int(o.Attributes.GetUserString("id")[len(p):]): o for o in objs
|
||||
if (o.Attributes.GetUserString("id") or "").startswith(p)}
|
||||
bb, sp, pad = by(f"{PID}-BB"), by(f"{PID}-SP"), by(f"{PID}-BP")
|
||||
n = len(ELX)
|
||||
check("bearing count", n, len(bb), tol=0)
|
||||
check("sole plate count", n, len(sp), tol=0)
|
||||
flag(f"all {2 * n} closed solids", all(o.Geometry.IsSolid for o in list(bb.values()) + list(sp.values())))
|
||||
for no in sorted(ELX):
|
||||
gb, gs, gp = bb[no].Geometry, sp[no].Geometry, pad[no].Geometry
|
||||
bx, sx = gb.GetBoundingBox(True), gs.GetBoundingBox(True)
|
||||
check(f"NO.{no} bearing bottom = EL.'X'", ELX[no], bx.Min.Z)
|
||||
check(f"NO.{no} bearing height h3", per(H3, no), bx.Max.Z - bx.Min.Z, tol=0.01)
|
||||
ab, cb_ = split_axes(edges_xy(gb))
|
||||
if no in BSIZE:
|
||||
check(f"NO.{no} bearing across (B)", BSIZE[no][1], cb_.Length, tol=0.5)
|
||||
check(f"NO.{no} bearing along (A)", BSIZE[no][0], ab.Length, tol=0.5)
|
||||
else:
|
||||
check(f"NO.{no} bearing across (W-{SHRINK})", W[no] - SHRINK, cb_.Length, tol=0.5)
|
||||
check(f"NO.{no} bearing along (L-{SHRINK})", per(L, no) - SHRINK, ab.Length, tol=0.5)
|
||||
as_, cs_ = split_axes(edges_xy(gs))
|
||||
check(f"NO.{no} sole plate across W", W[no], cs_.Length, tol=0.5)
|
||||
check(f"NO.{no} sole plate along L", per(L, no), as_.Length, tol=0.5)
|
||||
check(f"NO.{no} sole plate bottom = bearing top", bx.Max.Z, sx.Min.Z, tol=0.01)
|
||||
long_b, long_s, long_p = ab, as_, split_axes(edges_xy(gp, top=True))[0]
|
||||
pc = gp.GetBoundingBox(True).Center
|
||||
for nm, g in (("bearing", gb), ("sole plate", gs)):
|
||||
c = g.GetBoundingBox(True).Center
|
||||
check(f"NO.{no} {nm} plan centre offset from pad (mm)", 0, math.hypot(c.X - pc.X, c.Y - pc.Y), tol=1.0)
|
||||
check(f"NO.{no} bearing axis vs pad (deg)", axis_deg(long_p), axis_deg(long_b), tol=0.01)
|
||||
check(f"NO.{no} sole plate axis vs pad (deg)", axis_deg(long_p), axis_deg(long_s), tol=0.01)
|
||||
top = [v.Location for v in gs.Vertices if v.Location.Z > sx.Min.Z + 1]
|
||||
ua = rg.Vector3d(long_s.X, long_s.Y, 0)
|
||||
ua.Unitize()
|
||||
if ua.Y < 0:
|
||||
ua = -ua # along points to +Y
|
||||
c = rg.Point3d(sum(p.X for p in top) / 4, sum(p.Y for p in top) / 4, 0)
|
||||
proj = [((p - c) * ua, p.Z) for p in top]
|
||||
lo = [z for s_, z in proj if s_ < 0]
|
||||
hi = [z for s_, z in proj if s_ > 0]
|
||||
slope = (sum(hi) / len(hi) - sum(lo) / len(lo)) / per(L, no) * 100
|
||||
check(f"NO.{no} sole plate slope S (%) along +Y-ward axis", S[no], slope, tol=0.01)
|
||||
check(f"NO.{no} sole plate centre thickness T", per(T, no), sum(z for _, z in proj) / 4 - sx.Min.Z, tol=0.01)
|
||||
flag(f"NO.{no} thick end at {THICK}", sum(lo) > sum(hi) if THICK == "-Y" else sum(hi) > sum(lo))
|
||||
pairs = sorted(proj)
|
||||
check(f"NO.{no} no transverse slope", 0, max(abs(pairs[0][1] - pairs[1][1]), abs(pairs[2][1] - pairs[3][1])),
|
||||
tol=0.01)
|
||||
for a, b2, name in ((gb, gp, "bearing-pad"), (gb, gs, "bearing-sole plate")):
|
||||
r = rg.Brep.CreateBooleanIntersection(a, b2, 0.01)
|
||||
v = sum(rg.VolumeMassProperties.Compute(x).Volume for x in r) if r else 0.0
|
||||
check(f"NO.{no} overlap {name} (mm3)", 0, v, tol=1.0)
|
||||
cb, cs = bb[no].Attributes, sp[no].Attributes
|
||||
rgb = lambda a: (a.ObjectColor.R, a.ObjectColor.G, a.ObjectColor.B)
|
||||
flag(f"NO.{no} colours (bearing {COL_B}, sole plate {COL_S})",
|
||||
cb.ColorSource == ObjectColorSource.ColorFromObject and rgb(cb) == COL_B
|
||||
and cs.ColorSource == ObjectColorSource.ColorFromObject and rgb(cs) == COL_S)
|
||||
summ = dict(total=len(results), passed=sum(r["result"] == "PASS" for r in results),
|
||||
failed=sum(r["result"] == "FAIL" for r in results))
|
||||
json.dump(dict(summary=summ, results=results), open(RESULT_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
||||
print(json.dumps(summ))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,180 @@
|
||||
"""G2 stage: column rebar P1-P6 (centreline + pipe solid) from params `column_rebar`.
|
||||
|
||||
Run inside Rhino 8 via runner (globals PARAMS_PATH) after concrete.py.
|
||||
Ported from Code/교각모델링/build_rebar_rhino.py (Ungok P1, model v0.1.09). Rules: 설계서 v0.1.04 §5 (HITL H1-H3 approved 2026-10-07). Units mm, Z = EL x 1000.
|
||||
Each centreline carries `design_length` (schedule-style original length: straight + arc lengths
|
||||
before hook fillets) and the actual curve length is measured separately (box rule: 원길이 vs 곡선길이).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectAttributes
|
||||
from System.Drawing import Color
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
TOL = 0.01
|
||||
PID = P["pier_id"]
|
||||
MARK_DIA = {m: v["dia"] for m, v in P["rebar_marks"].items()}
|
||||
DIA = {d: float(d[1:]) for d in MARK_DIA.values()} # 'S32' -> 32.0
|
||||
COLORS = {m: tuple(c) for m, c in P["display"]["column_mark_colours"].items()}
|
||||
CL_RGB = tuple(P["display"]["centreline_colour"])
|
||||
Z_FT_TOP = P["footing"]["z_bottom"] + P["footing"]["thickness"] # ties/hoops below -> anchored in footing
|
||||
Z_CP_BOT = P["coping"]["z_bottom_flat"] # above -> anchored in coping
|
||||
|
||||
|
||||
def layer(path, color):
|
||||
idx = doc.Layers.FindByFullPath(path, -1)
|
||||
if idx >= 0:
|
||||
return idx
|
||||
parent = -1
|
||||
parts = path.split("::")
|
||||
for i in range(1, len(parts) + 1):
|
||||
sub = "::".join(parts[:i])
|
||||
idx = doc.Layers.FindByFullPath(sub, -1)
|
||||
if idx < 0:
|
||||
lay = Rhino.DocObjects.Layer()
|
||||
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
|
||||
if parent >= 0:
|
||||
lay.ParentLayerId = doc.Layers[parent].Id
|
||||
idx = doc.Layers.Add(lay)
|
||||
parent = idx
|
||||
return parent
|
||||
|
||||
|
||||
def min_bend_radius(dia_name):
|
||||
d = DIA[dia_name]
|
||||
inner = 2 * d if d <= 16 else 3.5 * d # box project rule (REBAR_BENDS_20260914)
|
||||
return (inner + d) / 2
|
||||
|
||||
|
||||
def add_bar(curve, mark, occ, design_len, hosts, src, coupler=0.0):
|
||||
d = DIA[MARK_DIA[mark]]
|
||||
# fitRail=False: fitRail=True fails on line-arc-line rails and crashed a headless Rhino (2026-10-07)
|
||||
pipes = rg.Brep.CreatePipe(curve, d / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
|
||||
if not pipes or not pipes[0].IsSolid:
|
||||
raise RuntimeError(f"{mark} {occ}: pipe failed")
|
||||
common = dict(mark=mark, dia=MARK_DIA[mark], shape=P["column_rebar"]["schedule"][mark]["shape"],
|
||||
occurrence=occ, host=f"{PID}-CL", anchor_hosts=hosts, design_length=f"{design_len:.3f}",
|
||||
coupler_length=f"{coupler:.3f}",
|
||||
src=src)
|
||||
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
|
||||
(pipes[0], layer(f"{PID}::철근::기둥::{mark}", COLORS[mark]), f"{PID}-R")):
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = lay
|
||||
oid = f"{prefix}-{mark}-{occ}"
|
||||
attr.Name = oid
|
||||
attr.SetUserString("id", oid)
|
||||
for k, v in common.items():
|
||||
attr.SetUserString(k, v)
|
||||
if isinstance(geo, rg.Brep):
|
||||
doc.Objects.AddBrep(geo, attr)
|
||||
else:
|
||||
doc.Objects.AddCurve(geo, attr)
|
||||
|
||||
|
||||
def pt(r, deg, z):
|
||||
t = math.radians(deg)
|
||||
return rg.Point3d(r * math.cos(t), r * math.sin(t), z)
|
||||
|
||||
|
||||
def arc_xy(r, a0, a1, z):
|
||||
"""Horizontal arc at height z from angle a0 to a1 (deg, counter-clockwise)."""
|
||||
am = (a0 + a1) / 2
|
||||
return rg.ArcCurve(rg.Arc(pt(r, a0, z), pt(r, am, z), pt(r, a1, z)))
|
||||
|
||||
|
||||
def hooked_half_ring(r, a0, z, hook, bend_r):
|
||||
"""Semicircle a0 -> a0+180 with inward 135-degree hooks at both ends (P3, shape VI)."""
|
||||
arc = arc_xy(r, a0, a0 + 180, z)
|
||||
pieces = []
|
||||
for at_start in (True, False):
|
||||
p = arc.PointAtStart if at_start else arc.PointAtEnd
|
||||
t = arc.TangentAtStart if at_start else arc.TangentAtEnd
|
||||
if at_start:
|
||||
t = -t # travel direction away from the arc
|
||||
n = rg.Vector3d(-p.X, -p.Y, 0)
|
||||
n.Unitize()
|
||||
d = t * math.cos(math.radians(135)) + n * math.sin(math.radians(135)) # back + inward
|
||||
d.Unitize()
|
||||
pieces.append(rg.LineCurve(p, p + d * hook))
|
||||
joined = rg.Curve.JoinCurves([pieces[0], arc, pieces[1]], TOL)[0]
|
||||
fil = rg.Curve.CreateFilletCornersCurve(joined, bend_r, TOL, math.radians(1))
|
||||
return fil or joined
|
||||
|
||||
|
||||
def main():
|
||||
for o in list(doc.Objects):
|
||||
if (o.Attributes.GetUserString("id") or "").startswith((f"{PID}-R-", f"{PID}-RC-")):
|
||||
doc.Objects.Delete(o, True)
|
||||
cr = P["column_rebar"]
|
||||
mb, hp, tie = cr["main_bars"], cr["hoops"], cr["ties"]
|
||||
sch = cr["schedule"]
|
||||
count = 0
|
||||
# Main bars P1 (outer, hooked) / P2 (inner, straight)
|
||||
p1, p2 = mb["p1"], mb["p2"]
|
||||
for i in range(mb["divisions"]):
|
||||
th = mb["start_deg"] * 0 + i * mb["pitch_deg"] # start angle 0 (CAD 0.001 deg)
|
||||
ro, ri = mb["r_outer"], mb["r_inner"]
|
||||
z_leg, R = p1["hook_leg_z"], p1["bend_r"]
|
||||
top = p1["straight_from"] + p1["straight_len"]
|
||||
leg = rg.LineCurve(pt(ro + R + p1["hook_leg"], th, z_leg), pt(ro + R, th, z_leg))
|
||||
bend = rg.ArcCurve(rg.Arc(pt(ro + R, th, z_leg),
|
||||
pt(ro + R - R * math.sin(math.radians(45)), th,
|
||||
z_leg + R - R * math.cos(math.radians(45))),
|
||||
pt(ro, th, z_leg + R)))
|
||||
vert = rg.LineCurve(pt(ro, th, z_leg + R), pt(ro, th, top))
|
||||
c1 = rg.Curve.JoinCurves([leg, bend, vert], TOL)[0]
|
||||
add_bar(c1, "P1", f"{i + 1:02d}", p1["straight_len"] + math.pi / 2 * R + p1["hook_leg"],
|
||||
f"{PID}-FT,{PID}-CP", f"angle {th:.4f} deg; D1 단면 C-C / 정면도; D3 P1")
|
||||
c2 = rg.LineCurve(pt(ri, th, p2["z0"]), pt(ri, th, p2["z0"] + p2["length"]))
|
||||
add_bar(c2, "P2", f"{i + 1:02d}", p2["length"], f"{PID}-FT,{PID}-CP",
|
||||
f"angle {th:.4f} deg; D1 단면 C-C / 정면도; D3 P2")
|
||||
count += 2
|
||||
# Hoops: P4 (V: 2 half arcs + 2 coupler gaps), P5 (IX: ring + 1 gap), P3 (VI: 2 hooked halves)
|
||||
r = hp["radius"]
|
||||
n3 = 0
|
||||
for k, (z, mark) in enumerate(zip(hp["levels"], hp["marks"])):
|
||||
r = hp.get("radius_by_mark", {}).get(mark, hp["radius"]) # Ungok P2: P3 R899, P4/P5 R904
|
||||
s = sch[mark]
|
||||
a = float(s["A"]) * 1000 if float(s["A"]) < 100 else float(s["A"])
|
||||
if mark in ("P4", "P5"):
|
||||
gap = float(s["B"])
|
||||
gap_deg = math.degrees(gap / r)
|
||||
arcs = ([(90 + gap_deg / 2, 270 - gap_deg / 2), (270 + gap_deg / 2, 450 - gap_deg / 2)]
|
||||
if mark == "P4" else [(90 + gap_deg / 2, 450 - gap_deg / 2)])
|
||||
curves = [arc_xy(r, a0, a1, z) for a0, a1 in arcs]
|
||||
for j, c in enumerate(curves):
|
||||
add_bar(c, mark, f"L{k + 1:02d}{'ab'[j] if len(curves) > 1 else ''}", a, "",
|
||||
f"EL {z / 1000:.3f}; coupler gap {gap:.0f} at {'90/270' if mark == 'P4' else '90'} deg",
|
||||
coupler=gap) # one coupler (b) per piece: P4 2 pieces/2 gaps, P5 1/1
|
||||
else:
|
||||
n3 += 1
|
||||
a0 = 90 if n3 % 2 else 0 # joins staggered by 90 deg
|
||||
hook = float(s["B"])
|
||||
for j, start in enumerate((a0, a0 + 180)):
|
||||
c = hooked_half_ring(r, start, z, hook, min_bend_radius(MARK_DIA["P3"]))
|
||||
add_bar(c, mark, f"L{k + 1:02d}{'ab'[j]}", a + 2 * hook,
|
||||
f"{PID}-CP" if z > Z_CP_BOT else "", f"EL {z / 1000:.3f}; joins at {a0}/{a0 + 180} deg")
|
||||
count += 1
|
||||
# Cross ties P6 (III): straight 1836 on diameters, 150 hooks down at r=918, sitting on hoop
|
||||
half = tie["straight"] / 2
|
||||
zoff = DIA[MARK_DIA["P4"]] / 2 + DIA[MARK_DIA["P6"]] / 2
|
||||
for k, z in enumerate(tie["levels"]):
|
||||
for ang in tie["angles_deg"]:
|
||||
zt = z + zoff
|
||||
pts = [pt(half, ang + 180, zt - tie["hook"]), pt(half, ang + 180, zt), pt(half, ang, zt),
|
||||
pt(half, ang, zt - tie["hook"])]
|
||||
pl = rg.PolylineCurve(pts)
|
||||
c = rg.Curve.CreateFilletCornersCurve(pl, min_bend_radius(MARK_DIA["P6"]), TOL, math.radians(1)) or pl
|
||||
add_bar(c, "P6", f"L{k + 1}-{ang:03d}", tie["straight"] + 2 * tie["hook"],
|
||||
f"{PID}-FT" if z < Z_FT_TOP else (f"{PID}-CP" if z > Z_CP_BOT else ""),
|
||||
f"EL {z / 1000:.3f} (+{zoff} on hoop); angle {ang} deg")
|
||||
count += 1
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"bars": count}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,205 @@
|
||||
"""G2 stage, form "ring" (Ulsan P1, 2026-10-13): circular column rebar with one main-bar ring, hoop zones of
|
||||
different hoop types and cross ties, from params `column_rebar` (centreline + pipe solid per bar).
|
||||
|
||||
Run inside Rhino 8 via runner (globals PARAMS_PATH) after the G1 builder. Units mm, Z = EL x 1000.
|
||||
params column_rebar:
|
||||
main {mark, count, start_deg, radius, hook_leg_z, bend_r, hook_leg, straight_from, straight_len}
|
||||
bar = horizontal leg pointing outwards at hook_leg_z, R bend, vertical straight part (shape VI)
|
||||
hoops {radius, zones: [{mark, type, levels, a, gap | lap+hook}]}
|
||||
type "coupler_half": two half arcs, coupler gaps `gap` at 90 / 270 deg (shape V)
|
||||
"coupler_ring": one ring, one coupler gap at 90 deg (shape X)
|
||||
"lap_half": two half rings, each extended by lap/2 at both ends, 135-deg hooks (shape VI);
|
||||
joins alternate between 90/270 and 0/180 deg
|
||||
ties {mark, levels, angles_deg, straight, hook, z_offset}: straight bars on diameters, hooks down
|
||||
`design_length` is the schedule-style original length; `coupler_length` the gap per piece (schedule L
|
||||
includes the coupler).
|
||||
Optional (Okseong P1, 2026-10-14; absent -> unchanged): main.inner {mark, radius} second ring at the same angles
|
||||
with the hook bent inwards; zone.radius per hoop zone; ties.shape "90-135" {leg, tail, y_lift, design_length}:
|
||||
straight on the X / Y diameters, 90-deg leg down at one end, 135-deg hook at the other, ends swapped per level,
|
||||
the Y tie y_lift above the X tie.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectAttributes
|
||||
from System.Drawing import Color
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
TOL = 0.01
|
||||
PID = P["pier_id"]
|
||||
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items()}
|
||||
DIA = {d: float(d[1:]) for d, _ in MARK.values()}
|
||||
CL_RGB = tuple(P["display"]["centreline_colour"])
|
||||
COLORS = {m: tuple(c) for m, c in P["display"].get("column_mark_colours", {}).items()}
|
||||
Z_FT_TOP = P["footing"]["z_bottom"] + P["footing"]["thickness"]
|
||||
Z_CP_BOT = Z_FT_TOP + P["column"]["height"]
|
||||
|
||||
|
||||
def layer(path, color):
|
||||
parent = -1
|
||||
parts = path.split("::")
|
||||
for i in range(1, len(parts) + 1):
|
||||
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
|
||||
if idx < 0:
|
||||
lay = Rhino.DocObjects.Layer()
|
||||
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
|
||||
if parent >= 0:
|
||||
lay.ParentLayerId = doc.Layers[parent].Id
|
||||
idx = doc.Layers.Add(lay)
|
||||
parent = idx
|
||||
return parent
|
||||
|
||||
|
||||
def min_bend_radius(dia_name):
|
||||
d = DIA[dia_name]
|
||||
return ((2 * d if d <= 16 else 3.5 * d) + d) / 2
|
||||
|
||||
|
||||
def hosts(z):
|
||||
return f"{PID}-FT" if z < Z_FT_TOP else (f"{PID}-CP" if z > Z_CP_BOT else "")
|
||||
|
||||
|
||||
def add_bar(curve, mark, occ, design_len, anchor, src, coupler=0.0):
|
||||
dia, shape = MARK[mark]
|
||||
pipes = rg.Brep.CreatePipe(curve, DIA[dia] / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
|
||||
if not pipes or not pipes[0].IsSolid:
|
||||
raise RuntimeError(f"{mark} {occ}: pipe failed")
|
||||
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=f"{PID}-CL", anchor_hosts=anchor,
|
||||
design_length=f"{design_len:.3f}", coupler_length=f"{coupler:.3f}", src=src)
|
||||
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
|
||||
(pipes[0], layer(f"{PID}::철근::기둥::{mark}", COLORS.get(mark, (200, 140, 90))), f"{PID}-R")):
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = lay
|
||||
oid = f"{prefix}-{mark}-{occ}"
|
||||
attr.Name = oid
|
||||
attr.SetUserString("id", oid)
|
||||
for k, v in common.items():
|
||||
attr.SetUserString(k, v)
|
||||
gid = doc.Objects.AddBrep(geo, attr) if isinstance(geo, rg.Brep) else doc.Objects.AddCurve(geo, attr)
|
||||
if gid == __import__("System").Guid.Empty:
|
||||
raise RuntimeError(f"{oid}: add failed")
|
||||
|
||||
|
||||
def pt(r, deg, z):
|
||||
t = math.radians(deg)
|
||||
return rg.Point3d(r * math.cos(t), r * math.sin(t), z)
|
||||
|
||||
|
||||
def arc_xy(r, a0, a1, z):
|
||||
return rg.ArcCurve(rg.Arc(pt(r, a0, z), pt(r, (a0 + a1) / 2, z), pt(r, a1, z)))
|
||||
|
||||
|
||||
def hooked_arc(r, a0, a1, z, hook, bend_r):
|
||||
"""Arc a0 -> a1 (deg, ccw) with inward 135-deg hooks at both ends, fillet bend_r."""
|
||||
arc = arc_xy(r, a0, a1, z)
|
||||
pieces = []
|
||||
for at_start in (True, False):
|
||||
p = arc.PointAtStart if at_start else arc.PointAtEnd
|
||||
t = -arc.TangentAtStart if at_start else arc.TangentAtEnd
|
||||
n = rg.Vector3d(-p.X, -p.Y, 0)
|
||||
n.Unitize()
|
||||
d = t * math.cos(math.radians(135)) + n * math.sin(math.radians(135))
|
||||
d.Unitize()
|
||||
pieces.append(rg.LineCurve(p, p + d * hook))
|
||||
joined = rg.Curve.JoinCurves([pieces[0], arc, pieces[1]], TOL)[0]
|
||||
return rg.Curve.CreateFilletCornersCurve(joined, bend_r, TOL, math.radians(1)) or joined
|
||||
|
||||
|
||||
def main():
|
||||
for o in list(doc.Objects):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if oid.startswith((f"{PID}-R-P", f"{PID}-RC-P")):
|
||||
doc.Objects.Delete(o, True)
|
||||
cr = P["column_rebar"]
|
||||
m = cr["main"]
|
||||
n = 0
|
||||
ro, R = m["radius"], m["bend_r"]
|
||||
top = m["straight_from"] + m["straight_len"]
|
||||
for i in range(m["count"]):
|
||||
th = m["start_deg"] + i * 360.0 / m["count"]
|
||||
leg = rg.LineCurve(pt(ro + R + m["hook_leg"], th, m["hook_leg_z"]), pt(ro + R, th, m["hook_leg_z"]))
|
||||
bend = rg.ArcCurve(rg.Arc(pt(ro + R, th, m["hook_leg_z"]),
|
||||
pt(ro + R - R * math.sin(math.radians(45)), th, m["hook_leg_z"] + R - R * math.cos(math.radians(45))),
|
||||
pt(ro, th, m["hook_leg_z"] + R)))
|
||||
vert = rg.LineCurve(pt(ro, th, m["hook_leg_z"] + R), pt(ro, th, top))
|
||||
c = rg.Curve.JoinCurves([leg, bend, vert], TOL)[0]
|
||||
add_bar(c, m["mark"], f"{i + 1:02d}", m["straight_len"] + math.pi / 2 * R + m["hook_leg"],
|
||||
f"{PID}-FT,{PID}-CP", f"angle {th:.4f} deg; 배근도(1) 정면도 갈고리 R{R:.0f}·{m['hook_leg']:.0f}; 단면 DIVISION {m['count']}")
|
||||
n += 1
|
||||
if "inner" in m: # second ring, hook bent inwards (Okseong P1)
|
||||
ri = m["inner"]["radius"]
|
||||
z0 = m["hook_leg_z"]
|
||||
leg = rg.LineCurve(pt(ri - R - m["hook_leg"], th, z0), pt(ri - R, th, z0))
|
||||
bend = rg.ArcCurve(rg.Arc(pt(ri - R, th, z0),
|
||||
pt(ri - R + R * math.sin(math.radians(45)), th, z0 + R - R * math.cos(math.radians(45))),
|
||||
pt(ri, th, z0 + R)))
|
||||
vert = rg.LineCurve(pt(ri, th, z0 + R), pt(ri, th, top))
|
||||
c = rg.Curve.JoinCurves([leg, bend, vert], TOL)[0]
|
||||
add_bar(c, m["inner"]["mark"], f"{i + 1:02d}", m["straight_len"] + math.pi / 2 * R + m["hook_leg"],
|
||||
f"{PID}-FT,{PID}-CP", f"angle {th:.4f} deg; inner ring r {ri:.0f}, hook inwards")
|
||||
n += 1
|
||||
hp = cr["hoops"]
|
||||
r = hp["radius"]
|
||||
k = 0
|
||||
for zone in hp["zones"]:
|
||||
mark, typ = zone["mark"], zone["type"]
|
||||
r = zone.get("radius", hp["radius"]) # per zone (Okseong P3 1,150 / P4 1,147)
|
||||
for j, z in enumerate(zone["levels"]):
|
||||
k += 1
|
||||
if typ == "coupler_half":
|
||||
gd = math.degrees(zone["gap"] / r)
|
||||
arcs = [(90 + gd / 2, 270 - gd / 2), (270 + gd / 2, 450 - gd / 2)]
|
||||
for q, (a0, a1) in enumerate(arcs):
|
||||
add_bar(arc_xy(r, a0, a1, z), mark, f"L{k:03d}{'ab'[q]}", zone["a"], hosts(z),
|
||||
f"EL {z / 1000:.3f}; coupler gaps {zone['gap']:.0f} at 90/270 deg [추론]", coupler=zone["gap"])
|
||||
elif typ == "coupler_ring":
|
||||
gd = math.degrees(zone["gap"] / r)
|
||||
add_bar(arc_xy(r, 90 + gd / 2, 450 - gd / 2, z), mark, f"L{k:03d}", zone["a"], hosts(z),
|
||||
f"EL {z / 1000:.3f}; coupler gap {zone['gap']:.0f} at 90 deg [추론]", coupler=zone["gap"])
|
||||
elif typ == "lap_half":
|
||||
ext = math.degrees(zone["lap"] / 2 / r)
|
||||
a0 = 90 if j % 2 == 0 else 0 # joins alternate by 90 deg [추론]
|
||||
for q, s in enumerate((a0, a0 + 180)):
|
||||
c = hooked_arc(r, s - ext, s + 180 + ext, z, zone["hook"], min_bend_radius(MARK[mark][0]))
|
||||
add_bar(c, mark, f"L{k:03d}{'ab'[q]}", zone["a"] + zone["lap"] + 2 * zone["hook"], hosts(z),
|
||||
f"EL {z / 1000:.3f}; laps {zone['lap']:.0f} at {a0}/{a0 + 180} deg [추론]")
|
||||
else:
|
||||
raise RuntimeError(f"unknown hoop type {typ}")
|
||||
n += 1
|
||||
tie = cr["ties"]
|
||||
if tie.get("shape") == "90-135": # Okseong P1 (2026-10-14), as column_rebar_straight.py
|
||||
rb = min_bend_radius(MARK[tie["mark"]][0])
|
||||
for q, z in enumerate(tie["levels"]):
|
||||
for axis, name in ((0, "X"), (1, "Y")):
|
||||
zt = z + (tie["y_lift"] if axis else 0)
|
||||
u = rg.Vector3d(1, 0, 0) if axis == 0 else rg.Vector3d(0, 1, 0)
|
||||
s = -1 if q % 2 else 1
|
||||
a = rg.Point3d(0, 0, zt) - u * (s * tie["straight"] / 2)
|
||||
b = rg.Point3d(0, 0, zt) + u * (s * tie["straight"] / 2)
|
||||
d = -u * s * math.cos(math.radians(45)) + rg.Vector3d(0, 0, -1) * math.sin(math.radians(45))
|
||||
pl = rg.PolylineCurve([a + rg.Vector3d(0, 0, -tie["leg"]), a, b, b + d * tie["tail"]])
|
||||
c = rg.Curve.CreateFilletCornersCurve(pl, rb, TOL, math.radians(1)) or pl
|
||||
add_bar(c, tie["mark"], f"L{q + 1:02d}{name}", tie["design_length"], hosts(zt),
|
||||
f"EL {zt / 1000:.3f}; {name} (Y +{tie['y_lift']}, 90/135 교번 [추론])")
|
||||
n += 1
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"bars": n}))
|
||||
return
|
||||
half = tie["straight"] / 2
|
||||
for q, z in enumerate(tie["levels"]):
|
||||
zt = z + tie["z_offset"]
|
||||
for ang in tie["angles_deg"]:
|
||||
pts = [pt(half, ang + 180, zt - tie["hook"]), pt(half, ang + 180, zt), pt(half, ang, zt), pt(half, ang, zt - tie["hook"])]
|
||||
pl = rg.PolylineCurve(pts)
|
||||
c = rg.Curve.CreateFilletCornersCurve(pl, min_bend_radius(MARK[tie["mark"]][0]), TOL, math.radians(1)) or pl
|
||||
add_bar(c, tie["mark"], f"L{q + 1:02d}-{ang:05.1f}", tie["straight"] + 2 * tie["hook"], hosts(z),
|
||||
f"EL {z / 1000:.3f} (+{tie['z_offset']} on hoop); angle {ang:.2f} deg")
|
||||
n += 1
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"bars": n}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,150 @@
|
||||
"""G2 stage, form "straight" (Ramp-A P1, 2026-10-13): column on a drilled shaft, no footing. Straight main bars
|
||||
in one ring (bottom coupler to the shaft cage, not modelled as a solid), lapped half-ring hoops with 135-deg hooks,
|
||||
cross ties in X and Y (90-deg leg at one end, 135-deg hook at the other), from params `column_rebar`.
|
||||
|
||||
Run inside Rhino 8 via runner (globals PARAMS_PATH) after the G1 builder. Units mm, Z = EL x 1000.
|
||||
params column_rebar:
|
||||
main {mark, count, start_deg, radius, z0, z1, coupler_length}
|
||||
hoops {mark, radius, levels, a, lap, hook, join_deg [first, second]} joins alternate per level (K4)
|
||||
ties {mark, levels, straight, leg, hook_tail, y_lift} X tie at the level, Y tie y_lift above (K3);
|
||||
the 90-deg leg (down) and the 135-deg hook swap ends per level (K4)
|
||||
design lengths {mark: schedule-style original length per piece}
|
||||
Hosts for cover / anchorage: below the column bottom = pile top segment, above the column top = coping.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectAttributes
|
||||
from System.Drawing import Color
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
TOL = 0.01
|
||||
PID = P["pier_id"]
|
||||
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items()}
|
||||
DIA = {d: float(d[1:]) for d, _ in MARK.values()}
|
||||
CL_RGB = tuple(P["display"]["centreline_colour"])
|
||||
COLORS = {m: tuple(c) for m, c in P["display"].get("column_mark_colours", {}).items()}
|
||||
Z_CL0, Z_CL1 = P["column"]["z0"], P["column"]["z1"]
|
||||
PILE_TOP = f"{PID}-PL{len(P['pile']['segments']):02d}"
|
||||
|
||||
|
||||
def layer(path, color):
|
||||
parent = -1
|
||||
parts = path.split("::")
|
||||
for i in range(1, len(parts) + 1):
|
||||
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
|
||||
if idx < 0:
|
||||
lay = Rhino.DocObjects.Layer()
|
||||
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
|
||||
if parent >= 0:
|
||||
lay.ParentLayerId = doc.Layers[parent].Id
|
||||
idx = doc.Layers.Add(lay)
|
||||
parent = idx
|
||||
return parent
|
||||
|
||||
|
||||
def min_bend_radius(dia_name):
|
||||
d = DIA[dia_name]
|
||||
return ((2 * d if d <= 16 else 3.5 * d) + d) / 2
|
||||
|
||||
|
||||
def hosts(z):
|
||||
return PILE_TOP if z < Z_CL0 else (f"{PID}-CP" if z > Z_CL1 else "")
|
||||
|
||||
|
||||
def add_bar(curve, mark, occ, design_len, anchor, src, coupler=0.0):
|
||||
dia, shape = MARK[mark]
|
||||
pipes = rg.Brep.CreatePipe(curve, DIA[dia] / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
|
||||
if not pipes or not pipes[0].IsSolid:
|
||||
raise RuntimeError(f"{mark} {occ}: pipe failed")
|
||||
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=f"{PID}-CL", anchor_hosts=anchor,
|
||||
design_length=f"{design_len:.3f}", coupler_length=f"{coupler:.3f}", src=src)
|
||||
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
|
||||
(pipes[0], layer(f"{PID}::철근::기둥::{mark}", COLORS.get(mark, (200, 140, 90))), f"{PID}-R")):
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = lay
|
||||
oid = f"{prefix}-{mark}-{occ}"
|
||||
attr.Name = oid
|
||||
attr.SetUserString("id", oid)
|
||||
for k, v in common.items():
|
||||
attr.SetUserString(k, v)
|
||||
gid = doc.Objects.AddBrep(geo, attr) if isinstance(geo, rg.Brep) else doc.Objects.AddCurve(geo, attr)
|
||||
if gid == __import__("System").Guid.Empty:
|
||||
raise RuntimeError(f"{oid}: add failed")
|
||||
|
||||
|
||||
def pt(r, deg, z):
|
||||
t = math.radians(deg)
|
||||
return rg.Point3d(r * math.cos(t), r * math.sin(t), z)
|
||||
|
||||
|
||||
def hooked_arc(r, a0, a1, z, hook, bend_r):
|
||||
"""Arc a0 -> a1 (deg, ccw) with inward 135-deg hooks at both ends, fillet bend_r (as column_rebar_ring.py)."""
|
||||
arc = rg.ArcCurve(rg.Arc(pt(r, a0, z), pt(r, (a0 + a1) / 2, z), pt(r, a1, z)))
|
||||
pieces = []
|
||||
for at_start in (True, False):
|
||||
p = arc.PointAtStart if at_start else arc.PointAtEnd
|
||||
t = -arc.TangentAtStart if at_start else arc.TangentAtEnd
|
||||
n = rg.Vector3d(-p.X, -p.Y, 0)
|
||||
n.Unitize()
|
||||
d = t * math.cos(math.radians(135)) + n * math.sin(math.radians(135))
|
||||
d.Unitize()
|
||||
pieces.append(rg.LineCurve(p, p + d * hook))
|
||||
joined = rg.Curve.JoinCurves([pieces[0], arc, pieces[1]], TOL)[0]
|
||||
return rg.Curve.CreateFilletCornersCurve(joined, bend_r, TOL, math.radians(1)) or joined
|
||||
|
||||
|
||||
def tie_curve(axis, z, half, leg, tail, flip):
|
||||
"""Straight part on the X (axis 0) or Y (axis 1) diameter at z; 90-deg leg `leg` down at one end, 135-deg hook
|
||||
(tail pointing down and back inwards) at the other; `flip` swaps the ends."""
|
||||
u = rg.Vector3d(1, 0, 0) if axis == 0 else rg.Vector3d(0, 1, 0)
|
||||
s = -1 if flip else 1
|
||||
a, b = rg.Point3d(0, 0, z) - u * (s * half), rg.Point3d(0, 0, z) + u * (s * half)
|
||||
d = -u * s * math.cos(math.radians(45)) + rg.Vector3d(0, 0, -1) * math.sin(math.radians(45))
|
||||
return rg.PolylineCurve([a + rg.Vector3d(0, 0, -leg), a, b, b + d * tail])
|
||||
|
||||
|
||||
def main():
|
||||
for o in list(doc.Objects):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if oid.startswith((f"{PID}-R-P", f"{PID}-RC-P")):
|
||||
doc.Objects.Delete(o, True)
|
||||
cr = P["column_rebar"]
|
||||
DL = cr["design_lengths"]
|
||||
m = cr["main"]
|
||||
n = 0
|
||||
for i in range(m["count"]):
|
||||
th = m["start_deg"] + i * 360.0 / m["count"]
|
||||
c = rg.LineCurve(pt(m["radius"], th, m["z0"]), pt(m["radius"], th, m["z1"]))
|
||||
add_bar(c, m["mark"], f"{i + 1:02d}", DL[m["mark"]], f"{PILE_TOP},{PID}-CP",
|
||||
f"angle {th:.4f} deg; D2 단면 C-C DIVISION {m['count']} r {m['radius']:.0f}; 정면 EL {m['z0'] / 1000:.3f}"
|
||||
f"~{m['z1'] / 1000:.3f}; 하단 커플러 {m['coupler_length']:.0f} (K1)", coupler=m["coupler_length"])
|
||||
n += 1
|
||||
h = cr["hoops"]
|
||||
r, ext = h["radius"], math.degrees(h["lap"] / 2 / h["radius"])
|
||||
for j, z in enumerate(h["levels"]):
|
||||
a0 = h["join_deg"][j % 2]
|
||||
for q, s in enumerate((a0, a0 + 180)):
|
||||
c = hooked_arc(r, s - ext, s + 180 + ext, z, h["hook"], min_bend_radius(MARK[h["mark"]][0]))
|
||||
add_bar(c, h["mark"], f"L{j + 1:03d}{'ab'[q]}", DL[h["mark"]], hosts(z),
|
||||
f"EL {z / 1000:.3f}; 겹이음 {h['lap']:.0f} at {a0}/{a0 + 180} deg (K4 [추론])")
|
||||
n += 1
|
||||
t = cr["ties"]
|
||||
rb = min_bend_radius(MARK[t["mark"]][0])
|
||||
for j, z in enumerate(t["levels"]):
|
||||
for axis, name in ((0, "X"), (1, "Y")):
|
||||
zt = z + (t["y_lift"] if axis == 1 else 0)
|
||||
pl = tie_curve(axis, zt, t["straight"] / 2, t["leg"], t["hook_tail"], flip=(j % 2 == 1))
|
||||
c = rg.Curve.CreateFilletCornersCurve(pl, rb, TOL, math.radians(1)) or pl
|
||||
add_bar(c, t["mark"], f"L{j + 1:02d}{name}", DL[t["mark"]], hosts(zt),
|
||||
f"EL {zt / 1000:.3f}; {name} 방향{' +' + str(t['y_lift']) + ' (K3 [추론])' if axis else ''}; "
|
||||
f"90° 다리·135° 갈고리 {'좌우 교번' if j % 2 else '기본'} (K4 [추론])")
|
||||
n += 1
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"bars": n}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,190 @@
|
||||
"""G1 stage: T-pier concrete (lean, footing, circular column, coping, piles, pedestals, pads).
|
||||
|
||||
Run inside Rhino 8 via runner with globals PARAMS_PATH (app schema pier-T/1).
|
||||
Only params are read; no drawing-name branching. Units: mm, Z = EL x 1000.
|
||||
Ported from Code/교각모델링/build_rhino.py (Ungok P1, model v0.1.09).
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectAttributes
|
||||
from System.Drawing import Color
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
TOL = 0.01
|
||||
PID = P["pier_id"]
|
||||
FCK = P["materials"]["fck"]
|
||||
|
||||
LAYERS = {k: (f"{PID}::{name}", Color.FromArgb(*rgb)) for k, (name, rgb) in P["display"]["concrete_layers"].items()}
|
||||
|
||||
|
||||
def layer_index(full_path, color):
|
||||
idx = doc.Layers.FindByFullPath(full_path, -1)
|
||||
if idx >= 0:
|
||||
return idx
|
||||
parent_name, child = full_path.split("::")
|
||||
parent = doc.Layers.FindByFullPath(parent_name, -1)
|
||||
if parent < 0:
|
||||
parent = doc.Layers.Add(parent_name, Color.Black)
|
||||
layer = __import__("Rhino").DocObjects.Layer()
|
||||
layer.Name, layer.Color = child, color
|
||||
layer.ParentLayerId = doc.Layers[parent].Id
|
||||
return doc.Layers.Add(layer)
|
||||
|
||||
|
||||
def add(brep, key, oid, role, fck, views):
|
||||
if brep is None or not brep.IsSolid:
|
||||
raise RuntimeError(f"{oid}: not a closed solid")
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = layer_index(*LAYERS[key])
|
||||
attr.Name = oid
|
||||
for k, v in (("id", oid), ("role", role), ("fck", fck), ("source_views", views)):
|
||||
attr.SetUserString(k, v)
|
||||
return doc.Objects.AddBrep(brep, attr)
|
||||
|
||||
|
||||
def box(x0, x1, y0, y1, z0, z1):
|
||||
return rg.Box(rg.BoundingBox(x0, y0, z0, x1, y1, z1)).ToBrep()
|
||||
|
||||
|
||||
def cylinder(x, y, z0, z1, r):
|
||||
circle = rg.Circle(rg.Plane(rg.Point3d(x, y, z0), rg.Vector3d.ZAxis), r)
|
||||
return rg.Cylinder(circle, z1 - z0).ToBrep(True, True)
|
||||
|
||||
|
||||
def arc_through(p_start, p_end, radius, y):
|
||||
"""Arc of given radius between two XZ points; center on the lower side (soffit)."""
|
||||
(x1, z1), (x2, z2) = p_start, p_end
|
||||
mx, mz = (x1 + x2) / 2, (z1 + z2) / 2
|
||||
dx, dz = x2 - x1, z2 - z1
|
||||
chord = math.hypot(dx, dz)
|
||||
h = math.sqrt(radius ** 2 - (chord / 2) ** 2)
|
||||
nx, nz = -dz / chord, dx / chord
|
||||
centers = [(mx + nx * h, mz + nz * h), (mx - nx * h, mz - nz * h)]
|
||||
cx, cz = min(centers, key=lambda c: c[1])
|
||||
a1, a2 = math.atan2(z1 - cz, x1 - cx), math.atan2(z2 - cz, x2 - cx)
|
||||
am = (a1 + a2) / 2
|
||||
mid = rg.Point3d(cx + radius * math.cos(am), y, cz + radius * math.sin(am))
|
||||
arc = rg.Arc(rg.Point3d(x1, y, z1), mid, rg.Point3d(x2, y, z2))
|
||||
return rg.ArcCurve(arc), (cx, cz)
|
||||
|
||||
|
||||
def coping_brep(c):
|
||||
y0 = -c["width_y"] / 2
|
||||
half = c["length_bottom_tip_to_tip"] / 2
|
||||
zb, rise, r = c["z_bottom_flat"], c["tip_bottom_rise"], c["soffit_radius"]
|
||||
flat, inset = c["flat_bottom_half_width"], c["end_batter_top_inset"]
|
||||
steps = c["top_steps"]
|
||||
right_arc, rc = arc_through((flat, zb), (half, zb + rise), r, y0)
|
||||
left_arc, lc = arc_through((-half, zb + rise), (-flat, zb), r, y0)
|
||||
pts = [(half, zb + rise), (half - inset, steps[-1]["z"])]
|
||||
for s_prev, s in zip(reversed(steps[1:]), reversed(steps[:-1])):
|
||||
pts += [(s_prev["x0"], s_prev["z"]), (s["x1"], s["z"])]
|
||||
pts += [(-half + inset, steps[0]["z"]), (-half, zb + rise)]
|
||||
top = rg.PolylineCurve([rg.Point3d(x, y0, z) for x, z in pts])
|
||||
bottom = rg.LineCurve(rg.Point3d(-flat, y0, zb), rg.Point3d(flat, y0, zb))
|
||||
joined = rg.Curve.JoinCurves([bottom, right_arc, top, left_arc], TOL)
|
||||
if len(joined) != 1 or not joined[0].IsClosed:
|
||||
raise RuntimeError("coping profile not closed")
|
||||
srf = rg.Surface.CreateExtrusion(joined[0], rg.Vector3d(0, c["width_y"], 0))
|
||||
brep = srf.ToBrep().CapPlanarHoles(TOL)
|
||||
if brep.SolidOrientation == rg.BrepSolidOrientation.Inward:
|
||||
brep.Flip()
|
||||
return brep, {"right_arc_center": rc, "left_arc_center": lc}
|
||||
|
||||
|
||||
def prism(pts_xy, z0, z1):
|
||||
poly = rg.PolylineCurve([rg.Point3d(x, y, z0) for x, y in pts_xy + pts_xy[:1]])
|
||||
srf = rg.Surface.CreateExtrusion(poly, rg.Vector3d(0, 0, z1 - z0))
|
||||
b = srf.ToBrep().CapPlanarHoles(TOL)
|
||||
if b is not None and b.SolidOrientation == rg.BrepSolidOrientation.Inward:
|
||||
b.Flip()
|
||||
return b
|
||||
|
||||
|
||||
def rotated_rect(cx, cy, along, across, rot_deg, z):
|
||||
"""Rectangle with `along` side on +Y rotated clockwise by rot_deg (plan view)."""
|
||||
t = math.radians(rot_deg)
|
||||
ua, uc = (math.sin(t), math.cos(t)), (math.cos(t), -math.sin(t))
|
||||
pts = [(cx + sa * ua[0] * along / 2 + sc * uc[0] * across / 2,
|
||||
cy + sa * ua[1] * along / 2 + sc * uc[1] * across / 2)
|
||||
for sa, sc in ((-1, -1), (1, -1), (1, 1), (-1, 1))]
|
||||
return rg.PolylineCurve([rg.Point3d(x, y, z) for x, y in pts + pts[:1]])
|
||||
|
||||
|
||||
def frustum(bottom, top):
|
||||
lofts = rg.Brep.CreateFromLoft([bottom, top], rg.Point3d.Unset, rg.Point3d.Unset,
|
||||
rg.LoftType.Straight, False)
|
||||
b = lofts[0].CapPlanarHoles(TOL)
|
||||
if b is not None and b.SolidOrientation == rg.BrepSolidOrientation.Inward:
|
||||
b.Flip()
|
||||
return b
|
||||
|
||||
|
||||
def add_bearing_seats(bs, steps):
|
||||
"""Pedestals (h1) per girder line split front/rear, and h2 pads (D2 TABLE A, B, h2)."""
|
||||
ped = bs["pedestal_girder5"]
|
||||
ref = bs["reference_girder"]
|
||||
mb, mt = bs["pad_margin_bottom"], bs["pad_margin_top"]
|
||||
by_no = {b["no"]: b for b in bs["bearings"]}
|
||||
for b in bs["bearings"]:
|
||||
shift = (b["girder_line"] - ref) * bs["pitch_x"]
|
||||
half = ped["front_half"] if b["row"] == "front" else ped["rear_half"]
|
||||
z0 = steps[b["girder_line"] - 1]["z"]
|
||||
z1 = z0 + b["h1"]
|
||||
tag = " [추론: 평행 이동]" if b["inferred"] else ""
|
||||
add(prism([(x + shift, y) for x, y in half], z0, z1), "seat", f"{PID}-BS{b['no']:02d}",
|
||||
f"bearing pedestal h1 NO.{b['no']} girder {b['girder_line']} {b['row']}{tag}", FCK["seat"],
|
||||
"D1 평면도, D1 단면도, D2 ELEVATION TABLE")
|
||||
cx, cy = b["centre"]
|
||||
bottom = rotated_rect(cx, cy, b["A"] + mb, b["B"] + mb, b["rotation_deg"], z1)
|
||||
top = rotated_rect(cx, cy, b["A"] + mt, b["B"] + mt, b["rotation_deg"], z1 + b["h2"])
|
||||
add(frustum(bottom, top), "pad", f"{PID}-BP{b['no']:02d}",
|
||||
f"bearing pad h2 NO.{b['no']} {b['type']}{tag}", "-",
|
||||
"D1 평면도, D1 단면도, D2 교량받침 상세·ELEVATION TABLE")
|
||||
return len(by_no) * 2
|
||||
|
||||
|
||||
def main():
|
||||
# Rebuild from scratch: remove previous P1 objects only.
|
||||
for o in list(doc.Objects):
|
||||
if o.Attributes.GetUserString("id"):
|
||||
doc.Objects.Delete(o, True)
|
||||
lc, ft, cl, cp, pl = (P[k] for k in ("lean_concrete", "footing", "column", "coping", "piles"))
|
||||
|
||||
ft_top = ft["z_bottom"] + ft["thickness"]
|
||||
col_top = ft_top + cl["height"]
|
||||
if abs(col_top - cp["z_bottom_flat"]) > TOL:
|
||||
raise RuntimeError(f"column top {col_top} != coping bottom {cp['z_bottom_flat']}")
|
||||
|
||||
pile_top = ft["z_bottom"] + pl["head_embedment"]
|
||||
px, py = pl.get("pitch_x", pl.get("pitch")), pl.get("pitch_y", pl.get("pitch")) # Ungok P2: X 1750 / Y 1550
|
||||
pile_xy = [(-((pl["count_x"] - 1) * px) / 2 + i * px,
|
||||
-((pl["count_y"] - 1) * py) / 2 + j * py)
|
||||
for j in range(pl["count_y"]) for i in range(pl["count_x"])]
|
||||
piles = [cylinder(x, y, pile_top - pl["length"], pile_top, pl["diameter"] / 2) for x, y in pile_xy]
|
||||
|
||||
lean = box(-lc["size_x"] / 2, lc["size_x"] / 2, -lc["size_y"] / 2, lc["size_y"] / 2,
|
||||
lc["z_bottom"], lc["z_top"])
|
||||
cut = rg.Brep.CreateBooleanDifference([lean], piles, TOL)
|
||||
if not cut or len(cut) != 1:
|
||||
raise RuntimeError("lean concrete boolean failed")
|
||||
add(cut[0], "lean", f"{PID}-LC", "lean concrete", FCK["lean"], "D1 정면도, D1 단면도")
|
||||
add(box(-ft["size_x"] / 2, ft["size_x"] / 2, -ft["size_y"] / 2, ft["size_y"] / 2,
|
||||
ft["z_bottom"], ft_top), "footing", f"{PID}-FT", "footing", FCK["footing"],
|
||||
"D1 정면도, D1 단면도, D2 기초평면도")
|
||||
add(cylinder(0, 0, ft_top, col_top, cl["diameter"] / 2), "column", f"{PID}-CL", "column", FCK["column"],
|
||||
"D1 정면도, D1 단면도, D1 단면 C-C/D-D/E-E, D2 기초평면도")
|
||||
cop, centers = coping_brep(cp)
|
||||
add(cop, "coping", f"{PID}-CP", "coping", FCK["coping"], "D1 평면도, D1 정면도, D1 단면도, D1 단면 A-A/B-B")
|
||||
pile_role = f"{pl['kind']} D{pl['diameter']:.0f} L{pl['length'] / 1000:g}m"
|
||||
for n, b in enumerate(piles, 1):
|
||||
add(b, "pile", f"{PID}-PL{n:02d}", pile_role, "-", "D1 정면도, D1 단면도")
|
||||
seats = add_bearing_seats(P["bearing_seats"], cp["top_steps"]) if "bearing_seats" in P else 0
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"objects": 4 + len(piles) + seats, "coping_arc_centers": centers}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,205 @@
|
||||
"""G1 stage, form "single shaft" (Ramp-A P1, 2026-10-13): drilled shaft in two diameters (no footing, no lean
|
||||
concrete), circular column, Y-shaped coping (R soffit arcs flaring from the column, battered ends that are convex
|
||||
in plan, a step in the top surface), one pedestal (box) and one h2 pad (frustum) per bearing.
|
||||
|
||||
Run inside Rhino 8 via runner with globals PARAMS_PATH. Units mm, Z = EL x 1000, origin = pier centre.
|
||||
params:
|
||||
pile {segments [{d, z0, z1}, ...] bottom to top}; column {shape "circular", diameter, z0, z1}
|
||||
coping {form "y_shaft", z_bottom (= column top), z_tip, half_bottom (column face), soffit {centre_x, centre_z,
|
||||
radius} (left arc; the right one is mirrored), z_top_left, z_top_right, top_half (straight top edge),
|
||||
plan_end {corner_x, tip_x, half_width} (bottom edge arc through (corner_x, +-half_width) and (tip_x, 0)),
|
||||
side {half_width_top, half_width_bottom} (2,200 above z_tip, straight taper to 2,000 at z_bottom)}
|
||||
bearing_seats {pedestal {size_x, size_y}, pad_margin_bottom, pad_margin_top,
|
||||
bearings [{no, centre [x, y], rotation_deg, seat_z, h1, h2, A, B, type}]}
|
||||
Coping = (front outline extruded along Y) n (side outline extruded along X) - two end cutters. The end face is
|
||||
ruled: straight top edge x = +-top_half at the top level, convex plan arc at z_tip. Each extrusion reaches past
|
||||
the faces the other one defines (no coincident or tangent faces in the booleans, see concrete_tapered.py).
|
||||
Not modelled (user decision 2026-10-13): the 30 mm decorative grooves (D1 detail "B").
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
import System
|
||||
from Rhino.DocObjects import ObjectAttributes
|
||||
from System.Drawing import Color
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
TOL = 0.01
|
||||
PID = P["pier_id"]
|
||||
FCK = P["materials"]["fck"]
|
||||
LAYERS = {k: (f"{PID}::{name}", Color.FromArgb(*rgb)) for k, (name, rgb) in P["display"]["concrete_layers"].items()}
|
||||
|
||||
|
||||
def layer_index(full_path, color):
|
||||
idx = doc.Layers.FindByFullPath(full_path, -1)
|
||||
if idx >= 0:
|
||||
return idx
|
||||
parent_name, child = full_path.split("::")
|
||||
parent = doc.Layers.FindByFullPath(parent_name, -1)
|
||||
if parent < 0:
|
||||
parent = doc.Layers.Add(parent_name, Color.Black)
|
||||
layer = __import__("Rhino").DocObjects.Layer()
|
||||
layer.Name, layer.Color = child, color
|
||||
layer.ParentLayerId = doc.Layers[parent].Id
|
||||
return doc.Layers.Add(layer)
|
||||
|
||||
|
||||
def add(brep, key, oid, role, fck, views):
|
||||
if brep is None or not brep.IsSolid:
|
||||
raise RuntimeError(f"{oid}: not a closed solid")
|
||||
if not brep.IsValid: # AddBrep silently refuses invalid breps
|
||||
ok, log = brep.IsValidWithLog()
|
||||
raise RuntimeError(f"{oid}: invalid brep: {log[:400]}")
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = layer_index(*LAYERS[key])
|
||||
attr.Name = oid
|
||||
for k, v in (("id", oid), ("role", role), ("fck", fck), ("source_views", views)):
|
||||
attr.SetUserString(k, v)
|
||||
gid = doc.Objects.AddBrep(brep, attr)
|
||||
if gid == System.Guid.Empty:
|
||||
raise RuntimeError(f"{oid}: AddBrep failed")
|
||||
return gid
|
||||
|
||||
|
||||
def solid(b):
|
||||
if b is not None and b.SolidOrientation == rg.BrepSolidOrientation.Inward:
|
||||
b.Flip()
|
||||
return b
|
||||
|
||||
|
||||
def cylinder(z0, z1, r):
|
||||
return rg.Cylinder(rg.Circle(rg.Plane(rg.Point3d(0, 0, z0), rg.Vector3d.ZAxis), r), z1 - z0).ToBrep(True, True)
|
||||
|
||||
|
||||
def extrude_closed(curve, vec):
|
||||
return solid(rg.Surface.CreateExtrusion(curve, vec).ToBrep().CapPlanarHoles(TOL))
|
||||
|
||||
|
||||
def rotated_rect(cx, cy, along, across, rot_deg, z):
|
||||
"""Rectangle with `along` side on +Y rotated clockwise by rot_deg (plan view)."""
|
||||
t = math.radians(rot_deg)
|
||||
ua, uc = (math.sin(t), math.cos(t)), (math.cos(t), -math.sin(t))
|
||||
pts = [(cx + sa * ua[0] * along / 2 + sc * uc[0] * across / 2, cy + sa * ua[1] * along / 2 + sc * uc[1] * across / 2)
|
||||
for sa, sc in ((-1, -1), (1, -1), (1, 1), (-1, 1))]
|
||||
return rg.PolylineCurve([rg.Point3d(x, y, z) for x, y in pts + pts[:1]])
|
||||
|
||||
|
||||
def frustum(bottom, top):
|
||||
lofts = rg.Brep.CreateFromLoft([bottom, top], rg.Point3d.Unset, rg.Point3d.Unset, rg.LoftType.Straight, False)
|
||||
return solid(lofts[0].CapPlanarHoles(TOL))
|
||||
|
||||
|
||||
def boolean(kind, a, b):
|
||||
f = rg.Brep.CreateBooleanIntersection if kind == "and" else rg.Brep.CreateBooleanDifference
|
||||
out = f(a, b, TOL)
|
||||
if not out or len(out) != 1:
|
||||
raise RuntimeError(f"coping boolean {kind} failed ({len(out) if out else 0} pieces)")
|
||||
return solid(out[0])
|
||||
|
||||
|
||||
def front_extrusion(c):
|
||||
"""Front outline (XZ): soffit arcs from the column face, continued past the end (cut later), vertical outer
|
||||
ends at +-ext, top levels with the step at x = 0. Extruded over y = +-(half_width_top + 100)."""
|
||||
s, zb = c["soffit"], c["z_bottom"]
|
||||
cx, cz, r = s["centre_x"], s["centre_z"], s["radius"]
|
||||
a0 = math.atan2(zb - cz, -c["half_bottom"] - cx) # left arc start (column face)
|
||||
ext = c["plan_end"]["tip_x"] + 400 # past the tip (abs)
|
||||
a1 = math.acos((-ext - cx) / r) if abs((-ext - cx) / r) <= 1 else None
|
||||
if a1 is None:
|
||||
raise RuntimeError("soffit arc does not reach the extension")
|
||||
am = (a0 + a1) / 2 # left arc through start, mid, end
|
||||
pl = [(cx + r * math.cos(a), cz + r * math.sin(a)) for a in (a0, am, a1)] # (x, z)
|
||||
w = c["side"]["half_width_top"] + 100
|
||||
to_xyz = lambda x, z: rg.Point3d(x, -w, z)
|
||||
arc_l = rg.ArcCurve(rg.Arc(*[to_xyz(x, z) for x, z in pl]))
|
||||
arc_r = rg.ArcCurve(rg.Arc(*[to_xyz(-x, z) for x, z in reversed(pl)]))
|
||||
zl, zr = c["z_top_left"], c["z_top_right"]
|
||||
xe, ze = pl[2] # left extension end (xe < 0)
|
||||
poly_top = rg.PolylineCurve([to_xyz(xe, ze), to_xyz(xe, zl), to_xyz(0, zl), to_xyz(0, zr), to_xyz(-xe, zr),
|
||||
to_xyz(-xe, ze)])
|
||||
bottom = rg.LineCurve(to_xyz(c["half_bottom"], zb), to_xyz(-c["half_bottom"], zb))
|
||||
j = rg.Curve.JoinCurves([arc_l, poly_top, arc_r, bottom], 0.01)
|
||||
if len(j) != 1 or not j[0].IsClosed:
|
||||
raise RuntimeError(f"front outline not closed ({len(j)} pieces)")
|
||||
return extrude_closed(j[0], rg.Vector3d(0, 2 * w, 0))
|
||||
|
||||
|
||||
def side_extrusion(c):
|
||||
"""Side outline (YZ): straight taper from the column width at z_bottom to the full width at z_tip, continued
|
||||
100 below z_bottom (no face on the coping bottom plane), vertical above, up past the top. Extruded over X."""
|
||||
sd, zb, zt = c["side"], c["z_bottom"], c["z_tip"]
|
||||
hb, ht = sd["half_width_bottom"], sd["half_width_top"]
|
||||
k = (ht - hb) / (zt - zb)
|
||||
y_low, z_low = hb - k * 100, zb - 100
|
||||
ztop = max(c["z_top_left"], c["z_top_right"]) + 200
|
||||
x0 = c["plan_end"]["tip_x"] + 600
|
||||
pts = [(-y_low, z_low), (y_low, z_low), (ht, zt), (ht, ztop), (-ht, ztop), (-ht, zt)]
|
||||
crv = rg.PolylineCurve([rg.Point3d(-x0, y, z) for y, z in pts + pts[:1]])
|
||||
return extrude_closed(crv, rg.Vector3d(2 * x0, 0, 0))
|
||||
|
||||
|
||||
def end_cutter(c, sx):
|
||||
"""Waste beyond the ruled end face of one end (sx = -1 left, +1 right). Two straight-lofted sections, below the
|
||||
tip level and above the top: on each, x(y) = arc(y) + t * (top_edge - arc(y)), t = (z - z_tip) / (z_top - z_tip),
|
||||
so the loft lines are the rulings of the end face. Plan arc sampled every 25 mm (chord error < 0.1 mm)."""
|
||||
pe, zt = c["plan_end"], c["z_tip"]
|
||||
ztop = c["z_top_left"] if sx < 0 else c["z_top_right"]
|
||||
hw, corner, tip, top = pe["half_width"], pe["corner_x"], pe["tip_x"], c["top_half"]
|
||||
sag = tip - corner
|
||||
R = (hw * hw + sag * sag) / (2 * sag)
|
||||
ys = [-hw - 200 + 25 * i for i in range(int((2 * hw + 400) / 25) + 1)]
|
||||
arc = lambda y: tip - (R - math.sqrt(R * R - y * y)) # abs x of the bottom edge
|
||||
secs = []
|
||||
for z in (zt - 700, ztop + 150):
|
||||
t = (z - zt) / (ztop - zt)
|
||||
pts = [(arc(y) + t * (top - arc(y)), y) for y in ys]
|
||||
pts += [(tip + 1500, ys[-1]), (tip + 1500, ys[0])]
|
||||
secs.append(rg.PolylineCurve([rg.Point3d(sx * x, y, z) for x, y in pts + pts[:1]]))
|
||||
lofts = rg.Brep.CreateFromLoft(secs, rg.Point3d.Unset, rg.Point3d.Unset, rg.LoftType.Straight, False)
|
||||
b = solid(lofts[0].CapPlanarHoles(TOL))
|
||||
if b is None or not b.IsSolid:
|
||||
raise RuntimeError("end cutter is not a closed solid")
|
||||
return b
|
||||
|
||||
|
||||
def coping_brep(c):
|
||||
body = boolean("and", [front_extrusion(c)], [side_extrusion(c)])
|
||||
return boolean("diff", [body], [end_cutter(c, -1), end_cutter(c, 1)])
|
||||
|
||||
|
||||
def main():
|
||||
for o in list(doc.Objects):
|
||||
if o.Attributes.GetUserString("id"):
|
||||
doc.Objects.Delete(o, True)
|
||||
pl, cl, cp, bs = (P[k] for k in ("pile", "column", "coping", "bearing_seats"))
|
||||
segs = pl["segments"]
|
||||
for a, b in zip(segs, segs[1:]):
|
||||
if abs(a["z1"] - b["z0"]) > TOL:
|
||||
raise RuntimeError("pile segments not continuous")
|
||||
if abs(segs[-1]["z1"] - cl["z0"]) > TOL or abs(cl["z1"] - cp["z_bottom"]) > TOL:
|
||||
raise RuntimeError("pile top / column / coping bottom levels do not meet")
|
||||
for i, s in enumerate(segs, 1):
|
||||
add(cylinder(s["z0"], s["z1"], s["d"] / 2), "pile", f"{PID}-PL{i:02d}",
|
||||
f"drilled shaft segment {i} D{s['d']:.0f}", FCK["pile"], "일반도 정면도·측면도")
|
||||
add(cylinder(cl["z0"], cl["z1"], cl["diameter"] / 2), "column", f"{PID}-CL", "column", FCK["column"],
|
||||
"일반도 정면도·측면도·기둥 단면도")
|
||||
add(coping_brep(cp), "coping", f"{PID}-CP", "coping (Y-shaped, single shaft)", FCK["coping"],
|
||||
"일반도 정면도·측면도·평면도")
|
||||
ped, mb, mt = bs["pedestal"], bs["pad_margin_bottom"], bs["pad_margin_top"]
|
||||
for b in bs["bearings"]:
|
||||
cx, cy = b["centre"]
|
||||
z0, z1 = b["seat_z"], b["seat_z"] + b["h1"]
|
||||
base = rotated_rect(cx, cy, ped["size_y"], ped["size_x"], b["rotation_deg"], z0)
|
||||
add(extrude_closed(base, rg.Vector3d(0, 0, b["h1"])), "seat", f"{PID}-BS{b['no']:02d}",
|
||||
f"bearing pedestal h1 NO.{b['no']}", FCK["seat"], "일반도 평면도·교량받침 상세도·ELEVATION TABLE")
|
||||
bottom = rotated_rect(cx, cy, b["A"] + mb, b["B"] + mb, b["rotation_deg"], z1)
|
||||
top = rotated_rect(cx, cy, b["A"] + mt, b["B"] + mt, b["rotation_deg"], z1 + b["h2"])
|
||||
add(frustum(bottom, top), "pad", f"{PID}-BP{b['no']:02d}", f"bearing pad h2 NO.{b['no']} {b['type']}", "-",
|
||||
"일반도 평면도·교량받침 상세도·ELEVATION TABLE")
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"objects": len(segs) + 2 + 2 * len(bs["bearings"])}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,267 @@
|
||||
"""G1 stage, form "tapered T" (Ulsan P1, 2026-10-13): lean, spread footing (no piles), circular column, coping
|
||||
with a straight sloped soffit, battered ends, a step in the top surface and rounded plan corners,
|
||||
one pedestal (box) and one h2 pad (frustum) per bearing.
|
||||
|
||||
Run inside Rhino 8 via runner with globals PARAMS_PATH. Units mm, Z = EL x 1000, origin = pier centre.
|
||||
params:
|
||||
lean_concrete {size_x, size_y, z_bottom, z_top}; footing {size_x, size_y, z_bottom, thickness}
|
||||
column {shape "circular", diameter, height}
|
||||
coping {form "tapered", profile_xz [[x, z], ...] (front-view outline, closed implicitly), width_y,
|
||||
plan_length (at the tips), top_length, z_tip, z_top_left, z_top_right, plan_corner_radius}
|
||||
bearing_seats {pedestal {size_x, size_y}, pad_margin_bottom, pad_margin_top,
|
||||
bearings [{no, centre [x, y], rotation_deg, seat_z, h1, h2, A, B, type}]}
|
||||
Coping = profile extrusion minus the four plan-corner wastes outside R (faceted 16-segment arcs, +0.5 mm).
|
||||
Optional (Okseong P1, 2026-10-14; absent -> the code paths above are unchanged):
|
||||
piles {count_x, count_y, pitch_x, pitch_y, diameter, length, head_embedment, kind}: pile grid under the footing,
|
||||
the lean concrete is cut by the piles (as concrete.py)
|
||||
coping.side_taper {half_width_top, z_taper_top, half_width_bottom}: body intersected with the side outline
|
||||
(full width above z_taper_top, straight taper to the bottom width at the coping bottom)
|
||||
coping.skew_deg: plan skew; the coping is sheared x' = x - tan(skew) * y (end faces / top-step lines at 90 + skew)
|
||||
footing.rot_deg (Ulsan P2, 2026-10-15; absent = 0): lean, footing and piles are drawn in the foundation's own axes and then
|
||||
rotated counter-clockwise (plan view, X right / Y up) about the pier centre; column, coping and seats stay in the pier axes
|
||||
bearing_seats.pedestals [{no, corners [[x, y] x4], z_bottom, top {y0, z0, y1, z1}}]: explicit pedestal solids (plan
|
||||
quadrilateral, top plane linear in y); bearings[].pedestal = its index; pads then sit on that plane
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
import System
|
||||
from Rhino.DocObjects import ObjectAttributes
|
||||
from System.Drawing import Color
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
TOL = 0.01
|
||||
PID = P["pier_id"]
|
||||
FCK = P["materials"]["fck"]
|
||||
LAYERS = {k: (f"{PID}::{name}", Color.FromArgb(*rgb)) for k, (name, rgb) in P["display"]["concrete_layers"].items()}
|
||||
|
||||
|
||||
def layer_index(full_path, color):
|
||||
idx = doc.Layers.FindByFullPath(full_path, -1)
|
||||
if idx >= 0:
|
||||
return idx
|
||||
parent_name, child = full_path.split("::")
|
||||
parent = doc.Layers.FindByFullPath(parent_name, -1)
|
||||
if parent < 0:
|
||||
parent = doc.Layers.Add(parent_name, Color.Black)
|
||||
layer = __import__("Rhino").DocObjects.Layer()
|
||||
layer.Name, layer.Color = child, color
|
||||
layer.ParentLayerId = doc.Layers[parent].Id
|
||||
return doc.Layers.Add(layer)
|
||||
|
||||
|
||||
def add(brep, key, oid, role, fck, views):
|
||||
if brep is None or not brep.IsSolid:
|
||||
raise RuntimeError(f"{oid}: not a closed solid")
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = layer_index(*LAYERS[key])
|
||||
attr.Name = oid
|
||||
for k, v in (("id", oid), ("role", role), ("fck", fck), ("source_views", views)):
|
||||
attr.SetUserString(k, v)
|
||||
if not brep.IsValid: # AddBrep silently refuses invalid breps (2026-10-13: coping lost)
|
||||
ok, log = brep.IsValidWithLog()
|
||||
raise RuntimeError(f"{oid}: invalid brep: {log[:400]}")
|
||||
gid = doc.Objects.AddBrep(brep, attr)
|
||||
if gid == System.Guid.Empty:
|
||||
raise RuntimeError(f"{oid}: AddBrep failed")
|
||||
return gid
|
||||
|
||||
|
||||
def solid(b):
|
||||
if b is not None and b.SolidOrientation == rg.BrepSolidOrientation.Inward:
|
||||
b.Flip()
|
||||
return b
|
||||
|
||||
|
||||
def box(x0, x1, y0, y1, z0, z1):
|
||||
return rg.Box(rg.BoundingBox(x0, y0, z0, x1, y1, z1)).ToBrep()
|
||||
|
||||
|
||||
def cylinder(x, y, z0, z1, r):
|
||||
return rg.Cylinder(rg.Circle(rg.Plane(rg.Point3d(x, y, z0), rg.Vector3d.ZAxis), r), z1 - z0).ToBrep(True, True)
|
||||
|
||||
|
||||
def extrude_closed(curve, vec):
|
||||
return solid(rg.Surface.CreateExtrusion(curve, vec).ToBrep().CapPlanarHoles(TOL))
|
||||
|
||||
|
||||
def rotated_rect(cx, cy, along, across, rot_deg, z):
|
||||
"""Rectangle with `along` side on +Y rotated clockwise by rot_deg (plan view)."""
|
||||
t = math.radians(rot_deg)
|
||||
ua, uc = (math.sin(t), math.cos(t)), (math.cos(t), -math.sin(t))
|
||||
pts = [(cx + sa * ua[0] * along / 2 + sc * uc[0] * across / 2, cy + sa * ua[1] * along / 2 + sc * uc[1] * across / 2)
|
||||
for sa, sc in ((-1, -1), (1, -1), (1, 1), (-1, 1))]
|
||||
return rg.PolylineCurve([rg.Point3d(x, y, z) for x, y in pts + pts[:1]])
|
||||
|
||||
|
||||
def frustum(bottom, top):
|
||||
lofts = rg.Brep.CreateFromLoft([bottom, top], rg.Point3d.Unset, rg.Point3d.Unset, rg.LoftType.Straight, False)
|
||||
return solid(lofts[0].CapPlanarHoles(TOL))
|
||||
|
||||
|
||||
def rrect(length, width, r, z):
|
||||
"""Closed rounded rectangle centred on the axis at height z (corners filleted with radius r)."""
|
||||
rect = rg.Rectangle3d(rg.Plane(rg.Point3d(0, 0, z), rg.Vector3d.ZAxis), rg.Interval(-length / 2, length / 2),
|
||||
rg.Interval(-width / 2, width / 2)).ToNurbsCurve()
|
||||
return rg.Curve.CreateFilletCornersCurve(rect, r, TOL, math.radians(1)) if r > 0 else rect
|
||||
|
||||
|
||||
def corner_waste(sx, sy, c, z_top):
|
||||
"""Region outside the R fillet of one plan corner (x sign sx, y sign sy), as a straight loft through three
|
||||
equal sections: vertical below the tip level (fillet centre at tip - R), then following the end batter up to
|
||||
z_top + 100 (centre moves with the end face). The arc is a 16-segment polyline offset 0.5 mm outwards
|
||||
(chord error < 0.6 mm), so no cutter face is tangent to or coincident with a coping face / knife edge."""
|
||||
w, R = c["width_y"], c["plan_corner_radius"]
|
||||
tip, top, z_tip = c["plan_length"] / 2, c["top_length"] / 2, c["z_tip"]
|
||||
batter = (tip - top) / (z_top - z_tip) # x shift per mm of height above the tip
|
||||
zs = [z_tip - 300, z_tip, z_top + 100]
|
||||
cxs = [tip - R, tip - R, tip - R - batter * (z_top + 100 - z_tip)]
|
||||
cy, r = w / 2 - R, R + 0.5
|
||||
secs = []
|
||||
for z, cx in zip(zs, cxs):
|
||||
pts = [(cx + r * math.cos(math.radians(a)), cy + r * math.sin(math.radians(a))) for a in
|
||||
[90 * i / 16 for i in range(17)]]
|
||||
pts += [(cx, cy + R + 100), (cx + R + 100, cy + R + 100), (cx + R + 100, cy)]
|
||||
secs.append(rg.PolylineCurve([rg.Point3d(sx * x, sy * y, z) for x, y in pts + pts[:1]]))
|
||||
lofts = rg.Brep.CreateFromLoft(secs, rg.Point3d.Unset, rg.Point3d.Unset, rg.LoftType.Straight, False)
|
||||
return solid(lofts[0].CapPlanarHoles(TOL))
|
||||
|
||||
|
||||
def side_cut(c, body):
|
||||
"""Intersect with the side outline extruded along X (Ramp-A style taper); reaches past the X ends."""
|
||||
st = c["side_taper"]
|
||||
zb = min(z for _, z in c["profile_xz"])
|
||||
zt, ht, hb = st["z_taper_top"], st["half_width_top"], st["half_width_bottom"]
|
||||
k = (ht - hb) / (zt - zb)
|
||||
y_low, z_low = hb - k * 100, zb - 100
|
||||
ztop = max(c["z_top_left"], c["z_top_right"], max(z for _, z in c["profile_xz"])) + 200
|
||||
x0 = c["plan_length"] / 2 + 3000
|
||||
pts = [(-y_low, z_low), (y_low, z_low), (ht, zt), (ht, ztop), (-ht, ztop), (-ht, zt)]
|
||||
side = extrude_closed(rg.PolylineCurve([rg.Point3d(-x0, y, z) for y, z in pts + pts[:1]]), rg.Vector3d(2 * x0, 0, 0))
|
||||
out = rg.Brep.CreateBooleanIntersection(body, side, TOL)
|
||||
if not out or len(out) != 1:
|
||||
raise RuntimeError(f"coping side taper failed ({len(out) if out else 0})")
|
||||
return solid(out[0])
|
||||
|
||||
|
||||
def skew_xf(deg):
|
||||
xf = rg.Transform.Identity
|
||||
xf.M01 = -math.tan(math.radians(deg)) # x' = x - tan(skew) * y
|
||||
return xf
|
||||
|
||||
|
||||
def coping_brep(c):
|
||||
"""Front profile (sloped soffit, battered ends, top step) extruded over the width as one solid, minus the four
|
||||
plan-corner wastes outside the R fillets (see corner_waste). Splitting the coping into parts and uniting them,
|
||||
or filleting the corner edges, failed on coplanar faces / the knife-edge tips (2026-10-13)."""
|
||||
w, R = c["width_y"], c["plan_corner_radius"]
|
||||
if "side_taper" in c or "skew_deg" in c: # Okseong form (2026-10-14): extrusion wider than the taper
|
||||
w2 = w + 400
|
||||
prof = rg.PolylineCurve([rg.Point3d(x, -w2 / 2, z) for x, z in c["profile_xz"] + c["profile_xz"][:1]])
|
||||
a = extrude_closed(prof, rg.Vector3d(0, w2, 0))
|
||||
if "side_taper" in c:
|
||||
a = side_cut(c, a)
|
||||
if c.get("skew_deg"):
|
||||
a.Transform(skew_xf(c["skew_deg"]))
|
||||
return a
|
||||
prof = rg.PolylineCurve([rg.Point3d(x, -w / 2, z) for x, z in c["profile_xz"] + c["profile_xz"][:1]])
|
||||
a = extrude_closed(prof, rg.Vector3d(0, w, 0))
|
||||
if R <= 0:
|
||||
return a
|
||||
cutters = [corner_waste(sx, sy, c, c["z_top_left"] if sx < 0 else c["z_top_right"]) for sx in (1, -1) for sy in (1, -1)]
|
||||
for q in cutters:
|
||||
if q is None or not q.IsSolid:
|
||||
raise RuntimeError("corner cutter is not a closed solid")
|
||||
out = rg.Brep.CreateBooleanDifference([a], cutters, TOL)
|
||||
if not out or len(out) != 1:
|
||||
raise RuntimeError(f"coping corner cut failed ({len(out) if out else 0})")
|
||||
return solid(out[0])
|
||||
|
||||
|
||||
def plane_z(top, y):
|
||||
return top["z0"] + (y - top["y0"]) * (top["z1"] - top["z0"]) / (top["y1"] - top["y0"])
|
||||
|
||||
|
||||
def add_shared_seats(bs):
|
||||
"""Pedestal = plan quadrilateral extruded from z_bottom to its sloped top plane; pad = frustum whose bottom lies
|
||||
on that plane and whose top is flat at (pedestal top at the bearing centre) + h2 (Okseong P1, O2 2026-10-14)."""
|
||||
mb, mt = bs["pad_margin_bottom"], bs["pad_margin_top"]
|
||||
for pd in bs["pedestals"]:
|
||||
cs = pd["corners"]
|
||||
bot = [rg.Point3d(x, y, pd["z_bottom"]) for x, y in cs]
|
||||
top = [rg.Point3d(x, y, plane_z(pd["top"], y)) for x, y in cs]
|
||||
b = rg.Brep.CreateFromBox(bot + top)
|
||||
if b is None or not b.IsSolid:
|
||||
raise RuntimeError(f"pedestal {pd['no']} failed")
|
||||
add(solid(b), "seat", f"{PID}-BS{pd['no']:02d}", f"bearing pedestal (shared) NO.{pd['no']}", FCK["seat"],
|
||||
"일반도 평면도·교량받침 상세·ELEVATION TABLE")
|
||||
for b in bs["bearings"]:
|
||||
top = bs["pedestals"][b["pedestal"]]["top"]
|
||||
cx, cy = b["centre"]
|
||||
bottom = rotated_rect(cx, cy, b["A"] + mb, b["B"] + mb, b["rotation_deg"], 0)
|
||||
bottom = rg.PolylineCurve([rg.Point3d(p.X, p.Y, plane_z(top, p.Y)) for p in bottom.ToPolyline()])
|
||||
zt = plane_z(top, cy) + b["h2"]
|
||||
add(frustum(bottom, rotated_rect(cx, cy, b["A"] + mt, b["B"] + mt, b["rotation_deg"], zt)), "pad",
|
||||
f"{PID}-BP{b['no']:02d}", f"bearing pad h2 NO.{b['no']} {b['type']}", "-", "일반도 교량받침 상세·ELEVATION TABLE")
|
||||
|
||||
|
||||
def main():
|
||||
for o in list(doc.Objects):
|
||||
if o.Attributes.GetUserString("id"):
|
||||
doc.Objects.Delete(o, True)
|
||||
lc, ft, cl, cp, bs = (P[k] for k in ("lean_concrete", "footing", "column", "coping", "bearing_seats"))
|
||||
ft_top = ft["z_bottom"] + ft["thickness"]
|
||||
col_top = ft_top + cl["height"]
|
||||
zmin = min(z for _, z in cp["profile_xz"])
|
||||
if abs(col_top - zmin) > TOL:
|
||||
raise RuntimeError(f"column top {col_top} != coping bottom {zmin}")
|
||||
rot = rg.Transform.Rotation(math.radians(ft.get("rot_deg", 0.0)), rg.Vector3d.ZAxis, rg.Point3d.Origin)
|
||||
lean = box(-lc["size_x"] / 2, lc["size_x"] / 2, -lc["size_y"] / 2, lc["size_y"] / 2, lc["z_bottom"], lc["z_top"])
|
||||
piles = []
|
||||
if "piles" in P: # Okseong P1 (2026-10-14): pile grid, lean cut by the piles
|
||||
pl = P["piles"]
|
||||
top = ft["z_bottom"] + pl["head_embedment"]
|
||||
piles = [cylinder(-(pl["count_x"] - 1) * pl["pitch_x"] / 2 + i * pl["pitch_x"],
|
||||
-(pl["count_y"] - 1) * pl["pitch_y"] / 2 + j * pl["pitch_y"], top - pl["length"], top,
|
||||
pl["diameter"] / 2) for j in range(pl["count_y"]) for i in range(pl["count_x"])]
|
||||
cut = rg.Brep.CreateBooleanDifference([lean], piles, TOL)
|
||||
if not cut or len(cut) != 1:
|
||||
raise RuntimeError("lean concrete boolean failed")
|
||||
lean = solid(cut[0])
|
||||
footing = box(-ft["size_x"] / 2, ft["size_x"] / 2, -ft["size_y"] / 2, ft["size_y"] / 2, ft["z_bottom"], ft_top)
|
||||
if ft.get("rot_deg"):
|
||||
for b in [lean, footing] + piles:
|
||||
b.Transform(rot)
|
||||
add(lean, "lean", f"{PID}-LC", "lean concrete", FCK["lean"], "일반도 정면도·단면도")
|
||||
add(footing, "footing", f"{PID}-FT", "footing on piles" if piles else "spread footing (no piles)", FCK["footing"],
|
||||
"일반도 정면도·단면도·기초 평면도")
|
||||
add(cylinder(0, 0, ft_top, col_top, cl["diameter"] / 2), "column", f"{PID}-CL", "column", FCK["column"],
|
||||
"일반도 정면도·단면도·단면 A-A")
|
||||
add(coping_brep(cp), "coping", f"{PID}-CP", "coping (tapered T)", FCK["coping"], "일반도 평면도·정면도·단면도")
|
||||
for n, b in enumerate(piles, 1):
|
||||
pl = P["piles"]
|
||||
add(b, "pile", f"{PID}-PL{n:02d}", f"{pl['kind']} D{pl['diameter']:.0f} L{pl['length'] / 1000:g}m", "-",
|
||||
"일반도 정면도·단면도·기초 평면도")
|
||||
if "pedestals" in bs: # Okseong P1: shared pedestals with a sloped top
|
||||
add_shared_seats(bs)
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"objects": 4 + len(piles) + len(bs["pedestals"]) + len(bs["bearings"])}))
|
||||
return
|
||||
ped, mb, mt = bs["pedestal"], bs["pad_margin_bottom"], bs["pad_margin_top"]
|
||||
for b in bs["bearings"]:
|
||||
cx, cy = b["centre"]
|
||||
z0, z1 = b["seat_z"], b["seat_z"] + b["h1"]
|
||||
base = rotated_rect(cx, cy, ped["size_y"], ped["size_x"], b["rotation_deg"], z0)
|
||||
add(extrude_closed(base, rg.Vector3d(0, 0, b["h1"])), "seat", f"{PID}-BS{b['no']:02d}",
|
||||
f"bearing pedestal h1 NO.{b['no']}", FCK["seat"], "일반도 교량받침 상세·ELEVATION TABLE")
|
||||
bottom = rotated_rect(cx, cy, b["A"] + mb, b["B"] + mb, b["rotation_deg"], z1)
|
||||
top = rotated_rect(cx, cy, b["A"] + mt, b["B"] + mt, b["rotation_deg"], z1 + b["h2"])
|
||||
add(frustum(bottom, top), "pad", f"{PID}-BP{b['no']:02d}", f"bearing pad h2 NO.{b['no']} {b['type']}", "-",
|
||||
"일반도 교량받침 상세·ELEVATION TABLE")
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"objects": 4 + 2 * len(bs["bearings"])}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,241 @@
|
||||
"""G4 stage: coping rebar C1-C16, S1, S2 (centreline + pipe) from params `coping_rebar`.
|
||||
|
||||
Run inside Rhino 8 via runner (globals PARAMS_PATH) after concrete.py, then rebar_colours.py.
|
||||
Ported from Code/교각모델링/build_coping_rebar_rhino.py (Ungok P1, model v0.1.09). Rules: 설계서 v0.1.07 §7. Units mm, X transverse, Y longitudinal, Z = EL x 1000.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectAttributes
|
||||
from System.Drawing import Color
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
TOL = 0.01
|
||||
PID = P["pier_id"]
|
||||
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items() if m[0] in "CS"}
|
||||
DIA = {d: float(d[1:]) for d, _ in MARK.values()} # 'H29' -> 29.0
|
||||
CL_RGB = tuple(P["display"]["centreline_colour"])
|
||||
CP_RGB = tuple(P["display"]["coping_layer_colour"])
|
||||
ARC_C = tuple(P["coping_rebar"]["soffit_arc_centre"]) # soffit arc centre (right); left is mirrored
|
||||
# coping end faces (concrete) from the coping outline: bottom tip (+-half, zb+rise) -> top (+-(half-inset), end step z)
|
||||
_C = P["coping"]
|
||||
HALF, INSET = _C["length_bottom_tip_to_tip"] / 2, _C["end_batter_top_inset"]
|
||||
Z_TIP = _C["z_bottom_flat"] + _C["tip_bottom_rise"]
|
||||
ZL, ZR = _C["top_steps"][0]["z"], _C["top_steps"][-1]["z"]
|
||||
|
||||
|
||||
def xl(z):
|
||||
return -(HALF - INSET) - INSET * (ZL - z) / (ZL - Z_TIP)
|
||||
|
||||
|
||||
def xr(z):
|
||||
return (HALF - INSET) + INSET * (ZR - z) / (ZR - Z_TIP)
|
||||
|
||||
|
||||
def unit(dx, dz):
|
||||
n = math.hypot(dx, dz)
|
||||
return dx / n, dz / n
|
||||
|
||||
|
||||
U_LEFT_DOWN, U_RIGHT_DOWN = unit(-INSET, -(ZL - Z_TIP)), unit(INSET, -(ZR - Z_TIP))
|
||||
|
||||
|
||||
def layer(path, color):
|
||||
parent = -1
|
||||
parts = path.split("::")
|
||||
for i in range(1, len(parts) + 1):
|
||||
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
|
||||
if idx < 0:
|
||||
lay = Rhino.DocObjects.Layer()
|
||||
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
|
||||
if parent >= 0:
|
||||
lay.ParentLayerId = doc.Layers[parent].Id
|
||||
idx = doc.Layers.Add(lay)
|
||||
parent = idx
|
||||
return parent
|
||||
|
||||
|
||||
def min_r(dia):
|
||||
d = DIA[dia]
|
||||
return ((2 * d if d <= 16 else 3.5 * d) + d) / 2 # box rule (REBAR_BENDS_20260914)
|
||||
|
||||
|
||||
def fillet(curve, dia):
|
||||
f = rg.Curve.CreateFilletCornersCurve(curve, min_r(dia), TOL, math.radians(1))
|
||||
return f or curve
|
||||
|
||||
|
||||
def add_bar(curve, mark, occ, design_len, src, extra=None):
|
||||
dia, shape = MARK[mark]
|
||||
pipes = rg.Brep.CreatePipe(curve, DIA[dia] / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
|
||||
if not pipes or not pipes[0].IsSolid:
|
||||
raise RuntimeError(f"{mark} {occ}: pipe failed")
|
||||
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=f"{PID}-CP", anchor_hosts="",
|
||||
design_length=f"{design_len:.3f}", coupler_length="0.000", src=src)
|
||||
common.update(extra or {})
|
||||
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
|
||||
(pipes[0], layer(f"{PID}::철근::코핑::{mark}", CP_RGB), f"{PID}-R")):
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = lay
|
||||
oid = f"{prefix}-{mark}-{occ}"
|
||||
attr.Name = oid
|
||||
attr.SetUserString("id", oid)
|
||||
for k, v in common.items():
|
||||
attr.SetUserString(k, v)
|
||||
(doc.Objects.AddBrep if isinstance(geo, rg.Brep) else doc.Objects.AddCurve)(geo, attr)
|
||||
|
||||
|
||||
def soffit_offset_z(x, cp):
|
||||
st = cp["stirrups"]
|
||||
if abs(x) <= cp["bottom"]["half_flat"]:
|
||||
return cp["bottom"]["z_flat"]
|
||||
cx = ARC_C[0] if x > 0 else -ARC_C[0]
|
||||
R = cp["bottom"]["R_offset"]
|
||||
return ARC_C[1] + math.sqrt(R * R - (x - cx) ** 2)
|
||||
|
||||
|
||||
def stirrup_top(x, cp):
|
||||
st = cp["stirrups"]
|
||||
bands = st["top_right"] if x >= 0 else st["top_left"]
|
||||
return [z for x0, z in bands if abs(x) >= x0][-1]
|
||||
|
||||
|
||||
def main():
|
||||
for o in list(doc.Objects):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if oid.startswith(tuple(f"{PID}-{k}-{m}" for k in ("R", "RC") for m in "CS")):
|
||||
doc.Objects.Delete(o, True)
|
||||
cp = P["coping_rebar"]
|
||||
ys = cp["y_positions"]
|
||||
n = 0
|
||||
# top bars (VIII)
|
||||
for tier, t in cp["top"]["tiers"].items():
|
||||
z, a, (bl, br) = t["z"], t["a"], t["legs"]
|
||||
d = (xr(z) - xl(z) - a) / 2
|
||||
x0, x1 = xl(z) + d, xr(z) - d
|
||||
for i, y in enumerate(ys):
|
||||
mark = t["marks"][i % 2]
|
||||
pts = [rg.Point3d(x0 + U_LEFT_DOWN[0] * bl, y, z + U_LEFT_DOWN[1] * bl), rg.Point3d(x0, y, z),
|
||||
rg.Point3d(x1, y, z), rg.Point3d(x1 + U_RIGHT_DOWN[0] * br, y, z + U_RIGHT_DOWN[1] * br)]
|
||||
add_bar(fillet(rg.PolylineCurve(pts), MARK[mark][0]), mark, f"t{tier}y{y:+.0f}", a + bl + br,
|
||||
f"tier {tier} EL {z / 1000:.3f}; end inset {d:.1f}; legs parallel to end faces",
|
||||
{"a_model": f"{x1 - x0:.3f}"})
|
||||
n += 1
|
||||
# bottom bars (VII)
|
||||
b = cp["bottom"]
|
||||
R, zf = b["R_offset"], b["z_flat"]
|
||||
hf = ARC_C[0] - math.sqrt(R * R - (zf - ARC_C[1]) ** 2) # exact flat/arc junction (params 1,073.9 is rounded)
|
||||
dth = b["arc_len"] / R
|
||||
for i, y in enumerate(ys):
|
||||
mark = b["marks"][i % 2]
|
||||
pieces = [rg.LineCurve(rg.Point3d(-hf, y, zf), rg.Point3d(hf, y, zf))]
|
||||
for side in (1, -1):
|
||||
cx = side * ARC_C[0]
|
||||
th0 = math.atan2(zf - ARC_C[1], side * hf - cx)
|
||||
th1 = th0 - side * dth
|
||||
thm = (th0 + th1) / 2
|
||||
p = lambda th: rg.Point3d(cx + R * math.cos(th), y, ARC_C[1] + R * math.sin(th))
|
||||
arc = rg.ArcCurve(rg.Arc(p(th0), p(thm), p(th1)))
|
||||
up = (-U_RIGHT_DOWN[0], -U_RIGHT_DOWN[1]) if side > 0 else (-U_LEFT_DOWN[0], -U_LEFT_DOWN[1])
|
||||
e = p(th1)
|
||||
leg = rg.LineCurve(e, rg.Point3d(e.X + up[0] * b["leg"], y, e.Z + up[1] * b["leg"]))
|
||||
pieces += [arc, leg]
|
||||
joined = rg.Curve.JoinCurves(pieces, TOL)
|
||||
if len(joined) != 1:
|
||||
raise RuntimeError(f"{mark} y{y}: bottom bar did not join ({len(joined)} pieces)")
|
||||
c = joined[0]
|
||||
add_bar(c, mark, f"y{y:+.0f}", 2 * hf + 2 * b["arc_len"] + 2 * b["leg"],
|
||||
"flat EL 40.511 +-1,073.9; arcs R 12,100 concentric with soffit, length 5,273; legs 650 // end faces")
|
||||
n += 1
|
||||
# stirrups (IV as closed rectangles, widths as drawn in section A-A)
|
||||
st = cp["stirrups"]
|
||||
for mark, xs in (("C10", st["x_c10"]), ("C11", st["x_c11"]), ("C9", st["x_c9"])):
|
||||
for x in xs:
|
||||
zb, zt = soffit_offset_z(x, cp), stirrup_top(x, cp)
|
||||
for k, (y0, y1) in enumerate(st["loops_y"]):
|
||||
ym = (y0 + y1) / 2
|
||||
pts = [rg.Point3d(x, ym, zb), rg.Point3d(x, y1, zb), rg.Point3d(x, y1, zt), rg.Point3d(x, y0, zt),
|
||||
rg.Point3d(x, y0, zb), rg.Point3d(x, ym, zb)]
|
||||
c = fillet(rg.PolylineCurve(pts), MARK[mark][0])
|
||||
w, h = y1 - y0, zt - zb
|
||||
b_s = h + st["b_offset"]
|
||||
add_bar(c, mark, f"x{x:+.0f}k{k + 1}", 2 * w + 2 * b_s + 2 * st["c_sched"],
|
||||
f"X {x}; loop {k + 1} Y {y0}..{y1}; laps 2x750 not drawn (length only)",
|
||||
{"b_sched": f"{b_s:.1f}", "w_model": f"{w:.1f}"})
|
||||
n += 1
|
||||
# horizontal closed loops C12 (lower 6 levels) / C13 (upper 3 levels), one inside each stirrup
|
||||
lp = cp.get("loops")
|
||||
if lp:
|
||||
R, inset = cp["bottom"]["R_offset"], lp["end_inset_face"]
|
||||
for z, mark in zip(lp["levels"], lp["marks_by_level"]):
|
||||
disc = R * R - (z - ARC_C[1]) ** 2
|
||||
xs = ARC_C[0] - math.sqrt(disc) if disc > 0 else 1e9 # soffit-offset limit (symmetric)
|
||||
x0, x1 = max(-xs, xl(z) + inset), min(xs, xr(z) - inset)
|
||||
for k, (y0, y1) in enumerate(lp["loops_y"]):
|
||||
xm = (x0 + x1) / 2
|
||||
pts = [rg.Point3d(xm, y0, z), rg.Point3d(x1, y0, z), rg.Point3d(x1, y1, z), rg.Point3d(x0, y1, z),
|
||||
rg.Point3d(x0, y0, z), rg.Point3d(xm, y0, z)]
|
||||
c = fillet(rg.PolylineCurve(pts), MARK[mark][0])
|
||||
a_m, w = x1 - x0, y1 - y0
|
||||
add_bar(c, mark, f"z{z}k{k + 1}", 2 * (a_m + lp["a_offset"]) + 2 * w + 4 * lp["c_sched"],
|
||||
f"EL {z / 1000:.3f}; X {x0:.1f}..{x1:.1f}; loop {k + 1} Y {y0}..{y1}; laps 4x580 length only",
|
||||
{"a_sched": f"{a_m + lp['a_offset']:.1f}", "w_model": f"{w:.1f}"})
|
||||
n += 1
|
||||
# C14 transverse (Y) bars with hooks down; C15 / C16 longitudinal (X) bars in the raised step zones
|
||||
if "c14" in cp:
|
||||
c14 = cp["c14"]
|
||||
h, hk = c14["a"] / 2, c14["hook"]
|
||||
up = [tuple(v) for v in c14.get("hooks_up", [])]
|
||||
for x, z in c14["points"]:
|
||||
s = 1 if (x, z) in up else -1 # hooks up only where a down hook would leave the soffit
|
||||
pts = [rg.Point3d(x, -h, z + s * hk), rg.Point3d(x, -h, z), rg.Point3d(x, h, z), rg.Point3d(x, h, z + s * hk)]
|
||||
add_bar(fillet(rg.PolylineCurve(pts), MARK["C14"][0]), "C14", f"x{x:+.0f}z{z:.0f}", c14["a"] + 2 * hk,
|
||||
"D1 정면도 점; hooks " + ("up (HITL fix)" if s > 0 else "down [추론]"))
|
||||
n += 1
|
||||
c15, c16 = cp["c15"], cp["c16"]
|
||||
for y in c15["y_positions"]:
|
||||
z, (h0, h1) = c15["z"], c15["hooks"]
|
||||
pts = [rg.Point3d(c15["x0"], y, z - h0), rg.Point3d(c15["x0"], y, z), rg.Point3d(c15["x1"], y, z),
|
||||
rg.Point3d(c15["x1"], y, z - h1)]
|
||||
add_bar(fillet(rg.PolylineCurve(pts), MARK["C15"][0]), "C15", f"y{y:+.0f}", c15["x1"] - c15["x0"] + h0 + h1,
|
||||
"D1 정면도 43,598.4 / 3,816~5,900")
|
||||
z = c16["z"]
|
||||
pts = [rg.Point3d(c16["x0"], y, z - c16["hook_left"]), rg.Point3d(c16["x0"], y, z),
|
||||
rg.Point3d(c16["x1"], y, z)]
|
||||
add_bar(fillet(rg.PolylineCurve(pts), MARK["C16"][0]), "C16", f"y{y:+.0f}", c16["x1"] - c16["x0"] + c16["hook_left"],
|
||||
"D1 정면도 43,454.7 / -1,195~4,066; hook at left end [추론]")
|
||||
n += 2
|
||||
# S1 / S2 bearing-seat reinforcement under each pedestal (rotated with the pedestal)
|
||||
if "s1s2" in cp and "bearing_seats" in P:
|
||||
bs, ss = P["bearing_seats"], cp["s1s2"]
|
||||
corners = bs["pedestal_girder5"]["front_half"] + bs["pedestal_girder5"]["rear_half"]
|
||||
c5 = (sum(p[0] for p in corners) / len(corners), sum(p[1] for p in corners) / len(corners))
|
||||
t = math.radians(bs["pedestal_girder5"]["rotation_deg"])
|
||||
ua, uc = (math.sin(t), math.cos(t)), (math.cos(t), -math.sin(t)) # along (2,500) / across (1,200)
|
||||
steps = P["coping"]["top_steps"]
|
||||
for g in range(1, len(steps) + 1):
|
||||
cx_, cy_ = c5[0] + (g - bs["reference_girder"]) * bs["pitch_x"], c5[1]
|
||||
top = steps[g - 1]["z"]
|
||||
pt = lambda s_al, s_ac, z: rg.Point3d(cx_ + ua[0] * s_al + uc[0] * s_ac, cy_ + ua[1] * s_al + uc[1] * s_ac, z)
|
||||
s1 = ss["s1"]
|
||||
for off in s1["offsets_across"]:
|
||||
z, h = top - s1["z_below_top"], s1["a"] / 2
|
||||
pts = [pt(-h, off, z - s1["leg"]), pt(-h, off, z), pt(h, off, z), pt(h, off, z - s1["leg"])]
|
||||
add_bar(fillet(rg.PolylineCurve(pts), MARK["S1"][0]), "S1", f"g{g}c{off:+.0f}", s1["a"] + 2 * s1["leg"],
|
||||
f"girder {g} pedestal; upper layer [추론]")
|
||||
n += 1
|
||||
s2 = ss["s2"]
|
||||
for off in s2["offsets_along"]:
|
||||
z, h = top - s2["z_below_top"], s2["a"] / 2
|
||||
pts = [pt(off, -h, z - s2["leg"]), pt(off, -h, z), pt(off, h, z), pt(off, h, z - s2["leg"])]
|
||||
add_bar(fillet(rg.PolylineCurve(pts), MARK["S2"][0]), "S2", f"g{g}a{off:+.0f}", s2["a"] + 2 * s2["leg"],
|
||||
f"girder {g} pedestal; lower layer")
|
||||
n += 1
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"coping_bars": n}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Final stage: apply the display state recorded in params `display` (hidden objects).
|
||||
|
||||
Run inside Rhino 8 via runner (globals PARAMS_PATH) after all build stages.
|
||||
"""
|
||||
import json
|
||||
|
||||
from Rhino.DocObjects import ObjectEnumeratorSettings
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
hide = set(P["display"].get("hidden_ids", []))
|
||||
s = ObjectEnumeratorSettings()
|
||||
s.HiddenObjects = True
|
||||
done = []
|
||||
for o in doc.Objects.GetObjectList(s):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if oid in hide and not o.IsHidden:
|
||||
doc.Objects.Hide(o, True)
|
||||
done.append(oid)
|
||||
missing = sorted(hide - {o.Attributes.GetUserString("id") for o in doc.Objects.GetObjectList(s)})
|
||||
print(json.dumps({"hidden": done, "missing": missing}))
|
||||
if missing:
|
||||
raise RuntimeError(f"display: hidden_ids not found {missing}")
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Fingerprint a .3dm (run inside Rhino 8 via runner). Globals: MODEL_PATH, OUT_PATH, optional FAST.
|
||||
|
||||
Opens MODEL_PATH headless (the active doc is untouched) and writes, per object id (user string `id`,
|
||||
falling back to the object name): layer path, geometry type, visibility, colour, bbox (0.01 mm),
|
||||
brep face count / solidity / volume (mm3, 1 decimal), curve length (0.001 mm) and all user strings.
|
||||
Hidden and locked objects are included.
|
||||
"""
|
||||
import json
|
||||
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectEnumeratorSettings
|
||||
|
||||
FAST = globals().get("FAST", False)
|
||||
d = Rhino.RhinoDoc.OpenHeadless(MODEL_PATH)
|
||||
if d is None:
|
||||
raise RuntimeError(f"cannot open {MODEL_PATH}")
|
||||
try:
|
||||
s = ObjectEnumeratorSettings()
|
||||
s.HiddenObjects = True
|
||||
s.LockedObjects = True
|
||||
objs, layers = {}, {}
|
||||
for o in d.Objects.GetObjectList(s):
|
||||
a = o.Attributes
|
||||
oid = a.GetUserString("id") or a.Name or str(o.Id)
|
||||
lay = d.Layers[a.LayerIndex]
|
||||
layers[lay.FullPath] = [lay.Color.R, lay.Color.G, lay.Color.B, lay.IsVisible]
|
||||
g = o.Geometry
|
||||
bb = g.GetBoundingBox(True)
|
||||
rec = {"layer": lay.FullPath, "type": type(g).__name__, "hidden": o.IsHidden, "locked": o.IsLocked,
|
||||
"colour_source": int(a.ColorSource), "colour": [a.ObjectColor.R, a.ObjectColor.G, a.ObjectColor.B],
|
||||
"bbox": [round(v, 2) for v in (bb.Min.X, bb.Min.Y, bb.Min.Z, bb.Max.X, bb.Max.Y, bb.Max.Z)],
|
||||
"user": {k: a.GetUserString(k) for k in sorted(a.GetUserStrings().AllKeys)}}
|
||||
b = g if isinstance(g, rg.Brep) else (rg.Brep.TryConvertBrep(g) if isinstance(g, rg.Extrusion) else None)
|
||||
if b is not None:
|
||||
rec["faces"], rec["solid"] = b.Faces.Count, b.IsSolid
|
||||
if FAST and "-R-" in oid: # rebar pipe: bbox + face count only (volume dominates run time)
|
||||
rec["volume"] = "skipped"
|
||||
else:
|
||||
mp = rg.VolumeMassProperties.Compute(b) if b.IsSolid else None
|
||||
rec["volume"] = round(mp.Volume, 1) if mp else None
|
||||
elif isinstance(g, rg.Curve):
|
||||
rec["length"] = round(g.GetLength(), 3)
|
||||
if oid in objs:
|
||||
oid = f"{oid}#dup{sum(k.startswith(oid) for k in objs)}"
|
||||
objs[oid] = rec
|
||||
out = {"model": MODEL_PATH, "units": str(d.ModelUnitSystem), "tolerance": d.ModelAbsoluteTolerance,
|
||||
"count": len(objs), "layers": layers, "objects": objs}
|
||||
json.dump(out, open(OUT_PATH, "w", encoding="utf-8"), ensure_ascii=False)
|
||||
print(json.dumps({"count": len(objs), "units": out["units"], "tolerance": out["tolerance"]}))
|
||||
finally:
|
||||
d.Dispose()
|
||||
@@ -0,0 +1,186 @@
|
||||
"""G3 stage: footing rebar B1-B12 (centreline + pipe solid) from params `footing_rebar`.
|
||||
|
||||
Run inside Rhino 8 via runner (globals PARAMS_PATH) after concrete.py.
|
||||
Ported from Code/교각모델링/build_footing_rebar_rhino.py (Ungok P1, model v0.1.09).
|
||||
Rules: 설계서 v0.1.06 §6 (HITL F1-F3 approved 2026-10-07). z in params is above the footing bottom.
|
||||
`design_length` = schedule original length (a, a+2b, 2(a+b), a+b+c); curve length is measured separately.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectAttributes
|
||||
from System.Drawing import Color
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
TOL = 0.01
|
||||
PID = P["pier_id"]
|
||||
FB = P["footing"]["z_bottom"]
|
||||
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items() if m.startswith("B")}
|
||||
DIA = {d: float(d[1:]) for d, _ in MARK.values()} # 'S25' -> 25.0
|
||||
CL_RGB = tuple(P["display"]["centreline_colour"])
|
||||
FT_RGB = tuple(P["display"]["footing_layer_colour"])
|
||||
LEGS = P["footing_rebar"]["legs"]
|
||||
|
||||
|
||||
def leg(mark):
|
||||
"""Signed end-leg length of a footing bar mark (+ up / - down) from params `legs`."""
|
||||
v = LEGS[mark]
|
||||
n = v.get("leg", v.get("hook"))
|
||||
return n if v["dir"].startswith("up") else -n
|
||||
|
||||
|
||||
def layer(path, color):
|
||||
parent = -1
|
||||
parts = path.split("::")
|
||||
for i in range(1, len(parts) + 1):
|
||||
sub = "::".join(parts[:i])
|
||||
idx = doc.Layers.FindByFullPath(sub, -1)
|
||||
if idx < 0:
|
||||
lay = Rhino.DocObjects.Layer()
|
||||
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
|
||||
if parent >= 0:
|
||||
lay.ParentLayerId = doc.Layers[parent].Id
|
||||
idx = doc.Layers.Add(lay)
|
||||
parent = idx
|
||||
return parent
|
||||
|
||||
|
||||
def min_r(dia):
|
||||
d = DIA[dia]
|
||||
return ((2 * d if d <= 16 else 3.5 * d) + d) / 2 # box rule (REBAR_BENDS_20260914)
|
||||
|
||||
|
||||
def poly(pts, dia):
|
||||
pl = rg.PolylineCurve([rg.Point3d(*p) for p in pts])
|
||||
if len(pts) < 3:
|
||||
return pl
|
||||
f = rg.Curve.CreateFilletCornersCurve(pl, min_r(dia), TOL, math.radians(1))
|
||||
return f or pl
|
||||
|
||||
|
||||
def add_bar(curve, mark, occ, design_len, src):
|
||||
dia, shape = MARK[mark]
|
||||
pipes = rg.Brep.CreatePipe(curve, DIA[dia] / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
|
||||
if not pipes or not pipes[0].IsSolid:
|
||||
raise RuntimeError(f"{mark} {occ}: pipe failed")
|
||||
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=f"{PID}-FT", anchor_hosts="",
|
||||
design_length=f"{design_len:.3f}", coupler_length="0.000", src=src)
|
||||
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
|
||||
(pipes[0], layer(f"{PID}::철근::기초::{mark}", FT_RGB), f"{PID}-R")):
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = lay
|
||||
oid = f"{prefix}-{mark}-{occ}"
|
||||
attr.Name = oid
|
||||
attr.SetUserString("id", oid)
|
||||
for k, v in common.items():
|
||||
attr.SetUserString(k, v)
|
||||
(doc.Objects.AddBrep if isinstance(geo, rg.Brep) else doc.Objects.AddCurve)(geo, attr)
|
||||
|
||||
|
||||
def bar_y(x, z, a, end, dz):
|
||||
"""Bar along Y (length a) at x, height z; end legs of length dz (+up / -down) or none."""
|
||||
h = a / 2
|
||||
if not end:
|
||||
return [(x, -h, z), (x, h, z)]
|
||||
return [(x, -h, z + dz), (x, -h, z), (x, h, z), (x, h, z + dz)]
|
||||
|
||||
|
||||
def bar_x(y, z, a, end, dz):
|
||||
h = a / 2
|
||||
if not end:
|
||||
return [(-h, y, z), (h, y, z)]
|
||||
return [(-h, y, z + dz), (-h, y, z), (h, y, z), (h, y, z + dz)]
|
||||
|
||||
|
||||
def sched(m):
|
||||
row = P["footing_rebar"]["schedule"][m]
|
||||
nums = [float(t) for t in row[3:] if t.replace(".", "", 1).isdigit()]
|
||||
return row, nums
|
||||
|
||||
|
||||
def mm(v):
|
||||
return v * 1000 if v < 100 else v
|
||||
|
||||
|
||||
def main():
|
||||
for o in list(doc.Objects):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if oid.startswith((f"{PID}-R-B", f"{PID}-RC-B")):
|
||||
doc.Objects.Delete(o, True)
|
||||
fr = P["footing_rebar"]
|
||||
L = fr["layers"]
|
||||
n = 0
|
||||
a_y, a_x = fr["a_y"], fr["a_x"] # D3 a of Y-bars (B1-B4, B12) / X-bars (B5-B8, B11)
|
||||
# bottom layer 1 (Y): B2 U (legs up) at the outermost positions and alternate from the next; B4 straight
|
||||
pos = L["bottom_Y1"]["pos"]
|
||||
edge = max(abs(x) for x in pos)
|
||||
inner = [x for x in pos if abs(x) != edge]
|
||||
for i, x in enumerate(pos):
|
||||
mark = "B2" if abs(x) == edge or inner.index(x) % 2 == 0 else "B4"
|
||||
z = FB + L["bottom_Y1"]["z"]
|
||||
if mark == "B2":
|
||||
c, dl = poly(bar_y(x, z, a_y, True, leg("B2")), MARK["B2"][0]), a_y + 2 * abs(leg("B2"))
|
||||
else:
|
||||
c, dl = poly(bar_y(x, z, a_y, False, 0), MARK["B4"][0]), a_y
|
||||
add_bar(c, mark, f"x{x:+.0f}", dl, "D1 정면도 하면 점 z122 / D1·D2 '70@125=8.750'")
|
||||
n += 1
|
||||
pos = L["bottom_X2"]["pos"]
|
||||
edge = max(abs(y) for y in pos)
|
||||
inner = [y for y in pos if abs(y) != edge]
|
||||
for y in pos:
|
||||
mark = "B6" if abs(y) == edge or inner.index(y) % 2 == 0 else "B8"
|
||||
z = FB + L["bottom_X2"]["z"]
|
||||
if mark == "B6":
|
||||
c, dl = poly(bar_x(y, z, a_x, True, leg("B6")), MARK["B6"][0]), a_x + 2 * abs(leg("B6"))
|
||||
else:
|
||||
c, dl = poly(bar_x(y, z, a_x, False, 0), MARK["B8"][0]), a_x
|
||||
add_bar(c, mark, f"y{y:+.0f}", dl, "D1 단면도 하면 점 z178 / D1 '58@125=7.250'")
|
||||
n += 1
|
||||
for key, mark, axis, a in (("bottom_Y3", "B3", "Y", a_y), ("bottom_X4", "B7", "X", a_x),
|
||||
("top_Y", "B1", "Y", a_y), ("top_X", "B5", "X", a_x)):
|
||||
z, dz, dia = FB + L[key]["z"], leg(mark), MARK[mark][0]
|
||||
for v in L[key]["pos"]:
|
||||
pts = bar_y(v, z, a, True, dz) if axis == "Y" else bar_x(v, z, a, True, dz)
|
||||
add_bar(poly(pts, dia), mark, f"{axis.lower()}{v:+.0f}", a + 2 * abs(dz),
|
||||
f"D1 {'정면도' if axis == 'Y' else '단면도'} 점 z{L[key]['z']}")
|
||||
n += 1
|
||||
for key, mark, axis, a in (("mid_X", "B11", "X", a_x), ("mid_Y", "B12", "Y", a_y)):
|
||||
for zs, vs in L[key]["levels"].items():
|
||||
z = FB + float(zs)
|
||||
dz = leg(mark)
|
||||
for v in vs:
|
||||
pts = bar_y(v, z, a, True, dz) if axis == "Y" else bar_x(v, z, a, True, dz)
|
||||
add_bar(poly(pts, MARK[mark][0]), mark, f"z{float(zs):.0f}{axis.lower()}{v:+.0f}", a + 2 * abs(dz),
|
||||
f"D1 {'정면도' if axis == 'Y' else '단면도'} 중앙 점 z{zs}")
|
||||
n += 1
|
||||
b10 = fr["b10"]
|
||||
hx, hy = b10["size_x"] / 2, b10["size_y"] / 2
|
||||
for k, zs in enumerate(b10["levels"]):
|
||||
z = FB + zs
|
||||
pts = [(-hx, 0, z), (-hx, -hy, z), (hx, -hy, z), (hx, hy, z), (-hx, hy, z), (-hx, 0.0, z)]
|
||||
c = poly(pts, MARK["B10"][0])
|
||||
add_bar(c, "B10", f"z{zs}", 2 * (b10["size_x"] + b10["size_y"]),
|
||||
"D1 정면도 '150','9@200'; D3 B10 9.198x7.644 (closed, start/end at -X mid side)")
|
||||
n += 1
|
||||
b9 = fr["b9"]
|
||||
rc = b9["c"] / math.pi # 180-degree hook developed length = c
|
||||
for i, x in enumerate(b9["grid_x"]):
|
||||
for j, y in enumerate(b9["grid_y"]):
|
||||
if (i + j) % 2 != b9["checker_parity"]:
|
||||
continue
|
||||
z0, z1 = FB + b9["z0"], FB + b9["z0"] + b9["a"]
|
||||
base = poly([(x + b9["b"], y, z0), (x, y, z0), (x, y, z1)], MARK["B9"][0])
|
||||
top = rg.ArcCurve(rg.Arc(rg.Point3d(x, y, z1), rg.Point3d(x + rc, y, z1 + rc),
|
||||
rg.Point3d(x + 2 * rc, y, z1)))
|
||||
c = rg.Curve.JoinCurves([base, top], TOL)[0]
|
||||
add_bar(c, "B9", f"x{x:+.0f}y{y:+.0f}", b9["a"] + b9["b"] + b9["c"],
|
||||
"D1 EREC grid @250 checker (i+j even) [추론]; count 493 [역산]")
|
||||
n += 1
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"footing_bars": n}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,138 @@
|
||||
"""G3 stage, form "mats" (Ulsan P1, 2026-10-13): footing rebar as explicit bar groups, perimeter hoops and
|
||||
vertical bars, from params `footing_rebar` (centreline + pipe solid per bar).
|
||||
|
||||
Run inside Rhino 8 via runner (globals PARAMS_PATH) after the G1 builder. z values are above the footing bottom.
|
||||
params footing_rebar:
|
||||
groups [{mark, axis "X"|"Y", z, pos [...], a, leg (signed: + up / - down / 0), lap, src}]
|
||||
a bar along `axis` of length a centred on the footing, at `pos` across, with end legs
|
||||
design length = a + 2|leg| + lap (lap only adds length)
|
||||
hoops {mark, half, levels [...], design_length}: closed square hoops of centreline half-width `half`
|
||||
verticals {mark, grid_x [...], grid_y [...], parity, z0, a, leg}: vertical bars on a checkerboard of the grid,
|
||||
legs of length `leg` towards +`leg_axis` ("X"|"Y") at both ends (shape I with b, SD400 catalogue)
|
||||
Optional (Ulsan P2, 2026-10-15; absent -> unchanged):
|
||||
footing.rot_deg all bars are built in the foundation's own axes and turned counter-clockwise about the pier centre
|
||||
hoops.half_x / half_y rectangular hoop (default: square `half`)
|
||||
verticals.points [[x, y], ...] explicit positions instead of the checkerboard; leg_bottom / leg_top leg lengths per end
|
||||
(default `leg`); design length = a + leg_bottom + leg_top
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
import System
|
||||
from Rhino.DocObjects import ObjectAttributes
|
||||
from System.Drawing import Color
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
TOL = 0.01
|
||||
PID = P["pier_id"]
|
||||
FB = P["footing"]["z_bottom"]
|
||||
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items()}
|
||||
DIA = {d: float(d[1:]) for d, _ in MARK.values()}
|
||||
CL_RGB = tuple(P["display"]["centreline_colour"])
|
||||
FT_RGB = tuple(P["display"]["footing_layer_colour"])
|
||||
|
||||
|
||||
def layer(path, color):
|
||||
parent = -1
|
||||
parts = path.split("::")
|
||||
for i in range(1, len(parts) + 1):
|
||||
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
|
||||
if idx < 0:
|
||||
lay = Rhino.DocObjects.Layer()
|
||||
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
|
||||
if parent >= 0:
|
||||
lay.ParentLayerId = doc.Layers[parent].Id
|
||||
idx = doc.Layers.Add(lay)
|
||||
parent = idx
|
||||
return parent
|
||||
|
||||
|
||||
ROT = math.radians(P["footing"].get("rot_deg", 0.0))
|
||||
|
||||
|
||||
def turn(pts):
|
||||
"""Foundation axes -> pier axes (counter-clockwise ROT about the origin)."""
|
||||
if not ROT:
|
||||
return pts
|
||||
c, s = math.cos(ROT), math.sin(ROT)
|
||||
return [(x * c - y * s, x * s + y * c, z) for x, y, z in pts]
|
||||
|
||||
|
||||
def min_r(dia):
|
||||
d = DIA[dia]
|
||||
return ((2 * d if d <= 16 else 3.5 * d) + d) / 2
|
||||
|
||||
|
||||
def poly(pts, dia):
|
||||
pl = rg.PolylineCurve([rg.Point3d(*p) for p in pts])
|
||||
if len(pts) < 3:
|
||||
return pl
|
||||
return rg.Curve.CreateFilletCornersCurve(pl, min_r(dia), TOL, math.radians(1)) or pl
|
||||
|
||||
|
||||
def add_bar(curve, mark, occ, design_len, src):
|
||||
dia, shape = MARK[mark]
|
||||
pipes = rg.Brep.CreatePipe(curve, DIA[dia] / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
|
||||
if not pipes or not pipes[0].IsSolid:
|
||||
raise RuntimeError(f"{mark} {occ}: pipe failed")
|
||||
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=f"{PID}-FT", anchor_hosts="",
|
||||
design_length=f"{design_len:.3f}", coupler_length="0.000", src=src)
|
||||
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
|
||||
(pipes[0], layer(f"{PID}::철근::기초::{mark}", FT_RGB), f"{PID}-R")):
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = lay
|
||||
oid = f"{prefix}-{mark}-{occ}"
|
||||
attr.Name = oid
|
||||
attr.SetUserString("id", oid)
|
||||
for k, v in common.items():
|
||||
attr.SetUserString(k, v)
|
||||
gid = doc.Objects.AddBrep(geo, attr) if isinstance(geo, rg.Brep) else doc.Objects.AddCurve(geo, attr)
|
||||
if gid == System.Guid.Empty:
|
||||
raise RuntimeError(f"{oid}: add failed")
|
||||
|
||||
|
||||
def main():
|
||||
for o in list(doc.Objects):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if oid.startswith((f"{PID}-R-B", f"{PID}-RC-B")):
|
||||
doc.Objects.Delete(o, True)
|
||||
fr = P["footing_rebar"]
|
||||
n = 0
|
||||
for g in fr["groups"]:
|
||||
h, z, leg = g["a"] / 2, FB + g["z"], g.get("leg", 0)
|
||||
for v in g["pos"]:
|
||||
if g["axis"] == "Y":
|
||||
ends = [(v, -h, z), (v, h, z)]
|
||||
else:
|
||||
ends = [(-h, v, z), (h, v, z)]
|
||||
pts = ends if not leg else [(ends[0][0], ends[0][1], z + leg), ends[0], ends[1], (ends[1][0], ends[1][1], z + leg)]
|
||||
add_bar(poly(turn(pts), MARK[g["mark"]][0]), g["mark"], f"{g['axis'].lower()}{v:+.0f}",
|
||||
g["a"] + 2 * abs(leg) + g.get("lap", 0), g.get("src", ""))
|
||||
n += 1
|
||||
hp = fr["hoops"]
|
||||
sx, sy = hp.get("half_x", hp.get("half")), hp.get("half_y", hp.get("half"))
|
||||
for zz in hp["levels"]:
|
||||
z = FB + zz
|
||||
pts = [(-sx, 0, z), (-sx, -sy, z), (sx, -sy, z), (sx, sy, z), (-sx, sy, z), (-sx, 0.0, z)]
|
||||
add_bar(poly(turn(pts), MARK[hp["mark"]][0]), hp["mark"], f"z{zz:.0f}", hp["design_length"], hp.get("src", ""))
|
||||
n += 1
|
||||
vt = fr["verticals"]
|
||||
if "points" in vt:
|
||||
where = [tuple(q) for q in vt["points"]]
|
||||
else:
|
||||
where = [(x, y) for i, x in enumerate(vt["grid_x"]) for j, y in enumerate(vt["grid_y"]) if (i + j) % 2 == vt["parity"]]
|
||||
lb, lt = vt.get("leg_bottom", vt.get("leg", 0)), vt.get("leg_top", vt.get("leg", 0))
|
||||
ux, uy = (1, 0) if vt.get("leg_axis", "X") == "X" else (0, 1)
|
||||
for x, y in where:
|
||||
z0, z1 = FB + vt["z0"], FB + vt["z0"] + vt["a"]
|
||||
pts = [(x + ux * lb, y + uy * lb, z0), (x, y, z0), (x, y, z1), (x + ux * lt, y + uy * lt, z1)]
|
||||
add_bar(poly(turn(pts), MARK[vt["mark"]][0]), vt["mark"], f"x{x:+.0f}y{y:+.0f}", vt["a"] + lb + lt, vt.get("src", ""))
|
||||
n += 1
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"footing_bars": n}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Negative test helper: move selected objects of the open doc (inside Rhino 8, via runner).
|
||||
|
||||
Globals: __rhino_doc__, PIER_ID, MOVES = [{"id": "BS08"} | {"mark": "P2"}, "d": [dx, dy, dz]}, ...],
|
||||
optional UNDO (move back by -d). A `mark` selector moves the first occurrence (sorted) of that mark,
|
||||
centreline and pipe together. Prints what was moved; raises if a selector matches nothing.
|
||||
"""
|
||||
import json
|
||||
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectEnumeratorSettings
|
||||
|
||||
doc = __rhino_doc__
|
||||
s = ObjectEnumeratorSettings()
|
||||
s.HiddenObjects = True
|
||||
s.LockedObjects = True
|
||||
objs = list(doc.Objects.GetObjectList(s))
|
||||
sign = -1 if globals().get("UNDO") else 1
|
||||
moved = []
|
||||
for mv in MOVES:
|
||||
if "id" in mv:
|
||||
sel = [o for o in objs if o.Attributes.GetUserString("id") == f"{PIER_ID}-{mv['id']}"]
|
||||
else:
|
||||
cand = [o for o in objs if o.Attributes.GetUserString("mark") == mv["mark"]
|
||||
and (o.Attributes.GetUserString("id") or "").startswith(f"{PIER_ID}-R")]
|
||||
first = sorted({o.Attributes.GetUserString("occurrence") for o in cand})[0] if cand else None
|
||||
sel = [o for o in cand if o.Attributes.GetUserString("occurrence") == first]
|
||||
if not sel:
|
||||
raise RuntimeError(f"perturb: nothing matches {mv}")
|
||||
dx, dy, dz = (sign * v for v in mv["d"])
|
||||
xf = rg.Transform.Translation(dx, dy, dz)
|
||||
for o in sel:
|
||||
doc.Objects.Transform(o.Id, xf, True)
|
||||
moved.append(o.Attributes.GetUserString("id"))
|
||||
print(json.dumps({"moved": moved, "undo": sign < 0}))
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Colour rebar pipe solids by nominal diameter (run inside Rhino 8 via runner after the rebar stages).
|
||||
|
||||
Globals: PARAMS_PATH. Ported from Code/교각모델링/apply_rebar_colors.py (Ungok P1, model v0.1.09).
|
||||
|
||||
Rule (context-migration/디자인지침_경로.md, R8 criteria 2026-10-07): same diameter -> same colour
|
||||
regardless of steel grade (S = SD500, H = SD400). Centrelines keep their layer colour.
|
||||
Prints a check: per-diameter object count and colour, and any pipe whose colour does not match.
|
||||
"""
|
||||
import json
|
||||
from collections import Counter
|
||||
|
||||
from Rhino.DocObjects import ObjectColorSource
|
||||
from System.Drawing import Color
|
||||
|
||||
doc = __rhino_doc__
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
PID = P["pier_id"]
|
||||
PALETTE = {int(k): tuple(v) for k, v in P["display"]["rebar_palette"].items()}
|
||||
|
||||
|
||||
def main():
|
||||
done, bad = Counter(), []
|
||||
for o in list(doc.Objects):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if not oid.startswith(f"{PID}-R-"):
|
||||
continue
|
||||
dia = int(o.Attributes.GetUserString("dia")[1:]) # 'S32' / 'H19' -> 32 / 19
|
||||
if dia not in PALETTE:
|
||||
bad.append((oid, "no palette entry"))
|
||||
continue
|
||||
attr = o.Attributes.Duplicate()
|
||||
attr.ObjectColor = Color.FromArgb(*PALETTE[dia])
|
||||
attr.ColorSource = ObjectColorSource.ColorFromObject
|
||||
doc.Objects.ModifyAttributes(o, attr, True)
|
||||
done[dia] += 1
|
||||
# check (re-read objects)
|
||||
seen = {}
|
||||
for o in doc.Objects:
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if oid.startswith(f"{PID}-R-"):
|
||||
dia = int(o.Attributes.GetUserString("dia")[1:])
|
||||
c = o.Attributes.ObjectColor
|
||||
ok = o.Attributes.ColorSource == ObjectColorSource.ColorFromObject and (c.R, c.G, c.B) == PALETTE[dia]
|
||||
if not ok:
|
||||
bad.append((oid, "colour mismatch"))
|
||||
seen.setdefault(dia, set()).add((c.R, c.G, c.B))
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps(dict(coloured=dict(done), colours_per_dia={k: list(v) for k, v in seen.items()},
|
||||
distinct_colours=len({tuple(PALETTE[k]) for k in seen}), mismatches=bad[:10],
|
||||
mismatch_count=len(bad))))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Generic rebar stage (Ulsan P1 coping, 2026-10-13): bars given as explicit centreline polylines in params.
|
||||
|
||||
Run inside Rhino 8 via runner (globals PARAMS_PATH). Used as the G4 builder (params["coping_rebar"]).
|
||||
params coping_rebar:
|
||||
host object id suffix of the host concrete (e.g. "CP")
|
||||
group layer group under 철근:: (e.g. "코핑")
|
||||
bars [{mark, occ, pts [[x, y, z], ...], r (corner fillet radius), design_length, src}]
|
||||
a closed bar repeats its first point at the end (start it mid-side so no corner sits at the seam)
|
||||
Ids: <pier>-R-<mark>-<occ> (pipe) and <pier>-RC-<mark>-<occ> (centreline).
|
||||
Section by gate (2026-10-14, Okseong P1): GATE "G3" -> params footing_rebar, otherwise coping_rebar.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
import System
|
||||
from Rhino.DocObjects import ObjectAttributes
|
||||
from System.Drawing import Color
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
TOL = 0.01
|
||||
PID = P["pier_id"]
|
||||
SEC = P["footing_rebar" if globals().get("GATE") == "G3" else "coping_rebar"]
|
||||
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items()}
|
||||
CL_RGB = tuple(P["display"]["centreline_colour"])
|
||||
LAY_RGB = tuple(P["display"]["coping_layer_colour"])
|
||||
|
||||
|
||||
def layer(path, color):
|
||||
parent = -1
|
||||
parts = path.split("::")
|
||||
for i in range(1, len(parts) + 1):
|
||||
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
|
||||
if idx < 0:
|
||||
lay = Rhino.DocObjects.Layer()
|
||||
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
|
||||
if parent >= 0:
|
||||
lay.ParentLayerId = doc.Layers[parent].Id
|
||||
idx = doc.Layers.Add(lay)
|
||||
parent = idx
|
||||
return parent
|
||||
|
||||
|
||||
def centreline(pts, r):
|
||||
pl = rg.PolylineCurve([rg.Point3d(*p) for p in pts])
|
||||
if len(pts) < 3 or r <= 0:
|
||||
return pl
|
||||
c = rg.Curve.CreateFilletCornersCurve(pl, r, TOL, math.radians(1))
|
||||
if c is None:
|
||||
raise RuntimeError(f"fillet failed (r {r})")
|
||||
return c
|
||||
|
||||
|
||||
def add_bar(b):
|
||||
mark, occ = b["mark"], b["occ"]
|
||||
dia, shape = MARK[mark]
|
||||
curve = centreline(b["pts"], b.get("r", 0))
|
||||
pipes = rg.Brep.CreatePipe(curve, float(dia[1:]) / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
|
||||
if not pipes or not pipes[0].IsSolid:
|
||||
raise RuntimeError(f"{mark} {occ}: pipe failed")
|
||||
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=f"{PID}-{SEC['host']}", anchor_hosts="",
|
||||
design_length=f"{b['design_length']:.3f}", coupler_length="0.000", src=b.get("src", ""))
|
||||
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
|
||||
(pipes[0], layer(f"{PID}::철근::{SEC['group']}::{mark}", LAY_RGB), f"{PID}-R")):
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = lay
|
||||
oid = f"{prefix}-{mark}-{occ}"
|
||||
attr.Name = oid
|
||||
attr.SetUserString("id", oid)
|
||||
for k, v in common.items():
|
||||
attr.SetUserString(k, v)
|
||||
gid = doc.Objects.AddBrep(geo, attr) if isinstance(geo, rg.Brep) else doc.Objects.AddCurve(geo, attr)
|
||||
if gid == System.Guid.Empty:
|
||||
raise RuntimeError(f"{oid}: add failed")
|
||||
|
||||
|
||||
def main():
|
||||
marks = {b["mark"] for b in SEC["bars"]}
|
||||
for o in list(doc.Objects):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if oid.startswith((f"{PID}-R-", f"{PID}-RC-")) and o.Attributes.GetUserString("mark") in marks:
|
||||
doc.Objects.Delete(o, True)
|
||||
for b in SEC["bars"]:
|
||||
add_bar(b)
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"bars": len(SEC["bars"]), "marks": len(marks)}))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,180 @@
|
||||
"""G3 stage, form "shaft cage" (Ramp-A P1, 2026-10-13): drilled-shaft reinforcement cage (no footing). Straight main
|
||||
bars in two pieces joined by couplers (upper / lower mark), lapped half-ring hoops with 135-deg hooks, X / Y cross
|
||||
ties, bottom U bars in vertical planes through the centre, and the anti-uplift plate under them.
|
||||
|
||||
Run inside Rhino 8 via runner (globals PARAMS_PATH) after the G1 / G2 builders. Units mm, Z = EL x 1000.
|
||||
params shaft_rebar:
|
||||
main {count, start_deg, radius, pieces: [{mark, z0, z1}, ...] bottom to top, coupler_length}
|
||||
hoops {mark, radius, levels, a, lap, hook, join_deg [first, second]} joins alternate per level (S5)
|
||||
ties {mark, levels, straight, leg, hook_tail, y_lift} as column_rebar_straight.py (S5)
|
||||
u_bars {mark, count, z_bottom, half_bottom, half_top, leg_top_z, top_z, bend_r} one U per plane at main-bar angle
|
||||
i (i < count): bottom +-half_bottom at z_bottom, sloped legs to +-half_top at leg_top_z, vertical to top_z (S3)
|
||||
plate {size, thickness, z_top, colour} square, centred (S4)
|
||||
design_lengths {mark: schedule-style original length per piece}
|
||||
Bars below the pile segment boundary are hosted by the lower pile segment, above it by the upper one.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
from Rhino.DocObjects import ObjectAttributes, ObjectColorSource
|
||||
from System.Drawing import Color
|
||||
|
||||
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
|
||||
doc = __rhino_doc__
|
||||
TOL = 0.01
|
||||
PID = P["pier_id"]
|
||||
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items()}
|
||||
DIA = {d: float(d[1:]) for d, _ in MARK.values()}
|
||||
CL_RGB = tuple(P["display"]["centreline_colour"])
|
||||
SEGS = P["pile"]["segments"]
|
||||
|
||||
|
||||
def layer(path, color):
|
||||
parent = -1
|
||||
parts = path.split("::")
|
||||
for i in range(1, len(parts) + 1):
|
||||
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
|
||||
if idx < 0:
|
||||
lay = Rhino.DocObjects.Layer()
|
||||
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
|
||||
if parent >= 0:
|
||||
lay.ParentLayerId = doc.Layers[parent].Id
|
||||
idx = doc.Layers.Add(lay)
|
||||
parent = idx
|
||||
return parent
|
||||
|
||||
|
||||
def min_bend_radius(dia_name):
|
||||
d = DIA[dia_name]
|
||||
return ((2 * d if d <= 16 else 3.5 * d) + d) / 2
|
||||
|
||||
|
||||
def host(z):
|
||||
for i, s in enumerate(SEGS, 1):
|
||||
if s["z0"] <= z <= s["z1"]:
|
||||
return f"{PID}-PL{i:02d}"
|
||||
return ""
|
||||
|
||||
|
||||
def add_bar(curve, mark, occ, design_len, src, coupler=0.0):
|
||||
dia, shape = MARK[mark]
|
||||
pipes = rg.Brep.CreatePipe(curve, DIA[dia] / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
|
||||
if not pipes or not pipes[0].IsSolid:
|
||||
raise RuntimeError(f"{mark} {occ}: pipe failed")
|
||||
bb = curve.GetBoundingBox(True)
|
||||
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=host((bb.Min.Z + bb.Max.Z) / 2),
|
||||
anchor_hosts="", design_length=f"{design_len:.3f}", coupler_length=f"{coupler:.3f}", src=src)
|
||||
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
|
||||
(pipes[0], layer(f"{PID}::철근::말뚝::{mark}", (110, 110, 110)), f"{PID}-R")):
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = lay
|
||||
oid = f"{prefix}-{mark}-{occ}"
|
||||
attr.Name = oid
|
||||
attr.SetUserString("id", oid)
|
||||
for k, v in common.items():
|
||||
attr.SetUserString(k, v)
|
||||
gid = doc.Objects.AddBrep(geo, attr) if isinstance(geo, rg.Brep) else doc.Objects.AddCurve(geo, attr)
|
||||
if gid == __import__("System").Guid.Empty:
|
||||
raise RuntimeError(f"{oid}: add failed")
|
||||
|
||||
|
||||
def pt(r, deg, z):
|
||||
t = math.radians(deg)
|
||||
return rg.Point3d(r * math.cos(t), r * math.sin(t), z)
|
||||
|
||||
|
||||
def hooked_arc(r, a0, a1, z, hook, bend_r):
|
||||
"""Arc a0 -> a1 (deg, ccw) with inward 135-deg hooks at both ends, fillet bend_r (as column_rebar_ring.py)."""
|
||||
arc = rg.ArcCurve(rg.Arc(pt(r, a0, z), pt(r, (a0 + a1) / 2, z), pt(r, a1, z)))
|
||||
pieces = []
|
||||
for at_start in (True, False):
|
||||
p = arc.PointAtStart if at_start else arc.PointAtEnd
|
||||
t = -arc.TangentAtStart if at_start else arc.TangentAtEnd
|
||||
n = rg.Vector3d(-p.X, -p.Y, 0)
|
||||
n.Unitize()
|
||||
d = t * math.cos(math.radians(135)) + n * math.sin(math.radians(135))
|
||||
d.Unitize()
|
||||
pieces.append(rg.LineCurve(p, p + d * hook))
|
||||
joined = rg.Curve.JoinCurves([pieces[0], arc, pieces[1]], TOL)[0]
|
||||
return rg.Curve.CreateFilletCornersCurve(joined, bend_r, TOL, math.radians(1)) or joined
|
||||
|
||||
|
||||
def tie_curve(axis, z, half, leg, tail, flip):
|
||||
"""As column_rebar_straight.py: straight part on the X / Y diameter, 90-deg leg down at one end, 135-deg hook."""
|
||||
u = rg.Vector3d(1, 0, 0) if axis == 0 else rg.Vector3d(0, 1, 0)
|
||||
s = -1 if flip else 1
|
||||
a, b = rg.Point3d(0, 0, z) - u * (s * half), rg.Point3d(0, 0, z) + u * (s * half)
|
||||
d = -u * s * math.cos(math.radians(45)) + rg.Vector3d(0, 0, -1) * math.sin(math.radians(45))
|
||||
return rg.PolylineCurve([a + rg.Vector3d(0, 0, -leg), a, b, b + d * tail])
|
||||
|
||||
|
||||
def main():
|
||||
for o in list(doc.Objects):
|
||||
oid = o.Attributes.GetUserString("id") or ""
|
||||
if oid.startswith((f"{PID}-PT",)) or (oid.startswith((f"{PID}-R-", f"{PID}-RC-"))
|
||||
and (o.Attributes.GetUserString("mark") or "") in SR_MARKS):
|
||||
doc.Objects.Delete(o, True)
|
||||
DL = SR["design_lengths"]
|
||||
m, n = SR["main"], 0
|
||||
for i in range(m["count"]):
|
||||
th = m["start_deg"] + i * 360.0 / m["count"]
|
||||
for pc in m["pieces"]:
|
||||
c = rg.LineCurve(pt(m["radius"], th, pc["z0"]), pt(m["radius"], th, pc["z1"]))
|
||||
add_bar(c, pc["mark"], f"{i + 1:02d}", DL[pc["mark"]],
|
||||
f"angle {th:.4f} deg; D4 단면 G-G·J-J 61개; r {m['radius']} (S2); EL {pc['z0'] / 1000:.3f}~{pc['z1'] / 1000:.3f}; "
|
||||
f"커플러 이음 (S6)", coupler=m["coupler_length"])
|
||||
n += 1
|
||||
h = SR["hoops"]
|
||||
r, ext = h["radius"], math.degrees(h["lap"] / 2 / h["radius"])
|
||||
for j, z in enumerate(h["levels"]):
|
||||
a0 = h["join_deg"][j % 2]
|
||||
for q, s in enumerate((a0, a0 + 180)):
|
||||
c = hooked_arc(r, s - ext, s + 180 + ext, z, h["hook"], min_bend_radius(MARK[h["mark"]][0]))
|
||||
add_bar(c, h["mark"], f"L{j + 1:03d}{'ab'[q]}", DL[h["mark"]],
|
||||
f"EL {z / 1000:.3f}; r {r} (S1); 겹이음 {h['lap']:.0f} at {a0}/{a0 + 180} deg (S5 [추론])")
|
||||
n += 1
|
||||
t = SR["ties"]
|
||||
rb = min_bend_radius(MARK[t["mark"]][0])
|
||||
for j, z in enumerate(t["levels"]):
|
||||
for axis, name in ((0, "X"), (1, "Y")):
|
||||
zt = z + (t["y_lift"] if axis == 1 else 0)
|
||||
pl = tie_curve(axis, zt, t["straight"] / 2, t["leg"], t["hook_tail"], flip=(j % 2 == 1))
|
||||
c = rg.Curve.CreateFilletCornersCurve(pl, rb, TOL, math.radians(1)) or pl
|
||||
add_bar(c, t["mark"], f"L{j + 1:02d}{name}", DL[t["mark"]],
|
||||
f"EL {zt / 1000:.3f}; {name} 방향 (S5 [추론])")
|
||||
n += 1
|
||||
u = SR["u_bars"]
|
||||
for i in range(u["count"]):
|
||||
th = m["start_deg"] + i * 360.0 / m["count"]
|
||||
e = rg.Vector3d(math.cos(math.radians(th)), math.sin(math.radians(th)), 0)
|
||||
P0 = lambda s, z: rg.Point3d(0, 0, z) + e * s
|
||||
pl = rg.PolylineCurve([P0(-u["half_top"], u["top_z"]), P0(-u["half_top"], u["leg_top_z"]),
|
||||
P0(-u["half_bottom"], u["z_bottom"]), P0(u["half_bottom"], u["z_bottom"]),
|
||||
P0(u["half_top"], u["leg_top_z"]), P0(u["half_top"], u["top_z"])])
|
||||
c = rg.Curve.CreateFilletCornersCurve(pl, u["bend_r"], TOL, math.radians(1)) or pl
|
||||
add_bar(c, u["mark"], f"{i + 1:02d}", DL[u["mark"]],
|
||||
f"plane {th:.4f} deg through the centre (S3 [추론]); bottom EL {u['z_bottom'] / 1000:.3f}; c {u['top_z'] - u['leg_top_z']:.0f} welded to P1-2")
|
||||
n += 1
|
||||
pt_ = SR["plate"]
|
||||
a = pt_["size"] / 2
|
||||
box = rg.Box(rg.BoundingBox(-a, -a, pt_["z_top"] - pt_["thickness"], a, a, pt_["z_top"])).ToBrep()
|
||||
attr = ObjectAttributes()
|
||||
attr.LayerIndex = layer(f"{PID}::철근::말뚝::PLATE", tuple(pt_["colour"]))
|
||||
attr.ObjectColor = Color.FromArgb(*pt_["colour"])
|
||||
attr.ColorSource = ObjectColorSource.ColorFromObject
|
||||
oid = f"{PID}-PT01"
|
||||
attr.Name = oid
|
||||
for k, v in (("id", oid), ("role", "anti-uplift plate"), ("host", host(pt_["z_top"])),
|
||||
("src", f"D4 떠오름 방지 PLATE {pt_['size']:.0f}x{pt_['size']:.0f}x{pt_['thickness']:.0f}; U형 바닥 아래 (S4 [추론])")):
|
||||
attr.SetUserString(k, v)
|
||||
if doc.Objects.AddBrep(box, attr) == __import__("System").Guid.Empty:
|
||||
raise RuntimeError("plate: add failed")
|
||||
doc.Views.Redraw()
|
||||
print(json.dumps({"bars": n, "plate": 1}))
|
||||
|
||||
|
||||
SR = P["shaft_rebar"]
|
||||
SR_MARKS = {pc["mark"] for pc in SR["main"]["pieces"]} | {SR[k]["mark"] for k in ("hoops", "ties", "u_bars")}
|
||||
main()
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Variability classes of the input bundles (stage A of the 입력 묶음 확인·확정 개편, 2026-10-16).
|
||||
|
||||
Every top-level item of params.json / expected.json is a bundle. Grades (기획서 4.4):
|
||||
V1 value variable numbers / text that change from drawing to drawing within the same form
|
||||
V2 list variable position lists, dimension chains, outline points
|
||||
V3 generated variable long lists made by a reading script (hand editing is blocked)
|
||||
F fixed defines the form; changing it means "another template" (edit needs a reason)
|
||||
`fixed_fields` are paths inside a non-F bundle that behave like F (e.g. coping.form); "*" matches every child of an object.
|
||||
`source` lists where the values normally come from: 표 / 표 밖 치수 / 판독 스크립트 / 사람 입력 / 템플릿 고정.
|
||||
The result is a draft until the template's owner confirms it (variability.json of each protected template).
|
||||
"""
|
||||
import json
|
||||
|
||||
SOURCES = ["표", "표 밖 치수", "판독 스크립트", "사람 입력", "템플릿 고정"]
|
||||
GRADES = {"V1": "값 변수", "V2": "목록 변수", "V3": "생성 변수", "F": "고정"}
|
||||
V3_MIN_BARS = 100 # a rebar bundle with at least this many explicit bars is a generated list
|
||||
|
||||
# key -> (grade, [sources], [fixed_fields], note); the same table serves params and expected (file-specific rows below)
|
||||
PARAMS = {
|
||||
"pier_id": ("F", ["템플릿 고정"], [], "교각 식별자"),
|
||||
"scope": ("F", ["템플릿 고정"], [], "모델링 게이트 범위(실행 설정)"),
|
||||
"builders": ("F", ["템플릿 고정"], [], "게이트별 생성기(실행 설정)"),
|
||||
"display": ("F", ["템플릿 고정"], [], "표시·색 설정"),
|
||||
"lean_concrete": ("V1", ["표 밖 치수"], [], "버림콘크리트 치수·EL"),
|
||||
"footing": ("V1", ["표 밖 치수"], [], "기초 치수·EL (rot_deg = 기초 회전 각도 값)"),
|
||||
"column": ("V1", ["표 밖 치수"], ["shape"], "기둥 지름·높이 (단면 형식 shape는 고정)"),
|
||||
"materials": ("V1", ["표 밖 치수"], [], "재료 강도 주기"),
|
||||
"piles": ("V1", ["표 밖 치수"], ["kind"], "말뚝 개수·간격·길이 (말뚝 유무는 템플릿 형식)"),
|
||||
"pile": ("V1", ["표 밖 치수"], ["kind"], "단일 말뚝 구간"),
|
||||
"coping": ("V2", ["표 밖 치수"], ["form"], "코핑 외곽·치수 (형식 form은 고정)"),
|
||||
"bearing_seats": ("V1", ["표", "표 밖 치수"], ["pedestal", "pedestals"], "받침 표 값 + 평면 위치 (대좌 구조는 고정)"),
|
||||
"sole_plates": ("V1", ["표"], ["bearing_size"], "솔플레이트 표 값 (받침 크기 규칙은 고정)"),
|
||||
"rebar_marks": ("V1", ["표"], ["*.shape"], "재료표 직경·형상 (마크 체계의 형상은 고정)"),
|
||||
"column_rebar": ("V2", ["표 밖 치수"], ["form"], "기둥 철근 사슬·위치"),
|
||||
"footing_rebar": ("V2", ["표 밖 치수"], ["form"], "기초 철근 위치 사슬"),
|
||||
"shaft_rebar": ("V2", ["표 밖 치수"], ["form"], "말뚝 철근"),
|
||||
"coping_rebar": ("V2", ["표 밖 치수"], ["form", "host", "group"], "코핑 철근"),
|
||||
}
|
||||
EXPECTED = {
|
||||
"pier_id": ("F", ["템플릿 고정"], [], "교각 식별자"),
|
||||
"tol_mm": ("V1", ["사람 입력"], [], "허용 오차"),
|
||||
"checkers": ("F", ["템플릿 고정"], [], "게이트별 검사기(실행 설정)"),
|
||||
"negative": ("F", ["템플릿 고정"], [], "음성 시험 대상(실행 설정)"),
|
||||
"rebar_palette": ("V1", ["사람 입력"], [], "철근 직경별 색 (사람 결정 기록)"),
|
||||
"planned_fail": ("V1", ["사람 입력"], [], "계획된 FAIL (사람 결정 기록)"),
|
||||
**{g: ("V2", ["표", "표 밖 치수"], [], "독립 기대값 (params와 따로 도면에서 옮긴 값)") for g in ("G1", "G2", "G3", "G4", "G5")},
|
||||
}
|
||||
|
||||
|
||||
def is_info(key):
|
||||
return key.startswith("_") or key.endswith("_src") or key.startswith("hitl_decisions")
|
||||
|
||||
|
||||
def _bars(value):
|
||||
"""Number of explicit per-bar entries of a rebar bundle (list `bars`), 0 when the bundle is rule based."""
|
||||
return len(value.get("bars", [])) if isinstance(value, dict) and isinstance(value.get("bars"), list) else 0
|
||||
|
||||
|
||||
def classify(which, key, value):
|
||||
"""-> {grade, source, fixed_fields, note, info}."""
|
||||
if is_info(key):
|
||||
return dict(grade="F", source=["템플릿 고정"], fixed_fields=[], note="참고 정보(게이트가 쓰지 않음)", info=True)
|
||||
grade, source, fixed, note = (PARAMS if which == "params" else EXPECTED).get(key, ("V1", ["사람 입력"], [], "미분류 — 담당자 확인"))
|
||||
if which == "params" and key in ("coping_rebar", "footing_rebar") and _bars(value) >= V3_MIN_BARS:
|
||||
grade, source, note = "V3", ["판독 스크립트"], note + " — 철근별 중심선 목록(판독 스크립트로 생성)"
|
||||
return dict(grade=grade, source=list(source), fixed_fields=list(fixed), note=note, info=False)
|
||||
|
||||
|
||||
def build(params, expected):
|
||||
"""variability.json content for a params / expected pair (draft)."""
|
||||
sections = {}
|
||||
for which, d in (("params", params), ("expected", expected)):
|
||||
for k, v in d.items():
|
||||
sections[f"{which}:{k}"] = classify(which, k, v)
|
||||
return {"_schema": "variability/1", "status": "초안 — 템플릿 담당자 확정 전", "grades": GRADES, "sections": sections}
|
||||
|
||||
|
||||
def _walk(value, parts):
|
||||
"""Values at a path with "*" wildcards, as a list of (path, value); a missing path yields (path, None)."""
|
||||
if not parts:
|
||||
return [("", value)]
|
||||
head, rest = parts[0], parts[1:]
|
||||
if head == "*":
|
||||
if not isinstance(value, dict):
|
||||
return []
|
||||
out = []
|
||||
for k, v in value.items():
|
||||
out += [(f"{k}.{p}".rstrip("."), x) for p, x in _walk(v, rest)]
|
||||
return out
|
||||
if not isinstance(value, dict) or head not in value:
|
||||
return [(head, None)]
|
||||
return [(f"{head}.{p}".rstrip("."), x) for p, x in _walk(value[head], rest)]
|
||||
|
||||
|
||||
def fixed_view(value, fixed_fields):
|
||||
"""{path: json text} of the fixed fields of a bundle value (what must not change silently)."""
|
||||
out = {}
|
||||
for f in fixed_fields:
|
||||
for p, x in _walk(value, f.split(".")):
|
||||
out[p] = json.dumps(x, sort_keys=True, ensure_ascii=False)
|
||||
return out
|
||||
|
||||
|
||||
def changed_fixed(old, new, fixed_fields):
|
||||
"""Paths of fixed fields whose value differs between two bundle values."""
|
||||
a, b = fixed_view(old, fixed_fields), fixed_view(new, fixed_fields)
|
||||
return sorted(p for p in set(a) | set(b) if a.get(p) != b.get(p))
|
||||
@@ -0,0 +1,416 @@
|
||||
"""App check ("앱 확인") of the input bundles — stage B of the 입력 묶음 확인·확정 개편 (2026-10-16).
|
||||
|
||||
The app compares every bundle of params.json / expected.json with what it can read independently and reports, per bundle,
|
||||
one of: g 일치 / w 주의 / b 불일치 / n 근거 없음 / x 해당 없음. The app never confirms anything: a person confirms (D1), the app only
|
||||
supplies the evidence. "일치" means "the independent source and the value agree", not "the drawing is right" (기획서 4.3).
|
||||
|
||||
Checks (C1~C8, 기획서 4.3):
|
||||
C1 출처 있음 the bundle (or its `<key>_src` sibling) records where its values come from
|
||||
C2 도면 재대조 table candidates (ELEVATION / 솔플레이트 / 철근재료표) read from the drawing texts vs the current values [strong]
|
||||
C2b 표 밖 치수 재대조 anchors followed into the drawing (engine/anchors.py): A candidates equal / differ, B only warn; own-drawing anchors are no evidence [strong]
|
||||
C3 템플릿 원본 잔존 the bundle still equals the protected template's original and nothing independent supports it
|
||||
C4 내부 정합 bar counts of the generated lists vs the schedule counts in expected [counts only]
|
||||
C5 params ↔ expected 독립 the expected source notes are not copies of the params notes
|
||||
C6 범위·단위 sizes > 0, EL / z in a plausible range, no NaN
|
||||
C7 변수 등급 위반 F bundles / fixed fields unchanged against the template original (or an edit reason is logged)
|
||||
C8 재생성 일치 V3 bundles: regenerated list equals the current one — no regeneration script is connected yet (해당 없음, 독립성 낮음)
|
||||
C9 params↔expected 일관성 values that describe the same quantity in both files agree (engine/consistency.py; 2026-10-20) — finds stale values before a gate is run;
|
||||
not independent evidence (b when they differ, g otherwise, never a strength)
|
||||
C3b 필드 단위 원본 잔존 fields of a CHANGED bundle that still equal the pier's creation snapshot while elevation-like siblings changed (origin_values.json; 2026-10-20)
|
||||
Status per bundle: any failed check -> b; template original without independent support -> n; strong support (C2) -> g (w when a check
|
||||
warns); counts only -> w ("수량만"); F bundle unchanged -> g; nothing -> n; reference-only / human decision records -> x.
|
||||
Everything is derived from the data files and the drawing text index; no gate is run.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
|
||||
from engine import consistency, reader, variability
|
||||
|
||||
VERSION = "B3"
|
||||
ST_NAME = {"g": "일치", "w": "주의", "b": "불일치", "n": "근거 없음", "x": "해당 없음"}
|
||||
DECISION_KEYS = {("expected", "planned_fail"), ("expected", "rebar_palette"), ("expected", "tol_mm")}
|
||||
SIZE_KEYS = re.compile(r"^(size_[xy]|thickness|diameter|height|length|width_y|plan_length|top_length|half|count_[xy]|nx|ny|dia|"
|
||||
r"half_.*|bottom_flat_half|tip_half|top_half|soffit_rise)$")
|
||||
SIZE_IN = {"d": ("column", "piles"), "dx": ("footing", "lean"), "dy": ("footing", "lean")} # short names only where they are sizes
|
||||
NOT_SIZE = ("translation_checks", "pitch", "negative", "chain") # directional / check values may be negative or 0
|
||||
Z_KEYS = re.compile(r"^(z|z_.*|el_x|z0|seat_z)$")
|
||||
Z_RANGE = (-20000.0, 400000.0) # mm, EL -20 m .. 400 m
|
||||
|
||||
|
||||
def chk(cid, name, st, text, pos="", strength=""):
|
||||
return {"id": cid, "name": name, "st": st, "text": text, "pos": pos, "strength": strength}
|
||||
|
||||
|
||||
def _walk(v, path=()):
|
||||
"""(path, leaf) of every scalar in a nested value."""
|
||||
if isinstance(v, dict):
|
||||
for k, x in v.items():
|
||||
yield from _walk(x, path + (str(k),))
|
||||
elif isinstance(v, list):
|
||||
for i, x in enumerate(v):
|
||||
yield from _walk(x, path + (str(i),))
|
||||
else:
|
||||
yield path, v
|
||||
|
||||
|
||||
def _src_texts(v):
|
||||
"""All source notes (`src`, `*_src`, `sources`) inside a bundle value, flattened to strings."""
|
||||
out = []
|
||||
|
||||
def rec(x, key=""):
|
||||
if isinstance(x, dict):
|
||||
for k, y in x.items():
|
||||
rec(y, k)
|
||||
elif isinstance(x, list):
|
||||
for y in x:
|
||||
rec(y, key)
|
||||
elif isinstance(x, str) and (key == "src" or key.endswith("_src") or key == "sources") and x.strip():
|
||||
out.append(x.strip())
|
||||
rec(v)
|
||||
return out
|
||||
|
||||
|
||||
def _count_bars(value, mark):
|
||||
return sum(1 for b in value.get("bars", []) if b.get("mark") == mark)
|
||||
|
||||
|
||||
def _planned(expected, gate, mark, what="bar count vs schedule"):
|
||||
for pf in expected.get("planned_fail", []):
|
||||
if pf.get("gate") == gate and pf.get("item", "").startswith(f"{mark} {what}"):
|
||||
return pf
|
||||
return None
|
||||
|
||||
|
||||
def count_pairs(params, expected):
|
||||
"""Counts derived from the generated rebar lists vs the schedule counts in expected: {gate: [(label, mark, got, want)]}."""
|
||||
out = {"G2": [], "G3": [], "G4": []}
|
||||
cr = params.get("column_rebar") or {}
|
||||
sn = (expected.get("G2") or {}).get("schedule_n")
|
||||
if cr.get("form") == "ring" and isinstance(sn, dict):
|
||||
try:
|
||||
out["G2"].append(("주철근", cr["main"]["mark"], cr["main"]["count"], sn.get(cr["main"]["mark"])))
|
||||
for z in cr["hoops"]["zones"]:
|
||||
out["G2"].append(("띠철근", z["mark"], len(z["levels"]), sn.get(z["mark"])))
|
||||
t = cr["ties"]
|
||||
out["G2"].append(("크로스타이", t["mark"], len(t["levels"]) * len(t["angles_deg"]), sn.get(t["mark"])))
|
||||
except (KeyError, TypeError):
|
||||
out["G2"] = []
|
||||
for gate, key in (("G3", "footing_rebar"), ("G4", "coping_rebar")):
|
||||
fr = params.get(key) or {}
|
||||
sch = (expected.get(gate) or {}).get("schedule")
|
||||
if not isinstance(sch, dict) or not fr:
|
||||
continue
|
||||
if fr.get("form") == "polylines":
|
||||
for m, v in sch.items():
|
||||
if isinstance(v, list) and len(v) == 2 and m in {b.get("mark") for b in fr.get("bars", [])} | set(sch):
|
||||
out[gate].append(("철근 수", m, _count_bars(fr, m), v[1]))
|
||||
elif fr.get("form") == "mats" and gate == "G3":
|
||||
for g in fr.get("groups", []):
|
||||
out[gate].append(("위치 목록", g["mark"], len(g["pos"]), (sch.get(g["mark"]) or [None, None])[1]))
|
||||
h, v = fr.get("hoops"), fr.get("verticals")
|
||||
if h:
|
||||
out[gate].append(("띠철근 단 수", h["mark"], len(h["levels"]), (sch.get(h["mark"]) or [None, None])[1]))
|
||||
if v:
|
||||
n = len(v["points"]) if "points" in v else sum(1 for i, _ in enumerate(v["grid_x"]) for j, _ in enumerate(v["grid_y"]) if (i + j) % 2 == v["parity"])
|
||||
out[gate].append(("수직근", v["mark"], n, (sch.get(v["mark"]) or [None, None])[1]))
|
||||
return {g: p for g, p in out.items() if p}
|
||||
|
||||
|
||||
def bundles_of_gate(gate):
|
||||
return {"G2": [("params", "column_rebar"), ("expected", "G2")], "G3": [("params", "footing_rebar"), ("expected", "G3")],
|
||||
"G4": [("params", "coping_rebar"), ("expected", "G4")]}[gate]
|
||||
|
||||
|
||||
def anchor_check(rows, missing, key_prefix):
|
||||
"""C2b of one bundle from the anchor rows of its fields (rows of engine.anchors.follow joined with the current values)."""
|
||||
mine = [r for r in rows if r["field"].startswith(key_prefix) and not r["self_source"]]
|
||||
miss = [f for f in missing if f.startswith(key_prefix)]
|
||||
if not mine:
|
||||
return None
|
||||
a_diff = [r for r in mine if r["grade"] == "A" and not r["same"]]
|
||||
b_diff = [r for r in mine if r["grade"] == "B" and not r["same"]]
|
||||
a_same = [r for r in mine if r["grade"] == "A" and r["same"]]
|
||||
b_same = [r for r in mine if r["grade"] == "B" and r["same"]]
|
||||
anchored = len(mine) + len(miss)
|
||||
cover = (len(a_same) + len(b_same)) / anchored if anchored else 0
|
||||
if a_diff:
|
||||
r = a_diff[0]
|
||||
d = "; ".join(f"{x['field'].split(':', 1)[1]}: 현재 {x['current']} ≠ 도면 {x['value']} ('{x['text']}' {x['dwg']})" for x in a_diff[:3])
|
||||
return chk("C2b", "표 밖 치수 재대조", "b", f"A등급 후보 {len(a_diff)}건이 현재 값과 다르다 — {d}", pos=f"{r['dwg']} ({r['x']:.0f}, {r['y']:.0f})", strength="S")
|
||||
if b_diff:
|
||||
r = b_diff[0]
|
||||
d = "; ".join(f"{x['field'].split(':', 1)[1]}: 현재 {x['current']} ≠ 도면 {x['value']}" for x in b_diff[:3])
|
||||
return chk("C2b", "표 밖 치수 재대조", "w", f"B등급 후보 {len(b_diff)}건이 현재 값과 다르다(후보가 틀렸을 수도 있다) — {d}", pos=f"{r['dwg']} ({r['x']:.0f}, {r['y']:.0f})")
|
||||
if a_same and cover >= 0.6:
|
||||
r = a_same[0]
|
||||
return chk("C2b", "표 밖 치수 재대조", "g", f"A등급 {len(a_same)}건·B등급 {len(b_same)}건이 현재 값과 같다 (앵커가 있는 {anchored}개 필드의 {cover:.0%})",
|
||||
pos=f"{r['dwg']} '{r['text']}' ({r['x']:.0f}, {r['y']:.0f})", strength="S")
|
||||
if a_same or b_same:
|
||||
return chk("C2b", "표 밖 치수 재대조", "w", f"앵커 필드 {anchored}개 중 {len(a_same) + len(b_same)}개만 같다 (A {len(a_same)} · B {len(b_same)}) — 대조 범위 {cover:.0%}")
|
||||
return None
|
||||
|
||||
|
||||
HEIGHT_TOKENS = {"z", "z0", "z_bottom", "z_top", "z_flat", "el_x", "datum_el", "step_top", "tier_z", "loop_z", "levels", "hook_leg_z", "straight_from",
|
||||
"c10_bottom", "c15_top", "c16_top", "top_right", "top_left", "tiers"}
|
||||
|
||||
|
||||
def height_like(path):
|
||||
"""True for a leaf that is an elevation (EL in mm): z-like keys, level lists, the z of a point [x, z], the z of an outline point."""
|
||||
last = path[-1]
|
||||
if last in HEIGHT_TOKENS or last.startswith("z_") or last.endswith(("_z", "_top", "_bottom")):
|
||||
return True
|
||||
if any(t in ("step_top", "tier_z", "loop_z", "levels") for t in path):
|
||||
return True
|
||||
if last == "1" and any(t in ("points", "hooks_up", "coping_outline", "top_right", "top_left") for t in path):
|
||||
return True
|
||||
if last == "0" and "c14_levels" in path:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
EL_MIN, EL_MAX = 10000.0, 60000.0 # mm: elevation-like numbers (EL 10 m .. 60 m)
|
||||
STALE_MIN_CHANGED = 3 # at least this many elevation-like siblings changed against the snapshot
|
||||
|
||||
|
||||
def stale_fields_check(which, key, value, origin_values, m):
|
||||
"""C3b: in a bundle that was changed against the pier's creation snapshot, elevation-like numbers that still equal the snapshot value are
|
||||
suspicious (the drawing moved the other heights but this one was left behind, e.g. a P2 coordinate in a P3 point list). Warns (w), never fails."""
|
||||
if not origin_values or which not in origin_values or key not in origin_values[which]:
|
||||
return chk("C3b", "필드 단위 원본 잔존", "x", "교각 생성 시점의 값 기록(origin_values.json)이 없어 필드 단위로 비교하지 못했다")
|
||||
old = dict(_walk(origin_values[which][key]))
|
||||
new = dict(_walk(value))
|
||||
el = [p for p, v in new.items() if isinstance(v, (int, float)) and not isinstance(v, bool) and EL_MIN <= abs(v) <= EL_MAX and p in old and height_like(p)]
|
||||
if not el or old == new:
|
||||
return chk("C3b", "필드 단위 원본 잔존", "x" if old == new else "g", "묶음 전체가 생성 시점 그대로라 C3에서 본다" if old == new else "높이로 보이는 값이 없다")
|
||||
changed = [p for p in el if old[p] != new[p]]
|
||||
kept = [p for p in el if old[p] == new[p]]
|
||||
if len(changed) < STALE_MIN_CHANGED:
|
||||
return chk("C3b", "필드 단위 원본 잔존", "g", f"높이로 보이는 값 {len(el)}개 중 바뀐 것이 {len(changed)}개뿐이라 비교하지 않는다")
|
||||
if kept:
|
||||
names = ", ".join(".".join(p) + f" = {new[p]:g}" for p in kept[:4])
|
||||
return chk("C3b", "필드 단위 원본 잔존", "w", f"바뀐 묶음인데 높이로 보이는 값 {len(el)}개 중 {len(kept)}개가 생성 시점 값 그대로다 — 도면과 대조: {names}")
|
||||
return chk("C3b", "필드 단위 원본 잔존", "g", f"높이로 보이는 값 {len(el)}개가 모두 생성 시점과 다르다")
|
||||
|
||||
|
||||
def verify(params, expected, texts, meta, origin_fixed=None, edit_log=None, current_fixed=None, template_values=None, anchor_rows=None, origin_values=None):
|
||||
"""-> {"params:key": {"st", "checks", "summary"}} for every bundle.
|
||||
meta[bundle] = variability.classify result + "origin" (True / False / None); origin_fixed[bundle] = fixed_view at copy time."""
|
||||
origin_fixed = origin_fixed or {}
|
||||
edit_log = edit_log or []
|
||||
template_values = template_values or {}
|
||||
arows, amiss = (anchor_rows or ([], []))[:2]
|
||||
docs = {"params": params, "expected": expected}
|
||||
# ---- C2: table candidates of the drawing texts vs the current values
|
||||
c2 = {}
|
||||
tables_ok = False
|
||||
if texts:
|
||||
tabs = reader.table_candidates(texts)
|
||||
cands = reader.field_candidates(tabs)
|
||||
tables_ok = bool(cands)
|
||||
for r in reader.proposals(cands, params, expected):
|
||||
c2.setdefault(f"{r['file']}:{r['path'][0]}", []).append(r)
|
||||
# ---- C9: params <-> expected consistency (rules of engine/consistency.py), per bundle of either file
|
||||
cons = consistency.check(params, expected)
|
||||
cons_by = {}
|
||||
for r in cons:
|
||||
cons_by.setdefault("params:" + consistency.bundles_of(r["p"]), []).append(r)
|
||||
cons_by.setdefault("expected:" + consistency.bundles_of(r["e"]), []).append(r)
|
||||
# ---- C4: counts per gate pair
|
||||
pairs = count_pairs(params, expected)
|
||||
out = {}
|
||||
for which in ("params", "expected"):
|
||||
for key, value in docs[which].items():
|
||||
b = f"{which}:{key}"
|
||||
m = meta.get(b) or variability.classify(which, key, value)
|
||||
checks = []
|
||||
if m.get("info"):
|
||||
out[b] = {"st": "x", "checks": [chk("C1", "참고 정보", "x", "게이트가 쓰지 않는 참고 정보라 확인하지 않는다")], "summary": "참고 정보"}
|
||||
continue
|
||||
# C1
|
||||
srcs = _src_texts(value) + (_src_texts(params.get(key + "_src")) if which == "params" and key + "_src" in params else [])
|
||||
if key + "_src" in docs[which] and isinstance(docs[which][key + "_src"], str):
|
||||
srcs.append(docs[which][key + "_src"])
|
||||
if m["grade"] == "F":
|
||||
checks.append(chk("C1", "출처 있음", "x", "고정(F) 항목: 값은 템플릿이 정한다"))
|
||||
elif srcs:
|
||||
checks.append(chk("C1", "출처 있음", "g", f"값의 출처 기록이 {len(srcs)}건 있다", pos=srcs[0][:80]))
|
||||
else:
|
||||
checks.append(chk("C1", "출처 있음", "n", "값의 출처 기록이 없다 (일반 출처: " + " · ".join(m["source"]) + ")"))
|
||||
# C2
|
||||
rows = c2.get(b)
|
||||
if rows:
|
||||
bad = [r for r in rows if not r["same"]]
|
||||
src0 = rows[0]["src"]
|
||||
if bad:
|
||||
d = "; ".join(f"{'.'.join(str(x) for x in r['path'])}: 현재 {r['current']} ≠ 표 {r['candidate']}" for r in bad[:3])
|
||||
checks.append(chk("C2", "도면 재대조", "b", f"표 후보 {len(rows)}건 중 {len(bad)}건이 다르다 — {d}", pos=bad[0]["src"], strength="S"))
|
||||
else:
|
||||
checks.append(chk("C2", "도면 재대조", "g", f"표 후보 {len(rows)}건이 현재 값과 모두 같다", pos=src0, strength="S"))
|
||||
elif not texts and "표" in m["source"]:
|
||||
checks.append(chk("C2", "도면 재대조", "n", "도면 문자 색인이 없어 표와 대조하지 못했다"))
|
||||
elif texts and "표" in m["source"] and not tables_ok:
|
||||
checks.append(chk("C2", "도면 재대조", "n", "도면에서 표를 인식하지 못했다"))
|
||||
else:
|
||||
checks.append(chk("C2", "도면 재대조", "x", "표 후보가 없는 묶음(표 밖 치수는 C단계 몫)"))
|
||||
# C2b
|
||||
ac = anchor_check(arows, amiss, f"{which}:{key}.") if arows else None
|
||||
if ac:
|
||||
checks.append(ac)
|
||||
# C4
|
||||
gate = next((g for g, bl in ((g, bundles_of_gate(g)) for g in ("G2", "G3", "G4")) if (which, key) in bl), None)
|
||||
if gate and gate in pairs:
|
||||
bad, soft, ok_n = [], [], 0
|
||||
for label, mark, got, want in pairs[gate]:
|
||||
if want is None:
|
||||
continue
|
||||
if got == want:
|
||||
ok_n += 1
|
||||
continue
|
||||
pf = _planned(expected, gate, mark)
|
||||
(soft if pf else bad).append(f"{mark} {label} {got} ≠ 재료표 {want}" + (f" (계획 FAIL: {pf.get('approved', '')})" if pf else ""))
|
||||
if bad:
|
||||
checks.append(chk("C4", "내부 정합", "b", "수량이 재료표와 다르다 — " + "; ".join(bad[:3]), strength="C"))
|
||||
elif soft:
|
||||
checks.append(chk("C4", "내부 정합", "w", "수량이 재료표와 다르지만 계획된 예외다 — " + "; ".join(soft[:3]), strength="C"))
|
||||
else:
|
||||
checks.append(chk("C4", "내부 정합", "g", f"생성 목록 수량 {ok_n}건이 재료표 수량과 같다 (위치·좌표는 확인하지 않음)", strength="C"))
|
||||
else:
|
||||
checks.append(chk("C4", "내부 정합", "x", "수량 대조 대상이 아니다"))
|
||||
# C5
|
||||
if which == "expected" and key in ("G1", "G2", "G3", "G4", "G5"):
|
||||
es = _src_texts(value)
|
||||
ps = {t for k in params for t in _src_texts(params[k])}
|
||||
if not es:
|
||||
checks.append(chk("C5", "params와 독립", "n", "expected에 출처 기록이 없어 독립 입력인지 알 수 없다"))
|
||||
elif any(len(t) > 12 and t in ps for t in es):
|
||||
checks.append(chk("C5", "params와 독립", "w", "expected의 출처 문구가 params와 같다 — 독립 입력인지 확인"))
|
||||
else:
|
||||
checks.append(chk("C5", "params와 독립", "g", "expected의 출처 문구가 params와 다르다"))
|
||||
else:
|
||||
checks.append(chk("C5", "params와 독립", "x", "해당 없음"))
|
||||
# C6
|
||||
bad6, warn6 = [], []
|
||||
for path, v in _walk(value):
|
||||
name = path[-1] if path else ""
|
||||
if isinstance(v, float) and (math.isnan(v) or math.isinf(v)):
|
||||
bad6.append(f"{'.'.join(path)} = {v}")
|
||||
elif isinstance(v, (int, float)) and not isinstance(v, bool):
|
||||
is_size = (SIZE_KEYS.match(name) or (name in SIZE_IN and len(path) > 1 and path[-2] in SIZE_IN[name])) and not any(p.startswith(NOT_SIZE) for p in path)
|
||||
if is_size and v <= 0:
|
||||
bad6.append(f"{'.'.join(path)} = {v} (0 이하)")
|
||||
elif Z_KEYS.match(name) and not (Z_RANGE[0] <= v <= Z_RANGE[1]):
|
||||
warn6.append(f"{'.'.join(path)} = {v} (EL 범위 밖)")
|
||||
if bad6:
|
||||
checks.append(chk("C6", "범위·단위", "b", "; ".join(bad6[:3])))
|
||||
elif warn6:
|
||||
checks.append(chk("C6", "범위·단위", "w", "; ".join(warn6[:3])))
|
||||
else:
|
||||
checks.append(chk("C6", "범위·단위", "g", "크기·EL이 상식 범위 안에 있다"))
|
||||
# C7
|
||||
ofix = origin_fixed.get(b)
|
||||
logged = [e for e in edit_log if e.get("section") == b]
|
||||
if m["grade"] == "F":
|
||||
if m.get("origin") is None:
|
||||
tv = template_values.get(b) or {}
|
||||
now = json.dumps(value, sort_keys=True, ensure_ascii=False)
|
||||
same = sorted(t for t, v in tv.items() if v == now)
|
||||
if key == "pier_id":
|
||||
checks.append(chk("C7", "변수 등급 위반", "x", "교각 식별자라 비교하지 않는다"))
|
||||
elif same:
|
||||
checks.append(chk("C7", "변수 등급 위반", "g", f"템플릿 복사 기록은 없지만 보호 템플릿 {len(same)}종({', '.join(same[:3])})과 같은 값이다", strength="F"))
|
||||
elif tv:
|
||||
checks.append(chk("C7", "변수 등급 위반", "w", "템플릿 복사 기록이 없고 어느 보호 템플릿과도 같지 않다 — 새 형식인지 확인"))
|
||||
else:
|
||||
checks.append(chk("C7", "변수 등급 위반", "n", "템플릿 원본 기록이 없어 고정 항목을 비교하지 못했다"))
|
||||
elif m["origin"]:
|
||||
checks.append(chk("C7", "변수 등급 위반", "g", "고정(F) 항목이 템플릿 원본과 같다", strength="F"))
|
||||
elif logged:
|
||||
checks.append(chk("C7", "변수 등급 위반", "w", f"고정(F) 항목이 원본과 다르다 — 사유 기록: {logged[-1]['reason']}"))
|
||||
else:
|
||||
checks.append(chk("C7", "변수 등급 위반", "b", "고정(F) 항목이 원본과 다르고 사유 기록이 없다"))
|
||||
elif m["fixed_fields"] and ofix is not None and current_fixed is not None:
|
||||
diff = [p for p in set(ofix) | set(current_fixed.get(b, {})) if ofix.get(p) != current_fixed.get(b, {}).get(p)]
|
||||
if not diff:
|
||||
checks.append(chk("C7", "변수 등급 위반", "g", "고정 필드가 템플릿 원본과 같다"))
|
||||
elif logged:
|
||||
checks.append(chk("C7", "변수 등급 위반", "w", f"고정 필드({', '.join(sorted(diff))})가 원본과 다르다 — 사유: {logged[-1]['reason']}"))
|
||||
else:
|
||||
checks.append(chk("C7", "변수 등급 위반", "b", f"고정 필드({', '.join(sorted(diff))})가 원본과 다르고 사유 기록이 없다"))
|
||||
else:
|
||||
checks.append(chk("C7", "변수 등급 위반", "x", "고정 필드 비교 대상이 없다"))
|
||||
# C8
|
||||
checks.append(chk("C8", "재생성 일치", "x", "생성 스크립트가 앱에 연결되어 있지 않다 (재생성 일치는 독립성이 낮아 근거로 쓰지 않는다)" if m["grade"] == "V3"
|
||||
else "V3 묶음이 아니다"))
|
||||
# C9
|
||||
rel = cons_by.get(b, [])
|
||||
badc = [r for r in rel if not r["ok"]]
|
||||
if badc:
|
||||
d = "; ".join(f"{r['text']}: params {r['p']} = {r['pv']:g} ≠ expected {r['e']} = {r['ev']:g}" if isinstance(r["pv"], float) and isinstance(r["ev"], float)
|
||||
else f"{r['text']}: {r['pv']} / {r['ev']}" for r in badc[:3])
|
||||
checks.append(chk("C9", "params↔expected 일관성", "b", f"서로 같아야 할 값 {len(rel)}건 중 {len(badc)}건이 다르다 — {d}"))
|
||||
elif rel:
|
||||
checks.append(chk("C9", "params↔expected 일관성", "g", f"서로 같아야 할 값 {len(rel)}건이 params와 expected에서 같다 (같은 도면 읽기에서 나왔다면 독립 근거는 아님)"))
|
||||
else:
|
||||
checks.append(chk("C9", "params↔expected 일관성", "x", "연동 값 규칙이 없는 묶음"))
|
||||
# C3b: fields of a changed bundle that still equal the creation snapshot
|
||||
checks.append(stale_fields_check(which, key, value, origin_values, m))
|
||||
# C3 (needs C2 / C4 results)
|
||||
strong = any(c["id"] in ("C2", "C2b") and c["st"] == "g" for c in checks)
|
||||
if m["grade"] == "F":
|
||||
checks.append(chk("C3", "템플릿 원본 잔존", "x", "고정(F) 항목은 C7에서 본다"))
|
||||
elif m.get("origin") is None:
|
||||
checks.append(chk("C3", "템플릿 원본 잔존", "x", "템플릿 복사 기록이 없어 원본과 비교하지 못했다"))
|
||||
elif m["origin"]:
|
||||
checks.append(chk("C3", "템플릿 원본 잔존", "g" if strong else "n",
|
||||
"원본과 같지만 도면(표·표 밖 치수)과 일치한다" if strong else "현재 값이 템플릿 원본 그대로이고 새 도면 근거가 없다"))
|
||||
else:
|
||||
checks.append(chk("C3", "템플릿 원본 잔존", "g", "원본과 다른 값이 들어 있다"))
|
||||
order = ("C1", "C2", "C2b", "C3", "C3b", "C4", "C5", "C6", "C7", "C8", "C9")
|
||||
checks.sort(key=lambda c: order.index(c["id"]))
|
||||
st = combine(checks, m, (which, key) in DECISION_KEYS)
|
||||
out[b] = {"st": st, "checks": checks, "summary": summarize(st, checks)}
|
||||
return out
|
||||
|
||||
|
||||
def combine(checks, m, decision):
|
||||
by = {c["id"]: c for c in checks}
|
||||
if decision:
|
||||
return "x"
|
||||
if any(c["st"] == "b" for c in checks):
|
||||
return "b"
|
||||
if by["C3"]["st"] == "n":
|
||||
return "n"
|
||||
warn = any(c["st"] == "w" for c in checks)
|
||||
if m["grade"] == "F":
|
||||
c7 = by["C7"]
|
||||
return "w" if warn else ("g" if c7["st"] == "g" else "n")
|
||||
if any(c["strength"] == "S" and c["st"] == "g" for c in checks):
|
||||
return "w" if warn else "g"
|
||||
if any(c["strength"] == "C" and c["st"] in ("g", "w") for c in checks):
|
||||
return "w"
|
||||
return "n"
|
||||
|
||||
|
||||
def summarize(st, checks):
|
||||
"""One sentence for the table: the check that decides the status."""
|
||||
if st == "x":
|
||||
return "사람 결정 기록 또는 참고 정보 — 앱이 도면과 대조하지 않는다"
|
||||
pick = {"b": [c for c in checks if c["st"] == "b"],
|
||||
"w": [c for c in checks if c["st"] == "w"] or [c for c in checks if c["strength"] == "C" and c["st"] == "g"],
|
||||
"g": [c for c in checks if c["strength"] in ("S", "F") and c["st"] == "g"],
|
||||
"n": [c for c in checks if c["id"] in ("C3", "C7", "C2") and c["st"] == "n"]}[st]
|
||||
if pick:
|
||||
c = pick[0]
|
||||
return f"{c['id']} {c['text']}" + (" — 수량만 확인, 위치·좌표는 도면과 직접 대조" if st == "w" and c["strength"] == "C" and c["st"] == "g" else "")
|
||||
return "앱이 독립적으로 대조할 근거가 없다 — 도면과 직접 대조한 뒤 사유와 함께 확정"
|
||||
|
||||
|
||||
def counts(results, required):
|
||||
"""Per-status counts of the required bundles."""
|
||||
c = {"g": 0, "w": 0, "b": 0, "n": 0, "x": 0}
|
||||
for b in required:
|
||||
r = results.get(b)
|
||||
if r:
|
||||
c[r["st"]] += 1
|
||||
return c
|
||||
Reference in new issue
Block a user