fix(B06·B05): 노면공 고름 세 갈래 · 형상 누름 = 목록 고름 · 카드 하나 = 그 측점(189-1)
- ② 카드 안 노면공 포장층 누름 = 목록 고름과 같은 길(범위 카드 전부 · 포장층 강조) — 카드 고름보다 먼저 가로챔 - ③ 카드 하나 누름 = 연 노면공 · 범위 강조 풂 · 그 카드 하나(165-1 노면공 지킴 · 167-3 재누름 그대로 없앰) - 카드 고름 세션 = 그 측점(구조물 id 는 목록 · 알약 · 넘김이 고른 것만) - 종단: 구조물 넘김이면 측점선 · 3D 띠 안 켬 - 새 시험 test_189_1_pave_pick · test_165 · test_188_1 기대값 41번 규칙으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -64,26 +64,38 @@ function standardEdges(edges: { left: Edge; right: Edge }, design: CrossDesign)
|
||||
};
|
||||
}
|
||||
|
||||
type PaveMark = { type_id: string; start_m?: number | null; end_m?: number | null };
|
||||
|
||||
/** 포장층 범위 표식(종류 · 시작 · 끝)이 그 노면공인가 — 겹친 노면공 가름. */
|
||||
function isPaveLayerOf(data: DOMStringMap, mark: PaveMark): boolean {
|
||||
const [low, high] = [mark.start_m ?? 0, mark.end_m ?? 0].sort((a, b) => a - b);
|
||||
return (
|
||||
data.paveType === mark.type_id &&
|
||||
Math.abs(Number(data.paveStart) - low) < 0.01 &&
|
||||
Math.abs(Number(data.paveEnd) - high) < 0.01
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 고른 노면공의 포장층만 `is-active`(152-5) — 범위 표식(종류 · 시작 · 끝)이 맞는 층. 겹친 노면공은 하나만.
|
||||
* `mark` 없음 = 모두 풂.
|
||||
*/
|
||||
export function highlightPavement(
|
||||
root: ParentNode,
|
||||
mark?: { type_id: string; start_m?: number | null; end_m?: number | null },
|
||||
): void {
|
||||
const [low, high] = [mark?.start_m ?? 0, mark?.end_m ?? 0].sort((a, b) => a - b);
|
||||
export function highlightPavement(root: ParentNode, mark?: PaveMark): void {
|
||||
for (const layer of root.querySelectorAll<SVGElement>(".b06-chart__pavement")) {
|
||||
const { paveType, paveStart, paveEnd } = layer.dataset;
|
||||
const same =
|
||||
!!mark &&
|
||||
paveType === mark.type_id &&
|
||||
Math.abs(Number(paveStart) - low) < 0.01 &&
|
||||
Math.abs(Number(paveEnd) - high) < 0.01;
|
||||
layer.classList.toggle("is-active", same);
|
||||
layer.classList.toggle("is-active", !!mark && isPaveLayerOf(layer.dataset, mark));
|
||||
}
|
||||
}
|
||||
|
||||
/** 누른 자리의 포장층 → 그 노면공(189-1 · 형상 누름 = 목록 고름) · 범위 표식 없는 층 · 밖 = undefined. */
|
||||
export function pavementAt<T extends PaveMark>(
|
||||
target: EventTarget | null,
|
||||
marks: ReadonlyArray<T>,
|
||||
): T | undefined {
|
||||
const layer = (target as Element | null)?.closest?.<SVGElement>(".b06-chart__pavement");
|
||||
if (!layer?.dataset.paveType) return undefined;
|
||||
return marks.find((mark) => isPaveLayerOf(layer.dataset, mark));
|
||||
}
|
||||
|
||||
/**
|
||||
* 포장층 — 노면선(= 포장 윗면) 아래로 두께만큼. 노면공 범위(`section.pavement`)면 그 두께 · 무늬 ·
|
||||
* 확폭 켬/끔(여러 건은 아래로 차례로 쌓음) · 없으면 카드 포장 켬일 때 표준 두께 채움만.
|
||||
|
||||
@@ -123,6 +123,8 @@ export interface B06StructuresPanel {
|
||||
selectionAt: (chainageM: number | null) => DesignSelection;
|
||||
/** 다음 카드 고름이 측점보다 먼저 열 구조물 — 알약 · 넘김값이 누른 것(null = 측점 규칙대로). */
|
||||
prefer: (structureId: string | null) => void;
|
||||
/** 지금 카드 고름이 먼저 열 구조물(목록 · 알약 · 넘김) — 없으면 null(카드만 고름 · 189-1). */
|
||||
preferred: () => string | null;
|
||||
/** 구조물 하나를 폼에 올리고 그 카드로 간다 — 알약을 누른 길(목록 클릭과 같은 흐름). */
|
||||
selectStructure: (structureId: string) => void;
|
||||
/** 지금 목록의 구조물 정본(미저장분 포함) — 카드 찾기에 쓴다. */
|
||||
@@ -164,8 +166,6 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
markTypes.find((type) => type.type_id === entry.type_id)?.group !== "G";
|
||||
/** 다음 카드 고름이 열 구조물 — 같은 측점에 관이 있어도 누른 것이 먼저다(52-15). */
|
||||
let preferredId: string | null = null;
|
||||
/** 폼에 열린 구조물 — 범위 안 빈 카드를 눌러도 연 노면공을 지킴(165-1). */
|
||||
let openId: string | null = null;
|
||||
/** 그 구조물로 갈 카드 측점 — 구간형은 범위 안 카드 중 기준점에 가까운 것(경계 기준점이 범위 밖 카드로 안 감 · 165-2). */
|
||||
const focusOf = (structure: StructureInstance): number => {
|
||||
const anchor = structureAnchorM(structure);
|
||||
@@ -209,7 +209,6 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
}
|
||||
},
|
||||
onSelect: (structure) => {
|
||||
openId = structure?.structure_id ?? null;
|
||||
if (!structure) {
|
||||
deps.markStructure?.(null); // 풂 = 알약도 끔(53-2)
|
||||
return;
|
||||
@@ -547,24 +546,15 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
prefer: (structureId) => {
|
||||
preferredId = structureId;
|
||||
},
|
||||
preferred: () => preferredId,
|
||||
selectStructure: (structureId) => section.selectById(structureId),
|
||||
showAtChainage: (chainageM) => {
|
||||
currentChainageM = null;
|
||||
const selection: DesignSelection = { chainageM, structureId: preferredId };
|
||||
// 계곡 통과 시설이 먼저다 — 같은 측점에 구조물 정본 항목이 겹쳐도 횡단도에
|
||||
// 그려지는 것은 이쪽이다(순서 · 거리는 `resolveSelection` 한 곳).
|
||||
// 카드 하나 고름 = 그 측점 기준 — 연 노면공은 풂(189-1 · 노면공은 덮어 열지 않음 152-5)
|
||||
const hit = resolveSelection(selection, pipeFacilities, structures, coverable);
|
||||
// 범위 안 빈 카드(관 · 덮는 구조물 없음) = 연 노면공 그대로 · 카드만 옮김(165-1)
|
||||
const open = structures.find((entry) => entry.structure_id === openId);
|
||||
if (
|
||||
chainageM !== null &&
|
||||
hit.pipeChainageM === null &&
|
||||
hit.structureId === null &&
|
||||
open &&
|
||||
!coverable(open) &&
|
||||
coversChainage(open, chainageM)
|
||||
)
|
||||
return { chainageM, structureId: openId };
|
||||
// 폼은 위에서 푼 값으로 엶 — 공용 `selectAt` 은 노면공을 덮어 엶(152-5)
|
||||
if (hit.pipeChainageM !== null) {
|
||||
currentChainageM = hit.pipeChainageM;
|
||||
|
||||
@@ -68,7 +68,7 @@ import {
|
||||
} from "./B06_Section_UI_Section_Common";
|
||||
import type { SectionViewController } from "./B06_Section_UI_Section_View_Types";
|
||||
import { createFillSlopeNotice } from "./B06_Section_UI_Cross_FillSlope_Notice";
|
||||
import { highlightPavement } from "./B06_Section_UI_Cross_Design_Surface";
|
||||
import { highlightPavement, pavementAt } from "./B06_Section_UI_Cross_Design_Surface";
|
||||
import { createSectionProfile } from "./B06_Section_UI_Section_View_Profile";
|
||||
import type { DesignPageShared } from "../A00_Common/design/design_shared";
|
||||
|
||||
@@ -270,6 +270,8 @@ export function createSectionView(
|
||||
|
||||
/** 카드 선택 변경 수신자(좌측 구조물 폼). 페이지가 등록한다. */
|
||||
let stationSelectListener: ((stationId: string | null) => void) | null = null;
|
||||
/** 카드 안 노면공 형상 누름 수신자 — 목록 고름과 같은 길(189-1). */
|
||||
let pavementSelectListener: ((structureId: string) => void) | null = null;
|
||||
|
||||
/**
|
||||
* 선택이 바뀐 흔적만 화면에 반영한다 — **재렌더 금지**.
|
||||
@@ -288,16 +290,6 @@ export function createSectionView(
|
||||
|
||||
/** 같은 측점을 다시 고르면 선택을 푼다(2026-08-02 사용자 지시) — 카드·측점선 어느 쪽이든. */
|
||||
const selectStation = (stationId: string, scroll: boolean): void => {
|
||||
// 고른 노면공 범위 안 카드는 다시 눌러도 그대로 — 풂은 빈 자리 · 목록 · 알약(167-3)
|
||||
const section = currentDetail?.cross_sections.find((entry) => entry.station_id === stationId);
|
||||
const held =
|
||||
section && isPavement(spanMarkId)
|
||||
? inSpan(
|
||||
section,
|
||||
spanMarks.find((entry) => entry.structure_id === spanMarkId),
|
||||
)
|
||||
: false;
|
||||
if (selectedStationId === stationId && held) return;
|
||||
const previous = selectedStationId;
|
||||
const deselect = selectedStationId === stationId;
|
||||
selectedStationId = deselect ? null : stationId;
|
||||
@@ -429,6 +421,17 @@ export function createSectionView(
|
||||
grid.addEventListener("click", (event) => {
|
||||
if (!(event.target as HTMLElement).closest(".b06-cross-card")) clearSelection();
|
||||
});
|
||||
// 노면공 형상 누름 = 목록 고름(범위 카드 전부 · 형상 강조) — 카드 고름보다 먼저 가로챔(189-1)
|
||||
grid.addEventListener(
|
||||
"click",
|
||||
(event) => {
|
||||
const pave = pavementAt(event.target, spanMarks);
|
||||
if (!pave?.structure_id || !isPavement(pave.structure_id)) return;
|
||||
event.stopPropagation();
|
||||
pavementSelectListener?.(pave.structure_id);
|
||||
},
|
||||
true,
|
||||
);
|
||||
// 열 수 = 사용자 값 · 폭 = (칸 폭 − 간격) ÷ 열 수(`_Section_Columns` · 138-3).
|
||||
cachedCardWidth = crossCardWidth(renderWidth, columnCount, CROSS_GRID_GAP);
|
||||
// CSS auto-fill의 자체 열 수와 JS columnCount가 어긋나면 같은 행 높이 그룹핑이 실제
|
||||
@@ -609,6 +612,9 @@ export function createSectionView(
|
||||
setStationSelectListener(listener) {
|
||||
stationSelectListener = listener;
|
||||
},
|
||||
setPavementSelectListener(listener) {
|
||||
pavementSelectListener = listener;
|
||||
},
|
||||
setStructureEdit(edit) {
|
||||
structureEdit = edit;
|
||||
},
|
||||
|
||||
@@ -56,6 +56,8 @@ export interface SectionViewController {
|
||||
/** 카드(측점) 선택이 바뀔 때 알림 — 좌측 「구조물 배치」 폼이 그 측점 구조물을
|
||||
* 올린다(2026-08-29 일원화). null = 선택 해제. */
|
||||
setStationSelectListener: (listener: (stationId: string | null) => void) => void;
|
||||
/** 카드 안 노면공 형상 누름(189-1) — 목록 고름과 같은 길로 보냄. */
|
||||
setPavementSelectListener: (listener: (structureId: string) => void) => void;
|
||||
clear: () => void;
|
||||
/** 종단 아래 구조물 알약 레인에 쓸 목록 — B05 와 같은 부품·같은 표기(2026-09-07 사용자
|
||||
* 지시 4 「구조물 표시 통일」). 좌측 「구조물 배치」가 목록을 받을 때마다 넘겨준다. */
|
||||
|
||||
Reference in New Issue
Block a user