Merge remote-tracking branch 'origin/dev' into sub_laptop_1
This commit is contained in:
@@ -12,6 +12,10 @@ import { NO_SELECTION, selectDesign } from "./design_selection";
|
||||
/** Esc 로 푼 고름의 출처 이름 — 구독자가 폼까지 닫을지 가르는 데 씀. */
|
||||
export const ESCAPE_FROM = "esc";
|
||||
|
||||
/** 평면 노선 편집 창이 낸 고름(눈금 클릭 · 구조물 마커 고름 풂) — 오른쪽 횡단 판은 제 것이라 거름 ·
|
||||
* 풂(값 없음)이면 Esc 처럼 좌측 폼까지 닫음(409-2). */
|
||||
export const PLAN_PICK_FROM = "b05-plan";
|
||||
|
||||
const OWN_ESCAPE =
|
||||
"input, select, textarea, [contenteditable='true'], [role='dialog'], .ui-modal__backdrop";
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
subscribeSelection,
|
||||
type DesignSelection,
|
||||
} from "./design_selection";
|
||||
import { bindEscapeRelease, ESCAPE_FROM } from "./design_pick_escape";
|
||||
import { bindEscapeRelease, ESCAPE_FROM, PLAN_PICK_FROM } from "./design_pick_escape";
|
||||
import { pipeIdAt } from "./design_pipe_ids";
|
||||
import { graphTargetAt } from "../../B05_Profile/B05_Profile_UI_Selection";
|
||||
import { irregularStationId } from "../../B05_Profile/B05_Profile_UI_IrregularStations";
|
||||
@@ -149,10 +149,12 @@ export function bindProfilePick(
|
||||
if (target.profile.drainage) paintMapPick(target.profile.drainage, selection);
|
||||
};
|
||||
host.onDispose(
|
||||
subscribeSelection(projectId, (_selection, from) => {
|
||||
subscribeSelection(projectId, (selection, from) => {
|
||||
paint();
|
||||
if (from === ESCAPE_FROM && host.visible() && target.form?.hasSelection())
|
||||
target.form.selectById(null);
|
||||
// 풀기 = Esc · 평면 마커 고름 풂(409-2) — 폼 닫기는 펴지 않음(좌측 패널 접힘 · 펼침 그대로 · 398-1)
|
||||
const release =
|
||||
from === ESCAPE_FROM || (from === PLAN_PICK_FROM && selection.chainageM === null);
|
||||
if (release && host.visible() && target.form?.hasSelection()) target.form.selectById(null);
|
||||
}),
|
||||
);
|
||||
bindEscapeRelease(projectId, host);
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
type DesignSelection,
|
||||
} from "../A00_Common/design/design_selection";
|
||||
import { useDesignStore } from "../A00_Common/design/design_store";
|
||||
import { PLAN_PICK_FROM } from "../A00_Common/design/design_pick_escape";
|
||||
import { stationSection } from "./logic/B05_Profile_Pipeline_Detail";
|
||||
import {
|
||||
buildCrossSvg,
|
||||
@@ -79,9 +80,6 @@ export interface CrossWatch {
|
||||
structure?: true;
|
||||
}
|
||||
|
||||
/** 공용 고름에 적는 이름 — 평면 눈금 클릭(제 알림은 거름). */
|
||||
export const PLAN_PICK_FROM = "b05-plan";
|
||||
|
||||
/** 법선을 긋는 측점 갈래 — 일반측점(시점 · 종점 · 곡선 측점 포함). */
|
||||
const isGridStation = (station: PipelineStation): boolean =>
|
||||
station.kind === "grid" || station.kind === "end" || station.kind === "curve";
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
* · 끌기 · 방향키(마커 고른 때 · 교각점 안 고른 때) = 노선 따라 정수 m · 1 m 씩 · 시점 · 종점 밖 안 감 ·
|
||||
* 놓을 때 · 뗄 때 한 번 옮김(`move` = 알약 끌기와 같은 길 · 관 · 구조물 초안 · [저장]이 정본).
|
||||
* · 그림 · 집기 차례 = 맨 위 · 맨 앞(409 · 재기 다음) — `_Render` · `_Pointer` 가 부름.
|
||||
* · 고름 풀기 = 마커 아닌 곳 누름 · Esc(409) — 공용 고름을 `PLAN_PICK_FROM` 으로 비움(오른쪽 횡단 판 그대로).
|
||||
* · 고름 풀기 = 마커 아닌 곳 누름 · Esc(409) — 공용 고름을 `PLAN_PICK_FROM` 으로 비움(오른쪽 횡단 판 그대로 ·
|
||||
* 좌측 목록 강조 · 폼은 풂 409-2).
|
||||
* 목록 · 고름 · 옮김은 종단 보기가 `setPlanMarkSource` 로 건넴. 걸음 셈은 `logic/…_StructureMarks`.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -38,7 +39,7 @@ import { HOLD_DELAY_MS, HOLD_INTERVAL_MS } from "./B05_Profile_UI_Profile_Edit";
|
||||
import { drawStationNormal } from "./B05_Profile_UI_RouteEdit_Hover";
|
||||
import { routePointAtScreen } from "./B05_Profile_UI_RouteEdit_Input";
|
||||
import type { RouteEditPointerParams } from "./B05_Profile_UI_RouteEdit_Pointer";
|
||||
import { PLAN_PICK_FROM } from "./B05_Profile_UI_RouteEdit_Cross";
|
||||
import { PLAN_PICK_FROM } from "../A00_Common/design/design_pick_escape";
|
||||
import type { RouteEditScene } from "./B05_Profile_UI_RouteEdit_Render";
|
||||
import { stationFrameAt } from "./logic/B05_Profile_UI_RouteEdit_Frame";
|
||||
import type { Vertex } from "./logic/B05_Profile_UI_RouteEdit_Curve";
|
||||
|
||||
@@ -68,7 +68,7 @@ const STUBS = {
|
||||
selection = value.chainageM === null ? null : value;
|
||||
},
|
||||
},
|
||||
"./B05_Profile_UI_RouteEdit_Cross": { PLAN_PICK_FROM: "b05-plan" },
|
||||
"../A00_Common/design/design_pick_escape": { PLAN_PICK_FROM: "b05-plan" },
|
||||
"../B06_Section/B06_Section_UI_Cross_Panel_Base": { lastPointerWithin: () => true },
|
||||
"./B05_Profile_UI_Profile_Edit": { HOLD_DELAY_MS: 500, HOLD_INTERVAL_MS: 100 },
|
||||
"./B05_Profile_Api_Structures": {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/* 409-2 평면 마커 고름 풂(b05-plan · 값 없음) = 좌측 폼 · 목록 강조도 풂 — 진짜 `design_selection` ·
|
||||
* `design_pick_escape` · `design_pick_profile` 을 Node 로 돌림. 종단도 · 폼 · 오른쪽 횡단 판은 흉내.
|
||||
* 표준출력 JSON 한 벌 · 짝 = test_409_mark_top_deselect.py.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
|
||||
function loadTs(file, stubs = {}) {
|
||||
const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, file), "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
const localRequire = (request) => {
|
||||
if (stubs[request]) return stubs[request];
|
||||
throw new Error(`stub 없음: ${request}`);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
globalThis.window = { addEventListener() {}, removeEventListener() {} };
|
||||
globalThis.document = { querySelector: () => null };
|
||||
|
||||
const store = new Map();
|
||||
const DSel = loadTs("A00_Common/design/design_selection.ts", {
|
||||
"../b_page_state": {
|
||||
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
||||
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
||||
},
|
||||
});
|
||||
const ESC = loadTs("A00_Common/design/design_pick_escape.ts", { "./design_selection": DSel });
|
||||
const IRR = { irregularStationId: (id) => `irregular:${id}`, isPipeStation: () => true };
|
||||
const SY = loadTs("B05_Profile/B05_Profile_UI_Selection.ts", {
|
||||
"../A00_Common/design/design_selection": DSel,
|
||||
"./B05_Profile_UI_IrregularStations": IRR,
|
||||
});
|
||||
const PP = loadTs("A00_Common/design/design_pick_profile.ts", {
|
||||
"./design_selection": DSel,
|
||||
"./design_pipe_ids": { pipeIdAt: (at) => `pipe-${at.toFixed(2)}` },
|
||||
"./design_pick_escape": ESC,
|
||||
"../../B05_Profile/B05_Profile_UI_Selection": SY,
|
||||
"../../B05_Profile/B05_Profile_UI_IrregularStations": IRR,
|
||||
});
|
||||
|
||||
const PID = "p409";
|
||||
const sel = (chainageM, structureId, from) =>
|
||||
DSel.selectDesign(PID, chainageM === null ? DSel.NO_SELECTION : { chainageM, structureId }, {
|
||||
from,
|
||||
});
|
||||
|
||||
/* 폼 — 관 폼을 닫으면 진짜 폼처럼 관 고름도 비움(`resetForm` → onPipeSelect(null) → b05 풂) */
|
||||
const form = { open: null, resets: 0 };
|
||||
const profile = { structure: null };
|
||||
PP.bindProfilePick(
|
||||
PID,
|
||||
{
|
||||
profile: {
|
||||
setSelectedStation: () => (profile.structure = null),
|
||||
setSelectedStructure: (id) => (profile.structure = id),
|
||||
},
|
||||
form: {
|
||||
hasSelection: () => form.open !== null,
|
||||
selectById: () => {
|
||||
const wasPipe = form.open === "pipe";
|
||||
form.open = null;
|
||||
form.resets += 1;
|
||||
if (wasPipe) sel(null, null, "b05");
|
||||
},
|
||||
},
|
||||
},
|
||||
{ visible: () => true, onDispose() {} },
|
||||
{ irregular: () => [], pipes: () => [{ chainage_m: 80.17 }], sections: () => [] },
|
||||
);
|
||||
/* 오른쪽 횡단 판 흉내 — 받은 알림(낸 쪽) */
|
||||
const crossSaw = [];
|
||||
DSel.subscribeSelection(PID, (selection, from) => crossSaw.push([selection.chainageM, from]));
|
||||
|
||||
const out = {};
|
||||
// 관 마커 고름 → 폼 열림 → 평면 마커 고름 풂
|
||||
sel(80.17, null, "b05");
|
||||
form.open = "pipe";
|
||||
crossSaw.length = 0;
|
||||
sel(null, null, ESC.PLAN_PICK_FROM);
|
||||
out.pipeRelease = { form: { ...form }, crossSaw: [...crossSaw], session: DSel.readSelection(PID) };
|
||||
// 구조물 고름 → 폼 → 풂
|
||||
sel(40, "A", "b05");
|
||||
form.open = "A";
|
||||
sel(null, null, ESC.PLAN_PICK_FROM);
|
||||
out.structureRelease = { form: { ...form }, pill: profile.structure };
|
||||
// 평면 눈금 클릭(값 있음 · b05-plan) = 폼 안 닫음(옛 그대로)
|
||||
form.open = "A";
|
||||
sel(20, null, ESC.PLAN_PICK_FROM);
|
||||
out.tickKeepsForm = form.open;
|
||||
// 폼 없음 = 안 부름
|
||||
form.open = null;
|
||||
const resets = form.resets;
|
||||
sel(null, null, ESC.PLAN_PICK_FROM);
|
||||
out.noFormNoReset = form.resets - resets;
|
||||
out.planName = ESC.PLAN_PICK_FROM;
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -60,6 +60,7 @@ const STUBS = {
|
||||
selectDesign() {},
|
||||
subscribeSelection: (_projectId, fn) => ((listener = fn), () => {}),
|
||||
},
|
||||
"../A00_Common/design/design_pick_escape": { PLAN_PICK_FROM: "b05-plan" },
|
||||
"../A00_Common/design/design_store": {
|
||||
useDesignStore: () => ({ subscribe: () => () => {}, get: () => null }),
|
||||
},
|
||||
|
||||
@@ -74,3 +74,49 @@ def test_오른쪽_판은_제_이름_알림을_거름() -> None:
|
||||
assert "if (from !== PLAN_PICK_FROM) 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
|
||||
|
||||
|
||||
# ── 409-2 마커 고름 풂 = 좌측 목록 강조 · 폼도 풂 · 패널 접힘 · 펼침 그대로 · 오른쪽 판 그대로 ──
|
||||
|
||||
HELPER_2 = Path(__file__).with_name("helper_409_2_plan_release_form.cjs")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got2() -> dict:
|
||||
run = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER_2)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def test_풂_관_폼_닫힘_오른쪽_판은_제_알림만(got2) -> None:
|
||||
release = got2["pipeRelease"]
|
||||
assert release["form"] == {"open": None, "resets": 1}
|
||||
assert release["crossSaw"] == [[None, "b05-plan"]] # 폼이 낸 b05 풂은 알림 안 감(되울림 막이)
|
||||
assert release["session"] is None
|
||||
|
||||
|
||||
def test_풂_구조물_폼_닫힘(got2) -> None:
|
||||
assert got2["structureRelease"] == {"form": {"open": None, "resets": 2}, "pill": None}
|
||||
|
||||
|
||||
def test_눈금_클릭은_폼_그대로(got2) -> None:
|
||||
assert got2["tickKeepsForm"] == "A"
|
||||
assert got2["noFormNoReset"] == 0
|
||||
assert got2["planName"] == "b05-plan"
|
||||
|
||||
|
||||
def test_폼_닫기는_패널_안_폄() -> None:
|
||||
panel = _read("B05_Profile", "B05_Profile_UI_Structures_Panel.ts")
|
||||
head = panel[panel.index(" selectById(structureId) {") :]
|
||||
assert head.index("if (structureId !== null) callbacks.onReveal?.();") < head.index(
|
||||
"resetForm();"
|
||||
)
|
||||
assert "is-collapsed" not in head[: head.index(" addAt(")] # 접힘 · 펼침 안 건드림
|
||||
pick = _read("A00_Common", "design", "design_pick_profile.ts")
|
||||
assert "from === PLAN_PICK_FROM && selection.chainageM === null" in pick
|
||||
|
||||
Reference in New Issue
Block a user