Merge remote-tracking branches 'origin/sub_laptop_1', 'origin/sub_laptop_2', 'origin/sub_laptop_3', 'origin/sub_laptop_5' and 'origin/sub_laptop_7' into main_laptop_1

This commit is contained in:
43 changed files with 4769 additions and 161 deletions
@@ -49,6 +49,7 @@ import {
type WallPath,
} from "./B05_Profile_UI_Corridor_Structures_Path";
import { computeRevetmentLayout } from "../B06_Section/B06_Section_UI_Cross_Revetment";
import { withoutGuardedFill } from "../B06_Section/B06_Section_UI_Cross_Body_Guard";
/** 스윕 프레임 하나 — 노선 위 한 지점의 중심 XY·좌향 단위벡터와 종단 표고 오프셋. */
export interface StructureFrame {
@@ -274,7 +275,8 @@ export function buildCorridorStructures(
const solids: CorridorStructure[] = [];
for (const section of crossSections) {
const layout = culvertLayoutOf(section);
const fordLayout = fordLayoutOf(section);
// 흙막이가 선 쪽은 횡단과 같은 자리 함수로 — 측벽 걷고 관 · 바닥판이 흙막이까지(89-1)
const fordLayout = withoutGuardedFill(fordLayoutOf(section), layout);
// 독립 기슭막이(옛 D군 경로) — 측점에 배관/숨김 기슭막이 세트가 붙으면 그쪽(위
// culvertLayoutOf)이 그린다. 아직 이관 안 된 정본만 이 옛 경로로 스윕한다
// (2026-08-28 이관 이중그리기 방지 — 횡단도 가드와 같은 규칙).
@@ -449,12 +451,11 @@ export function buildCorridorStructures(
if (fordLayout) {
// 구체는 월류 폭만큼 도로 방향으로 이어지고 기준 측점 전후로 절반씩 걸친다.
const half = Math.max(fordLayout.ford.span_m, 0.5) / 2;
const invertPoints = fordLayout.pipe.outer;
const fordBore = pipeBore(
fordLayout.ford.diameter_m,
{ offset: invertPoints[3].offset, elevation: invertPoints[3].elevation },
{ offset: invertPoints[2].offset, elevation: invertPoints[2].elevation },
);
// 관 끝 = 윗 꼭짓점 자리(흙막이 사면에서 안 튀어나옴 · 89-1) · 높이 = invert
const [topLeft, topRight, invertRight, invertLeft] = fordLayout.pipe.outer;
const pipeStart = { offset: topLeft.offset, elevation: invertLeft.elevation };
const pipeEnd = { offset: topRight.offset, elevation: invertRight.elevation };
const fordBore = pipeBore(fordLayout.ford.diameter_m, pipeStart, pipeEnd);
// 관은 련수만큼 폭 안에 등간격 — 구멍도 관마다 하나씩 낸다(2026-08-25 사용자 ⑥).
const count = Math.max(fordLayout.ford.pipe_count, 1);
const pipeChainages = Array.from(
@@ -518,7 +519,6 @@ export function buildCorridorStructures(
),
);
// 관은 련수만큼 폭 안에 등간격으로 놓는다(단면엔 1개만 보이지만 실물은 여러 련).
const invert = fordLayout.pipe.outer;
for (const at of pipeChainages) {
const frame = frameAt(at);
solids.push({
@@ -526,8 +526,8 @@ export function buildCorridorStructures(
kind: "pipe",
key: "pipe",
pipe: {
start: [invert[3].offset, invert[3].elevation],
end: [invert[2].offset, invert[2].elevation],
start: [pipeStart.offset, pipeStart.elevation],
end: [pipeEnd.offset, pipeEnd.elevation],
diameterM: fordLayout.ford.diameter_m,
wallThicknessM: pipeWallThicknessM(
fordLayout.ford.pipe_kind,
@@ -161,6 +161,22 @@ export function buildStructuresForm(options: {
optionRow,
{ get: groupBoxes.get, pick: groupBoxes.pick, clear: groupBoxes.clear, set: groupBoxes.setOne },
options.onFacilityChange,
// 노면공 길이 칸(87-1) = 시작~종료 출력 — 못 셈이면 빈 글(「—」)
(_c, item) => {
if (item.id !== "length") return "";
const step = options.getInterval();
const start = startFields.read(step, false);
const end = endFields.read(step, false);
return start !== null && end !== null && end > start
? String(Number((end - start).toFixed(2)))
: "";
},
);
// 시작 · 종료를 고치면 길이 출력을 다시 그림
[startFields, endFields].forEach((fields) =>
[fields.station, fields.remainder].forEach((input) =>
input.addEventListener("change", () => structureSpec.refresh()),
),
);
groupBoxes.watch(structureSpec.paintLinks);
const staleBox = document.createElement("fieldset");
@@ -56,6 +56,7 @@ import {
DEFAULT_INTERVAL_LENGTH_M,
groupOptions,
hasComputedRange,
isRangeOnly,
typeOptions,
readBeforeOption,
readLengthOption,
@@ -238,7 +239,7 @@ export function createStructuresSection(
// [?] 설명서가 먼저 여는 항목 = `구조물_<종류 이름>.md`(54-2 · 종류 안내 글은 그 파일로 옮김)
root.dataset.helpTopic = type ? `구조물_${type.name}` : "";
// 정의가 기준 측점을 끈 종류(48-3) — 기준 측점이 곧 위치인 종류(점형 · 범위 계산)는 늘 보임
anchorFields.wrap.hidden = isInterval && !showsAnchor(type!);
anchorFields.wrap.hidden = isInterval && (!showsAnchor(type!) || isRangeOnly(type));
anchorFields.wrap.querySelector("span")!.textContent = "기준 측점"; // 빈 칸 안내 없음(54-43)
syncRangeDisplay();
}
@@ -314,7 +314,10 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
return;
}
// 기준 측점(마킹 위치) — 비우면 시작 측점. 시작~종료 밖은 서버도 거절한다.
anchor = ctx.anchorFields.read(step, false) ?? start;
// 범위 직접 입력형(노면공 · 87-1) = 기준 칸 없음 → 늘 시점.
anchor = STRUCTURE_SPECS[type.type_id]?.byRange
? start
: (ctx.anchorFields.read(step, false) ?? start);
if (anchor < start || anchor > end) {
ctx.anchorFields.station.classList.add("is-invalid");
ctx.anchorFields.station.focus();
@@ -20,6 +20,7 @@ import {
} from "./B05_Profile_Api_Structures";
import type { DesignSelection, ResolvedSelection } from "../A00_Common/design/design_selection";
import type { GroupItems, SummaryItem } from "../common_util/common_util_summary_item";
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
/** 구조물군 표시 이름. 리스트 기호(A~G)만으로는 무엇인지 알기 어렵다.
* 우클릭 메뉴(종단그래프·배수유역도)도 같은 이름을 쓴다(2026-08-18 일원화). */
@@ -209,6 +210,10 @@ export function readBeforeOption(inputs: OptionEntry[]): number {
return length !== null && before > length ? length : before;
}
/** 시작 · 종료 측점만 받는 명세 종류인가(노면공 · 87-1) — 기준 측점 칸 없음 · 길이 출력. */
export const isRangeOnly = (type: { type_id: string } | null): boolean =>
!!type && !!STRUCTURE_SPECS[type.type_id]?.byRange;
/** 구간을 직접 입력하지 않는 타입인가 — C군 사면안정 5종처럼 B05 길이 옵션이
* 있으면 시작·종료 입력 칸을 없애고 기준 측점 + 길이로 범위를 계산해 보여만
* 준다(2026-08-19 사용자 지시). */
@@ -217,6 +222,7 @@ export function hasComputedRange(type: StructureType | null): boolean {
!!type &&
!type.managed_by &&
type.placement === "interval" &&
!isRangeOnly(type) &&
type.options.some((option) => option.key === "length_m" && isB05Option(option))
);
}
@@ -12,6 +12,7 @@ import type { StructureType } from "./B05_Profile_Api_Structures";
import {
createSpecForm,
type SpecForm,
type SpecFormHooks,
type SpecLinks,
} from "../A00_Common/design/design_structure_form";
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
@@ -29,8 +30,9 @@ export function createStructureSpecForm(
optionRow: HTMLElement,
links: SpecLinks,
onChange: () => void,
auto?: SpecFormHooks["auto"],
): SpecForm {
const form = createSpecForm(links, { onChange });
const form = createSpecForm(links, { onChange, auto });
form.root.hidden = true;
forms.set(optionRow, form);
const lock = (): void => {
@@ -49,7 +51,7 @@ export function createStructureSpecForm(
/** 정의 옵션 칸으로 안 그리는 키 — 명세 키 · 명세가 뺀 옛 키. */
export function specOwnedKeys(type: StructureType | null): Set<string> {
const spec = structureSpecOf(type);
return new Set(spec ? [...specKeys(spec).keys, ...spec.retired] : []);
return new Set(spec ? [...specKeys(spec).keys, ...spec.retired, ...(spec.dropped ?? [])] : []);
}
/** 종류 · 값으로 명세 폼을 다시 그림(명세 없는 종류면 가림). */
@@ -85,5 +87,5 @@ export function specRenderedKeys(
if (!spec) return [];
const { keys, prefixes } = specKeys(spec);
const own = Object.keys(previous ?? {}).filter((key) => prefixes.some((p) => key.startsWith(p)));
return [...keys, ...own];
return [...keys, ...own, ...(spec.dropped ?? [])];
}