feat(B05): 부재키 일원화 — 세월교·BOX암거까지
- 이름표를 횡단도 이름으로 통일 — 세월교 유입·유출, BOX 좌·우, 본체는 body - 조정창 하나가 여러 부재를 다루므로 `측이름:부재` 꼴 — 강조는 전체 키, 조정창은 앞칸만 - B06 갈래 태우기 신규 모듈 — 세월교/BOX/기슭막이 조정창으로 각각 보냄 - 저장 코리도 만료(BUILD_VERSION 91→92) — 재계산 비용 실측 차이 없음(5.1초 vs 4.7초) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -225,7 +225,9 @@ function fnv1a(text: string): string {
|
|||||||
// 91 = 구조물 솔리드에 **부재키**(`key`)를 싣는다 — 3D 개별 선택이 그 표로 부재를
|
// 91 = 구조물 솔리드에 **부재키**(`key`)를 싣는다 — 3D 개별 선택이 그 표로 부재를
|
||||||
// 가려낸다(2026-09-04). 옛 저장본엔 키가 없어 3D에서 골라도 B06 조정창으로 넘길
|
// 가려낸다(2026-09-04). 옛 저장본엔 키가 없어 3D에서 골라도 B06 조정창으로 넘길
|
||||||
// 부재를 못 정하므로 만료한다.
|
// 부재를 못 정하므로 만료한다.
|
||||||
const BUILD_VERSION = 91;
|
// 92 = 세월교·BOX암거 부재까지 **부재키를 일원화**한다 — 횡단도가 부르는 이름 그대로
|
||||||
|
// (세월교 유입·유출, BOX 좌·우, 본체는 `body`). 91 저장본엔 배수관 세트 키만 있다.
|
||||||
|
const BUILD_VERSION = 92;
|
||||||
|
|
||||||
/** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */
|
/** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */
|
||||||
export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string {
|
export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string {
|
||||||
|
|||||||
@@ -461,10 +461,19 @@ export function buildCorridorStructures(
|
|||||||
);
|
);
|
||||||
for (const side of fordLayout.sides) {
|
for (const side of fordLayout.sides) {
|
||||||
for (const part of side.parts) {
|
for (const part of side.parts) {
|
||||||
pushPierced("basin", part.points, half, half, fordBore, pipeChainages);
|
pushPierced(
|
||||||
|
"basin",
|
||||||
|
part.points,
|
||||||
|
half,
|
||||||
|
half,
|
||||||
|
fordBore,
|
||||||
|
pipeChainages,
|
||||||
|
null,
|
||||||
|
`${side.role}:${part.kind}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pushSwept("basin", fordLayout.slabBridge, half, half);
|
pushSwept("basin", fordLayout.slabBridge, half, half, null, "body");
|
||||||
// 날개벽 4매 — BOX암거와 같은 규칙(2026-08-25 사용자). 뿌리는 **벽 외측면**
|
// 날개벽 4매 — BOX암거와 같은 규칙(2026-08-25 사용자). 뿌리는 **벽 외측면**
|
||||||
// (성토 경사측면과 마주치는 면) 모서리다(2026-08-25 ⑤ — 종전 바닥판 바깥 끝은
|
// (성토 경사측면과 마주치는 면) 모서리다(2026-08-25 ⑤ — 종전 바닥판 바깥 끝은
|
||||||
// 틀린 자리: 거긴 날개벽 투영이 만든 에이프런 끝이다). 발치는 바닥판 밑면,
|
// 틀린 자리: 거긴 날개벽 투영이 만든 에이프런 끝이다). 발치는 바닥판 밑면,
|
||||||
@@ -494,6 +503,8 @@ export function buildCorridorStructures(
|
|||||||
(point) => [point.offset, point.elevation] as [number, number],
|
(point) => [point.offset, point.elevation] as [number, number],
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
key: `${side.role}:wing`,
|
||||||
|
apronKey: `${side.role}:apron`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ export function boxSolids(
|
|||||||
{
|
{
|
||||||
chainage_m: chainage,
|
chainage_m: chainage,
|
||||||
kind: "basin",
|
kind: "basin",
|
||||||
|
key: "body",
|
||||||
polygons: repeat(layout.topSlab, bodyRings.length),
|
polygons: repeat(layout.topSlab, bodyRings.length),
|
||||||
rings: bodyRings,
|
rings: bodyRings,
|
||||||
// 아령 외곽선의 **손잡이** — 에이프런과 합쳐 탑뷰 경계를 이룬다(2026-08-27 사용자).
|
// 아령 외곽선의 **손잡이** — 에이프런과 합쳐 탑뷰 경계를 이룬다(2026-08-27 사용자).
|
||||||
@@ -189,6 +190,7 @@ export function boxSolids(
|
|||||||
{
|
{
|
||||||
chainage_m: chainage,
|
chainage_m: chainage,
|
||||||
kind: "basin",
|
kind: "basin",
|
||||||
|
key: "body",
|
||||||
polygons: repeat(layout.bottomSlab, bodyRings.length),
|
polygons: repeat(layout.bottomSlab, bodyRings.length),
|
||||||
rings: bodyRings,
|
rings: bodyRings,
|
||||||
},
|
},
|
||||||
@@ -204,6 +206,7 @@ export function boxSolids(
|
|||||||
solids.push({
|
solids.push({
|
||||||
chainage_m: chainage,
|
chainage_m: chainage,
|
||||||
kind: "basin",
|
kind: "basin",
|
||||||
|
key: "body",
|
||||||
polygons: repeat(layout.barrel, wallRings.length),
|
polygons: repeat(layout.barrel, wallRings.length),
|
||||||
rings: wallRings,
|
rings: wallRings,
|
||||||
});
|
});
|
||||||
@@ -235,6 +238,7 @@ export function boxSolids(
|
|||||||
solids.push({
|
solids.push({
|
||||||
chainage_m: chainage,
|
chainage_m: chainage,
|
||||||
kind: "basin",
|
kind: "basin",
|
||||||
|
key: `${side.role}:apron`,
|
||||||
polygons: apronPlan.map((at) => {
|
polygons: apronPlan.map((at) => {
|
||||||
const inner = innerAt(at);
|
const inner = innerAt(at);
|
||||||
return [
|
return [
|
||||||
@@ -281,6 +285,7 @@ function wingSolids(
|
|||||||
topElevation: side.topElevation,
|
topElevation: side.topElevation,
|
||||||
bottomElevation: side.bottomElevation,
|
bottomElevation: side.bottomElevation,
|
||||||
wing: wingOf(section, side.role) ?? box.wing_out,
|
wing: wingOf(section, side.role) ?? box.wing_out,
|
||||||
|
key: `${side.role}:wing`,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export interface WingAnchor {
|
|||||||
bottomElevation: number;
|
bottomElevation: number;
|
||||||
/** 그 측에 적용할 날개벽 제원. */
|
/** 그 측에 적용할 날개벽 제원. */
|
||||||
wing: FordWingSpec;
|
wing: FordWingSpec;
|
||||||
|
/** 날개벽 부재키 — `그측이름:wing`(예 `inlet:wing`·`left:wing`). */
|
||||||
|
key?: string;
|
||||||
|
/** 에이프런 부재키 — `그측이름:apron`. 날개벽과 따로 밝아져야 해 나눠 둔다. */
|
||||||
|
apronKey?: string;
|
||||||
/**
|
/**
|
||||||
* 세월교 전용 — 바닥 에이프런(2026-08-26 사용자 ④). 있으면 **날개 끝점과 바닥판
|
* 세월교 전용 — 바닥 에이프런(2026-08-26 사용자 ④). 있으면 **날개 끝점과 바닥판
|
||||||
* 종방향 끝점을 이은 직선**을 외곽으로 하는 평면 삼각형을 세운다.
|
* 종방향 끝점을 이은 직선**을 외곽으로 하는 평면 삼각형을 세운다.
|
||||||
@@ -170,7 +174,15 @@ export function buildWingSolids(
|
|||||||
// 성토끝 박스 옆선이 노선 흐름을 타게 된다(2026-08-26 사용자). BOX는 안 싣는다.
|
// 성토끝 박스 옆선이 노선 흐름을 타게 된다(2026-08-26 사용자). BOX는 안 싣는다.
|
||||||
const wingMode = end ? "route" : "station";
|
const wingMode = end ? "route" : "station";
|
||||||
const wingAxis: [number, number] | undefined = end ? [base.leftX, base.leftY] : undefined;
|
const wingAxis: [number, number] | undefined = end ? [base.leftX, base.leftY] : undefined;
|
||||||
solids.push({ chainage_m: chainageM, kind, polygons, rings, wing: wingMode, wingAxis });
|
solids.push({
|
||||||
|
chainage_m: chainageM,
|
||||||
|
kind,
|
||||||
|
key: anchor.key,
|
||||||
|
polygons,
|
||||||
|
rings,
|
||||||
|
wing: wingMode,
|
||||||
|
wingAxis,
|
||||||
|
});
|
||||||
const apron = anchor.apron;
|
const apron = anchor.apron;
|
||||||
if (!apron || apron.floor.length < 3) continue;
|
if (!apron || apron.floor.length < 3) continue;
|
||||||
// 밑변은 **바닥판 끝면 그 자리**에서 시작한다 — 판을 밀어넣은 뿌리(rootX)에서
|
// 밑변은 **바닥판 끝면 그 자리**에서 시작한다 — 판을 밀어넣은 뿌리(rootX)에서
|
||||||
@@ -203,6 +215,7 @@ export function buildWingSolids(
|
|||||||
solids.push({
|
solids.push({
|
||||||
chainage_m: chainageM,
|
chainage_m: chainageM,
|
||||||
kind: "basin",
|
kind: "basin",
|
||||||
|
key: anchor.apronKey,
|
||||||
polygons: apronPolygons,
|
polygons: apronPolygons,
|
||||||
rings: apronRings,
|
rings: apronRings,
|
||||||
wing: wingMode,
|
wing: wingMode,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
import { maxToeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
|
import { maxToeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
|
||||||
import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||||
import { consumeStructurePick } from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session";
|
import { consumeStructurePick } from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session";
|
||||||
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
import { applyStructurePick } from "./B06_Section_UI_Page_Structure_Pick";
|
||||||
import { type CrossDesignChange, createSectionView } from "./B06_Section_UI_Section_View";
|
import { type CrossDesignChange, createSectionView } from "./B06_Section_UI_Section_View";
|
||||||
import { hasStaleDesigns } from "./B06_Section_UI_Section_Common";
|
import { hasStaleDesigns } from "./B06_Section_UI_Section_Common";
|
||||||
import { revetWallSpec } from "./B06_Section_UI_Cross_Culvert_Const";
|
import { revetWallSpec } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||||
@@ -57,9 +57,6 @@ import { loadSectionDetail } from "./B06_Section_Section_Store";
|
|||||||
import { buildGroup, createSampleWidener, L } from "./B06_Section_UI_Page_Common";
|
import { buildGroup, createSampleWidener, L } from "./B06_Section_UI_Page_Common";
|
||||||
import "@util/common_util_mass_haul.css";
|
import "@util/common_util_mass_haul.css";
|
||||||
|
|
||||||
/** B05 3D가 넘긴 부재키 중 **기슭막이 조정창이 받는 것**(2026-09-04). 집수정·배관은 빠진다. */
|
|
||||||
const REVET_KEYS = /^(inlet|outlet|own|extra\d+|bextra\d+)$/;
|
|
||||||
|
|
||||||
export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||||
let currentRouteId: number | null = null;
|
let currentRouteId: number | null = null;
|
||||||
@@ -518,24 +515,14 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
|||||||
confirmButton.disabled = sectionDetail === null;
|
confirmButton.disabled = sectionDetail === null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** B05 3D에서 고른 구조물을 이어받는다(2026-09-04) — 값은 읽는 즉시 지워져 한 번만 쓴다. */
|
||||||
* B05 3D에서 고른 구조물을 이어받는다(2026-09-04 사용자 확정) — 세션 한 칸에 남긴
|
|
||||||
* `{ 측점, 부재키 }`로 그 측점 카드를 고르고, 기슭막이 부재면 조정창까지 연다.
|
|
||||||
* 값은 읽는 즉시 지워지므로 진입 한 번만 따라간다.
|
|
||||||
*/
|
|
||||||
function restoreStructurePick(): void {
|
function restoreStructurePick(): void {
|
||||||
const handoff = consumeStructurePick(projectId);
|
applyStructurePick(consumeStructurePick(projectId), sectionDetail, {
|
||||||
if (!handoff) return;
|
focusStation: (stationId) => sectionView.focusStation(stationId),
|
||||||
const target = sectionDetail?.cross_sections.find(
|
refreshCard: (chainageM) => sectionView.refreshCard(chainageM),
|
||||||
(section) => Math.abs(section.chainage_m - handoff.at) < 0.01,
|
revetSelect: (chainageM, key) => stationControls.revetOffset.select(chainageM, key),
|
||||||
);
|
fordSelect: (chainageM, role) => stationControls.ford.select(chainageM, role),
|
||||||
if (!target) return;
|
boxSelect: (chainageM, role) => stationControls.box.select(chainageM, role),
|
||||||
// 카드가 다 선 뒤에 고른다 — 스크롤이 자리를 잡아야 한다.
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
sectionView.focusStation(target.station_id);
|
|
||||||
if (!handoff.key || !REVET_KEYS.test(handoff.key)) return;
|
|
||||||
stationControls.revetOffset.select(target.chainage_m, handoff.key as RevetKey);
|
|
||||||
sectionView.refreshCard(target.chainage_m);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B06_Section_UI_Page_Structure_Pick.ts
|
||||||
|
* B05 3D에서 고른 구조물을 **B06 화면에 얹는다**(2026-09-04 사용자 확정) — 그 측점
|
||||||
|
* 카드를 고르고 화면에 드러낸 뒤, 부재키가 있으면 그 시설의 조정창까지 연다.
|
||||||
|
*
|
||||||
|
* 부재키는 **횡단도가 그 부재를 부르는 이름 그대로**다. 이름이 겹쳐도(배수관 유입 ↔
|
||||||
|
* 세월교 유입) 한 측점에는 시설이 하나뿐이라 헷갈리지 않는다 — 그 측점이 세월교면
|
||||||
|
* 세월교 조정창, BOX암거면 BOX 조정창, 아니면 기슭막이 조정창으로 보낸다.
|
||||||
|
* 본체(`body` — 세월교 월류부 상판·BOX 구체)는 고르는 부재가 아니라 카드까지만 연다.
|
||||||
|
*
|
||||||
|
* 세월교·BOX는 조정창 하나가 그 측 부재 여럿(벽·바닥·날개벽·에이프런)을 함께 다루므로
|
||||||
|
* 부재키가 `측이름:부재`(예 `inlet:wall`) 꼴이다 — **앞칸이 조정창 이름**이다. 3D 강조는
|
||||||
|
* 부재 하나만 켜야 해서 뒤칸까지 나눠 두었다(2026-09-04 사용자 확정).
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||||
|
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||||
|
import type { StructurePickHandoff } from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session";
|
||||||
|
|
||||||
|
/** 기슭막이 조정창이 받는 부재키 — 집수정·배관·본체는 여기서 빠진다. */
|
||||||
|
const REVET_KEYS = /^(inlet|outlet|own|extra\d+|bextra\d+)$/;
|
||||||
|
|
||||||
|
interface PickTargets {
|
||||||
|
focusStation: (stationId: string) => void;
|
||||||
|
refreshCard: (chainageM: number) => void;
|
||||||
|
revetSelect: (chainageM: number, key: RevetKey) => void;
|
||||||
|
fordSelect: (chainageM: number, role: "inlet" | "outlet") => void;
|
||||||
|
boxSelect: (chainageM: number, role: "left" | "right") => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 넘김값을 화면에 적용한다. 값이 없거나 그 측점이 이 노선에 없으면 아무것도 안 한다.
|
||||||
|
* 카드가 다 선 뒤에 고른다 — 스크롤이 자리를 잡아야 한다.
|
||||||
|
*/
|
||||||
|
export function applyStructurePick(
|
||||||
|
handoff: StructurePickHandoff | null,
|
||||||
|
detail: SectionDetailResponse | null,
|
||||||
|
targets: PickTargets,
|
||||||
|
): void {
|
||||||
|
if (!handoff) return;
|
||||||
|
const target = detail?.cross_sections.find(
|
||||||
|
(section) => Math.abs(section.chainage_m - handoff.at) < 0.01,
|
||||||
|
);
|
||||||
|
if (!target) return;
|
||||||
|
// 조정창은 앞칸(측 이름)만 본다 — 뒤칸은 3D 강조를 부재 하나로 좁히는 몫이다.
|
||||||
|
const key = handoff.key?.split(":")[0];
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
targets.focusStation(target.station_id);
|
||||||
|
if (!key || key === "body") return;
|
||||||
|
if (target.ford) {
|
||||||
|
if (key !== "inlet" && key !== "outlet") return;
|
||||||
|
targets.fordSelect(target.chainage_m, key);
|
||||||
|
} else if (target.box) {
|
||||||
|
if (key !== "left" && key !== "right") return;
|
||||||
|
targets.boxSelect(target.chainage_m, key);
|
||||||
|
} else {
|
||||||
|
if (!REVET_KEYS.test(key)) return;
|
||||||
|
targets.revetSelect(target.chainage_m, key as RevetKey);
|
||||||
|
}
|
||||||
|
targets.refreshCard(target.chainage_m);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user