Merge remote-tracking branches 'origin/sub_laptop_2' and 'origin/sub_laptop_6' into main_laptop_1

This commit is contained in:
2026-10-08 18:29:38 +09:00
18 changed files with 485 additions and 86 deletions
+173
View File
@@ -0,0 +1,173 @@
"""B07 평면도 종류마다 축척 고르기 (PLAN 119장).
① 축척 목록 1/600 · 1/1,000 · 1/1,200(기본) — 추가 · 업데이트 · 다시 읽어 같음 · 목록 밖 거절 ·
옛 파일(축척 없음 · 목록 밖) = 1/1,200 · 유역도 양식 = 1/6,000 고정
② 1/600 → 장 수 늚 · 1/1,200 = 지금과 같음(기본 인자 장 나눔)
③ 그림 — 축척 글 「S = 1/600」 · 도곽 축척 칸 600 · A3 1200 · 1/1,200 은 기본 인자와 같은 그림
④ 종이 mm → 실거리 환산이 축척 따라감 — 측점 눈금 · 시종점 막대 종이 길이 그대로 ·
IP 표 밑 자름 폭 · 필지 라벨 상자 실거리 절반
"""
import json
import math
from pathlib import Path
import pytest
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import _COLUMNS, _ROW_H, ip_table_piece
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import plan_chunks
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import (
build_plan_kind_drawing,
layer_id,
)
from B07_DesignDetail.B07_DesignDetail_Router_PlanKinds import _state
from B07_DesignDetail.B07_DesignDetail_Router_Support_Landuse import label_rects
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
_kinds_dir,
add_kind,
list_kinds,
plan_kind_items,
update_kind,
)
_STATIONS = [
{"chainage_m": float(c), "center_x": 0.0, "center_y": float(c)} for c in range(0, 2001, 20)
]
def test_scale_choices_add_update_and_old_files(tmp_path: Path) -> None:
root = tmp_path / "project"
state = _state(root)
assert state["scales"] == [600, 1000, 1200] and state["default_scale"] == 1200
assert all(k["scale"] == (6000 if k["layout"] == "watershed" else 1200) for k in state["kinds"])
added = add_kind(root, "용지 600", ["contour"], "plan", 600)
assert added["scale"] == 600
assert next(k for k in list_kinds(root) if k["id"] == added["id"]) == added
assert add_kind(root, "기본 축척", ["contour"])["scale"] == 1200
update_kind(root, added["id"], "용지 1000", ["contour"], 1000)
assert next(k for k in list_kinds(root) if k["id"] == added["id"])["scale"] == 1000
update_kind(root, added["id"], "용지 1000", ["contour"]) # 축척 없이 = 지금 값
assert next(k for k in list_kinds(root) if k["id"] == added["id"])["scale"] == 1000
with pytest.raises(ValueError):
add_kind(root, "이상한 축척", ["contour"], "plan", 500)
with pytest.raises(ValueError):
update_kind(root, added["id"], "용지 1000", ["contour"], 777)
watershed = add_kind(root, "유역 둘", [], "watershed", 600) # 유역도 양식은 고정
assert watershed["scale"] == 6000
folder = _kinds_dir(root)
for stem, extra in (("old", {}), ("odd", {"scale": 777})):
(folder / f"{stem}.json").write_text(
json.dumps({"name": stem, "pieces": ["contour"], "order": 99, **extra}), "utf-8"
)
kinds = {k["id"]: k for k in list_kinds(root)}
assert kinds["old"]["scale"] == 1200 and kinds["odd"]["scale"] == 1200
def test_sheet_count_follows_scale(tmp_path: Path) -> None:
root = tmp_path / "project"
add_kind(root, "용지 600", ["contour"], "plan", 600)
items = plan_kind_items(root, {"stations": _STATIONS})
big = [item for item in items if item["plan_kind_name"] == "용지 600"]
terrain = [item for item in items if item["plan_kind"] == "terrain"]
stations = [(s["chainage_m"], s["center_x"], s["center_y"]) for s in _STATIONS]
assert len(terrain) == len(plan_chunks(stations)) == len(plan_chunks(stations, 1200))
assert len(big) == len(plan_chunks(stations, 600)) > len(terrain)
assert big[-1]["label"].endswith(f"{len(big)}장")
def _texts(drawing: dict) -> str:
return json.dumps(drawing["entities"], ensure_ascii=False)
def _frame_labels(drawing: dict) -> set[str]:
return {
(e.get("shapeData") or {}).get("label")
for e in drawing["entities"]
if e.get("layerId") == "b08-frame" and e["type"] == "Text"
}
def _segment(entity: dict) -> tuple[float, float, float]:
"""두 점 PolyLine(자식 Line 하나) — (길이, 가운데 x, 가운데 y) 종이 mm."""
(child,) = entity["children"]
a, b = child["shapeData"]["startPoint"], child["shapeData"]["endPoint"]
return (
math.hypot(b["x"] - a["x"], b["y"] - a["y"]),
(a["x"] + b["x"]) / 2.0,
(a["y"] + b["y"]) / 2.0,
)
def test_drawing_scale_text_frame_and_paper_lengths() -> None:
box = (0.0, 0.0, 400.0, 300.0)
pieces = {
"station": {
"items": [
{"x": 100.0, "y": 100.0, "nx": 1.0, "ny": 0.0, "label": "NO.5", "major": True}
]
},
"endpoints": {"items": [{"label": "시점", "x": 50.0, "y": 50.0, "nx": 0.0, "ny": 1.0}]},
}
base = build_plan_kind_drawing("plan_x", "시험", box, pieces)
assert build_plan_kind_drawing("plan_x", "시험", box, pieces, 1200) == base
small = build_plan_kind_drawing("plan_x", "시험", box, pieces, 600)
assert "S = 1/1,200" in _texts(base) and "S = 1/600" in _texts(small)
assert "S = 1/1,200" not in _texts(small)
scales = [
e
for e in small["entities"]
if str((e.get("shapeData") or {}).get("label")).startswith("S = ")
]
assert [e["shapeData"]["label"] for e in scales] == ["S = 1/600"]
# 도곽 축척 칸 — A1 분모 600 · A3 1200 (1/1,200 그림은 1200 · 2400)
assert {"600", "1200"} <= _frame_labels(small) and "2400" not in _frame_labels(small)
assert {"1200", "2400"} <= _frame_labels(base) and "600" not in _frame_labels(base)
# 종이 mm 기호는 축척과 무관하게 같은 길이 · 실거리 자리는 축척만큼 멀어짐
for piece, paper_mm in (("station", 3.0), ("endpoints", 12.0)):
lines = [
[
e
for e in d["entities"]
if e.get("layerId") == layer_id(piece) and e["type"] != "Text"
]
for d in (base, small)
]
(length, x, y), (length_s, x_s, y_s) = _segment(lines[0][0]), _segment(lines[1][0])
assert length == pytest.approx(paper_mm) and length_s == pytest.approx(paper_mm)
assert (x_s, y_s) == (pytest.approx(2 * x), pytest.approx(2 * y)) # 실거리 자리는 2배
# test_b07_plan_kinds 의 IP 표 시험과 같은 노선 · 곡선
_ROUTE = [(0.0, 0.0, 0.0), (85.0, 85.0, 0.0), (115.0, 100.0, 15.0), (200.0, 100.0, 100.0)]
_CURVES = [
{"apex": [500.0, 500.0], "start": [490.0, 500.0], "end": [500.0, 510.0], "radius_m": 10.0},
{"apex": [100.0, 0.0], "start": [85.0, 0.0], "end": [100.0, 15.0], "radius_m": 15.0},
]
def _gap(mm: float) -> tuple[float, int]:
"""y = 0 등고선에서 잘려 나간 길이(m) · 그 줄을 덮는 표 수."""
box = (-20.0, -20.0, 300.0, 200.0)
pieces = {"contour": {"lines": [[[-20.0, 0.0], [300.0, 0.0]]]}}
table = ip_table_piece(
pieces, _CURVES, _ROUTE, box, {"number": 1, "start_m": 0.0, "end_m": 200.0}, 1, mm
)
kept = sum(line[-1][0] - line[0][0] for line in pieces["contour"]["lines"])
covering = sum(
1
for b in table["blocks"]
if box[1] + (b["origin"][1] - b["rows"] * _ROW_H) / mm < 0.0 < box[1] + b["origin"][1] / mm
)
return 320.0 - kept, covering
def test_ip_table_clip_and_parcel_label_follow_scale() -> None:
assert _gap(1000.0 / 1200) == (pytest.approx(43.2), 1) # 지금 그림(test_b07_plan_kinds)과 같음
# 표 종이 폭 36 mm 그대로 → 1/600 실거리 21.6 m 씩(덮는 표마다)
removed, covering = _gap(1000.0 / 600)
assert covering >= 1 and removed == pytest.approx(sum(_COLUMNS) * 0.6 * covering)
items = [{"lines": ["1-1", "임야"], "center": [0.0, 0.0]}]
(big,) = label_rects(items)
(half,) = label_rects(items, 1000.0 / 600)
assert half[2] - half[0] == pytest.approx((big[2] - big[0]) / 2)
assert half[3] - half[1] == pytest.approx((big[3] - big[1]) / 2)
+2
View File
@@ -247,6 +247,7 @@ def test_update_kind_default_and_release_confirmed(tmp_path: Path) -> None:
"name": "지형2",
"pieces": ["contour", "centerline"],
"layout": "plan",
"scale": 1200,
}
assert [k["id"] for k in list_kinds(root)][0] == "terrain" # 차례 그대로
drawings = _read_manifest(root)["drawings"]
@@ -375,6 +376,7 @@ def test_watershed_kind_layout_one_sheet(tmp_path: Path) -> None:
"name": "배수유역도",
"pieces": [],
"layout": "watershed",
"scale": 6000,
}
# 측점 0 ~ 2,000 m — 1/1,200 평면도는 여러 장 · 유역도는 한 장
stations = [
+55 -1
View File
@@ -2,6 +2,7 @@
- 114-1 면적 상세를 열 때 화면(`sentRanges` + 책)이 `도면_{연결}` 이름정의를 둠 → 행 넣기 → 면적 [저장]
→ 서버 `refresh_drawing` 이 그 이름을 연결표 줄에 적고 도면 표 칸 글 = 옮겨진 칸.
- 114-2 셀 밀기 · 당기기(칸 삽입 · 삭제 창)에 이름 따라감 · 카드 도면 저장도 표 없는 연결표 줄 뺌(`fitLinks`).
TS 는 Node 로(`AreaToDrawing` 은 그 자리 변환 · 스프레드시트 모듈은 `helper_spreadsheet_d_bundle.cjs`).
"""
@@ -66,7 +67,36 @@ const { applyCommand } = ss("spreadsheet_commands");
names: book.시트[0].이름정의,
};
applyCommand(book, { 종류: "행넣기", 시트: "s1", at: 0, 수: 1 }); // 1행 위에 한 행
console.log(JSON.stringify({ opened, book }));
// 114-2 — 셀 밀기 · 당기기(칸 삽입 · 삭제 창)에 이름 따라감 · 되돌리기 한 걸음
const dlg = ss("spreadsheet_insert_dialog");
const E7J17 = { r0: 6, c0: 4, r1: 16, c1: 9 };
const shift = (fn, range) => {
const b = { 종류: "통합문서", 판: 1, 열: "t", 서식: [{}], 시트: [
{ id: "s1", 이름: "s1", 칸: { E7: { 값: 1 } }, 이름정의: [{ 이름: "도면_T1", 범위: E7J17 }] },
] };
const sent = [];
const ctx = { book: b, readOnly: false, selection: { 시트: "s1" }, sheet: () => b.시트[0],
dispatch: (c) => (sent.push(c.종류 === "묶음" ? c.명령.map((x) => x.종류).join("+") : c.종류), applyCommand(b, c)) };
dlg[fn](ctx, range);
const n = b.시트[0].이름정의.map((x) => x.범위);
return { names: n, sent, cell: Object.keys(b.시트[0].칸) };
};
const shifted = {
downInside: shift("insertShiftDown", { r0: 9, c0: 4, r1: 9, c1: 9 }),
downAbove: shift("insertShiftDown", { r0: 4, c0: 4, r1: 4, c1: 9 }),
downPartial: shift("insertShiftDown", { r0: 4, c0: 4, r1: 4, c1: 5 }),
upAll: shift("deleteShiftUp", E7J17),
right: shift("insertShiftRight", { r0: 6, c0: 3, r1: 16, c1: 3 }),
left: shift("deleteShiftLeft", { r0: 6, c0: 0, r1: 16, c1: 1 }),
};
// 114-2 — 카드 도면 저장도 표 없는 연결표 줄을 뺌(도면 상세 저장과 같은 몸)
const tbl = (id) => ({ type: "Table", shapeData: { cells: [[{ text: "x" }]], style: { 연결: id } } });
const row = (id) => ({ 연결: id, 시트: "s1", 범위: "A1", 값: {} });
const fit = {
kept: toDrawing.fitLinks({ 도면: { entities: [tbl("T1")] }, 연결표: [row("T1"), row("T2")] }).연결표,
plain: "연결표" in toDrawing.fitLinks({ 도면: { entities: [tbl("T9")] } }),
};
console.log(JSON.stringify({ opened, book, shifted, fit }));
})().catch((e) => {
console.error(e);
process.exit(1);
@@ -134,3 +164,27 @@ def test_옛_연결_열기_행_넣기_저장_도면_표_값_따라감_114_1(worl
cells = doc["도면"]["entities"][0]["shapeData"]["cells"]
assert _texts(cells) == _texts(want["표"]["shapeData"]["cells"]) # 옮겨진 칸 = 보낸 그 칸
assert _texts(cells)[0] == ["제목", "1,234.50"]
def test_셀_밀기_당기기에_이름_따라감_띠에_걸치기만_하면_그대로_114_2(world): # noqa: F811
with store.at(world["system"]):
area = store.read("area", "rv")
s = _node({"book": area["문서"], "drawing": {"연결표": []}})["shifted"]
box = lambda r0, c0, r1, c1: [{"r0": r0, "c0": c0, "r1": r1, "c1": c1}] # noqa: E731
assert s["downInside"]["names"] == box(6, 4, 17, 9) # 범위 안에 밀어 넣음 = 늘어남 E7:J18
assert s["downAbove"]["names"] == box(7, 4, 17, 9) # 위에서 밀림 = 통째로 E8:J18
assert s["downPartial"]["names"] == box(6, 4, 16, 9) # 띠(E:F)에 걸치기만 = 그대로
assert s["upAll"]["names"] == [] # 다 지운 자리 = 이름 빠짐(저장 때 연결 끊김)
assert s["right"]["names"] == box(6, 5, 16, 10) # F7:K17
assert s["left"]["names"] == box(6, 2, 16, 7) # C7:H17
assert s["downInside"]["sent"] == ["시트통째+칸"] # 한 묶음 = 되돌리기 한 걸음
assert s["downPartial"]["sent"] == ["칸"] # 안 바뀌면 시트통째 안 끼움
assert s["downAbove"]["cell"] == ["E8"] # 값도 함께 밀림
def test_카드_도면_저장도_표_없는_연결표_줄_뺌_114_2(world): # noqa: F811
with store.at(world["system"]):
area = store.read("area", "rv")
fit = _node({"book": area["문서"], "drawing": {"연결표": []}})["fit"]
assert fit["kept"] == [{"연결": "T1", "시트": "s1", "범위": "A1", "값": {}, "장": ""}]
assert fit["plain"] is False # 기록 없던 도면은 칸을 안 만듦
+2 -2
View File
@@ -40,9 +40,9 @@ def test_메뉴는_숨기고_저장은_문서_전체():
assert part in card.split("const HIDE_EDIT")[1].split(";\n")[0]
# 도번 · 열 · 판 은 읽은 그대로 · 모든 장(지금 탭 하나가 1장을 덮던 흠 · 105-3)
assert (
'const next = { ...doc, ...((await handle.getDoc()) as Pick<StructureDoc, "도면" | "더한장">) };'
'const next = fitLinks({\n ...doc,\n ...((await handle.getDoc()) as Pick<StructureDoc, "도면" | "더한장">),\n });'
in card
)
) # 지운 표의 연결표 줄은 뺌(114-2 · 도면 상세 저장과 같은 몸)
assert "async function doSave(): Promise<void>" in card
assert 'write("structure", opt.column, version, next)' in card
for key in ("L · PL", "E · Delete", "Ctrl+Z · Ctrl+Y", "Ctrl+S"):