From e488605d7268f602179e7618a619a579b3925e96 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 18 Oct 2026 12:15:29 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(B06=20=EA=B5=90=EC=B0=A8=20=EC=9B=90):?= =?UTF-8?q?=20=EC=A0=88=ED=86=A0=20=C2=B7=20=EC=84=B1=ED=86=A0=20=EB=B0=94?= =?UTF-8?q?=EB=80=9C=20=EC=9E=90=EB=A6=AC(=EB=85=B8=EA=B2=AC=20=EB=81=9D?= =?UTF-8?q?=20=3D=20=EC=9B=90=EC=A7=80=EB=B0=98)=EB=A7=8C=20=EC=9B=90=20?= =?UTF-8?q?=C2=B7=20=ED=8F=89=EC=86=8C=20=ED=88=AC=EB=AA=85=20=C2=B7=20?= =?UTF-8?q?=EC=98=AC=EB=A6=AC=EA=B1=B0=EB=82=98=20=EA=B3=A0=EB=A5=B4?= =?UTF-8?q?=EB=A9=B4=20=EB=B3=B4=EC=9E=84=20(414-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- .../B06_Section_UI_Cross_Design_Pick.ts | 9 +++-- .../B06_Section_UI_Style_Cross_Areas.css | 5 ++- resources/tester/helper_370_2_toe_stop.cjs | 34 +++++++++++++------ resources/tester/test_370_2_toe_stop.py | 21 +++++++----- 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Design_Pick.ts b/B06_Section/B06_Section_UI_Cross_Design_Pick.ts index 187d80178..9f04ccd0a 100644 --- a/B06_Section/B06_Section_UI_Cross_Design_Pick.ts +++ b/B06_Section/B06_Section_UI_Cross_Design_Pick.ts @@ -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; /** 있으면 쪽마다 노견 끝 = 원지반(절·성토 없음 · 379-1) — 손잡이 값 앞 알림. */ bare?: ReturnType; @@ -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)); diff --git a/B06_Section/B06_Section_UI_Style_Cross_Areas.css b/B06_Section/B06_Section_UI_Style_Cross_Areas.css index 11a69d08d..6020d3fb5 100644 --- a/B06_Section/B06_Section_UI_Style_Cross_Areas.css +++ b/B06_Section/B06_Section_UI_Style_Cross_Areas.css @@ -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; } diff --git a/resources/tester/helper_370_2_toe_stop.cjs b/resources/tester/helper_370_2_toe_stop.cjs index 5bc7fabef..29ba90f41 100644 --- a/resources/tester/helper_370_2_toe_stop.cjs +++ b/resources/tester/helper_370_2_toe_stop.cjs @@ -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; diff --git a/resources/tester/test_370_2_toe_stop.py b/resources/tester/test_370_2_toe_stop.py index 58204060c..95041b47a 100644 --- a/resources/tester/test_370_2_toe_stop.py +++ b/resources/tester/test_370_2_toe_stop.py @@ -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: From 79907405699a15b82ef37c24a2708fad78b2a6d5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 18 Oct 2026 12:15:30 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(B05=20=ED=8F=89=EB=A9=B4=20=EA=B3=A1?= =?UTF-8?q?=EC=84=A0=20=EB=84=A4=EB=AA=A8):=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=EA=B3=A1=EC=84=A0=20BC=20=C2=B7=20EC=20=EB=84=A4=EB=AA=A8?= =?UTF-8?q?=EB=A5=BC=20=EC=98=88=EC=83=81=EB=85=B8=EC=84=A0=20=EB=92=A4=20?= =?UTF-8?q?=C2=B7=20=EB=85=B8=EC=84=A0=20=EC=95=9E=EC=9C=BC=EB=A1=9C=20(41?= =?UTF-8?q?4-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- .../B05_Profile_UI_RouteEdit_Render.ts | 49 ++++++++++--------- .../tester/test_381_3_routeedit_node_pick.py | 13 ++++- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts index 1f88a7a86..89e44c769 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts @@ -236,6 +236,32 @@ export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: Rou style.getPropertyValue("--color-text-secondary") || "#9ca3af", 1.6, ); + // 곡선 시작·끝점 — 잡아서 직선 각도와 R 을 함께 바꾸는 손잡이(2026-09-07 사용자 지시). + // **속을 비우고 테두리를 굵게** 그린다 — 선·노드와 색이 같으면 눈에도 안 띄고 집기도 어렵다. + // 예상노선 뒤 · 노선 앞(414-2) — 노선이 네모 위를 지나감 · 고른 곡선은 `drawPickedCurve` 가 위에. + context.save(); + context.lineWidth = 2; + scene.curveInfo.forEach((curve) => { + // **늘 보인다**(2026-09-07 사용자 지시) — 직선이 곡선에 닿는 자리는 손잡이이기 이전에 + // **읽을 정보**다. 한때 고른 곡선만 내보였더니 「표기가 다 사라졌다」는 지적을 받았다. + // 노드를 못 집던 문제는 집기 우선순위(노드가 먼저)로 따로 풀었으므로 다 내놓아도 된다. + if (scene.curveOn[curve.node_first] === false) return; // 곡선을 지운 자리에는 접선점도 없다. + // 고른 곡선은 속을 채워 도드라지게 — 지금 끌 수 있는 것이 무엇인지 보이게. + const isPicked = curve.node_first === scene.picked; + [curve.start, curve.end].forEach((point) => { + const [x, y] = toScreen([point[0], point[1]]); + context.beginPath(); + const size = isPicked ? CURVE_HANDLE_PX + 1 : CURVE_HANDLE_PX; + context.rect(x - size, y - size, size * 2, size * 2); + context.fillStyle = isPicked + ? style.getPropertyValue("--map-route") || "#f97316" + : style.getPropertyValue("--map-halo") || "rgba(255,255,255,0.95)"; + context.fill(); + context.strokeStyle = style.getPropertyValue("--map-route") || "#f97316"; + context.stroke(); + }); + }); + context.restore(); // 선은 **폴리라인**(원호 포함)을 그리고, 잡는 동그라미는 **노드**에만 찍는다. // 노드를 옮기는 동안에는 폴리라인이 없으므로 노드를 곧바로 이어 미리 보인다. strokePolyline( @@ -294,29 +320,6 @@ export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: Rou } }); - // 곡선 시작·끝점 — 잡아서 직선 각도와 R 을 함께 바꾸는 손잡이(2026-09-07 사용자 지시). - // **속을 비우고 테두리를 굵게** 그린다 — 선·노드와 색이 같으면 눈에도 안 띄고 집기도 어렵다. - context.lineWidth = 2; - scene.curveInfo.forEach((curve) => { - // **늘 보인다**(2026-09-07 사용자 지시) — 직선이 곡선에 닿는 자리는 손잡이이기 이전에 - // **읽을 정보**다. 한때 고른 곡선만 내보였더니 「표기가 다 사라졌다」는 지적을 받았다. - // 노드를 못 집던 문제는 집기 우선순위(노드가 먼저)로 따로 풀었으므로 다 내놓아도 된다. - if (scene.curveOn[curve.node_first] === false) return; // 곡선을 지운 자리에는 접선점도 없다. - // 고른 곡선은 속을 채워 도드라지게 — 지금 끌 수 있는 것이 무엇인지 보이게. - const isPicked = curve.node_first === scene.picked; - [curve.start, curve.end].forEach((point) => { - const [x, y] = toScreen([point[0], point[1]]); - context.beginPath(); - const size = isPicked ? CURVE_HANDLE_PX + 1 : CURVE_HANDLE_PX; - context.rect(x - size, y - size, size * 2, size * 2); - context.fillStyle = isPicked - ? style.getPropertyValue("--map-route") || "#f97316" - : style.getPropertyValue("--map-halo") || "rgba(255,255,255,0.95)"; - context.fill(); - context.strokeStyle = style.getPropertyValue("--map-route") || "#f97316"; - context.stroke(); - }); - }); context.restore(); drawPickedCurve(context, scene, style); drawPickHover(context, scene, line, style, NODE_R); diff --git a/resources/tester/test_381_3_routeedit_node_pick.py b/resources/tester/test_381_3_routeedit_node_pick.py index 977ab495c..f4f56f738 100644 --- a/resources/tester/test_381_3_routeedit_node_pick.py +++ b/resources/tester/test_381_3_routeedit_node_pick.py @@ -23,12 +23,23 @@ def test_노드는_라벨_뒤_맨_위_회전_안(): scene = render[render.index("export function drawRouteEditScene(") :] scene = scene[: scene.index("context.restore(); // 회전 끝")] labels = scene.index("drawContourLabels(context, scene, band, style);") - for later in ("scene.planned.forEach(", "scene.curveInfo.forEach(", "drawPickHover("): + for later in ("scene.planned.forEach(", "drawPickedCurve(", "drawPickHover("): assert labels < scene.index(later), later for earlier in ("drawStationMarks(", "scene.overStations?.(", "drawMeasureMarks("): assert scene.index(earlier) < labels, earlier +def test_곡선_네모는_예상노선_뒤_노선_앞(): + """414-2 — 모든 곡선 BC · EC 네모 = 예상노선 뒤 · 노선 앞(노선이 네모 위를 지나감).""" + render = _read("B05_Profile_UI_RouteEdit_Render.ts") + scene = render[render.index("export function drawRouteEditScene(") :] + expected = scene.index("scene.expected,") + squares = scene.index("scene.curveInfo.forEach(") + route = scene.index("strokePolyline(", squares) + assert expected < squares < route + assert scene[route:].split(",")[2].strip() == "line" + + def test_집기_맴돔_한_함수(): pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts") assert "pickAtScreen(" in pointer