Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1

This commit is contained in:
2026-10-18 23:25:20 +09:00
20 changed files with 689 additions and 305 deletions
@@ -205,13 +205,16 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
? inside.reduce((best, at) => (Math.abs(at - anchor) < Math.abs(best - anchor) ? at : best))
: anchor;
};
/** 종단 알약 레인에 올릴 목록 — 구조물 정본 + 관 정본(가상 구조물). */
const pushMarks = (): void =>
/** 종단 알약 레인에 올릴 목록 — 구조물 정본 + 관 정본(가상 구조물). 알약 · 측점선 · 관 id 장부 = 페이지
* 시설 줄 한 벌(462 · 숨은 B05 평면 마커도 따라감) · `carryEdits` = 사용자 고침(불러옴은 false). */
const pushMarks = (carryEdits = true): void => {
deps.shared.lane.set({ structures, pipes: pipeFacilities }, carryEdits);
deps.onMarks?.(
[...structures, ...pipesToStructureMarks(pipeFacilities)],
markTypes,
pipeFacilities,
);
};
// 폼 한 벌(계획서 24-3b) — 횡단 보기가 보일 때 폼 알림을 받는 동작만 건다. 상세 옵션 칸은
// 한 벌이 보기에 따라 그린다(횡단 보기에서만).
const section = sharedStructures(deps.shared);
@@ -396,8 +399,10 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
]);
section.setTypes(types, listPick("b06")); // 표 목록 both + b06(322-1)
markTypes = types;
deps.shared.lane.setTypes(types);
// 저장하지 않고 나갔던 조작분이 있으면 그것으로 화면을 세운다(B05와 같은 규칙).
storedStructures = stored.structures;
deps.shared.lane.setStored(storedStructures);
storedPipes = pipeResponse.pipe_points;
structures = readPendingStructures(projectId) ?? stored.structures;
const note = staleDefinitionNotice(projectId, stored.definition_version); // 322-2
@@ -438,7 +443,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
if (!pendingPipes) snapOldPipes(projectId);
section.setPipeFacilities(pipeFacilities);
// 종단 알약 레인 — 구조물 정본 + 계곡 통과 시설(관 정본)을 합쳐 B05 와 같은 표기로.
pushMarks();
pushMarks(false);
// 목록이 늦게 도착하면 그 사이에 고른 시설은 강조될 자리가 없었다 — 다시 세운다
// (2026-09-04 사용자 보고: B06 좌측 목록만 하이라이트가 안 붙음). 폼에 아직 아무것도
// 없으면 세션에 남은 선택(카드형 — 부재키 없는 측점)도 같은 규칙으로 세운다.
+2 -3
View File
@@ -670,11 +670,10 @@ export function createSectionView(
setStructureEdit(edit) {
structureEdit = edit;
},
setStructureMarks(structures, types, pipes) {
setStructureMarks(structures, types) {
spanMarks = structures;
spanTypes = types;
syncSpanHighlights();
profile.setMarks(structures, types, pipes);
syncSpanHighlights(); // 종단 알약 · 측점선은 시설 줄 한 벌(462)
// 검증용 훅 — 알약 레인이 무엇을 받았는지 화면 밖에서 수치로 본다
// (`__corridorBuild` 등과 같은 용도).
(window as unknown as { __b06Marks?: unknown }).__b06Marks = {
@@ -5,7 +5,8 @@
*
* 옛 위쪽 붙박이 종단(자체 그래프 · 유토곡선 없음 · 흙덮기 막기 없음)을 갈음한다.
* 도구(▲▼ · 되돌리기 · 직선화 · 옮기기 · 확대 · 표 · 유토곡선 · 범례 · 뱃지)와 막기 규칙
* (최소 흙덮기 · 기울기 상한)은 종단도 한 벌이 맡고, 여기서는 이음만 한다.
* (최소 흙덮기 · 기울기 상한)은 종단도 한 벌이 맡고, 여기서는 이음만 한다. 알약 · 측점선 · 흙덮기 관은
* 페이지 시설 줄 한 벌(`shared.lane` · 462)이 종단도에 씀 — 여기는 읽기만.
* ========================================================================== */
import type { DesignProfile } from "../A00_Common/design/design_profile";
@@ -32,24 +33,13 @@ import type {
HaulEquipmentLimit,
SectionDetailResponse,
} from "./B06_Section_Api_Fetch";
import {
structureAnchorM,
type StructureInstance,
type StructureType,
} from "../B05_Profile/B05_Profile_Api_Structures";
import {
irregularStationId,
isPipeStation,
type IrregularStation,
} from "../B05_Profile/B05_Profile_UI_IrregularStations";
import { structureAnchorM } from "../B05_Profile/B05_Profile_Api_Structures";
import { irregularStationId, isPipeStation } from "../B05_Profile/B05_Profile_UI_IrregularStations";
import {
isPipeMark,
moveMarkById,
type SectionStructureEdit,
} from "./B06_Section_UI_Section_View_Menu";
import { planStations, type PlanPipe } from "../common_util/common_util_structure_station";
import { plannedIrregular } from "../A00_Common/design/design_structure_status";
import { withPipeIds } from "../A00_Common/design/design_pipe_ids";
/** 관 · 측점을 같은 자리로 보는 거리(m) — 계획서 24-1 `PIPE_MATCH_M` 과 같은 값. */
const MATCH_M = PIPE_MATCH_M;
@@ -79,11 +69,6 @@ export interface SectionProfile {
haulLimits?: HaulEquipmentLimit[];
naturalSpoilMinSlope?: number;
}) => void;
setMarks: (
structures: ReadonlyArray<StructureInstance>,
types: ReadonlyArray<StructureType>,
pipes?: ReadonlyArray<PlanPipe>,
) => void;
/** 카드가 바뀌었다 — 유토곡선 · 뱃지만 다시 싣는다(편집 초안은 그대로). */
redraw: () => void;
/** 카드를 골랐다 — 종단도 선택 표시가 따라간다. */
@@ -92,26 +77,6 @@ export interface SectionProfile {
selectMark: (structureId: string | null) => void;
}
/** 종단 측점선 = 측점 계획 한 벌(322-3 · 관 · 측점 심는 구조물 모두 · 종단 보기 · 카드와 같은 목록).
* 관 구간(독립 기슭막이 시작 · 끝)은 받은 관 목록에서 · 없으면 알약에서 기준만. */
function planMarkStations(
marks: ReadonlyArray<StructureInstance>,
pipes: ReadonlyArray<PlanPipe> | null,
types: ReadonlyArray<StructureType>,
intervalM: number,
): IrregularStation[] {
const pipeList =
pipes ??
marks.filter(isPipeMark).map((mark) => ({
chainage_m: structureAnchorM(mark),
facility: mark.type_id,
options: mark.options,
}));
const structures = marks.filter((mark) => !isPipeMark(mark));
// 덮는 구조물 id = 관 세션 id(323-5 · 알약 id 와 같음)
return plannedIrregular(planStations(withPipeIds(pipeList), structures, types), intervalM);
}
/**
* 배수유역도에 노선 평면선을 싣는다 — 종단 보기가 두고 간 최신 노선 캐시를 먼저 쓰고,
* 없으면 한 번 읽는다(읽기만 · 계산 없음).
@@ -143,13 +108,10 @@ export function createSectionProfile(
): SectionProfile {
let profile: DesignProfile | null = null;
let detail: SectionDetailResponse | null = null;
let marks: ReadonlyArray<StructureInstance> = [];
let markPipes: ReadonlyArray<PlanPipe> | null = null;
/** 알약 · 비정규 측점 = 시설 줄 한 벌(462 · 읽기만). */
const lane = shared.lane;
/** 마지막으로 세운 알약 — 카드 고름이 측점선을 맞출 때 지킬지 가름(53-1). */
let markedId: string | null = null;
let types: ReadonlyArray<StructureType> = [];
let stations: IrregularStation[] = [];
let intervalM = 20;
/** 마지막으로 받은 관 목록 지문(자리 · 종류 · 관경) — 바뀔 때만 카드를 다시 얹음. */
let pipeSignature: string | null = null;
@@ -166,7 +128,7 @@ export function createSectionProfile(
/** 종단도 측점 id → 카드 id. 관 측점(`irregular:`)은 누가거리로 카드를 찾는다. */
const cardOf = (stationId: string | null): string | null => {
if (stationId === null) return null;
const pipe = stations.find((entry) => irregularStationId(entry.id) === stationId);
const pipe = lane.stations().find((entry) => irregularStationId(entry.id) === stationId);
if (pipe) return nearestCard(pipe.chainage_m)?.station_id ?? null;
return stationId;
};
@@ -180,22 +142,6 @@ export function createSectionProfile(
{ from: "map" },
);
const syncMarks = (): void => {
if (!profile) return;
profile.setStructureTypes([...types]);
profile.setStructures([...marks]);
stations = planMarkStations(marks, markPipes, types, intervalM);
profile.setIrregularStations(stations);
// 판(배수유역도)이 없는 보기 — 관 목록을 직접 넘겨 최소 흙덮기를 판정한다.
profile.setMinCoverPipes(
marks.filter(isPipeMark).map((mark) => ({
chainage_m: structureAnchorM(mark),
facility: mark.type_id as never,
options: mark.options,
})),
);
};
/** 한 벌 종단도에 횡단 보기 알림을 건다 — 횡단 보기가 보일 때만 이쪽으로 온다. */
const build = (): DesignProfile => {
bindDesignView(shared, "cross", {
@@ -245,7 +191,7 @@ export function createSectionProfile(
selectDesign(shared.projectId, NO_SELECTION, { from: ESCAPE_FROM });
return;
}
const hit = marks.find((entry) => entry.structure_id === structureId);
const hit = lane.marks().find((entry) => entry.structure_id === structureId);
if (!hit) return;
const edit = host.edit();
if (!isPipeMark(hit) && hit.structure_id && edit) {
@@ -253,7 +199,7 @@ export function createSectionProfile(
return;
}
// 측점 없는 관 = 카드 안 고름 · 스크롤 없음 · 그 관 폼 · 알약만(요구 178 · 307)
if (edit?.selectPipe && !plantsStation(hit, types)) {
if (edit?.selectPipe && !plantsStation(hit, lane.types())) {
edit.selectPipe(structureAnchorM(hit));
return;
}
@@ -266,7 +212,7 @@ export function createSectionProfile(
const edit = host.edit();
if (!edit) return;
moveMarkById(structureId, toChainageM, {
structures: marks,
structures: lane.marks(),
stations: detail?.longitudinal.stations ?? [],
edit,
});
@@ -287,10 +233,9 @@ export function createSectionProfile(
const fresh = !shared.profile;
profile = build();
if (fresh) loadRoute(id, profile);
syncMarks();
}
detail = input.detail;
intervalM = input.stationInterval;
lane.setIntervalM(input.stationInterval);
const routeId = Number(route);
profile.render(
input.detail,
@@ -304,13 +249,7 @@ export function createSectionProfile(
naturalSpoilMinSlope: input.naturalSpoilMinSlope,
});
}
syncMarks();
},
setMarks(nextMarks, nextTypes, nextPipes) {
marks = nextMarks;
types = nextTypes;
markPipes = nextPipes ?? null;
syncMarks();
lane.paint(); // 다시 그린 종단도에 시설 줄(추가 측점이 늦게 왔으면 다시 셈)
},
redraw() {
profile?.redraw();
@@ -325,12 +264,14 @@ export function createSectionProfile(
profile.setSelectedStation(null);
return;
}
const pipe = stations.find(
(entry) => Math.abs(entry.chainage_m - section.chainage_m) < MATCH_M,
);
const pipe = lane
.stations()
.find((entry) => Math.abs(entry.chainage_m - section.chainage_m) < MATCH_M);
profile.setSelectedStation(pipe ? irregularStationId(pipe.id) : section.station_id);
// 일반 측점은 측점선 짝 알약이 없어 꺼짐 — 고른 구조물이 이 측점을 덮으면 그 알약을 지킴(53-1)
const marked = markedId ? marks.find((entry) => entry.structure_id === markedId) : undefined;
const marked = markedId
? lane.marks().find((entry) => entry.structure_id === markedId)
: undefined;
if (!pipe && marked && coversChainage(marked, section.chainage_m))
profile.setSelectedStructure(markedId, true);
},