diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index 4aaa34321..e05df96aa 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -475,16 +475,11 @@ export async function mountCrossView( handoff, sectionDetail, { - // 넘겨받은 구조물 id 가 그 측점 관보다 먼저다(52-15). - focusStation: (stationId) => { - structuresPanel.prefer(handoff?.structureId ?? null); - sectionView.focusStation(stationId); - structuresPanel.prefer(null); - // 그 카드가 이미 골라져 있으면 카드 고름이 안 돌아 폼이 옛 것으로 남음 — id 로 직접 엶(53-1) - const sid = handoff?.structureId; - if (sid && structuresPanel.getStructures().some((entry) => entry.structure_id === sid)) - structuresPanel.selectStructure(sid); - }, + // 넘겨받은 구조물 id 가 먼저(52-15) · 폼은 id 로 직접(53-1) · 측점 없는 구조물은 카드 · 스크롤 없음(307) + focusStation: (stationId) => + structuresPanel.focusPicked(handoff?.structureId ?? null, () => + sectionView.focusStation(stationId), + ), refreshCard: (chainageM) => sectionView.refreshCard(chainageM), revetSelect: (chainageM, key) => stationControls.revetOffset.select(chainageM, key), fordSelect: (chainageM, role) => stationControls.ford.select(chainageM, role), diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts index 7a3dbac79..44b82ef41 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts @@ -43,6 +43,8 @@ export interface RevetOffsetControl { highlightFor: (section: CrossSection) => RevetPick | null; /** `ownerM` = 폼에 올릴 시설 자리를 누른 쪽이 앎(물넘이 콘크리트 · 294-7) — 없으면 카드로 셈. */ select: (chainageM: number, key: RevetPick | null, ownerM?: number) => void; + /** 고른 「관」이 물넘이 콘크리트인가 — 그 카드는 콘크리트만 켬 · 관 그림은 안 켬(307). */ + shapePicked?: () => boolean; /** 선택이 바뀌면 화면이 연동 카드 강조를 다시 칠한다 — 목록 쪽이 꽂는다. */ onSelectionChange?: () => void; /** 기하가 실제로 적용한 조작값을 되받아 담는다(한계에 걸린 요청값을 잘라 낸다). */ diff --git a/B06_Section/B06_Section_UI_Cross_Shape_Pick.ts b/B06_Section/B06_Section_UI_Cross_Shape_Pick.ts index 1aa025e3f..79e5a1cd0 100644 --- a/B06_Section/B06_Section_UI_Cross_Shape_Pick.ts +++ b/B06_Section/B06_Section_UI_Cross_Shape_Pick.ts @@ -12,6 +12,8 @@ * 노면선 보라 판정 띠(굵기 10 · 위아래 5) 바깥 아래를 누르면 형상(294-7). * ========================================================================== */ +import type { RevetHighlightSetter } from "./B06_Section_UI_Cross_Culvert"; + const SVG_NS = "http://www.w3.org/2000/svg"; /** 관 지점이 아닌 구조물(맹암거) 형상 누름 — 카드 밖(구조물 목록)이 id 로 고름(288-2). */ export const SHAPE_PICK_EVENT = "b06-structure-shape-pick"; @@ -105,12 +107,26 @@ export function shapePickRoute( }; } -/** 카드 안 형상 강조 — 고른 구조물 id 와 같은 형상만(목록 · 알약 · 폼 고름이 따라옴). */ +/** 카드 「관」 강조를 관 그림 · 물넘이 콘크리트로 가름(307) — 콘크리트를 고름(`concrete`) = 콘크리트만 · + * 관을 고름 = 관만(관 카드에 걸친 물넘이포장 콘크리트를 눌러도 관 형상은 안 켬). */ +export function splitPipePick( + base: RevetHighlightSetter, + setShape: (active: boolean) => void, + concrete: () => boolean, +): RevetHighlightSetter { + return (key) => { + const shape = key === "pipe" && concrete(); + base(shape ? null : key); + setShape(shape); + }; +} + +/** 카드 안 형상 강조 — 고른 구조물 id 와 같은 형상만(목록 · 알약 · 폼 고름이 따라옴 · 맹암거). 물넘이 + * 콘크리트(`pipe-…`)는 형상을 직접 누른 「관」 고름만 켬 — 측점 길(측점선 · 알약 · 목록)은 안 켬(307). */ export function highlightShapes(root: ParentNode, structureId: string | null): void { for (const shape of root.querySelectorAll("[data-structure-id]")) { - shape.classList.toggle( - "is-active", - !!structureId && shape.getAttribute("data-structure-id") === structureId, - ); + const id = shape.getAttribute("data-structure-id") ?? ""; + if (id.startsWith("pipe-")) continue; + shape.classList.toggle("is-active", !!structureId && id === structureId); } } diff --git a/B06_Section/B06_Section_UI_Cross_View.ts b/B06_Section/B06_Section_UI_Cross_View.ts index 2fb647b19..dd7f71188 100644 --- a/B06_Section/B06_Section_UI_Cross_View.ts +++ b/B06_Section/B06_Section_UI_Cross_View.ts @@ -21,7 +21,7 @@ import { appendFordSurfaceDropPlan, } from "./B06_Section_UI_Cross_Ford_Pavement"; import { ownWallsDrawAt } from "./B06_Section_UI_Cross_Culvert_Wire"; -import { attachShapePick, shapePickRoute } from "./B06_Section_UI_Cross_Shape_Pick"; +import { attachShapePick, shapePickRoute, splitPipePick } from "./B06_Section_UI_Cross_Shape_Pick"; import { stackPickTargets } from "./B06_Section_UI_Cross_Pick_Order"; import { appendUnderdrainOverlay } from "./B06_Section_UI_Cross_Underdrain"; import { raiseWalls } from "./B06_Section_UI_Cross_Wall"; @@ -232,9 +232,11 @@ export function createCrossSectionCard( }, selectCard: () => onSelect(section.station_id), }); + const shapePicked = (): boolean => revetOffset?.shapePicked?.() ?? false; const toggleRevet = (key: RevetPick, ownerM?: number): void => { const wasSelected = isSelected; - activeRevet = activeRevet === key ? null : key; + // 같은 「관」이라도 콘크리트 ↔ 관 그림을 바꿔 누르면 풀지 않고 옮김(307) + activeRevet = activeRevet === key && (ownerM !== undefined) === shapePicked() ? null : key; showRevetControl(activeRevet !== null); // 강조는 **연동된 카드 전부**라 제어기를 거친다(제어기가 목록에 다시 칠하기를 // 알린다 — 2026-08-30 사용자). 배선이 없어도 이 카드만은 즉시 맞춘다. @@ -595,12 +597,9 @@ export function createCrossSectionCard( revetOffset && shapePickRoute(svg, (at) => toggleRevet("pipe", at)), ); if (setShapeActive) { - const baseSetActive = setRevetActive; - setRevetActive = (key) => { - baseSetActive(key); - setShapeActive(key === "pipe"); - }; - setShapeActive(highlightRevet === "pipe"); + // 「관」 = 관 그림 또는 콘크리트 하나만(307 — 연동 관 카드 콘크리트 누름에 관 형상 안 켬) + setRevetActive = splitPipePick(setRevetActive, setShapeActive, shapePicked); + setRevetActive(highlightRevet); } // 보라 선 고름 — 가림 · 판정면 차례가 정해진 뒤(137-2). const lineDesign = section.design; diff --git a/B06_Section/B06_Section_UI_Page_Station_Controls.ts b/B06_Section/B06_Section_UI_Page_Station_Controls.ts index 79bc25659..390a81de5 100644 --- a/B06_Section/B06_Section_UI_Page_Station_Controls.ts +++ b/B06_Section/B06_Section_UI_Page_Station_Controls.ts @@ -183,8 +183,8 @@ export function createStationControls(deps: StationControlDeps): StationControls ); const revetShifts = revetSession.values; /** 지금 고른 벽 **하나** — 조정창이 뜬 측점(누가거리 키)과 벽 키(2026-08-30 개편: - * 강조는 연동으로 이어진 카드 전부, 조정창은 고른 카드 하나). */ - let revetSelection: { at: string; key: RevetPick } | null = null; + * 강조는 연동으로 이어진 카드 전부, 조정창은 고른 카드 하나). `shape` = 「관」이 물넘이 콘크리트(307). */ + let revetSelection: { at: string; key: RevetPick; shape?: boolean } | null = null; const revetKey = (chainageM: number, role: RevetKey): string => `${chainageM.toFixed(2)}:${role}`; function loadRevetShifts(): void { @@ -268,14 +268,17 @@ export function createStationControls(deps: StationControlDeps): StationControls if (!owner || !here || Math.abs(here.chainage_m - owner.chainage_m) > 0.01) return null; return wallStandsAt(owner, section, revetSelection.key) ? revetSelection.key : null; }, - select: (chainageM, key) => { - revetSelection = key ? { at: chainageM.toFixed(2), key } : null; + select: (chainageM, key, ownerM) => { + // 「관」 = 물넘이 콘크리트 — 형상 누름(ownerM) · 관 없는 카드 · 걸친 관 그림은 안 켬(307) + const shape = key === "pipe" && (ownerM !== undefined || !sectionAt(chainageM)?.culvert); + revetSelection = key ? { at: chainageM.toFixed(2), key, shape } : null; // 벽을 고르면 같은 측점 세월교 · 박스 고름을 풂 — 측벽 · 흙막이 동시 고름 금지(107-1) const { ford, box } = bodyControls; if (key && ford.control.selectedFor(chainageM)) ford.control.select(chainageM, null); if (key && box.control.selectedFor(chainageM)) box.control.select(chainageM, null); revetOffsetControl.onSelectionChange?.(); }, + shapePicked: () => revetSelection?.shape === true, syncApplied: (chainageM, role, applied) => { // 기하가 실제로 적용한 값(한계 절삭 후)을 되받아 담는다 — 숫자만 커지는 것 방지. const key = revetKey(chainageM, role); diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 2b18e04d5..4cd5736e3 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -87,7 +87,7 @@ export interface B06StructuresPanelDeps { wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null; /** 측점 간격(m) — 측점번호+잔여거리 환산용. */ stationInterval: () => number; - /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다(revealOnly = 고르지 않고 스크롤만 · 177-4). */ + /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다(revealOnly = 고르지도 스크롤도 않음 · 측점 없는 구조물 · 307). */ focusChainage: (chainageM: number, revealOnly?: boolean) => void; /** 목록에서 고른 줄을 다시 눌러 풂 — 횡단 카드 선택도 끔(54-8). */ releaseCard?: () => void; @@ -130,8 +130,11 @@ export interface B06StructuresPanel { preferred: () => string | null; /** 구조물 하나를 폼에 올리고 그 카드로 간다 — 알약을 누른 길(목록 클릭과 같은 흐름). */ selectStructure: (structureId: string) => void; - /** 측점 없는 관(물넘이포장) 하나를 폼에 올림 — 카드 안 고름 · 스크롤만 · 알약 · 고름 값에 알약 id(요구 178). */ + /** 측점 없는 관 하나를 폼에 올림 — 카드 안 고름 · 스크롤 없음 · 알약 · 고름 값에 알약 id(요구 178 · 307). */ selectPipeAt: (chainageM: number) => void; + /** 넘김값(B05 알약 · 3D · 지도)의 카드 고름 — `focus` 가 그 카드를 고름 · 넘긴 구조물이 먼저(52-15) · + * 폼은 id 로 직접 엶(53-1) · 측점 없는 구조물은 카드 고름 · 스크롤 없이 폼만(요구 178 · 307). */ + focusPicked: (structureId: string | null, focus: () => void) => void; /** 배수유역도가 관 초안에 채운 빈 월류 높이(126-1)를 목록 · 폼에 받는다(298-1). */ adoptFordHeights: () => void; /** 지금 목록의 구조물 정본(미저장분 포함) — 카드 찾기에 쓴다. */ @@ -224,7 +227,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc } currentChainageM = null; // 폼 = 이 구조물 — 옛 관을 붙든 채 남지 않게(165-2) // 카드 고름이 측점으로 다시 풀면 같은 측점 관이 먼저 열린다 — 누른 구조물을 넘긴다. - // 노면공 = 카드 하나 안 고름 · 범위 카드 전부 테두리 · 그 카드로 스크롤만 · 폼 유지(177-4) + // 노면공 = 카드 하나 안 고름 · 범위 카드 전부 테두리 · 스크롤 없음 · 폼 유지(177-4 · 307) // 측점 없는 구조물(제 측점을 안 심음)도 같은 길 — 근처 카드 측점선을 켜지 않음(요구 178) const revealOnly = !coverable(structure) || !plantsStation(structure, markTypes); const id = structure.structure_id ?? null; @@ -586,6 +589,15 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc }, preferred: () => preferredId, selectStructure: (structureId) => section.selectById(structureId), + focusPicked: (structureId, focus) => { + const known = structures.find((entry) => entry.structure_id === structureId); + if (!known || plantsStation(known, markTypes)) { + preferredId = structureId; + focus(); + preferredId = null; + } + if (known) section.selectById(structureId); + }, showAtChainage: (chainageM) => { currentChainageM = null; const selection: DesignSelection = { chainageM, structureId: preferredId }; @@ -616,7 +628,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc ); if (!pipe) return; const id = `pipe-${pipe.chainage_m.toFixed(2)}`; - deps.focusChainage(pipe.chainage_m, true); // 카드 안 고름 · 스크롤만 — 옛 카드 · 측점선 풂(177-4 길) + deps.focusChainage(pipe.chainage_m, true); // 카드 안 고름 · 스크롤 없음 — 옛 카드 · 측점선 풂(177-4 길 · 307) currentChainageM = pipe.chainage_m; refreshSpecDefaults(pipe.chainage_m); section.selectPipeByChainage(pipe.chainage_m); diff --git a/B06_Section/B06_Section_UI_Page_Structures_Selection.ts b/B06_Section/B06_Section_UI_Page_Structures_Selection.ts index 73181ca11..954eb5130 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Selection.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Selection.ts @@ -90,7 +90,7 @@ export function wireStructureSelection( ownerM ?? (key === "chute" ? chainageM : ownerChainageOf(chainageM, key ?? undefined)); // 푸는 것은 고름이 아님 — 표시를 켜면 뒤이은 카드 고름이 선택 칸에 안 적힘(53-1) if (key) markWallSelecting(owner); - origRevet(chainageM, key); + origRevet(chainageM, key, ownerM); // ownerM = 콘크리트 형상 누름 표식(307) if (key) remember?.(owner, key); if (key) panel.showAtChainage(owner); else if (!nested) { diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index 92a38767a..e0570dda1 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -244,8 +244,11 @@ export function createSectionView( let spanMarks: ReadonlyArray = []; let spanTypes: ReadonlyArray = []; let spanMarkId: string | null = null; - /** 고른 구조물(알약 · 목록 · 폼) — 물넘이포장 · 맹암거 형상 강조(287-5 · 288-2). */ + /** 고른 구조물(알약 · 목록 · 폼) — 맹암거 형상 강조(288-2 · 물넘이 콘크리트는 「관」 고름 몫 · 307). */ let pickedMarkId: string | null = null; + /** 포장층을 직접 누른 노면공 — 그 포장층만 강조 · 측점 길(알약 · 목록) 고름은 안 켬(307). */ + let paveShapeId: string | null = null; + let paveShapeTurn = false; const inSpan = (section: CrossSection, mark: StructureInstance | undefined): boolean => { const { start_m: start, end_m: end } = mark ?? {}; if (start == null || end == null || start === end) return false; @@ -261,7 +264,7 @@ export function createSectionView( ? spanMarks.find((entry) => entry.structure_id === spanMarkId) : undefined; const span = inSpan(section, mark); - const pave = span && isPavement(spanMarkId); + const pave = span && isPavement(spanMarkId) && paveShapeId === spanMarkId; card.classList.toggle("b06-cross-card--span", span); highlightPavement(card, pave ? mark : undefined); highlightShapes(card, pickedMarkId); @@ -430,7 +433,14 @@ export function createSectionView( const pave = pavementAt(event.target, spanMarks); if (!pave?.structure_id || !isPavement(pave.structure_id)) return; event.stopPropagation(); - pavementSelectListener?.(pave.structure_id); + paveShapeTurn = true; + try { + pavementSelectListener?.(pave.structure_id); + } finally { + paveShapeTurn = false; + } + paveShapeId = pave.structure_id; + syncSpanHighlights(); }, true, ); @@ -605,19 +615,20 @@ export function createSectionView( ); } }, - revealStation(stationId) { - // 노면공 고름 = 카드 하나 안 고름 · 옛 카드는 리스너 없이 풂(폼 유지) · 그 카드로 스크롤만(177-4) + revealStation() { + // 측점 없는 구조물(노면공 등) 고름 = 카드 하나 안 고름 · 옛 카드는 리스너 없이 풂(폼 유지) · + // 강조할 횡단도가 없어 스크롤도 안 함(177-4 · 요구 178 · 307) const previous = selectedStationId; selectedStationId = null; activeAreaKey = null; markCard(previous); profile.showCard(null); - revealCard(stationId, "smooth"); }, selectMark: (structureId, span = true) => { - // span 끔 = 알약만 · 카드는 고른 측점 하나(137-4 — 측점 고름 리스너) · 노면공 포장층은 테두리가 아니라 늘 켬(165-2) + // span 끔 = 알약만 · 카드는 고른 측점 하나(137-4 — 측점 고름 리스너) · 노면공 범위 테두리는 늘(165-2) · 포장층은 누름만(307) spanMarkId = span || isPavement(structureId) ? structureId : null; pickedMarkId = structureId; + if (!paveShapeTurn) paveShapeId = null; // 포장층 강조 = 포장층 누름 턴만(307) syncSpanHighlights(); profile.selectMark(structureId); }, diff --git a/B06_Section/B06_Section_UI_Section_View_Profile.ts b/B06_Section/B06_Section_UI_Section_View_Profile.ts index ccf3c0b14..a6ece2772 100644 --- a/B06_Section/B06_Section_UI_Section_View_Profile.ts +++ b/B06_Section/B06_Section_UI_Section_View_Profile.ts @@ -248,7 +248,7 @@ export function createSectionProfile( edit.selectStructure(hit.structure_id); return; } - // 측점 없는 관(물넘이포장) = 카드 안 고름 · 그 자리로 스크롤만 · 그 관 폼 · 알약만(요구 178) + // 측점 없는 관 = 카드 안 고름 · 스크롤 없음 · 그 관 폼 · 알약만(요구 178 · 307) if (edit?.selectPipe && !plantsStation(hit, types)) { edit.selectPipe(structureAnchorM(hit)); return; diff --git a/B06_Section/B06_Section_UI_Section_View_Types.ts b/B06_Section/B06_Section_UI_Section_View_Types.ts index a67c0d508..5a96a7a4d 100644 --- a/B06_Section/B06_Section_UI_Section_View_Types.ts +++ b/B06_Section/B06_Section_UI_Section_View_Types.ts @@ -48,7 +48,7 @@ export interface SectionViewController { /** 좌측 구조물 목록에서 고른 측점 카드를 선택하고 화면에 드러낸다 — 재클릭 토글 없음 * (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). null = 카드 선택 풂(54-8). */ focusStation: (stationId: string | null) => void; - /** 카드를 고르지 않고 스크롤만 — 고른 카드는 알림 없이 풂(노면공 목록 고름 · 177-4). */ + /** 카드를 고르지 않음 · 스크롤 없음 — 고른 카드는 알림 없이 풂(측점 없는 구조물 고름 · 177-4 · 307). */ revealStation: (stationId: string) => void; /** 종단 알약 강조를 그 구조물로(null = 풀기 — 카드 측점선 짝을 따름). * `span` = 구간형 범위 안 카드 전부 테두리(기본) · 끄면 카드는 고른 측점 하나(137-4). */ diff --git a/resources/tester/helper_307_pick_highlight.cjs b/resources/tester/helper_307_pick_highlight.cjs new file mode 100644 index 000000000..cf10fb03d --- /dev/null +++ b/resources/tester/helper_307_pick_highlight.cjs @@ -0,0 +1,134 @@ +/* 307 B06 고름 강조 헬퍼 — 진짜 TS 를 Node 로 돌려 표준출력 JSON 한 벌로. + * ① 「관」 setter 가름(splitPipePick) ② 측점 길 형상 강조(highlightShapes) ③ 벽 고름 콘크리트 표식(Station_Controls). + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const cache = new Map(); +const load = (file) => { + if (cache.has(file)) return cache.get(file); + const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(file, box.exports); + const near = (m) => { + if (m.startsWith("@config/")) return load(path.join(ROOT, "config", `${m.slice(8)}.ts`)); + if (m.endsWith(".json")) return require(path.join(path.dirname(file), m)); + if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`)); + return new Proxy({}, { get: () => () => {} }); + }; + new Function("exports", "module", "require", js)(box.exports, box, near); + return box.exports; +}; +const memory = new Map(); +globalThis.sessionStorage = { + getItem: (k) => (memory.has(k) ? memory.get(k) : null), + setItem: (k, v) => memory.set(k, String(v)), + removeItem: (k) => memory.delete(k), +}; +globalThis.window = { setTimeout, clearTimeout, sessionStorage: globalThis.sessionStorage }; + +const B = (f) => path.join(ROOT, "B06_Section", f); +const shapePick = load(B("B06_Section_UI_Cross_Shape_Pick.ts")); + +// ① 「관」 setter — 콘크리트 고름 = 콘크리트만 · 관 고름 = 관만 · 다른 벽 = 그대로 +const split = (concrete, key) => { + const calls = { base: [], shape: [] }; + const setter = shapePick.splitPipePick( + (k) => calls.base.push(k), + (on) => calls.shape.push(on), + () => concrete, + ); + setter(key); + return calls; +}; + +// ② 측점 길 형상 강조 — 맹암거는 id 대로 · 물넘이 콘크리트(pipe-…)는 안 건드림 +class Node { + constructor(id, cls = "") { + this.attrs = { "data-structure-id": id, class: cls }; + } + getAttribute(k) { + return this.attrs[k] ?? null; + } + get classList() { + const names = () => (this.attrs.class ?? "").split(" ").filter(Boolean); + return { + contains: (c) => names().includes(c), + toggle: (c, on) => { + const rest = names().filter((n) => n !== c); + this.attrs.class = (on ? [...rest, c] : rest).join(" "); + }, + }; + } +} +const drain = new Node("ud-1"); +const concreteLit = new Node("pipe-90.00", "is-active"); // 형상 누름 「관」이 켠 콘크리트 +const concreteOff = new Node("pipe-90.00"); +const root = { querySelectorAll: () => [drain, concreteLit, concreteOff] }; +shapePick.highlightShapes(root, "pipe-90.00"); // 물넘이포장 측점 길(알약 · 목록 · 측점선) = 알약 id +const byStationPath = { + concreteOff: concreteOff.classList.contains("is-active"), + concreteLitKept: concreteLit.classList.contains("is-active"), +}; +shapePick.highlightShapes(root, "ud-1"); +const drainLit = drain.classList.contains("is-active"); +shapePick.highlightShapes(root, null); // 풂(Esc · 다른 것) — 콘크리트는 「관」 고름 풂이 끔 +const afterRelease = { + drain: drain.classList.contains("is-active"), + concreteLit: concreteLit.classList.contains("is-active"), +}; + +// ③ 벽 고름 콘크리트 표식 — 관 카드(80) · 물넘이 제 카드(90 · 관 없음) +const stations = load(B("B06_Section_UI_Page_Station_Controls.ts")); +const sections = [ + { station_id: "s80", chainage_m: 80, culvert: { inlet: {}, outlet: {} }, samples: [] }, + { station_id: "s90", chainage_m: 90, ford_pavement: { chainage_m: 90 }, samples: [] }, +]; +let changes = 0; +const controls = stations.createStationControls({ + sessionKey: () => null, + refreshCard: () => {}, + detail: () => ({ cross_sections: sections }), + crossHalfWidth: () => undefined, + sampledHalfWidth: () => 20, + ensureSampledWidth: async () => true, + projectId: () => null, +}); +const revet = controls.revetOffset; +revet.onSelectionChange = () => (changes += 1); +const flag = (chainageM, key, ownerM) => { + revet.select(chainageM, key, ownerM); + return { + shape: revet.shapePicked(), + selected: revet.selectedFor(sections[chainageM === 80 ? 0 : 1]), + }; +}; +const revetCases = { + pipeOnPipeCard: flag(80, "pipe"), // 관 그림 누름 = 관 + concreteOnPipeCard: flag(80, "pipe", 90), // 연동 관 카드 콘크리트 누름 = 콘크리트 + concreteOnOwnCard: flag(90, "pipe", 90), // 물넘이 제 카드 콘크리트 누름 + handoffOnOwnCard: flag(90, "pipe"), // 3D 넘김 「관」(관 없는 카드) = 콘크리트 + wall: flag(80, "inlet"), + released: flag(80, null), + changes: 0, +}; +revetCases.changes = changes; + +process.stdout.write( + JSON.stringify({ + split: { + concretePipe: split(true, "pipe"), + pipePipe: split(false, "pipe"), + concreteWall: split(true, "inlet"), + none: split(false, null), + }, + byStationPath, + drainLit, + afterRelease, + revetCases, + }), +); diff --git a/resources/tester/test_118_pipe_pick.py b/resources/tester/test_118_pipe_pick.py index 8a5254b20..5a6b3683d 100644 --- a/resources/tester/test_118_pipe_pick.py +++ b/resources/tester/test_118_pipe_pick.py @@ -106,7 +106,10 @@ def test_좌측_관_상자_강조_스크롤() -> None: def test_벽_본체_동시_고름_금지_그대로() -> None: """107-1 — 관도 같은 고름 저장소라 세월교 · 박스 고름과 함께 서지 않음.""" station = _src("B06_Section/B06_Section_UI_Page_Station_Controls.ts") - assert "let revetSelection: { at: string; key: RevetPick } | null = null;" in station + assert ( + "let revetSelection: { at: string; key: RevetPick; shape?: boolean } | null = null;" + in station + ) assert ( "if (key && ford.control.selectedFor(chainageM)) ford.control.select(chainageM, null);" in station diff --git a/resources/tester/test_177_4_pavement_span_box.py b/resources/tester/test_177_4_pavement_span_box.py index fe4782090..02381f337 100644 --- a/resources/tester/test_177_4_pavement_span_box.py +++ b/resources/tester/test_177_4_pavement_span_box.py @@ -60,12 +60,12 @@ def test_범위_카드_테두리는_노면공도(): def test_스크롤만_길은_리스너를_안_거침(): view = _read("B06_Section", "B06_Section_UI_Section_View.ts") - body = re.search(r"revealStation\(stationId\) \{(.*?)\n \},", view, re.S) + body = re.search(r"revealStation\(\) \{(.*?)\n \},", view, re.S) assert body, "revealStation 없음" text = body.group(1) assert "selectedStationId = null;" in text assert "profile.showCard(null);" in text - assert 'revealCard(stationId, "smooth");' in text + assert "revealCard" not in text # 측점 없는 구조물 = 스크롤도 없음(307) assert "stationSelectListener" not in text # 폼을 닫지 않음 cross = _read("A00_Common", "design", "design_view_cross.ts") assert "if (revealOnly) sectionView.revealStation(nearest.station_id);" in cross diff --git a/resources/tester/test_287_ford_pavement_followup.py b/resources/tester/test_287_ford_pavement_followup.py index 01700dfde..0762b3afe 100644 --- a/resources/tester/test_287_ford_pavement_followup.py +++ b/resources/tester/test_287_ford_pavement_followup.py @@ -115,7 +115,8 @@ def test_형상_누름은_고름_끌기는_고름_아님(ts): def test_형상_강조와_표식(ts): assert ts["activeAfterSet"] is True - assert ts["activeAfterOther"] is False and ts["activeAfterSame"] is True + # 물넘이 콘크리트는 「관」 고름(형상 누름) 몫 — 측점 길 강조(highlightShapes)가 끄지도 켜지도 않음(307) + assert ts["activeAfterOther"] is True and ts["activeAfterSame"] is True assert ts["fordTag"] == ["pipe-230.00"] # 포장층 표식 = 관 지점 알약 id diff --git a/resources/tester/test_294_6_ford_linked_mark.py b/resources/tester/test_294_6_ford_linked_mark.py index f825fa3fb..d4b76d423 100644 --- a/resources/tester/test_294_6_ford_linked_mark.py +++ b/resources/tester/test_294_6_ford_linked_mark.py @@ -60,7 +60,8 @@ def test_주인_카드_옛_꼴(out): def test_고름은_주인_구조물(out): for card in (out["neighbor"], out["owner"]): assert card["pick"] == ["pipe-90.00"] - assert "is-active" in card["classes"][0].split() + # 측점 길 고름(알약 · 목록 id)은 콘크리트를 안 켬 — 형상 누름 「관」 고름만(307) + assert "is-active" not in card["classes"][0].split() def test_CSS_연결_꼴_강조가_먼저(): diff --git a/resources/tester/test_294_7_ford_slab_pick.py b/resources/tester/test_294_7_ford_slab_pick.py index 28349da3d..66597f863 100644 --- a/resources/tester/test_294_7_ford_slab_pick.py +++ b/resources/tester/test_294_7_ford_slab_pick.py @@ -79,7 +79,8 @@ def test_배선_경사각도_상자(): view = _read("B06_Section_UI_Cross_View.ts") assert 'shapePickRoute(svg, (at) => toggleRevet("pipe", at))' in view assert "revetOffset?.select(section.chainage_m, activeRevet, ownerM);" in view - assert 'setShapeActive(key === "pipe");' in view + # 「관」 = 관 그림 · 콘크리트 하나만(307) + assert "splitPipePick(setRevetActive, setShapeActive, shapePicked)" in view assert len(view.splitlines()) <= 700 shape = _read("B06_Section_UI_Cross_Shape_Pick.ts") assert 'thickenDown(shape.getAttribute("points") ?? "")' in shape diff --git a/resources/tester/test_307_pick_highlight.py b/resources/tester/test_307_pick_highlight.py new file mode 100644 index 000000000..e14f611de --- /dev/null +++ b/resources/tester/test_307_pick_highlight.py @@ -0,0 +1,165 @@ +"""307 B06 고름 강조 — 측점 고름은 형상 안 켬 · 연동 관 = 콘크리트만 · 측점 없는 구조물 = 카드 · 스크롤 없음. + +공통(「관」 setter 가름 · 벽 고름 콘크리트 표식)은 진짜 TS 를 Node 로 돌려 보고(helper_307), +길마다(측점선 · 알약 · 좌측 목록 측점 · 영역 × 노면공 · 물넘이포장 · 연동 관 · 측점 없는 구조물)는 +그 길이 모이는 자리의 배선을 본다. +""" + +import json +import re +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +HELPER = Path(__file__).with_name("helper_307_pick_highlight.cjs") + + +def _read(*parts: str) -> str: + return (ROOT.joinpath(*parts)).read_text(encoding="utf-8") + + +@pytest.fixture(scope="module") +def out() -> dict: + run = subprocess.run( + ["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", cwd=ROOT + ) + assert run.returncode == 0, run.stderr + return json.loads(run.stdout) + + +# ── 공통 ─────────────────────────────────────────────────────────────── + + +def test_공통_관_setter_콘크리트와_관_가름(out): + split = out["split"] + assert split["concretePipe"] == {"base": [None], "shape": [True]} # 콘크리트만 + assert split["pipePipe"] == {"base": ["pipe"], "shape": [False]} # 관만 + assert split["concreteWall"] == {"base": ["inlet"], "shape": [False]} # 다른 벽 그대로 + assert split["none"] == {"base": [None], "shape": [False]} + + +def test_공통_벽고름_콘크리트_표식(out): + cases = out["revetCases"] + assert cases["pipeOnPipeCard"] == {"shape": False, "selected": "pipe"} + assert cases["concreteOnPipeCard"] == {"shape": True, "selected": "pipe"} + assert cases["concreteOnOwnCard"] == {"shape": True, "selected": "pipe"} + assert cases["handoffOnOwnCard"] == {"shape": True, "selected": "pipe"} + assert cases["wall"] == {"shape": False, "selected": "inlet"} + assert cases["released"] == {"shape": False, "selected": None} + assert cases["changes"] == 6 # 고를 때마다 연동 카드 다시 칠함(그대로) + + +# ── 물넘이포장 × 측점 길 넷 ──────────────────────────────────────────── + + +def test_물넘이포장_측점길은_콘크리트_안켬(out): + # 측점선 · 알약 · 좌측 목록 측점 · 영역 = 끝에 `selectMark("pipe-…")` → highlightShapes + assert out["byStationPath"] == {"concreteOff": False, "concreteLitKept": True} + view = _read("B06_Section", "B06_Section_UI_Section_View.ts") + assert "highlightShapes(card, pickedMarkId);" in view + shape = _read("B06_Section", "B06_Section_UI_Cross_Shape_Pick.ts") + assert 'if (id.startsWith("pipe-")) continue;' in shape + # 네 길이 모이는 자리 — 카드 고름 리스너 · 목록 관 줄 · 폼 관 열기 · 측점 없는 관 + cross = _read("A00_Common", "design", "design_view_cross.ts") + assert "sectionView.selectMark(mark, false);" in cross + panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") + assert "deps.markStructure?.(`pipe-${hit.pipeChainageM.toFixed(2)}`);" in panel # 카드 → 폼 + assert "deps.markStructure?.(chainageM === null ? null : `pipe-${" in panel # 목록 관 줄 + # 측점 길은 「관」 벽 고름을 안 엶 — 카드만 고르면 벽 고름을 비움(그대로) + assert "stationControls.revetOffset.select(target.chainage_m, null);" in cross + + +def test_물넘이포장_형상누름은_콘크리트(out): + view = _read("B06_Section", "B06_Section_UI_Cross_View.ts") + assert 'shapePickRoute(svg, (at) => toggleRevet("pipe", at))' in view + assert "splitPipePick(setRevetActive, setShapeActive, shapePicked)" in view + assert "setRevetActive(highlightRevet);" in view + assert out["revetCases"]["concreteOnOwnCard"]["shape"] is True + + +# ── 노면공 × 측점 길 넷 ────────────────────────────────────────────── + + +def test_노면공_포장층은_포장층_누름만(): + view = _read("B06_Section", "B06_Section_UI_Section_View.ts") + assert "const pave = span && isPavement(spanMarkId) && paveShapeId === spanMarkId;" in view + # 알약 · 목록 · 측점 길 = 누름 턴 밖 selectMark → 포장층 표식 풂 + assert "if (!paveShapeTurn) paveShapeId = null;" in view + # 포장층 누름 = 턴 안에서 목록 고름 길을 타고 · 끝나면 그 노면공 포장층만 켬 + grid = re.search( + r"const pave = pavementAt\(event\.target, spanMarks\);(.*?)\n \},", view, re.S + ) + assert grid, "포장층 누름 처리 없음" + body = grid.group(1) + assert body.index("paveShapeTurn = true;") < body.index("pavementSelectListener?.(") + assert body.index("paveShapeTurn = false;") < body.index("paveShapeId = pave.structure_id;") + assert body.index("paveShapeId = pave.structure_id;") < body.index("syncSpanHighlights();") + # 범위 테두리(--span)는 그대로 — 다른 구간형 구조물과 같은 길 + assert 'card.classList.toggle("b06-cross-card--span", span);' in view + assert "spanMarkId = span || isPavement(structureId) ? structureId : null;" in view + + +# ── 연동 관 ─────────────────────────────────────────────────────────── + + +def test_연동관_콘크리트누름은_관형상_안켬(out): + assert out["revetCases"]["concreteOnPipeCard"] == {"shape": True, "selected": "pipe"} + assert out["split"]["concretePipe"]["base"] == [None] # 관 그림 setter 엔 「관」 안 감 + select = _read("B06_Section", "B06_Section_UI_Page_Structures_Selection.ts") + assert "origRevet(chainageM, key, ownerM);" in select + view = _read("B06_Section", "B06_Section_UI_Cross_View.ts") + # 콘크리트 ↔ 관 그림 바꿔 누름 = 풀지 않고 옮김 · 같은 것 다시 누름 = 풂 + assert ( + "activeRevet = activeRevet === key && (ownerM !== undefined) === shapePicked() ? null : key;" + in view + ) + # 좌측 하위 컨테이너 켬(경사각도 상자)은 그대로 — 「관」 벽 고름 길 + dock = _read("B06_Section", "B06_Section_UI_Adjust_Dock.ts") + assert 'side === "pipe" && (!own || own.closest("[hidden]")) ? visibleBox(SLOPE_BOX)' in dock + + +# ── 측점 없는 구조물 ────────────────────────────────────────────────── + + +def test_측점없는_구조물_카드_스크롤_없음(): + view = _read("B06_Section", "B06_Section_UI_Section_View.ts") + body = re.search(r"revealStation\(\) \{(.*?)\n \},", view, re.S) + assert body, "revealStation 없음" + assert "revealCard" not in body.group(1) + assert "markCard(previous);" in body.group(1) # 옛 카드는 풂 + panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") + # B06 알약 · 좌측 목록 = 폼 onSelect · 측점 없는 관 = selectPipeAt — 둘 다 revealOnly 길 + assert ( + "const revealOnly = !coverable(structure) || !plantsStation(structure, markTypes);" in panel + ) + assert "deps.focusChainage(pipe.chainage_m, true);" in panel + # B05 알약 · 3D · 지도 넘김 = focusPicked — 측점 없는 구조물은 카드 고름을 안 부름 + focus = re.search(r"focusPicked: \(structureId, focus\) => \{(.*?)\n \},", panel, re.S) + assert focus, "focusPicked 없음" + assert "if (!known || plantsStation(known, markTypes)) {" in focus.group(1) + assert "if (known) section.selectById(structureId);" in focus.group(1) + cross = _read("A00_Common", "design", "design_view_cross.ts") + assert "structuresPanel.focusPicked(handoff?.structureId ?? null, () =>" in cross + profile = _read("B06_Section", "B06_Section_UI_Section_View_Profile.ts") + assert "edit.selectStructure(hit.structure_id);" in profile + + +# ── 지킬 것 ─────────────────────────────────────────────────────────── + + +def test_맹암거_형상강조_그대로(out): + assert out["drainLit"] is True + assert out["afterRelease"] == {"drain": False, "concreteLit": True} + + +def test_줄_한도(): + assert len(_read("A00_Common", "design", "design_view_cross.ts").splitlines()) <= 685 + for name in ( + "B06_Section_UI_Cross_View.ts", + "B06_Section_UI_Section_View.ts", + "B06_Section_UI_Page_Station_Controls.ts", + "B06_Section_UI_Page_Structures_Panel.ts", + ): + assert len(_read("B06_Section", name).splitlines()) <= 700, name