fix(500-1 곡선 누름 고름 유지): B05 평면 교각점 · 손잡이 · 호 누름 = 측점 고름 유지 · 풀림 = 오른쪽 판도 비움 · 임시 횡단 띠 ±12 px · 창 단추 제목 줄 · 제목 줄 끌어 옮김
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,8 +22,12 @@
|
||||
|
||||
.b05-crossmodal__head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8, 8px);
|
||||
cursor: move;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.b05-crossmodal__station {
|
||||
@@ -45,10 +49,12 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 단추 = 제목 줄 안 오른쪽 맞춤(500-1) */
|
||||
.b05-crossmodal__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-8, 8px);
|
||||
margin-left: auto;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 임시 횡단이 열리는 자리 가까이 표시(490-1) — 누름은 아래 그래프 몫(통과) · 세로 축(5) 밑 */
|
||||
|
||||
@@ -130,6 +130,50 @@ export function mountCrossHint(
|
||||
chart.addEventListener("pointerleave", hide);
|
||||
}
|
||||
|
||||
/** 제목 줄 끌어 창 옮김(500-1) — 종단 칸 · 화면 안으로 가둠 · 단추 누름은 단추 몫 · 돌려줌 = 끌어 둔 자리 다시 가둠. */
|
||||
function bindHeadDrag(root: HTMLElement, head: HTMLElement): () => void {
|
||||
let start: { id: number; x: number; y: number; left: number; top: number } | null = null;
|
||||
const put = (left: number, top: number): void => {
|
||||
const box = root.parentElement!.getBoundingClientRect();
|
||||
const now = root.getBoundingClientRect();
|
||||
const [dx, dy] = [left - root.offsetLeft, top - root.offsetTop];
|
||||
const [minX, maxX] = [
|
||||
Math.max(box.left, 0) - now.left,
|
||||
Math.min(box.right, innerWidth) - now.right,
|
||||
];
|
||||
const [minY, maxY] = [
|
||||
Math.max(box.top, 0) - now.top,
|
||||
Math.min(box.bottom, innerHeight) - now.bottom,
|
||||
];
|
||||
root.style.left = `${root.offsetLeft + Math.max(minX, Math.min(dx, maxX))}px`;
|
||||
root.style.top = `${root.offsetTop + Math.max(minY, Math.min(dy, maxY))}px`;
|
||||
root.style.right = "auto";
|
||||
};
|
||||
head.addEventListener("pointerdown", (event) => {
|
||||
if (event.button !== 0 || (event.target as Element).closest("button")) return;
|
||||
start = {
|
||||
id: event.pointerId,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
left: root.offsetLeft,
|
||||
top: root.offsetTop,
|
||||
};
|
||||
head.setPointerCapture(event.pointerId);
|
||||
});
|
||||
head.addEventListener("pointermove", (event) => {
|
||||
if (start?.id !== event.pointerId) return;
|
||||
put(start.left + event.clientX - start.x, start.top + event.clientY - start.y);
|
||||
});
|
||||
const end = (event: PointerEvent): void => {
|
||||
if (start?.id === event.pointerId) start = null;
|
||||
};
|
||||
head.addEventListener("pointerup", end);
|
||||
head.addEventListener("pointercancel", end);
|
||||
return () => {
|
||||
if (root.style.left) put(root.offsetLeft, root.offsetTop);
|
||||
};
|
||||
}
|
||||
|
||||
interface CrossModal {
|
||||
open(request: CrossModalOpen): void;
|
||||
}
|
||||
@@ -145,14 +189,14 @@ function createCrossModal(host: HTMLElement): CrossModal {
|
||||
<div class="b05-crossmodal__head">
|
||||
<strong>임시 횡단</strong>
|
||||
<span class="b05-crossmodal__station"></span>
|
||||
<div class="b05-crossmodal__actions">
|
||||
<button type="button" class="ui-button" data-act="add">측점 추가</button>
|
||||
<button type="button" class="ui-button" data-act="remove">삭제</button>
|
||||
<button type="button" class="ui-button" data-act="close">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="b05-crossmodal__box"></div>
|
||||
<div class="b05-crossmodal__foot"></div>
|
||||
<div class="b05-crossmodal__actions">
|
||||
<button type="button" class="ui-button" data-act="add">측점 추가</button>
|
||||
<button type="button" class="ui-button" data-act="remove">삭제</button>
|
||||
<button type="button" class="ui-button" data-act="close">닫기</button>
|
||||
</div>`;
|
||||
<div class="b05-crossmodal__foot"></div>`;
|
||||
const $ = <T extends HTMLElement>(selector: string): T => root.querySelector<T>(selector)!;
|
||||
const station = $(".b05-crossmodal__station");
|
||||
const box = $(".b05-crossmodal__box");
|
||||
@@ -164,6 +208,7 @@ function createCrossModal(host: HTMLElement): CrossModal {
|
||||
root.addEventListener(type, (event) => event.stopPropagation()),
|
||||
);
|
||||
host.append(root);
|
||||
const refit = bindHeadDrag(root, $(".b05-crossmodal__head"));
|
||||
|
||||
let current: CrossModalOpen | null = null;
|
||||
let turn = 0;
|
||||
@@ -272,6 +317,7 @@ function createCrossModal(host: HTMLElement): CrossModal {
|
||||
current = request;
|
||||
station.textContent = stationLabel(request.chainageM, request.intervalM);
|
||||
root.hidden = false;
|
||||
refit();
|
||||
syncButtons();
|
||||
watchPipe();
|
||||
void draw(request.chainageM);
|
||||
|
||||
@@ -352,7 +352,8 @@ export function createCrossPreview(params: CrossPreviewParams): CrossPreviewWind
|
||||
showStation(station?.key ?? null, station?.chainageM ?? at, structure, open);
|
||||
}
|
||||
const stopPick = subscribeSelection(params.projectId, (selection, from) => {
|
||||
if (from !== PLAN_PICK_FROM) follow(selection);
|
||||
// 평면 이름 = 눈금 누름(제가 이미 그림)만 거름 · 마커 고름 풂(값 없음)은 따라 비움(500-1)
|
||||
if (from !== PLAN_PICK_FROM || selection.chainageM === null) follow(selection);
|
||||
});
|
||||
attach();
|
||||
// 들어올 때 이미 고른 것 — 판에 그 측점 횡단(494-1 · 안 그리면 고른 채 빈 판 · 다시 누르면 고름이 풀림) · 판은 안 폄
|
||||
|
||||
@@ -243,8 +243,13 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
return;
|
||||
}
|
||||
marks.deselect(); // 마커 아닌 곳 = 마커 고름 풂(오른쪽 횡단 판은 그대로)
|
||||
const pick = choice.pick;
|
||||
// 교각점 · 손잡이 · 호(곡선 고름) = 마커 고름 유지(500-1) · 그 밖 마커 아닌 곳 = 풂(오른쪽 판도 비움)
|
||||
const onCurve =
|
||||
(!!pick && "node" in pick) ||
|
||||
choice.handle !== null ||
|
||||
(!(pick && "chainage" in pick) && arcAtScreen(state.curveInfo, toScreen, px, py) >= 0);
|
||||
if (!onCurve) marks.deselect();
|
||||
// 노드 가운데(6px)는 노드 — 헤어핀처럼 곡선이 몰린 데서도 노드를 집음(2026-09-07 사용자 지적 ④) ·
|
||||
// 그 밖은 횡단 눈금 · 손잡이와 가까운 쪽(381-3 `pickAt` · 433-14 ③ 낮은 확대 R 12 손잡이).
|
||||
const hitNode = pick && "node" in pick ? pick.node : -1;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* · 끌기 · 방향키(마커 고른 때 · 교각점 안 고른 때) = 노선 따라 정수 m · 1 m 씩 · 시점 · 종점 밖 안 감 ·
|
||||
* 놓을 때 · 뗄 때 한 번 옮김(`move` = 알약 끌기와 같은 길 · 관 · 구조물 초안 · [저장]이 정본).
|
||||
* · 그림 · 집기 차례 = 맨 위 · 맨 앞(409 · 재기 다음) — `_Render` · `_Pointer` 가 부름.
|
||||
* · 고름 풀기 = 마커 아닌 곳 누름 · Esc(409) — 공용 고름을 `PLAN_PICK_FROM` 으로 비움(오른쪽 횡단 판 그대로 ·
|
||||
* · 고름 풀기 = 마커 아닌 곳 누름 · Esc(409) — 공용 고름을 `PLAN_PICK_FROM` 으로 비움(오른쪽 횡단 판도 비움 500-1 ·
|
||||
* 좌측 목록 강조 · 폼은 풂 409-2).
|
||||
* 목록 · 고름 · 옮김은 종단 보기가 `setPlanMarkSource` 로 건넴. 걸음 셈은 `logic/…_StructureMarks`.
|
||||
* ========================================================================== */
|
||||
@@ -333,7 +333,7 @@ export function bindStructureMarks(
|
||||
return moving;
|
||||
};
|
||||
|
||||
/** 고름 풂 — 공용 고름을 비움 · 오른쪽 횡단 판은 제 이름(`PLAN_PICK_FROM`)이라 안 비움. */
|
||||
/** 고름 풂 — 공용 고름을 비움 · 오른쪽 횡단 판도 값 없음이라 따라 비움(500-1). */
|
||||
const deselect = (): boolean => {
|
||||
if (!planMarks(canvas)?.picked) return false;
|
||||
selectDesign(projectId, NO_SELECTION, { from: PLAN_PICK_FROM });
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
/** 원지반선을 눌렀다고 볼 위아래 여유(px). */
|
||||
export const GROUND_SLACK_PX = 8;
|
||||
export const GROUND_SLACK_PX = 12;
|
||||
|
||||
/** 포인터 아래 = 추가 측점 선(그 누가거리) · 다른 측점선 · 단추("other") · 빈 곳(null). */
|
||||
export type CrossPickHit = { plusM: number } | "other" | null;
|
||||
|
||||
@@ -141,7 +141,7 @@ out.structureMoved = { ...shownNow(), at: preview.watched()?.chainageM ?? null }
|
||||
// ③ 관 고름(누가거리만) = 그 측점 횡단
|
||||
pick(80.17, null, "b05");
|
||||
out.pipePick = { ...shownNow(), structure: preview.watched()?.structure ?? null };
|
||||
// ④ 평면 마커 고름 풂(b05-plan) = 판 그대로
|
||||
// ④ 평면 마커 고름 풂(b05-plan) = 판도 비움(500-1)
|
||||
pick(null, null, "b05-plan");
|
||||
out.planRelease = shownNow();
|
||||
// ⑤ 일반측점 고름 = 그 측점 횡단 · 법선(structure 없음)
|
||||
|
||||
@@ -78,7 +78,10 @@ def test_맴돔_커서_마커_먼저() -> None:
|
||||
|
||||
def test_오른쪽_판은_제_이름_알림을_거름() -> None:
|
||||
cross = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Cross.ts")
|
||||
assert "if (from !== PLAN_PICK_FROM) follow(selection);" in cross
|
||||
# 500-1 — 눈금 누름(값 있음)만 거름 · 마커 고름 풂(값 없음)은 따라 비움
|
||||
assert (
|
||||
"if (from !== PLAN_PICK_FROM || selection.chainageM === null) follow(selection);" in cross
|
||||
)
|
||||
marks = _read("B05_Profile", "B05_Profile_UI_RouteEdit_StructureMarks.ts")
|
||||
assert "selectDesign(projectId, NO_SELECTION, { from: PLAN_PICK_FROM });" in marks
|
||||
|
||||
|
||||
@@ -40,8 +40,12 @@ def test_관_고름_그_측점_횡단(got) -> None:
|
||||
assert got["pipePick"] == {"station": "80.17", "foot": "횡단 80.17", "structure": True}
|
||||
|
||||
|
||||
def test_평면_마커_풂은_판_그대로(got) -> None:
|
||||
assert got["planRelease"] == {"station": "80.17", "foot": "횡단 80.17"}
|
||||
def test_평면_마커_풂도_판_비움(got) -> None:
|
||||
# 500-1(요구 300) — 고름 풀림 = 오른쪽 판도 비움(옛 409 「판 그대로」 걷음)
|
||||
assert got["planRelease"] == {
|
||||
"station": "",
|
||||
"foot": "측점 눈금을 누르면 그 측점 횡단이 뜹니다.",
|
||||
}
|
||||
|
||||
|
||||
def test_일반측점_그대로(got) -> None:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""490-1 B05 종단 — 임시 횡단이 열리는 자리 가까이 표시 = 누름과 같은 판정(`crossPickAt`).
|
||||
|
||||
판정 함수 = 옛 누름 판정(원지반선 위아래 8 px · 정수 m · 추가 측점 선 = 그 측점 · 측점선 · 단추 = null)과
|
||||
판정 함수 = 옛 누름 판정(원지반선 위아래 12 px · 500-1 8 → 12 · 정수 m · 추가 측점 선 = 그 측점 · 측점선 · 단추 = null)과
|
||||
같은 값 · 허용 밖 null. TS 쪽 = `helper_490_1_cross_pick.cjs`(Node). 화면 조작은 ORCA 로 확인.
|
||||
"""
|
||||
|
||||
@@ -39,14 +39,14 @@ def _old_pick(hit, chainage: float, pointer_y: float) -> int | None:
|
||||
a, b = valid[after - 1], valid[after]
|
||||
t = (chainage - a["chainage_m"]) / (b["chainage_m"] - a["chainage_m"] or 1)
|
||||
ground = a["elevation_m"] + (b["elevation_m"] - a["elevation_m"]) * t
|
||||
return round(chainage) if abs(pointer_y - _ground_y(ground)) <= 8 else None
|
||||
return round(chainage) if abs(pointer_y - _ground_y(ground)) <= 12 else None
|
||||
|
||||
|
||||
CASES = [
|
||||
{"hit": None, "chainageM": 10.2, "pointerY": 69.8}, # 지반 y = 69.8 · 딱 위
|
||||
{"hit": None, "chainageM": 10.2, "pointerY": 77.8}, # 8 px 끝
|
||||
{"hit": None, "chainageM": 10.2, "pointerY": 78.3}, # 허용 밖
|
||||
{"hit": None, "chainageM": 10.2, "pointerY": 61.3}, # 위쪽 허용 밖
|
||||
{"hit": None, "chainageM": 10.2, "pointerY": 81.8}, # 12 px 끝(옛 8 px 밖)
|
||||
{"hit": None, "chainageM": 10.2, "pointerY": 82.3}, # 허용 밖
|
||||
{"hit": None, "chainageM": 10.2, "pointerY": 57.3}, # 위쪽 허용 밖
|
||||
{"hit": None, "chainageM": 50.0, "pointerY": 93.0}, # 빈 표고 건너 20→60 보간
|
||||
{"hit": None, "chainageM": 0.0, "pointerY": 80.0}, # 첫 표본 = null(옛 판정 그대로)
|
||||
{"hit": None, "chainageM": 70.0, "pointerY": 100.0}, # 끝 뒤
|
||||
@@ -72,7 +72,7 @@ def _run() -> dict:
|
||||
|
||||
def test_판정_함수_옛_누름과_같은_값() -> None:
|
||||
result = _run()
|
||||
assert result["slack"] == 8
|
||||
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]
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""500-1 B05 평면 — 곡선(교각점 · 손잡이 · 호) 누름 = 측점 고름 유지 · 풀림 = 오른쪽 판도 비움 · 임시 횡단 창 단추 = 제목 줄 · 제목 줄 끌기.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_500_1_curve_keeps_pick.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _read(*parts: str) -> str:
|
||||
return ROOT.joinpath(*parts).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _down() -> str:
|
||||
"""평면 누름 — 마커 갈래 뒤 ~ 교각점 갈래 앞."""
|
||||
pointer = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Pointer.ts")
|
||||
start = pointer.index("if (choice.mark && marks.press(px, py)) {")
|
||||
return pointer[start : pointer.index("if (hitNode >= 0) {", start)]
|
||||
|
||||
|
||||
# 누름별 — 고름 유지(풂 건너뜀) 조건 한 줄씩
|
||||
KEEP = {
|
||||
"교각점": '(!!pick && "node" in pick)',
|
||||
"손잡이": "choice.handle !== null",
|
||||
"호(눈금 아님)": '(!(pick && "chainage" in pick) && arcAtScreen(state.curveInfo, toScreen, px, py) >= 0)',
|
||||
}
|
||||
|
||||
|
||||
def test_곡선_누름은_고름_유지() -> None:
|
||||
down = _down()
|
||||
for name, cond in KEEP.items():
|
||||
assert cond in down, name
|
||||
assert "if (!onCurve) marks.deselect();" in down # 그 밖(빈 땅 · 등고선 · 눈금) = 풂
|
||||
assert down.count("marks.deselect()") == 1 # 무조건 풂 걷힘
|
||||
|
||||
|
||||
def test_차례_마커_다음_교각점_앞() -> None:
|
||||
down = _down()
|
||||
assert down.index("canvas.setPointerCapture(event.pointerId);") < down.index("const onCurve")
|
||||
assert down.index("const onCurve") < down.index("if (!onCurve) marks.deselect();")
|
||||
|
||||
|
||||
def test_풀림은_오른쪽_판도_비움() -> None:
|
||||
cross = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Cross.ts")
|
||||
line = "if (from !== PLAN_PICK_FROM || selection.chainageM === null) follow(selection);"
|
||||
assert line in cross
|
||||
follow = cross[cross.index("function follow(") : cross.index(line)]
|
||||
assert "watching = null;" in follow and "current.show(null, interval());" in follow
|
||||
# 풂(값 없음 · 평면 이름)을 내는 곳 = 마커 풂 하나 · Esc 도 같은 길
|
||||
marks = _read("B05_Profile", "B05_Profile_UI_RouteEdit_StructureMarks.ts")
|
||||
assert marks.count("selectDesign(projectId, NO_SELECTION, { from: PLAN_PICK_FROM });") == 1
|
||||
assert "!activeMoving(canvas) && deselect()" in marks
|
||||
|
||||
|
||||
def test_임시_횡단_단추_제목_줄_오른쪽() -> None:
|
||||
modal = _read("B05_Profile", "B05_Profile_UI_Profile_CrossModal.ts")
|
||||
head = modal[
|
||||
modal.index('<div class="b05-crossmodal__head">') : modal.index(
|
||||
'<div class="b05-crossmodal__box">'
|
||||
)
|
||||
]
|
||||
for act in ("add", "remove", "close"):
|
||||
assert f'data-act="{act}"' in head
|
||||
assert head.index("임시 횡단") < head.index("b05-crossmodal__actions")
|
||||
css = _read("B05_Profile", "B05_Profile_UI_Profile_CrossModal.css")
|
||||
actions = css[css.index(".b05-crossmodal__actions {") :]
|
||||
assert "margin-left: auto;" in actions[: actions.index("}")]
|
||||
|
||||
|
||||
def test_제목_줄_끌기_화면_안() -> None:
|
||||
modal = _read("B05_Profile", "B05_Profile_UI_Profile_CrossModal.ts")
|
||||
assert 'bindHeadDrag(root, $(".b05-crossmodal__head"))' in modal
|
||||
drag = modal[modal.index("function bindHeadDrag(") : modal.index("interface CrossModal {")]
|
||||
assert '.closest("button")' in drag # 단추 누름은 단추 몫
|
||||
assert "innerWidth" in drag and "innerHeight" in drag # 화면 밖 안 나감
|
||||
assert "root.parentElement!.getBoundingClientRect()" in drag # 종단 칸 안
|
||||
assert "refit();" in modal # 다시 열 때 가둠
|
||||
Reference in New Issue
Block a user