feat(490-1 임시 횡단 표시): B05 종단 그래프 — 누르면 임시 횡단이 열리는 자리 가까이 원 · 세로 점선 · 측점 글 · 손가락 커서
- 판정 셈을 logic/B05_Profile_Profile_CrossPick.ts 한 함수(crossPickAt)로 뺌 — 누름 · 표시가 같은 판정 - 원지반선 위아래 8 px · 추가 측점 선 = 그 측점 · 측점선 · 단추 · 도구 중 · 끌기 · 스크롤 = 안 그림 - 포인터 이동 소수 px 를 누름과 같이 내려 맞춤 — 0.5 m 경계 1 m 어긋남 막음 - 시험 test_490_1_cross_pick · test_368_3 연결 자리 갱신 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,3 +50,54 @@
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-8, 8px);
|
||||
}
|
||||
|
||||
/* 임시 횡단이 열리는 자리 가까이 표시(490-1) — 누름은 아래 그래프 몫(통과) · 세로 축(5) 밑 */
|
||||
.b05-crosshint {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 4;
|
||||
width: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.b05-crosshint[hidden],
|
||||
.b05-crosshint__dot[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b05-crosshint__line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-left: 1px dashed var(--color-accent);
|
||||
}
|
||||
|
||||
.b05-crosshint__dot {
|
||||
position: absolute;
|
||||
top: var(--crosshint-y);
|
||||
left: 0;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 2px solid var(--color-accent);
|
||||
border-radius: 50%;
|
||||
background: var(--color-surface);
|
||||
transform: translate(-50%, -50%);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.b05-crosshint__label {
|
||||
position: absolute;
|
||||
top: var(--crosshint-y);
|
||||
left: 10px;
|
||||
transform: translateY(-50%);
|
||||
padding: 1px 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-4, 4px);
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text-body);
|
||||
font-size: var(--text-caption);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* `pipelineCrossAtChainage` · 368-4) — 격자 없음 · 분석 지형 다름이면 서버 미리 뜬 횡단(`preview-stations` ·
|
||||
* 저장 계획선 기준 · 글 한 줄) · 평면 우측 횡단과 같은 그림(`buildCrossSvg`) · 확대 · 이동 · 묶음 cross 차례마다 다시.
|
||||
* [측점 추가] = 추가 측점 초안(일반측점 · [저장] 한 번) · 같은 m 에 측점이 있으면 막음 · [삭제] = 추가
|
||||
* 측점일 때만 · [닫기].
|
||||
* 측점일 때만 · [닫기]. 열리는 자리 가까이 = 원 · 세로 점선 · 측점 글(`mountCrossHint` · 490-1).
|
||||
* ========================================================================== */
|
||||
|
||||
import "./B05_Profile_UI_Profile_CrossModal.css";
|
||||
@@ -30,9 +30,13 @@ import {
|
||||
CROSS_PLOT_PAD,
|
||||
summarizeCross,
|
||||
} from "./B05_Profile_UI_RouteEdit_Cross_Draw";
|
||||
import {
|
||||
crossPickAt,
|
||||
groundElevationAt,
|
||||
type CrossPickHit,
|
||||
type GroundSample,
|
||||
} from "./logic/B05_Profile_Profile_CrossPick";
|
||||
|
||||
/** 원지반선을 눌렀다고 볼 위아래 여유(px). */
|
||||
const GROUND_SLACK_PX = 8;
|
||||
/** 같은 측점으로 볼 거리(m) — 정수 m 측점끼리. */
|
||||
const SAME_M = 0.5;
|
||||
|
||||
@@ -43,42 +47,87 @@ export interface CrossModalOpen {
|
||||
intervalM: number;
|
||||
}
|
||||
|
||||
/** 그래프 누름 → 원지반선 근처면 그 누가거리(정수 m) · 아니면 null. */
|
||||
export function groundPickAt(
|
||||
/** 그래프 위 포인터 → 임시 횡단 자리(정수 m · 그 자리 원지반선 y px) · 아니면 null.
|
||||
* 누름(작은 창) · 가까이 표시가 같이 씀(490-1) — 판정은 `crossPickAt` 한 벌. */
|
||||
export function crossPickFromEvent(
|
||||
chart: HTMLElement,
|
||||
event: MouseEvent,
|
||||
chainageAtPx: (px: number) => number,
|
||||
samples: ReadonlyArray<{ chainage_m?: number; elevation_m?: number | null }>,
|
||||
): number | null {
|
||||
samples: readonly GroundSample[],
|
||||
): { chainageM: number; groundY: number | null } | null {
|
||||
const target = event.target as Element;
|
||||
const marker = target.closest(".b06-chart__station");
|
||||
const plusRaw = marker?.querySelector(".b06-chart__station-line--plus")
|
||||
? Number(marker.getAttribute("data-chainage"))
|
||||
: null;
|
||||
const hit: CrossPickHit =
|
||||
plusRaw !== null ? { plusM: plusRaw } : marker || target.closest("button") ? "other" : null;
|
||||
const svg = chart.querySelector<SVGSVGElement>(`svg[${Y_WINDOW_ATTRS.auto}="1"]`);
|
||||
if (!svg) return null;
|
||||
const read = (name: string): number => Number(svg.getAttribute(name));
|
||||
const read = (name: string): number => Number(svg?.getAttribute(name));
|
||||
const view: ChartWindow = {
|
||||
center: read(Y_WINDOW_ATTRS.center),
|
||||
pxPerM: read(Y_WINDOW_ATTRS.pxPerM),
|
||||
zero: read(Y_WINDOW_ATTRS.zero),
|
||||
...parseWindowTransform(
|
||||
svg.querySelector(`.${Y_WINDOW_CLASS}`)?.getAttribute("transform") ?? null,
|
||||
svg?.querySelector(`.${Y_WINDOW_CLASS}`)?.getAttribute("transform") ?? null,
|
||||
),
|
||||
};
|
||||
if (![view.center, view.pxPerM, view.zero].every(Number.isFinite) || view.pxPerM <= 0)
|
||||
return null;
|
||||
const rect = chart.getBoundingClientRect();
|
||||
const chainage = chainageAtPx(event.clientX - rect.left);
|
||||
const valid = samples.filter(
|
||||
(s): s is { chainage_m: number; elevation_m: number } =>
|
||||
Number.isFinite(s.chainage_m ?? NaN) && Number.isFinite(s.elevation_m ?? NaN),
|
||||
);
|
||||
const after = valid.findIndex((s) => s.chainage_m >= chainage);
|
||||
if (!Number.isFinite(chainage) || after <= 0) return null;
|
||||
const [a, b] = [valid[after - 1], valid[after]];
|
||||
const t = (chainage - a.chainage_m) / (b.chainage_m - a.chainage_m || 1);
|
||||
const ground = a.elevation_m + (b.elevation_m - a.elevation_m) * t;
|
||||
const top = svg.getBoundingClientRect().top - rect.top;
|
||||
const groundY = chartYAtElevation(view, ground) + top;
|
||||
return Math.abs(event.clientY - rect.top - groundY) <= GROUND_SLACK_PX
|
||||
? Math.round(chainage)
|
||||
: null;
|
||||
const top = svg ? svg.getBoundingClientRect().top - rect.top : 0;
|
||||
const viewOk =
|
||||
!!svg && [view.center, view.pxPerM, view.zero].every(Number.isFinite) && view.pxPerM > 0;
|
||||
const groundY = (elevation: number): number =>
|
||||
viewOk ? chartYAtElevation(view, elevation) + top : NaN;
|
||||
// 포인터 이동은 소수 px · 누름은 정수 px — 내려 맞춰 같은 자리 = 같은 m(0.5 m 경계에서 1 m 어긋남 막음).
|
||||
const chainageM = crossPickAt({
|
||||
hit,
|
||||
chainageM: chainageAtPx(Math.floor(event.clientX) - rect.left),
|
||||
pointerY: Math.floor(event.clientY) - rect.top,
|
||||
samples,
|
||||
groundY,
|
||||
});
|
||||
if (chainageM === null || !Number.isFinite(chainageM)) return null;
|
||||
const ground = groundElevationAt(samples, chainageM);
|
||||
const y = ground === null ? NaN : groundY(ground);
|
||||
return { chainageM, groundY: Number.isFinite(y) ? y : null };
|
||||
}
|
||||
|
||||
/** 임시 횡단이 열리는 자리 가까이 = 작은 원 · 세로 점선 · 측점 글 · 손가락 커서(490-1).
|
||||
* 멀어짐 · 측점선 · 단추 · 도구 중 · 끌기 · 스크롤 = 안 그림. */
|
||||
export function mountCrossHint(
|
||||
chart: HTMLElement,
|
||||
scroller: HTMLElement,
|
||||
options: {
|
||||
pick: (event: MouseEvent) => { chainageM: number; groundY: number | null } | null;
|
||||
x: (chainageM: number) => number;
|
||||
intervalM: number;
|
||||
blocked: () => boolean;
|
||||
},
|
||||
): void {
|
||||
const hint = document.createElement("div");
|
||||
hint.className = "b05-crosshint";
|
||||
hint.hidden = true;
|
||||
hint.innerHTML = `<i class="b05-crosshint__line"></i><i class="b05-crosshint__dot"></i><span class="b05-crosshint__label"></span>`;
|
||||
const dot = hint.querySelector<HTMLElement>(".b05-crosshint__dot")!;
|
||||
const label = hint.querySelector<HTMLElement>(".b05-crosshint__label")!;
|
||||
chart.append(hint);
|
||||
const hide = (): void => {
|
||||
hint.hidden = true;
|
||||
chart.style.cursor = "";
|
||||
};
|
||||
chart.addEventListener("pointermove", (event) => {
|
||||
const at = event.buttons || options.blocked() ? null : options.pick(event);
|
||||
if (!at) return hide();
|
||||
hint.hidden = false;
|
||||
hint.style.left = `${options.x(at.chainageM)}px`;
|
||||
dot.hidden = at.groundY === null;
|
||||
// 원 · 글 높이 = 원지반선(없으면 위쪽) — 글이 위 ▲ 단추를 가리지 않게 원 옆에 둠.
|
||||
hint.style.setProperty("--crosshint-y", `${at.groundY ?? 12}px`);
|
||||
label.textContent = `${stationLabel(at.chainageM, options.intervalM)} · 눌러 임시 횡단`;
|
||||
chart.style.cursor = "pointer";
|
||||
scroller.addEventListener("scroll", hide, { once: true, passive: true });
|
||||
});
|
||||
chart.addEventListener("pointerleave", hide);
|
||||
}
|
||||
|
||||
interface CrossModal {
|
||||
|
||||
@@ -53,7 +53,8 @@ import {
|
||||
import { irregularStationId, type IrregularStation } from "./B05_Profile_UI_IrregularStations";
|
||||
import { mountStructureMenu } from "./B05_Profile_UI_Profile_Structures";
|
||||
import {
|
||||
groundPickAt,
|
||||
crossPickFromEvent,
|
||||
mountCrossHint,
|
||||
openCrossModal,
|
||||
removePlusStation,
|
||||
} from "./B05_Profile_UI_Profile_CrossModal";
|
||||
@@ -328,19 +329,11 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
}
|
||||
// 원지반선 근처 · 추가 측점 선 누름 = 그 자리 임시 횡단 작은 창(368-3)
|
||||
// 다른 측점선 · 단추 누름은 그 몫(측점 고름 · ▲▼)
|
||||
const target = event.target as Element;
|
||||
const marker = target.closest(".b06-chart__station");
|
||||
const plusAt = marker?.querySelector(".b06-chart__station-line--plus")
|
||||
? marker.getAttribute("data-chainage")
|
||||
: null;
|
||||
if ((marker && !plusAt) || target.closest("button")) return;
|
||||
const toChainageAt = chainageInverter(longitudinal, width, layout.originOffset);
|
||||
const at = plusAt
|
||||
? Math.round(Number(plusAt))
|
||||
: groundPickAt(chartWrap, event, toChainageAt, graphData.samples);
|
||||
const at = crossPickFromEvent(chartWrap, event, toChainageAt, graphData.samples);
|
||||
if (at === null) return;
|
||||
openCrossModal(body.parentElement ?? body, {
|
||||
chainageM: at,
|
||||
chainageM: at.chainageM,
|
||||
stations: graphLongitudinal.stations.map((station) => station.chainage_m),
|
||||
intervalM: stationIntervalM ?? 20,
|
||||
});
|
||||
@@ -495,6 +488,13 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
}),
|
||||
);
|
||||
}
|
||||
// 임시 횡단이 열리는 자리 가까이 = 원 · 세로 점선 · 측점 글(490-1 · 누름과 같은 판정).
|
||||
mountCrossHint(chartWrap, body, {
|
||||
pick: (event) => crossPickFromEvent(chartWrap, event, toChainage, graphData.samples),
|
||||
x,
|
||||
intervalM: stationIntervalM ?? 20,
|
||||
blocked: ctx.toolActive,
|
||||
});
|
||||
// 캔버스 높이 = 종단도 영역만. 서브패널이 차지한 아래 공간을 덮지 않는다(밀어올리기).
|
||||
// 구조물 알약 레인이 그래프 밑에 한 줄 붙으므로 그만큼 캔버스가 커진다(2026-08-17 지시 6).
|
||||
canvas.style.height = `${chartHeight + STRUCTURE_LANE_HEIGHT_PX}px`;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/* =============================================================================
|
||||
* logic/B05_Profile_Profile_CrossPick.ts
|
||||
* 종단 그래프 한 점 → 임시 횡단을 열 누가거리(정수 m) · 아니면 null(368-3 · 490-1).
|
||||
*
|
||||
* 누름(작은 창 열기)과 가까이 표시가 같은 판정을 씀 — 추가 측점 선 = 그 측점 · 다른 측점선 · 단추 =
|
||||
* 그 몫(null) · 빈 곳 = 원지반선 위아래 `GROUND_SLACK_PX` 안.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 원지반선을 눌렀다고 볼 위아래 여유(px). */
|
||||
export const GROUND_SLACK_PX = 8;
|
||||
|
||||
/** 포인터 아래 = 추가 측점 선(그 누가거리) · 다른 측점선 · 단추("other") · 빈 곳(null). */
|
||||
export type CrossPickHit = { plusM: number } | "other" | null;
|
||||
|
||||
export type GroundSample = { chainage_m?: number; elevation_m?: number | null };
|
||||
|
||||
export interface CrossPickInput {
|
||||
hit: CrossPickHit;
|
||||
/** 포인터 x 의 누가거리. */
|
||||
chainageM: number;
|
||||
/** 포인터 y(그래프 px). */
|
||||
pointerY: number;
|
||||
samples: readonly GroundSample[];
|
||||
/** 표고 → 그래프 y px(세로 창 없으면 NaN). */
|
||||
groundY: (elevationM: number) => number;
|
||||
}
|
||||
|
||||
/** 누가거리의 원지반 표고(앞뒤 표본 직선 보간) · 첫 표본 앞 · 끝 뒤 · 빈 표본 = null. */
|
||||
export function groundElevationAt(
|
||||
samples: readonly GroundSample[],
|
||||
chainageM: number,
|
||||
): number | null {
|
||||
const valid = samples.filter(
|
||||
(s): s is { chainage_m: number; elevation_m: number } =>
|
||||
Number.isFinite(s.chainage_m ?? NaN) && Number.isFinite(s.elevation_m ?? NaN),
|
||||
);
|
||||
const after = valid.findIndex((s) => s.chainage_m >= chainageM);
|
||||
if (!Number.isFinite(chainageM) || after <= 0) return null;
|
||||
const [a, b] = [valid[after - 1], valid[after]];
|
||||
const t = (chainageM - a.chainage_m) / (b.chainage_m - a.chainage_m || 1);
|
||||
return a.elevation_m + (b.elevation_m - a.elevation_m) * t;
|
||||
}
|
||||
|
||||
/** 임시 횡단을 열 누가거리(정수 m) · 아니면 null. */
|
||||
export function crossPickAt(input: CrossPickInput): number | null {
|
||||
if (input.hit === "other") return null;
|
||||
if (input.hit) return Math.round(input.hit.plusM);
|
||||
const ground = groundElevationAt(input.samples, input.chainageM);
|
||||
if (ground === null) return null;
|
||||
return Math.abs(input.pointerY - input.groundY(ground)) <= GROUND_SLACK_PX
|
||||
? Math.round(input.chainageM)
|
||||
: null;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/* 490-1 임시 횡단 판정 — 진짜 `logic/B05_Profile_Profile_CrossPick.ts` 를 Node 로 돌려 냄.
|
||||
* 입력 argv[2] JSON {samples, cases: [{hit, chainageM, pointerY}]} · groundY = 100 - 표고 × 2 px.
|
||||
* 짝 = test_490_1_cross_pick.py. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const file = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"B05_Profile",
|
||||
"logic",
|
||||
"B05_Profile_Profile_CrossPick.ts",
|
||||
);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, require);
|
||||
const logic = box.exports;
|
||||
|
||||
const job = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
const groundY = (elevation) => 100 - elevation * 2;
|
||||
const out = job.cases.map((c) => ({
|
||||
pick: logic.crossPickAt({ ...c, samples: job.samples, groundY }),
|
||||
ground: logic.groundElevationAt(job.samples, c.chainageM),
|
||||
}));
|
||||
process.stdout.write(JSON.stringify({ slack: logic.GROUND_SLACK_PX, out }));
|
||||
@@ -24,7 +24,7 @@ def test_추가_측점_선_점선_끌기_없음() -> None:
|
||||
|
||||
def test_누름_작은_창_우클릭_삭제() -> None:
|
||||
render = _read("B05_Profile/B05_Profile_UI_Profile_Render.ts")
|
||||
assert "groundPickAt(chartWrap, event" in render and "openCrossModal(" in render
|
||||
assert "crossPickFromEvent(chartWrap, event" in render and "openCrossModal(" in render
|
||||
assert "onRemovePlus: removePlusStation" in render
|
||||
menu = _read("B05_Profile/B05_Profile_UI_Profile_Structures.ts")
|
||||
assert '"측점 삭제", () => options.onRemovePlus?.(near.station.chainage_m)' in menu
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""490-1 B05 종단 — 임시 횡단이 열리는 자리 가까이 표시 = 누름과 같은 판정(`crossPickAt`).
|
||||
|
||||
판정 함수 = 옛 누름 판정(원지반선 위아래 8 px · 정수 m · 추가 측점 선 = 그 측점 · 측점선 · 단추 = null)과
|
||||
같은 값 · 허용 밖 null. TS 쪽 = `helper_490_1_cross_pick.cjs`(Node). 화면 조작은 ORCA 로 확인.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
SAMPLES = [
|
||||
{"chainage_m": 0.0, "elevation_m": 10.0},
|
||||
{"chainage_m": 20.0, "elevation_m": 20.0},
|
||||
{"chainage_m": 40.0, "elevation_m": None}, # 빈 표고는 건너뜀
|
||||
{"chainage_m": 60.0, "elevation_m": 0.0},
|
||||
]
|
||||
|
||||
|
||||
def _ground_y(elevation: float) -> float:
|
||||
return 100 - elevation * 2
|
||||
|
||||
|
||||
def _old_pick(hit, chainage: float, pointer_y: float) -> int | None:
|
||||
"""옛 `groundPickAt` + 누름 처리(Render.ts 329~346) 그대로."""
|
||||
if hit == "other":
|
||||
return None
|
||||
if hit:
|
||||
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)
|
||||
if after <= 0:
|
||||
return 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
|
||||
|
||||
|
||||
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": 50.0, "pointerY": 93.0}, # 빈 표고 건너 20→60 보간
|
||||
{"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}, # 측점선 · 단추
|
||||
]
|
||||
|
||||
|
||||
def _run() -> dict:
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / "job.json"
|
||||
path.write_text(json.dumps({"samples": SAMPLES, "cases": CASES}), encoding="utf-8")
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_490_1_cross_pick.cjs"), str(path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_판정_함수_옛_누름과_같은_값() -> None:
|
||||
result = _run()
|
||||
assert result["slack"] == 8
|
||||
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]
|
||||
|
||||
|
||||
def test_누름_표시_같은_길() -> None:
|
||||
render = (ROOT / "B05_Profile/B05_Profile_UI_Profile_Render.ts").read_text(encoding="utf-8")
|
||||
assert render.count("crossPickFromEvent(chartWrap, event") == 2 # 누름 · 표시
|
||||
assert "mountCrossHint(chartWrap, body" in render and "blocked: ctx.toolActive" in render
|
||||
modal = (ROOT / "B05_Profile/B05_Profile_UI_Profile_CrossModal.ts").read_text(encoding="utf-8")
|
||||
assert "crossPickAt({" in modal and "event.buttons || options.blocked()" in modal
|
||||
assert 'scroller.addEventListener("scroll", hide' in modal
|
||||
Reference in New Issue
Block a user