feat(500-2 측점선 띠): B05 종단 — 원지반선 ±12 px 띠 안 측점선 = 임시 횡단 먼저(표시 · 누름 같은 판정) · 띠 밖 측점선 = 측점 고름
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -61,7 +61,13 @@ export function crossPickFromEvent(
|
||||
? Number(marker.getAttribute("data-chainage"))
|
||||
: null;
|
||||
const hit: CrossPickHit =
|
||||
plusRaw !== null ? { plusM: plusRaw } : marker || target.closest("button") ? "other" : null;
|
||||
plusRaw !== null
|
||||
? { plusM: plusRaw }
|
||||
: target.closest("button")
|
||||
? "other"
|
||||
: marker
|
||||
? "station"
|
||||
: null;
|
||||
const svg = chart.querySelector<SVGSVGElement>(`svg[${Y_WINDOW_ATTRS.auto}="1"]`);
|
||||
const read = (name: string): number => Number(svg?.getAttribute(name));
|
||||
const view: ChartWindow = {
|
||||
@@ -93,7 +99,7 @@ export function crossPickFromEvent(
|
||||
}
|
||||
|
||||
/** 임시 횡단이 열리는 자리 가까이 = 작은 원 · 세로 점선 · 측점 글 · 손가락 커서(490-1).
|
||||
* 멀어짐 · 측점선 · 단추 · 도구 중 · 끌기 · 스크롤 = 안 그림. */
|
||||
* 멀어짐 · 띠 밖 측점선 · 단추 · 도구 중 · 끌기 · 스크롤 = 안 그림. */
|
||||
export function mountCrossHint(
|
||||
chart: HTMLElement,
|
||||
scroller: HTMLElement,
|
||||
|
||||
@@ -308,36 +308,44 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
chartWrap.style.height = `${chartHeight}px`;
|
||||
// 빈 곳 누름은 고름을 안 풂 — 풀기는 Esc · 같은 것 다시 누름 한 길(3D 와 같은 규칙 · 233-4).
|
||||
// 도구([직선화] · [쉬프트] · [자유지정]) 중 빈 곳 누름은 그 도구 몫 그대로.
|
||||
chartWrap.addEventListener("click", (event) => {
|
||||
// [직선화]·[쉬프트] 모드에서는 그래프 클릭이 도구 선택으로 간다 — 측점선을 눌렀으면
|
||||
// 그 측점, 빈 곳이면 그 x의 누가거리로 직선 구간을 고른다(2026-09-02).
|
||||
if (ctx.toolActive()) {
|
||||
const marker = (event.target as HTMLElement).closest(".b06-chart__station");
|
||||
const raw = marker?.getAttribute("data-chainage");
|
||||
if (raw !== null && raw !== undefined) {
|
||||
if (ctx.handleToolPick(Number(raw))) return;
|
||||
} else {
|
||||
const rect = chartWrap.getBoundingClientRect();
|
||||
const chainage = chainageInverter(
|
||||
longitudinal,
|
||||
width,
|
||||
layout.originOffset,
|
||||
)(event.clientX - rect.left + chartWrap.scrollLeft);
|
||||
if (ctx.handleToolPick(Number.isFinite(chainage) ? chainage : null)) return;
|
||||
// 잡는 단계(capture) — 띠 안 측점선 누름은 임시 횡단이 먼저라 측점선 자기 고름을 막아야 함(500-2).
|
||||
chartWrap.addEventListener(
|
||||
"click",
|
||||
(event) => {
|
||||
// [직선화]·[쉬프트] 모드에서는 그래프 클릭이 도구 선택으로 간다 — 측점선을 눌렀으면
|
||||
// 그 측점, 빈 곳이면 그 x의 누가거리로 직선 구간을 고른다(2026-09-02).
|
||||
if (ctx.toolActive()) {
|
||||
const marker = (event.target as HTMLElement).closest(".b06-chart__station");
|
||||
const raw = marker?.getAttribute("data-chainage");
|
||||
if (raw !== null && raw !== undefined) {
|
||||
if (ctx.handleToolPick(Number(raw))) return;
|
||||
} else {
|
||||
const rect = chartWrap.getBoundingClientRect();
|
||||
const chainage = chainageInverter(
|
||||
longitudinal,
|
||||
width,
|
||||
layout.originOffset,
|
||||
)(event.clientX - rect.left + chartWrap.scrollLeft);
|
||||
if (ctx.handleToolPick(Number.isFinite(chainage) ? chainage : null)) return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 원지반선 근처 · 추가 측점 선 누름 = 그 자리 임시 횡단 작은 창(368-3)
|
||||
// 다른 측점선 · 단추 누름은 그 몫(측점 고름 · ▲▼)
|
||||
const toChainageAt = chainageInverter(longitudinal, width, layout.originOffset);
|
||||
const at = crossPickFromEvent(chartWrap, event, toChainageAt, graphData.samples);
|
||||
if (at === null) return;
|
||||
openCrossModal(body.parentElement ?? body, {
|
||||
chainageM: at.chainageM,
|
||||
stations: graphLongitudinal.stations.map((station) => station.chainage_m),
|
||||
intervalM: stationIntervalM ?? 20,
|
||||
});
|
||||
});
|
||||
// 원지반선 근처 · 추가 측점 선 누름 = 그 자리 임시 횡단 작은 창(368-3)
|
||||
// 띠 밖 측점선 · 단추 누름은 그 몫(측점 고름 · ▲▼) — 띠 안 측점선은 임시 횡단이 먼저(500-2)
|
||||
const toChainageAt = chainageInverter(longitudinal, width, layout.originOffset);
|
||||
const at = crossPickFromEvent(chartWrap, event, toChainageAt, graphData.samples);
|
||||
if (at === null) return;
|
||||
const marker = (event.target as Element).closest(".b06-chart__station");
|
||||
if (marker && !marker.querySelector(".b06-chart__station-line--plus"))
|
||||
event.stopPropagation();
|
||||
openCrossModal(body.parentElement ?? body, {
|
||||
chainageM: at.chainageM,
|
||||
stations: graphLongitudinal.stations.map((station) => station.chainage_m),
|
||||
intervalM: stationIntervalM ?? 20,
|
||||
});
|
||||
},
|
||||
true,
|
||||
);
|
||||
const derived = deriveProfile(ctx, detail, alignment, irregularStations);
|
||||
const { designProfiles, graphData, graphLongitudinal } = derived;
|
||||
// 세로 자동 맞춤 — 지금 화면에 보이는 누가거리 구간만 보고 Y 창을 잡는다(2026-09-04
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
* logic/B05_Profile_Profile_CrossPick.ts
|
||||
* 종단 그래프 한 점 → 임시 횡단을 열 누가거리(정수 m) · 아니면 null(368-3 · 490-1).
|
||||
*
|
||||
* 누름(작은 창 열기)과 가까이 표시가 같은 판정을 씀 — 추가 측점 선 = 그 측점 · 다른 측점선 · 단추 =
|
||||
* 그 몫(null) · 빈 곳 = 원지반선 위아래 `GROUND_SLACK_PX` 안.
|
||||
* 누름(작은 창 열기)과 가까이 표시가 같은 판정을 씀 — 추가 측점 선 = 그 측점 · 단추 = 그 몫(null) ·
|
||||
* 다른 측점선 · 빈 곳 = 원지반선 위아래 `GROUND_SLACK_PX` 안(측점선은 띠 밖이면 측점 고름 몫 · 500-2).
|
||||
* ========================================================================== */
|
||||
|
||||
/** 원지반선을 눌렀다고 볼 위아래 여유(px). */
|
||||
export const GROUND_SLACK_PX = 12;
|
||||
|
||||
/** 포인터 아래 = 추가 측점 선(그 누가거리) · 다른 측점선 · 단추("other") · 빈 곳(null). */
|
||||
export type CrossPickHit = { plusM: number } | "other" | null;
|
||||
/** 포인터 아래 = 추가 측점 선(그 누가거리) · 다른 측점선("station") · 단추("other") · 빈 곳(null). */
|
||||
export type CrossPickHit = { plusM: number } | "station" | "other" | null;
|
||||
|
||||
export type GroundSample = { chainage_m?: number; elevation_m?: number | null };
|
||||
|
||||
@@ -44,9 +44,10 @@ export function groundElevationAt(
|
||||
/** 임시 횡단을 열 누가거리(정수 m) · 아니면 null. */
|
||||
export function crossPickAt(input: CrossPickInput): number | null {
|
||||
if (input.hit === "other") return null;
|
||||
if (input.hit) return Math.round(input.hit.plusM);
|
||||
if (typeof input.hit === "object" && input.hit) return Math.round(input.hit.plusM);
|
||||
const ground = groundElevationAt(input.samples, input.chainageM);
|
||||
if (ground === null) return null;
|
||||
// "station"·null 모두 띠 안이면 임시 횡단 — 측점선은 띠 밖에서만 측점 고름 몫.
|
||||
return Math.abs(input.pointerY - input.groundY(ground)) <= GROUND_SLACK_PX
|
||||
? Math.round(input.chainageM)
|
||||
: null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""490-1 B05 종단 — 임시 횡단이 열리는 자리 가까이 표시 = 누름과 같은 판정(`crossPickAt`).
|
||||
|
||||
판정 함수 = 옛 누름 판정(원지반선 위아래 12 px · 500-1 8 → 12 · 정수 m · 추가 측점 선 = 그 측점 · 측점선 · 단추 = null)과
|
||||
같은 값 · 허용 밖 null. TS 쪽 = `helper_490_1_cross_pick.cjs`(Node). 화면 조작은 ORCA 로 확인.
|
||||
판정 함수 = 옛 누름 판정(원지반선 위아래 12 px · 500-1 8 → 12 · 정수 m · 추가 측점 선 = 그 측점 · 단추 = null)과
|
||||
같은 값 · 허용 밖 null. 500-2 = 다른 측점선("station")은 띠 안이면 임시 횡단 · 띠 밖이면 null(측점 고름 몫). TS 쪽 = `helper_490_1_cross_pick.cjs`(Node). 화면 조작은 ORCA 로 확인.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -30,7 +30,7 @@ def _old_pick(hit, chainage: float, pointer_y: float) -> int | None:
|
||||
"""옛 `groundPickAt` + 누름 처리(Render.ts 329~346) 그대로."""
|
||||
if hit == "other":
|
||||
return None
|
||||
if hit:
|
||||
if isinstance(hit, dict):
|
||||
return round(hit["plusM"])
|
||||
valid = [s for s in SAMPLES if s["elevation_m"] is not None]
|
||||
after = next((i for i, s in enumerate(valid) if s["chainage_m"] >= chainage), -1)
|
||||
@@ -51,7 +51,11 @@ CASES = [
|
||||
{"hit": None, "chainageM": 0.0, "pointerY": 80.0}, # 첫 표본 = null(옛 판정 그대로)
|
||||
{"hit": None, "chainageM": 70.0, "pointerY": 100.0}, # 끝 뒤
|
||||
{"hit": {"plusM": 33.6}, "chainageM": 10.0, "pointerY": 0.0}, # 추가 측점 선
|
||||
{"hit": "other", "chainageM": 10.4, "pointerY": 70.0}, # 측점선 · 단추
|
||||
{"hit": "other", "chainageM": 10.4, "pointerY": 70.0}, # 단추
|
||||
{"hit": "station", "chainageM": 10.4, "pointerY": 69.0}, # 측점선 · 띠 안 = 임시 횡단
|
||||
{"hit": "station", "chainageM": 10.4, "pointerY": 81.0}, # 측점선 · 띠 끝 안
|
||||
{"hit": "station", "chainageM": 10.4, "pointerY": 90.0}, # 측점선 · 띠 밖 = 측점 고름
|
||||
{"hit": "station", "chainageM": 0.0, "pointerY": 80.0}, # 측점선 · 첫 표본 = null
|
||||
]
|
||||
|
||||
|
||||
@@ -75,7 +79,7 @@ def test_판정_함수_옛_누름과_같은_값() -> None:
|
||||
assert result["slack"] == 12
|
||||
picks = [row["pick"] for row in result["out"]]
|
||||
assert picks == [_old_pick(c["hit"], c["chainageM"], c["pointerY"]) for c in CASES]
|
||||
assert picks == [10, 10, None, None, 50, None, None, 34, None]
|
||||
assert picks == [10, 10, None, None, 50, None, None, 34, None, 10, 10, None, None]
|
||||
|
||||
|
||||
def test_누름_표시_같은_길() -> None:
|
||||
|
||||
@@ -78,3 +78,18 @@ def test_제목_줄_끌기_화면_안() -> None:
|
||||
assert "innerWidth" in drag and "innerHeight" in drag # 화면 밖 안 나감
|
||||
assert "root.parentElement!.getBoundingClientRect()" in drag # 종단 칸 안
|
||||
assert "refit();" in modal # 다시 열 때 가둠
|
||||
|
||||
|
||||
def test_500_2_측점선_띠_안_임시_횡단_먼저() -> None:
|
||||
modal = _read("B05_Profile", "B05_Profile_UI_Profile_CrossModal.ts")
|
||||
# 단추 = other 먼저 · 추가 측점선 = plusM · 그 밖 측점선 = station(띠 안이면 임시 횡단)
|
||||
flat = " ".join(modal.split())
|
||||
assert 'target.closest("button") ? "other" : marker ? "station" : null' in flat
|
||||
logic = _read("B05_Profile", "logic", "B05_Profile_Profile_CrossPick.ts")
|
||||
assert '"station"' in logic and 'input.hit === "other"' in logic
|
||||
# 누름 = 잡는 단계에서 띠 안 측점선의 자기 고름을 막고 임시 횡단을 엶
|
||||
render = _read("B05_Profile", "B05_Profile_UI_Profile_Render.ts")
|
||||
start = render.index("// 잡는 단계(capture)")
|
||||
click = render[start : render.index("const derived = deriveProfile", start)]
|
||||
assert "event.stopPropagation()" in click and "openCrossModal(" in click
|
||||
assert " ".join(click.split()).endswith("}, true, );")
|
||||
|
||||
Reference in New Issue
Block a user