feat(b06): 상세 닫힌 영역 고름 · 면적 · 둘레 · 표 칸 → 그래프 · 표 강조 바깥 한 줄 (259-2 · 259-3)
- 나눈 조각으로 면 찾기(열린 영역 제외) · 영역 안 누름 = 고름 · 채움 강조 · 면적 · 둘레 - 선 고름 경사비율 · 닫힌 선 둘레 · 띠 고름 값 글 - 표 칸 누름 = 저장된 조각 그래프 고름 · 띠 강조 + 값 글 - 고른 칸 묶음 바깥 한 줄(겹선 없음) · 줄 물들임 없앰 · 합친 묶음 이름 칸 키 맞춤 - 259-2 · 259-3 이 같은 파일(상세 표 배선)이라 한 커밋 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
@@ -130,4 +130,93 @@ out.rockSource = S.selectionSource("length", [rockPieces[1]]);
|
||||
out.rockLocal = T.localCellValue(out.rockSource, design, samples);
|
||||
out.rockLength = G.pathLength(rockPieces[1].points);
|
||||
|
||||
/* ③ 닫힌 영역(259-2) — 면 찾기 · 열린 영역 제외 · 영역 고름 값 · 붙은 두 영역 = 맞닿은 변 뺌 */
|
||||
const faces = G.findFaces(pieces);
|
||||
out.faces = faces.map((face) => ({
|
||||
area: face.area,
|
||||
lines: face.edges.map((i) => pieces[i].line).sort(),
|
||||
}));
|
||||
const one = faces.map((face) =>
|
||||
S.regionLabel(
|
||||
[face],
|
||||
face.edges.map((i) => pieces[i]),
|
||||
),
|
||||
);
|
||||
out.faceLabels = one;
|
||||
const union = S.regionEdges(faces);
|
||||
const unionPieces = union.map((i) => pieces[i]);
|
||||
out.union = {
|
||||
lines: unionPieces.map((p) => p.line).sort(),
|
||||
label: S.regionLabel(faces, unionPieces),
|
||||
selectLabel: S.selectionLabel(design, unionPieces),
|
||||
source: S.selectionSource("area", unionPieces),
|
||||
};
|
||||
out.union.local = T.localCellValue(out.union.source, design, samples);
|
||||
// 열린 원지반만 = 면 없음
|
||||
out.openFaces = G.findFaces(pieces.filter((p) => p.line === "ground")).length;
|
||||
// 정사각형 하나 + 매달린 꼬리 — 꼬리 지우고 면 하나(넓이 1)
|
||||
out.square = G.findFaces([
|
||||
{
|
||||
line: "ground",
|
||||
points: [
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
line: "design",
|
||||
points: [
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
[0, 1],
|
||||
],
|
||||
},
|
||||
{
|
||||
line: "rock",
|
||||
points: [
|
||||
[0, 1],
|
||||
[0, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
line: "spoil",
|
||||
points: [
|
||||
[1, 1],
|
||||
[3, 3],
|
||||
],
|
||||
},
|
||||
]).map((face) => ({ area: face.area, edges: face.edges.sort() }));
|
||||
|
||||
/* ④ 표 칸 → 그래프(259-2) — 저장된 조각 짝 · 칸 그래프 짝 · 띠 값 글 */
|
||||
const stored = G.pieceSource(rockPieces[1]);
|
||||
out.same = {
|
||||
forward: S.samePiece(rockPieces[1], stored),
|
||||
reverse: S.samePiece(rockPieces[1], { ...stored, start: stored.end, end: stored.start }),
|
||||
otherLine: S.samePiece(rockPieces[1], { ...stored, line: "ground" }),
|
||||
otherPiece: S.samePiece(rockPieces[0], stored),
|
||||
};
|
||||
const table = {
|
||||
cells: {
|
||||
fence: out.rockSource,
|
||||
cut_soil: { src: "area", key: "cut_rock" },
|
||||
"r:r1:0": { src: "text", text: "글" },
|
||||
},
|
||||
rows: [],
|
||||
columns: [],
|
||||
};
|
||||
out.cellGraph = ["fence", "cut_soil", "r:r1:0", "embankment"].map((key) => T.cellGraph(table, key));
|
||||
out.bandLabel = [
|
||||
T.bandLabel("cut_soil", { cut_soil_area_m2: 1.234 }),
|
||||
T.bandLabel("cut_soil", {}),
|
||||
T.bandLabel("ground", { cut_soil_area_m2: 1 }),
|
||||
];
|
||||
|
||||
/* ⑤ 표 강조 바깥 한 줄(259-3) — 합친 묶음 이름 칸(두 줄) + 둘째 줄 이름 · 값 = ㄴ 모양 */
|
||||
const P = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Pick.ts"));
|
||||
out.outline = P.unionOutline([
|
||||
[0, 0, 10, 20],
|
||||
[10, 10, 20, 20],
|
||||
[20, 10, 30, 20],
|
||||
]);
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
|
||||
@@ -80,8 +80,9 @@ def test_상세_표_칸_키_누름_배선():
|
||||
) as f:
|
||||
src = f.read()
|
||||
assert "element.dataset.key = key" in src
|
||||
assert "highlightDetail(svg, state.section.design, picked)" in src
|
||||
assert 'closest<HTMLElement>("[data-key]")' in src
|
||||
# 259-2 — 저장 띠 출처면 그 띠 · 저장 조각이면 강조 없음(그래프 고름) · 값 칸(data-cell)도 누름
|
||||
assert "highlightDetail(svg, state.section.design, band)" in src
|
||||
assert 'closest<HTMLElement>("[data-key], [data-cell]")' in src
|
||||
|
||||
|
||||
def test_상세_큰_창_공용_막_그림_글_표_순서():
|
||||
@@ -93,6 +94,8 @@ def test_상세_큰_창_공용_막_그림_글_표_순서():
|
||||
css = f.read().replace(chr(13), "")
|
||||
assert "wide: true" in src and "backdropClass" not in src
|
||||
assert "body.append(cad, note, tableBox)" in src
|
||||
assert "pointer-events: none" not in css and "b06-detail__backdrop" not in css
|
||||
# 막 없음 — 259-3 표 강조 바깥 줄(누름 통과)만 pointer-events: none
|
||||
backdrop = css.split(".b06-detail__pick-outline {")[0]
|
||||
assert "pointer-events: none" not in backdrop and "b06-detail__backdrop" not in css
|
||||
assert ".ui-modal.b06-detail > .ui-modal__body {\n overflow: hidden;" in css
|
||||
assert "flex: 1;\n min-height: 0;" in css and "height: 100%;" in css # 173-4 칸 가득
|
||||
|
||||
@@ -105,10 +105,12 @@ def test_강조_붉은_한_벌_고름_hover_다름_빈칸():
|
||||
".b06-detail__cad .b06-chart__center.is-active line {\n stroke: var(--color-danger);",
|
||||
".b06-detail__cad .b06-chart__spoil-fill.is-active {\n stroke: var(--color-danger);",
|
||||
".b06-detail__qtable .is-picked {\n background: color-mix(in srgb, var(--color-danger) 18%",
|
||||
".b06-detail__qtable tr:has(.is-picked) {",
|
||||
".b06-detail__qtable [data-key][data-empty] {\n cursor: default;",
|
||||
):
|
||||
assert selector in css, selector
|
||||
# 259-3 — 줄 물들임 · 칸마다 테두리 없앰(묶음 바깥 한 줄)
|
||||
assert "tr:has(.is-picked)" not in css
|
||||
assert "inset 0 0 0 1px var(--color-danger)" not in css
|
||||
assert "royal-amethyst" not in css and "--color-warning);" not in css.split("/* 그림 강조")[1]
|
||||
assert (
|
||||
".b06-detail__qtable [data-key]:hover {\n background: color-mix(in srgb, var(--color-danger) 8%"
|
||||
@@ -118,11 +120,13 @@ def test_강조_붉은_한_벌_고름_hover_다름_빈칸():
|
||||
|
||||
def test_빈칸_누름_없음_이름_칸_첫_키():
|
||||
src = _read("B06_Section_UI_Cross_Detail.ts")
|
||||
assert 'el.toggleAttribute("data-empty", !present.has(el.dataset.key ?? ""))' in src
|
||||
assert 'if (!key || el.hasAttribute("data-empty")) return;' in src
|
||||
# 179-4 — 묶음 이름 칸 = 하위 줄만큼(양식 2줄 · 더한 줄만큼 늘어남) · 그 줄 첫 값 칸 키
|
||||
assert "nameCell(groupL, { row: groupSpan(layout.left, index) }, keyL, id, 0)" in src
|
||||
assert "nameCell(groupM, { row: groupSpan(layout.middle, index) }, keyM, id, 1)" in src
|
||||
assert "!present.has(el.dataset.key) && !tablePick.graph(el.dataset.key)" in src
|
||||
assert '(!el.dataset.key || el.hasAttribute("data-empty"))) return;' in src
|
||||
# 179-4 — 묶음 이름 칸 = 하위 줄만큼(양식 2줄 · 더한 줄만큼 늘어남) · 그 줄 첫 값 칸 키 ·
|
||||
# 259-3 덮는 줄 키 모두(data-keys)
|
||||
flat = " ".join(src.split())
|
||||
assert "nameCell( groupL, { row: groupSpan(layout.left, index) }, keyL, id, 0," in flat
|
||||
assert "nameCell( groupM, { row: groupSpan(layout.middle, index) }, keyM, id, 1," in flat
|
||||
|
||||
|
||||
def test_표_글자_14px_이름_칸_머리색_빈칸_값칸만():
|
||||
|
||||
@@ -117,11 +117,11 @@ def test_원지반_포함_닫힌_영역_넓이(out):
|
||||
def test_정보창_값(out):
|
||||
label = out["label"]
|
||||
length = 2 * hypot(4 / 3, 2) + 4 + 2 * CROSS_X
|
||||
assert label["ring"] == f"4개 · 길이 합 {length:.2f}m\n면적 {AREA:.2f}㎡"
|
||||
assert label["ring"] == f"4개 · 길이 합 {length:.2f}m\n면적 {AREA:.2f}㎡ · 둘레 {length:.2f}m"
|
||||
assert label["open"].endswith("면적 닫히지 않음")
|
||||
assert label["single"] == "길이 4.00m · 경사비율 0.0%\n수평 4.00m · 높이 0.00m" # 카드 꼴
|
||||
assert label["ground"] == (
|
||||
f"원지반선 길이 {2 * CROSS_X:.2f}m\n수평 {2 * CROSS_X:.2f}m · 높이 0.00m"
|
||||
f"원지반선 길이 {2 * CROSS_X:.2f}m · 경사비율 0.0%\n수평 {2 * CROSS_X:.2f}m · 높이 0.00m"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
거리이정표 꼴(빈 곳 왼쪽 끌기 팬)은 끌기 끝 click 만 막음.
|
||||
- 259-1 암 경계선 — 설계선과 만나는 점에서 나뉨 · 고름 값 · 저장 꼴 · 서버 `pick_lines` 거울 ·
|
||||
암 프리셋 아니면 다시 고름.
|
||||
- 259-2 닫힌 영역 — 면 찾기(열린 영역 · 매달린 꼬리 제외) · 영역 고름 값(면적 · 둘레) · 붙은 영역 =
|
||||
맞닿은 변 뺌 · 서버 거울 · 표 칸 → 저장된 조각 · 띠 짝 · 띠 값 글.
|
||||
- 259-3 표 강조 — 고른 칸 묶음 바깥 한 줄(칸 사이 겹선 없음).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_259_detail_region.py -q
|
||||
"""
|
||||
@@ -13,6 +16,7 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from math import hypot
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -45,6 +49,7 @@ AXES = {
|
||||
"exaggeration": 2,
|
||||
}
|
||||
ROCK_X = 2 + 4 / 6 # 경사(4 : 6)가 암 경계선 99 m 와 만나는 offset
|
||||
GROUND_X = 2 + 4 / 3 # 경사가 원지반 100 m 와 만나는 offset
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||
@@ -98,7 +103,9 @@ def test_암_경계선이_설계선과_만나는_점에서_나뉨(out):
|
||||
def test_암_경계선_고름_값_저장_꼴(out):
|
||||
length = 2 * ROCK_X
|
||||
assert out["rockLength"] == pytest.approx(length, abs=1e-3)
|
||||
assert out["rockLabel"] == f"암 경계선 길이 {length:.2f}m\n수평 {length:.2f}m · 높이 0.00m"
|
||||
assert out["rockLabel"] == (
|
||||
f"암 경계선 길이 {length:.2f}m · 경사비율 0.0%\n수평 {length:.2f}m · 높이 0.00m"
|
||||
)
|
||||
source = out["rockSource"]
|
||||
assert source["src"] == "length" and source["pieces"][0]["line"] == "rock"
|
||||
patch = CrossSectionPatch(chainage_m=40.0, detail_table={"cells": {"fence": source}})
|
||||
@@ -113,3 +120,84 @@ def test_암_경계선_서버_거울(out):
|
||||
soil = {**DESIGN, "geometry_preset": "soil"}
|
||||
assert pick_lines(soil, SAMPLES)["rock"] == []
|
||||
assert cell_value(out["rockSource"], soil, SAMPLES) == (None, None, True)
|
||||
|
||||
|
||||
# ── 259-2 닫힌 영역 · 영역 고름 · 면적 ────────────────────────────────────
|
||||
|
||||
SLOPE_1M = hypot(4 / 6, 1.0) # 경사 조각 높이 1 m 길이
|
||||
FACE_TOP = (2 * GROUND_X + 2 * ROCK_X) / 2 # 원지반 100 ~ 암 99 사이 사다리꼴
|
||||
FACE_LOW = (2 * ROCK_X + 4) / 2 # 암 99 ~ 바닥 98 사이 사다리꼴
|
||||
|
||||
|
||||
def test_면_찾기_열린_영역_제외(out):
|
||||
faces = sorted(out["faces"], key=lambda f: -f["area"])
|
||||
assert [f["area"] for f in faces] == pytest.approx([FACE_TOP, FACE_LOW], abs=1e-6)
|
||||
assert faces[0]["lines"] == ["design", "design", "ground", "rock"]
|
||||
assert faces[1]["lines"] == ["design", "design", "design", "rock"]
|
||||
assert out["openFaces"] == 0 # 원지반만 = 열림
|
||||
assert out["square"] == [{"area": pytest.approx(1.0), "edges": [0, 1, 2]}] # 꼬리 지움
|
||||
|
||||
|
||||
def test_영역_고름_면적_둘레(out):
|
||||
top = 2 * GROUND_X + 2 * ROCK_X + 2 * SLOPE_1M
|
||||
low = 2 * ROCK_X + 4 + 2 * SLOPE_1M
|
||||
assert sorted(out["faceLabels"]) == sorted(
|
||||
[
|
||||
f"영역 면적 {FACE_TOP:.2f}㎡\n둘레 {top:.2f}m",
|
||||
f"영역 면적 {FACE_LOW:.2f}㎡\n둘레 {low:.2f}m",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_붙은_두_영역_맞닿은_변_뺌(out):
|
||||
union = out["union"]
|
||||
assert union["lines"] == ["design"] * 5 + ["ground"] # 암 경계선 빠짐
|
||||
area = FACE_TOP + FACE_LOW
|
||||
around = 2 * GROUND_X + 4 + 4 * SLOPE_1M
|
||||
assert union["label"] == f"영역 2개 · 면적 {area:.2f}㎡\n둘레 {around:.2f}m"
|
||||
assert (
|
||||
union["selectLabel"]
|
||||
== f"6개 · 길이 합 {around:.2f}m\n면적 {area:.2f}㎡ · 둘레 {around:.2f}m"
|
||||
)
|
||||
assert union["local"] == pytest.approx(area, abs=1e-3)
|
||||
number, _text, again = cell_value(union["source"], DESIGN, SAMPLES)
|
||||
assert not again and number == pytest.approx(union["local"], abs=1e-6)
|
||||
|
||||
|
||||
# ── 259-2 표 칸 → 그래프 ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_표_칸_저장된_조각을_그래프에서_고름(out):
|
||||
assert out["same"] == {
|
||||
"forward": True,
|
||||
"reverse": True,
|
||||
"otherLine": False,
|
||||
"otherPiece": False,
|
||||
}
|
||||
pieces, band, text, none = out["cellGraph"]
|
||||
assert pieces == {"pieces": out["rockSource"]["pieces"]}
|
||||
assert band == {"band": "cut_rock"}
|
||||
assert text is None and none is None
|
||||
|
||||
|
||||
def test_띠_값_글(out):
|
||||
assert out["bandLabel"] == ["깍기(토사) 면적 1.23㎡", None, None]
|
||||
|
||||
|
||||
# ── 259-3 표 강조 바깥 한 줄 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_고른_칸_묶음_바깥_한_줄(out):
|
||||
parts = {"M" + p for p in out["outline"].split("M") if p}
|
||||
assert parts == {
|
||||
"M0 0H10",
|
||||
"M0 0V10",
|
||||
"M10 0V10",
|
||||
"M0 20H10",
|
||||
"M0 10V20",
|
||||
"M10 10H20",
|
||||
"M10 20H20",
|
||||
"M20 10H30",
|
||||
"M20 20H30",
|
||||
"M30 10V20",
|
||||
} # 칸 사이(10 · 20 세로) 겹선 없음
|
||||
|
||||
Reference in New Issue
Block a user