BLOCK8 중심선과 단면 근거로 3D 모델 생성
This commit is contained in:
1 parent
e7d025075e
commit
9d11b4ed62
13 files changed
+18693
-269
No files matched your search
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env python3
|
||||
"""BLOCK8 단면 구조선과 블록 내부 콘크리트선을 모델 데이터로 변환한다."""
|
||||
|
||||
import json
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "public/drawings/광터지하차도_BLOCK8.dxf"
|
||||
OUTPUT = ROOT / "src/model/block8.json"
|
||||
|
||||
|
||||
def read_pairs(path):
|
||||
lines = path.read_text(encoding="utf-8-sig").splitlines()
|
||||
if len(lines) % 2:
|
||||
raise ValueError("DXF 그룹 코드 줄 수가 짝수가 아닙니다.")
|
||||
return [(int(lines[i].strip()), lines[i + 1].strip()) for i in range(0, len(lines), 2)]
|
||||
|
||||
|
||||
def records(pairs, start, end):
|
||||
result = []
|
||||
current = None
|
||||
for code, value in pairs[start:end]:
|
||||
if code == 0:
|
||||
if current:
|
||||
result.append(current)
|
||||
current = {"type": value.strip(), "data": []}
|
||||
elif current:
|
||||
current["data"].append((code, value.strip()))
|
||||
if current:
|
||||
result.append(current)
|
||||
return result
|
||||
|
||||
|
||||
def parse_sections(pairs, section_name):
|
||||
begin = next(i for i, (code, value) in enumerate(pairs) if code == 0 and value.strip() == "SECTION" and pairs[i + 1][0] == 2 and pairs[i + 1][1].strip() == section_name)
|
||||
end = next(i for i in range(begin + 2, len(pairs)) if pairs[i][0] == 0 and pairs[i][1].strip() == "ENDSEC")
|
||||
return begin, end
|
||||
|
||||
|
||||
def parse_blocks(pairs):
|
||||
begin, end = parse_sections(pairs, "BLOCKS")
|
||||
blocks = {}
|
||||
i = begin + 2
|
||||
while i < end:
|
||||
if pairs[i][0] != 0 or pairs[i][1].strip() != "BLOCK":
|
||||
i += 1
|
||||
continue
|
||||
block_start = i
|
||||
j = i + 1
|
||||
while j < end and not (pairs[j][0] == 0 and pairs[j][1].strip() == "ENDBLK"):
|
||||
j += 1
|
||||
header_end = next((k for k in range(block_start + 1, j) if pairs[k][0] == 0), j)
|
||||
header = pairs[block_start + 1:header_end]
|
||||
header_values = [value.strip() for code, value in header]
|
||||
name_pos = next(k for k, (code, value) in enumerate(header) if code == 100 and value.strip() == "AcDbBlockBegin")
|
||||
name = next(value.strip() for code, value in header[name_pos + 1:] if code == 2)
|
||||
base = []
|
||||
for code, value in header[name_pos + 1:]:
|
||||
if code in (10, 20, 30):
|
||||
base.append(float(value))
|
||||
if len(base) == 3:
|
||||
break
|
||||
base += [0.0] * (3 - len(base))
|
||||
blocks[name] = {"base": base, "entities": records(pairs, header_end, j)}
|
||||
i = j + 1
|
||||
return blocks
|
||||
|
||||
|
||||
def parse_modelspace(pairs):
|
||||
begin, end = parse_sections(pairs, "ENTITIES")
|
||||
return records(pairs, begin + 2, end)
|
||||
|
||||
|
||||
def props(entity):
|
||||
result = defaultdict(list)
|
||||
for code, value in entity["data"]:
|
||||
result[code].append(value)
|
||||
return result
|
||||
|
||||
|
||||
def first(p, code, default=None):
|
||||
return p[code][0] if code in p else default
|
||||
|
||||
|
||||
def layer(entity):
|
||||
return first(props(entity), 8, "0")
|
||||
|
||||
|
||||
def line_points(entity):
|
||||
p = props(entity)
|
||||
return [(float(first(p, 10)), float(first(p, 20))), (float(first(p, 11)), float(first(p, 21)))]
|
||||
|
||||
|
||||
def normalized_segment(entity, center_x, y0, el0):
|
||||
points = line_points(entity)
|
||||
points.sort(key=lambda p: (round(p[0], 6), p[1]))
|
||||
profile = [[(x - center_x) / 1000, el0 + (y - y0) / 1000] for x, y in points]
|
||||
key = (layer(entity), round((points[0][0] - center_x) / 1000, 3), round((points[1][0] - center_x) / 1000, 3))
|
||||
average = sum(y for x, y in profile) / 2
|
||||
slope = profile[1][1] - profile[0][1]
|
||||
return key, (average, slope), profile
|
||||
|
||||
|
||||
def cross_section(entities, y_bounds, center_x, y0, el0):
|
||||
grouped = defaultdict(list)
|
||||
for entity in entities:
|
||||
if entity["type"] != "LINE" or layer(entity) not in ("CS-CONC-MAJR", "CS-CONC-MINR"):
|
||||
continue
|
||||
points = line_points(entity)
|
||||
if not all(y_bounds[0] <= y <= y_bounds[1] for x, y in points):
|
||||
continue
|
||||
if not all(1_280_000 <= x <= 1_310_000 for x, y in points):
|
||||
continue
|
||||
key, sort_key, profile = normalized_segment(entity, center_x, y0, el0)
|
||||
grouped[key].append({"profile": profile, "sort": sort_key, "handle": first(props(entity), 5, "")})
|
||||
return grouped
|
||||
|
||||
|
||||
def arc_points(entity, steps=16):
|
||||
p = props(entity)
|
||||
cx, cy = float(first(p, 10)), float(first(p, 20))
|
||||
radius = float(first(p, 40))
|
||||
start = math.radians(float(first(p, 50)))
|
||||
end = math.radians(float(first(p, 51)))
|
||||
while end < start:
|
||||
end += 2 * math.pi
|
||||
return [(cx + radius * math.cos(start + (end - start) * i / steps), cy + radius * math.sin(start + (end - start) * i / steps)) for i in range(steps + 1)]
|
||||
|
||||
|
||||
def block_entity_points(entity):
|
||||
p = props(entity)
|
||||
if entity["type"] == "LINE":
|
||||
return [(float(first(p, 10)), float(first(p, 20))), (float(first(p, 11)), float(first(p, 21)))]
|
||||
if entity["type"] == "ARC":
|
||||
return arc_points(entity, 16)
|
||||
return []
|
||||
|
||||
|
||||
def transform_block(points, insert, base):
|
||||
p = props(insert)
|
||||
ix, iy = float(first(p, 10)), float(first(p, 20))
|
||||
sx, sy = float(first(p, 41, "1")), float(first(p, 42, "1"))
|
||||
angle = math.radians(float(first(p, 50, "0")))
|
||||
result = []
|
||||
for x, y in points:
|
||||
x, y = (x - base[0]) * sx, (y - base[1]) * sy
|
||||
result.append((ix + x * math.cos(angle) - y * math.sin(angle), iy + x * math.sin(angle) + y * math.cos(angle)))
|
||||
return result
|
||||
|
||||
|
||||
def intersect_line_arc(line, arc):
|
||||
(x0, y0), (x1, y1) = line
|
||||
p = props(arc)
|
||||
cx, cy, radius = float(first(p, 10)), float(first(p, 20)), float(first(p, 40))
|
||||
dx, dy = x1 - x0, y1 - y0
|
||||
fx, fy = x0 - cx, y0 - cy
|
||||
aa = dx * dx + dy * dy
|
||||
bb = 2 * (fx * dx + fy * dy)
|
||||
cc = fx * fx + fy * fy - radius * radius
|
||||
disc = bb * bb - 4 * aa * cc
|
||||
if disc < 0:
|
||||
return []
|
||||
roots = [(-bb - math.sqrt(disc)) / (2 * aa), (-bb + math.sqrt(disc)) / (2 * aa)]
|
||||
start, end = float(first(p, 50)), float(first(p, 51))
|
||||
points = []
|
||||
for t in roots:
|
||||
if not 0 <= t <= 1:
|
||||
continue
|
||||
x, y = x0 + t * dx, y0 + t * dy
|
||||
theta = math.degrees(math.atan2(y - cy, x - cx)) % 360
|
||||
if start - 0.01 <= theta <= end + 0.01:
|
||||
points.append((theta, x, y))
|
||||
return points
|
||||
|
||||
|
||||
def alignment_units(entities):
|
||||
arcs = []
|
||||
for entity in entities:
|
||||
p = props(entity)
|
||||
if entity["type"] == "ARC" and first(p, 8) == "CS-CNTL" and abs(float(first(p, 40, "0")) - 1_700_000) < 1:
|
||||
arcs.append(entity)
|
||||
cross_lines = []
|
||||
for entity in entities:
|
||||
if entity["type"] != "LINE" or layer(entity) != "CS-CONC-MAJR":
|
||||
continue
|
||||
(x0, y0), (x1, y1) = line_points(entity)
|
||||
if -90_000 < y0 < -60_000 and -90_000 < y1 < -60_000 and abs(y1 - y0) > 19_000 and abs(y1 - y0) < 21_000 and abs(x1 - x0) < 200:
|
||||
cross_lines.append(entity)
|
||||
if len(arcs) != 2:
|
||||
raise ValueError(f"평면 중심선 원호 수가 예상과 다릅니다: {len(arcs)}")
|
||||
center_x_origin = sum(float(first(props(arc), 10)) for arc in arcs) / len(arcs)
|
||||
center_y_origin = float(first(props(arcs[0]), 20)) - float(first(props(arcs[0]), 40))
|
||||
units = []
|
||||
for index, arc in enumerate(sorted(arcs, key=lambda e: float(first(props(e), 10)))):
|
||||
acx = float(first(props(arc), 10))
|
||||
candidates = [line for line in cross_lines if abs(sum(x for x, y in line_points(line)) / 2 - acx) < 12_000]
|
||||
intersections = []
|
||||
boundary_handles = []
|
||||
for line in candidates:
|
||||
found = intersect_line_arc(line_points(line), arc)
|
||||
if found:
|
||||
intersections.extend(found)
|
||||
boundary_handles.append(first(props(line), 5, ""))
|
||||
intersections.sort()
|
||||
if len(intersections) != 2:
|
||||
raise ValueError(f"평면 중심선 {index + 1}의 구조선 교점이 2개가 아닙니다: {len(intersections)}")
|
||||
theta0, theta1 = intersections[0][0], intersections[1][0]
|
||||
radius_m = float(first(props(arc), 40)) / 1000
|
||||
length = radius_m * math.radians(theta1 - theta0)
|
||||
units.append({
|
||||
"id": f"alignment-{index + 1}",
|
||||
"name": f"BLOCK8 평면 중심선 {index + 1}",
|
||||
"center": [(acx - center_x_origin) / 1000, (float(first(props(arc), 20)) - center_y_origin) / 1000],
|
||||
"planOrigin": [center_x_origin, center_y_origin],
|
||||
"radius": radius_m,
|
||||
"startAngle": theta0,
|
||||
"endAngle": theta1,
|
||||
"length": length,
|
||||
"centerlineHandle": first(props(arc), 5, ""),
|
||||
"boundaryHandles": boundary_handles,
|
||||
})
|
||||
return units
|
||||
|
||||
|
||||
def add_block_parts(parts, entities, blocks, center_x, sections):
|
||||
block_name = "A$C2017021411375356"
|
||||
block = blocks[block_name]
|
||||
concrete_entities = [e for e in block["entities"] if layer(e) in ("CS-CONC-MAJR", "CS-CONC-MINR") and e["type"] in ("LINE", "ARC")]
|
||||
inserts = []
|
||||
for entity in entities:
|
||||
if entity["type"] != "INSERT" or first(props(entity), 2) != block_name:
|
||||
continue
|
||||
p = props(entity)
|
||||
x, y = float(first(p, 10)), float(first(p, 20))
|
||||
if 1_280_000 < x < 1_310_000 and any(abs(y - sy) < 100 for sy in (-76_987.31555584459, -96_306.66955584464)):
|
||||
inserts.append(entity)
|
||||
# 각 단면에서 좌우 삽입점 한 개씩 확인한다.
|
||||
section_inserts = []
|
||||
for section in sections:
|
||||
chosen = [e for e in inserts if abs(float(first(props(e), 20)) - section["insertY"]) < 1]
|
||||
chosen.sort(key=lambda e: float(first(props(e), 10)))
|
||||
if len(chosen) != 2:
|
||||
raise ValueError(f"블록 내부 콘크리트 삽입점 수가 예상과 다릅니다: {len(chosen)}")
|
||||
section_inserts.append(chosen)
|
||||
result = []
|
||||
for side_idx, side_name in enumerate(("left", "right")):
|
||||
start_insert, end_insert = section_inserts[0][side_idx], section_inserts[1][side_idx]
|
||||
for entity_idx, block_entity in enumerate(concrete_entities):
|
||||
start_raw = transform_block(block_entity_points(block_entity), start_insert, block["base"])
|
||||
end_raw = transform_block(block_entity_points(block_entity), end_insert, block["base"])
|
||||
start_profile = [[(x - center_x) / 1000, sections[0]["elTop"] + (y - sections[0]["yTop"]) / 1000] for x, y in start_raw]
|
||||
end_profile = [[(x - center_x) / 1000, sections[1]["elTop"] + (y - sections[1]["yTop"]) / 1000] for x, y in end_raw]
|
||||
result.append({
|
||||
"id": f"insert-{side_name}-{entity_idx + 1:02d}",
|
||||
"name": f"블록 내부 윤곽 {side_name} {entity_idx + 1:02d}",
|
||||
"layer": layer(block_entity),
|
||||
"group": "insert",
|
||||
"sourceHandle": first(props(block_entity), 5, ""),
|
||||
"sourceInsertHandles": [first(props(start_insert), 5, ""), first(props(end_insert), 5, "")],
|
||||
"entityType": block_entity["type"],
|
||||
"a": start_profile,
|
||||
"b": end_profile,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
pairs = read_pairs(SOURCE)
|
||||
blocks = parse_blocks(pairs)
|
||||
entities = parse_modelspace(pairs)
|
||||
center_x = 1_296_487.109248054
|
||||
section_defs = [
|
||||
{"id": "A", "station": 319.000, "yBounds": (-80_000, -70_000), "y0": -78_059.37855593952, "el0": 101.777, "yTop": -72_621.37855593952, "elTop": 107.215, "centerY": -77_742.31539793951, "insertY": -76_987.31555584459},
|
||||
{"id": "B", "station": 339.000, "yBounds": (-99_000, -89_000), "y0": -97_286.37855593952, "el0": 101.292, "yTop": -90_721.37855593952, "elTop": 107.857, "centerY": -97_061.6690599395, "insertY": -96_306.66955584464},
|
||||
]
|
||||
grouped_a = cross_section(entities, section_defs[0]["yBounds"], center_x, section_defs[0]["y0"], section_defs[0]["el0"])
|
||||
grouped_b = cross_section(entities, section_defs[1]["yBounds"], center_x, section_defs[1]["y0"], section_defs[1]["el0"])
|
||||
if set(grouped_a) != set(grouped_b):
|
||||
raise ValueError("A-A/B-B 구조선의 X 범위/레이어 구성이 일치하지 않습니다.")
|
||||
parts = []
|
||||
line_no = defaultdict(int)
|
||||
for key in sorted(grouped_a):
|
||||
a_list = sorted(grouped_a[key], key=lambda item: (item["sort"][0], item["sort"][1]))
|
||||
b_list = sorted(grouped_b[key], key=lambda item: (item["sort"][0], item["sort"][1]))
|
||||
if len(a_list) != len(b_list):
|
||||
raise ValueError(f"단면 대응 선 개수가 다릅니다: {key}")
|
||||
for a_item, b_item in zip(a_list, b_list):
|
||||
source_layer = key[0]
|
||||
group = "major" if source_layer == "CS-CONC-MAJR" else "minor"
|
||||
line_no[group] += 1
|
||||
parts.append({
|
||||
"id": f"{group}-{line_no[group]:03d}",
|
||||
"name": f"{source_layer} 구조선 {line_no[group]:03d}",
|
||||
"layer": source_layer,
|
||||
"group": group,
|
||||
"sourceHandles": [a_item["handle"], b_item["handle"]],
|
||||
"entityType": "LINE",
|
||||
"a": a_item["profile"],
|
||||
"b": b_item["profile"],
|
||||
})
|
||||
if line_no != {"major": 108, "minor": 6}:
|
||||
raise ValueError(f"구조선 수 확인 실패: {dict(line_no)}")
|
||||
parts += add_block_parts(parts, entities, blocks, center_x, section_defs)
|
||||
units = alignment_units(entities)
|
||||
for unit in units:
|
||||
unit["parts"] = [dict(part, id=f"{unit['id']}-{part['id']}") for part in parts]
|
||||
data = {
|
||||
"source": "광터지하차도_BLOCK8.dxf",
|
||||
"unit": "m",
|
||||
"coordinateNote": "CAD 좌표 차이를 치수 문자열로 검산해 mm로 해석. 표고는 종단/단면 EL 주석에 연결.",
|
||||
"sections": [
|
||||
{"id": "A-A", "station": 319.000, "centerEL": section_defs[0]["elTop"] + (section_defs[0]["centerY"] - section_defs[0]["yTop"]) / 1000, "topEL": 107.215, "baseTopEL": 101.777, "baseBottomEL": 100.877, "sourceHandles": ["STA.0K+319.000", "CS-CNTL center mark", "EL.102.094", "EL.107.215", "EL.100.877"]},
|
||||
{"id": "B-B", "station": 339.000, "centerEL": section_defs[1]["elTop"] + (section_defs[1]["centerY"] - section_defs[1]["yTop"]) / 1000, "topEL": 107.857, "baseTopEL": 101.292, "baseBottomEL": 100.392, "sourceHandles": ["STA.0K+339.000", "CS-CNTL center mark", "EL.101.517", "EL.107.857", "EL.100.392"]},
|
||||
],
|
||||
"sectionStationRange": [319.000, 339.000],
|
||||
"modelStationRange": [319.010, 338.990],
|
||||
"modelLength": 19.980,
|
||||
"sectionTrim": 0.010,
|
||||
"sectionLineCount": {"CS-CONC-MAJR": 108, "CS-CONC-MINR": 6},
|
||||
"expandedBlock": {"name": "A$C2017021411375356", "layerCounts": {"CS-CONC-MAJR": 18, "CS-CONC-MINR": 0}, "curveCount": 2, "insertCount": 4},
|
||||
"profileWidth": {"dimensionText": 20.300, "outerLineEnvelope": 20.500, "status": "dimension value and outer contour differ; retain separately"},
|
||||
"units": units,
|
||||
}
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"units": len(units), "lengths": [round(u["length"], 6) for u in units], "partsPerUnit": len(parts), "layers": dict(line_no), "expandedBlockPartsPerSide": 20}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write a dependency-free SVG preview from src/model/block8.json."""
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MODEL = ROOT / "src/model/block8.json"
|
||||
OUTPUT = ROOT / "docs/BLOCK8_3D모델_미리보기.svg"
|
||||
COLORS = {"major": "#78917c", "minor": "#d0aa69", "insert": "#9a9074"}
|
||||
|
||||
|
||||
def point_at(unit, profile_a, profile_b, index, t):
|
||||
theta = math.radians(unit["startAngle"] + (unit["endAngle"] - unit["startAngle"]) * t)
|
||||
cx = unit["center"][0] + unit["radius"] * math.cos(theta)
|
||||
cz = unit["center"][1] + unit["radius"] * math.sin(theta)
|
||||
normal_x, normal_z = -math.cos(theta), -math.sin(theta)
|
||||
station_t = (0.010 + t * 19.980) / 20
|
||||
u = profile_a[index][0] + (profile_b[index][0] - profile_a[index][0]) * station_t
|
||||
el = profile_a[index][1] + (profile_b[index][1] - profile_a[index][1]) * station_t
|
||||
return [cx + u * normal_x, el, cz + u * normal_z]
|
||||
|
||||
|
||||
def project(point):
|
||||
x, y, z = point
|
||||
yaw, pitch, scale = -0.66, 0.43, 14.0
|
||||
x1 = x * math.cos(yaw) - z * math.sin(yaw)
|
||||
z1 = x * math.sin(yaw) + z * math.cos(yaw)
|
||||
y1 = y - 104
|
||||
return [600 + x1 * scale, 330 - (y1 * math.cos(pitch) - z1 * math.sin(pitch)) * scale, y1 * math.sin(pitch) + z1 * math.cos(pitch)]
|
||||
|
||||
|
||||
def main():
|
||||
model = json.loads(MODEL.read_text(encoding="utf-8"))
|
||||
faces = []
|
||||
for unit in model["units"]:
|
||||
for part in unit["parts"]:
|
||||
if len(part["a"]) != len(part["b"]):
|
||||
continue
|
||||
for step in range(8):
|
||||
t0, t1 = step / 8, (step + 1) / 8
|
||||
for edge in range(len(part["a"]) - 1):
|
||||
corners = [
|
||||
point_at(unit, part["a"], part["b"], edge, t0),
|
||||
point_at(unit, part["a"], part["b"], edge + 1, t0),
|
||||
point_at(unit, part["a"], part["b"], edge + 1, t1),
|
||||
point_at(unit, part["a"], part["b"], edge, t1),
|
||||
]
|
||||
projected = [project(point) for point in corners]
|
||||
depth = sum(point[2] for point in projected) / 4
|
||||
faces.append((depth, part["group"], projected))
|
||||
faces.sort(key=lambda face: face[0])
|
||||
|
||||
svg = [
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 660" role="img" aria-labelledby="title desc">',
|
||||
'<title id="title">광터지하차도 BLOCK8 도면 기반 3D 구조 모델</title>',
|
||||
'<desc id="desc">두 평면 중심선을 따라 시점과 종점 단면 구조선을 연결한 열린 표면 검토 모델. 일부 배치 대응은 확인 필요.</desc>',
|
||||
'<defs><linearGradient id="bg" x2="0" y2="1"><stop stop-color="#f7faf6"/><stop offset="1" stop-color="#e8eee8"/></linearGradient></defs>',
|
||||
'<rect width="1200" height="660" rx="22" fill="url(#bg)"/>',
|
||||
'<text x="62" y="61" fill="#728579" font-family="sans-serif" font-size="14" font-weight="700" letter-spacing="2">광터지하차도 · 도면 기반 검토 모델</text>',
|
||||
'<text x="62" y="105" fill="#263a32" font-family="sans-serif" font-size="29" font-weight="700">BLOCK8 · 구조선 연결 표면</text>',
|
||||
'<g fill="none" stroke="#d4dfd6" stroke-width="1">',
|
||||
]
|
||||
for x in range(170, 1031, 60):
|
||||
svg.append(f'<path d="M{x} 165 L{x} 540"/>')
|
||||
for y in range(180, 541, 36):
|
||||
svg.append(f'<path d="M145 {y} L1055 {y}"/>')
|
||||
svg.append('</g>')
|
||||
for depth, group, points in faces:
|
||||
coordinates = ' '.join(f'{p[0]:.1f},{p[1]:.1f}' for p in points)
|
||||
fill = COLORS.get(group, COLORS["major"])
|
||||
opacity = 0.64 if group != "insert" else 0.46
|
||||
svg.append(f'<polygon points="{coordinates}" fill="{fill}" fill-opacity="{opacity}" stroke="#53685a" stroke-opacity=".48" stroke-width=".55"/>')
|
||||
|
||||
for idx, unit in enumerate(model["units"]):
|
||||
left = point_at(unit, [[0, 104]], [[0, 104]], 0, 0)
|
||||
right = point_at(unit, [[0, 104]], [[0, 104]], 0, 1)
|
||||
p0, p1 = project(left), project(right)
|
||||
svg.append(f'<g font-family="sans-serif" font-size="12" fill="#55715c"><text x="{p0[0]:.1f}" y="{p0[1]-13:.1f}">중심선 {idx+1} · STA 319.010</text><text x="{p1[0]:.1f}" y="{p1[1]-12:.1f}">STA 338.990</text></g>')
|
||||
|
||||
svg.extend([
|
||||
'<g font-family="sans-serif" font-size="12"><rect x="62" y="570" width="1076" height="54" rx="9" fill="#f8f3e8" stroke="#e5dbc4"/>',
|
||||
'<text x="82" y="593" fill="#8c774e">길이 19.980m · 평면 중심선 교점으로 계산 · 시점/종점 단면 구조선 114개씩 + 블록 내부 윤곽</text>',
|
||||
'<text x="82" y="612" fill="#8c774e">열린 표면 모델이며 닫힌 체적은 아닙니다. 두 중심선에 같은 단면을 적용한 대응은 검토가 필요합니다.</text></g>',
|
||||
'</svg>',
|
||||
])
|
||||
OUTPUT.write_text('\n'.join(svg) + '\n', encoding="utf-8")
|
||||
print(f"{OUTPUT.relative_to(ROOT)}: {len(faces)} polygons")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user