fix(B06 교차 원): 절토 · 성토 바뀜 자리(노견 끝 = 원지반)만 원 · 평소 투명 · 올리거나 고르면 보임 (414-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
2026-10-18 12:15:29 +09:00
co-authored by Claude Opus 5.5
parent 19ab7d54b9
commit e488605d72
4 changed files with 44 additions and 25 deletions
@@ -441,7 +441,7 @@ export function attachDesignLinePick(input: {
stationId?: string;
/** 있으면 중심점 고름(끝 조각 · 키 `center`) — 그 측점 누가거리(356-3). */
center?: number;
/** 있으면 쪽마다 교차점 손잡이(설계선 ∩ 원지반 · 키 `toe:left|right` · 늘 보임 · 370-2). */
/** 있으면 쪽마다 교차점 손잡이(설계선 ∩ 원지반 · 키 `toe:left|right` · `bare` 쪽만 · 370-2 · 414-1). */
toes?: ReturnType<typeof toePoints>;
/** 있으면 쪽마다 노견 끝 = 원지반(절·성토 없음 · 379-1) — 손잡이 값 앞 알림. */
bare?: ReturnType<typeof edgeOnGround>;
@@ -476,7 +476,10 @@ export function attachDesignLinePick(input: {
: edges.right.elevation_m -
((edges.left.elevation_m - edges.right.elevation_m) * edges.right.offset_m) /
(edges.left.offset_m - edges.right.offset_m);
const toeSides = (["left", "right"] as const).filter((at) => input.toes?.[at]);
// 손잡이 = 그 쪽이 절 ↔ 성 바뀜 자리(노견 끝 = 원지반 · `bare`)일 때만(414-1)
const toeSides = (["left", "right"] as const).filter(
(at) => input.toes?.[at] && input.bare?.[at],
);
const rockLines = input.rock ? kids.filter((el) => el.classList.contains(ROCK_LINE_CLASS)) : [];
const keys = [
...pieceKeys(design, pieces),
@@ -502,7 +505,7 @@ export function attachDesignLinePick(input: {
// 중심점(356-3) = 끝 조각 · 판정 원은 고르기 차례가 보라 선 위로(`stackPickTargets`)
const center = centerZ === null ? null : centerHit(x0, toDisplayY(centerZ));
if (center) hits.push(center);
// 교차점 손잡이(370-2) — 쪽마다 원 · 늘 보임 · 고르면 그 쪽 사면 ◀▶(중심점 원과 같은 고르기 차례)
// 교차점 손잡이(370-2) — 바뀜 자리 쪽 원 · 평소 투명 · 고르면 그 쪽 사면 ◀▶(중심점 원과 같은 고르기 차례)
for (const at of toeSides) {
const toe = input.toes?.[at] as { offset_m: number; elevation_m: number };
const hit = centerHit(x(toe.offset_m), toDisplayY(toe.elevation_m));
@@ -102,16 +102,15 @@
cursor: ew-resize;
}
/* 교차점 손잡이(370-2) — 사면 ∩ 원지반 작은 원 · 늘 보임 · 고르면 채움 · 올리면 같은 모양(379-1). */
/* 교차점 손잡이(370-2 · 414-1) — 절 ↔ 성 바뀜 자리 작은 원 · 평소 투명 · 올리거나 고르면 채움. */
.b06-chart__toe-hit {
fill: var(--color-surface);
stroke: var(--color-royal-amethyst);
stroke-width: 1.5;
}
.b06-chart__toe-hit:hover,
.b06-chart__toe-hit.is-active {
fill: var(--color-royal-amethyst);
stroke: var(--color-royal-amethyst);
cursor: pointer;
}
+23 -11
View File
@@ -241,7 +241,7 @@ out.stop = {
};
}
// ② 교차점 손잡이 — 쪽마다 원 · 늘 보임 · 키 toe:left|right · 고르면 ◀▶ 가 멈춤 자리로 보냄
// ② 교차점 손잡이 — 바뀜 자리(노견 끝 = 원지반) 쪽만 원 · 키 toe:left|right · 고르면 ◀▶ 로 절 ↔ 성(414-1)
const x = (o) => 300 - o * 20;
const y = (e) => 400 - (e - 100) * 20;
const attach = (sec, changes) => {
@@ -260,21 +260,28 @@ const attach = (sec, changes) => {
});
return { layer, handle, picks };
};
const toesOf = (layer) => layer.children.filter((k) => k.classList.contains("b06-chart__toe-hit"));
const near = section(crossing[0]); // 보통 단면(우 = 절토 · 바뀜 자리 아님)
const nearToes = toesOf(attach(near, []).layer);
const changes = [];
const near = section(crossing[0]);
const { layer, handle, picks } = attach(near, changes);
const toes = layer.children.filter((k) => k.classList.contains("b06-chart__toe-hit"));
const keys = pick.designLineMoves(near, () => undefined).toes;
const bareSec = section(out.stop.value); // 바뀜 자리(우 노견 끝 = 원지반)
const { layer, handle, picks } = attach(bareSec, changes);
const toes = toesOf(layer);
const moves = pick.designLineMoves(bareSec, () => undefined);
const keys = moves.toes;
const rightToeHit = toes.find((t) => Number(t.getAttribute("cx")) > x(0));
out.toe = {
nearCount: nearToes.length,
nearSides: ["left", "right"].filter((s) => pick.designLineMoves(near, () => undefined).toes[s]),
count: toes.length,
bareSides: ["left", "right"].filter((s) => keys[s] && moves.bare[s]),
centerClass: toes.every((t) => t.classList.contains("b06-chart__center-hit")),
sides: ["left", "right"].filter((s) => keys[s]),
rightOnGround:
keys.right && Math.abs(keys.right.elevation_m - groundAt(keys.right.offset_m)) < 1e-9,
rightCx: toes.length === 2 ? Number(toes[1].getAttribute("cx")) : null,
rightCx: rightToeHit ? Number(rightToeHit.getAttribute("cx")) : null,
rightX: keys.right ? x(keys.right.offset_m) : null,
};
toes[1].click();
rightToeHit.click();
handle.setActive(picks[0]);
// 아래 띠 공용 칸(370-3) — 제목 · 버튼 줄(끝 = 값 글)
const cellOf = (h) => ({
@@ -287,9 +294,12 @@ const cellOf = (h) => ({
});
const cell = cellOf(handle);
cell.button(x(-1) > x(0) ? "right" : "left").click(); // 화면 → 바깥(우 = −offset)
cell.button(x(-1) > x(0) ? "left" : "right").click(); // 화면 → 안쪽
out.toePick = {
picked: picks,
active: toes[1].classList.contains("is-active"),
active: rightToeHit.classList.contains("is-active"),
modeBefore: bareSec.design.section_mode,
modes: changes.map((c) => section(c.shoulder_extend.valueM).design.section_mode),
value: cell.value,
title: cell.title,
panelOpen: !handle.cell.classList.contains("is-empty"),
@@ -321,8 +331,10 @@ out.inherit.label = cellOf(after.handle).title;
// ⑤ 370-5 — 손잡이를 고른 뒤 넘어간 사면이 원지반과 못 만나면(손잡이 없음) 같은 쪽 첫 사면 선이 이어받음
pick.setPickedLine("S1", null);
{
const again = attach(near, []);
again.layer.children.filter((k) => k.classList.contains("b06-chart__toe-hit"))[1].click();
const again = attach(bareSec, []);
toesOf(again.layer)
.find((t) => Number(t.getAttribute("cx")) > x(0))
.click();
pick.setPickedLine("S1", again.picks[0]);
let extend = crossing[1];
let open = null;
+13 -8
View File
@@ -61,7 +61,7 @@ def test_반올림이_교차_앞이어도_다음_걸음은_넘어감(out) -> Non
def test_손잡이_없는_넘어감도_고름_유지(out) -> None:
"""370-5 — 넘어간 사면이 원지반과 못 만나 손잡이가 사라져도 같은 쪽 사면 선 · 암 칸으로 안 넘어감."""
"""370-5 — 바뀜 자리 손잡이를 고른 뒤 사면이 원지반과 못 만나도 같은 쪽 사면 선 · 암 칸으로 안 넘어감."""
got = out["noToe"]
assert got["toeGone"] is True
assert got["index"] is not None
@@ -70,23 +70,28 @@ def test_손잡이_없는_넘어감도_고름_유지(out) -> None:
assert got["buttons"] == ["left", "reset", "right"] # ◀↺▶ 남음
def test_교차점_손잡이_쪽마다_늘_보임(out) -> None:
def test_교차점_손잡이는_바뀜_자리만(out) -> None:
"""414-1 — 원 = 그 쪽 노견 끝 = 원지반(±5 mm · `bare`)일 때만 · 보통 단면은 원 0."""
toe = out["toe"]
assert toe["count"] == len(toe["sides"]) == 2
assert toe["nearSides"] == ["left", "right"] # 사면 ∩ 원지반 자리는 있어도
assert toe["nearCount"] == 0 # 보통 단면(절 · 성 정해짐) = 원 0
assert toe["count"] == len(toe["bareSides"]) == 1 and toe["bareSides"] == ["right"]
assert toe["centerClass"] is True # 중심점 원과 같은 고르기 차례
assert toe["rightOnGround"] is True
assert abs(toe["rightCx"] - toe["rightX"]) < 1e-9
def test_손잡이를_고르면_그_쪽_사면_좌우_이동(out) -> None:
def test_손잡이를_고르면_좌우로_절성토_넘김(out) -> None:
"""414-1 — 바뀜 자리 원을 고르면 ▶(바깥) = 성토 · ◀(안쪽) = 절토."""
got = out["toePick"]
assert got["active"] is True and got["panelOpen"] is True
assert got["title"] == "사면 이동 (우)" # 아래 띠 공용 칸(370-3)
assert got["value"].startswith("노선에서 ")
assert len(got["sent"]) == 1 and got["sent"][0]["side"] == "right"
assert got["value"].startswith("절·성토 없음 · 노선에서 ")
assert [c["side"] for c in got["sent"]] == ["right", "right"]
low, high = out["stop"]["crossing"]
assert abs(got["sent"][0]["valueM"] - out["stop"]["value"]) < 1e-9 # ◀▶ 도 멈춤 자리
assert low < got["sent"][0]["valueM"] < high
outward, inward = (c["valueM"] for c in got["sent"])
assert outward >= high and inward <= low # 멈춤 자리에서 다시 안 섬
assert got["modes"] == ["left_cut", "both_cut"] # 우 = 성토 → 절토
def test_절에서_성으로_넘어가도_고름_유지(out) -> None: