feat(구조물표): 323-5 고름 한 칸 — 관 세션 id(옮겨도 그대로 · 저장 안 함) · 고름 강조 한 규칙(pickLights · 카드 형상 · 3D 같은 함수) · 그린 것마다 신원표(data-structure-id + data-part) · 같은 자리 거리 = PIPE_MATCH_M 하나 · 322-0 이 깬 옛 시험 도움 파일 · 줄 수 고침

- `design_pipe_ids.ts` — 장부(세션 pipe-ids) · pipe-s{차례} · 자리 → id(찾는 거리 안 가장 가까운 것) · followMovedPipe 가 id 를 새 자리로(고름 id 그대로 · 순환 import 없게 design_selection 에서 옮김) · 설계 페이지가 프로젝트를 정함(usePipeIds) · 자리 이름 만들기 열 곳 · 되읽기 둘 걷음 · 측점 계획 주인 = 세션 id(withPipeIds)
- `design_pick_match.ts` — pickLights(① id ② 같은 자리 ③ 고른 부분) · paintPicked(카드 통째 형상 · 부분 · 관 계열 · 포장층은 제 손잡이) · tagShape — 관 세트 벽 · 집수정 · 관 · 세월교 측벽 · 몸체 · BOX 끝 · C군 벽 · 물넘이 콘크리트에 신원표 · 형상 판정면은 통째 형상 · 콘크리트만
- 3D — 부재 맞춤 = pickLights(옛 0.01 걷음) · C군 벽 솔리드 = 구조물 id(덮는 측점 어디서 골라도 한 번 스윕한 벽이 켜짐)
- 322-0 이 깬 것 고침 — 노면공 흐름 도움 파일(165 · 231-1) 정의 판 확인 흉내 · design_view_profile 700줄(322-0 이 704)
- 시험: test_323_5_pick(세션 id · 옮김 · 강조 규칙 · 칠하기) · 옛 시험 도움 파일 흉내 · 기대 글 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-14 18:06:13 +09:00
co-authored by Claude Opus 5.5
parent c45814f910
commit cb4bab7bf2
37 changed files with 488 additions and 81 deletions
+2 -1
View File
@@ -15,6 +15,7 @@ import { readState, writeState } from "../A00_Common/b_page_state";
import { noteSnapshotFailed } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import type { GroupItems, SummaryItem } from "../common_util/common_util_summary_item";
import { useCatalogDefinition } from "../common_util/common_util_structure_catalog";
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
import {
type DefinedRule,
@@ -324,7 +325,7 @@ export function pipesToStructureMarks(
}>,
): StructureInstance[] {
return pipes.map((pipe) => ({
structure_id: `pipe-${pipe.chainage_m.toFixed(2)}`,
structure_id: pipeIdAt(pipe.chainage_m), // 관 세션 id(323-5)
type_id: pipe.facility,
placement: "point",
chainage_m: pipe.chainage_m,
+6 -1
View File
@@ -382,7 +382,12 @@ export function createCorridorGroup(build: CorridorBuildResult, bounds: ModelBou
(build.structures ?? []).forEach((structure, index) => {
// 신원표 — 3D 클릭이 이 값으로 부재를 가려낸다(2026-09-04). 이름 규칙은 그대로 둔다
// (다른 코드가 이름으로 찾는다). 배관은 겉·속 두 겹이라 자식까지 같은 표를 단다.
const tag = { chainageM: structure.chainage_m, kind: structure.kind, key: structure.key };
const tag = {
chainageM: structure.chainage_m,
kind: structure.kind,
key: structure.key,
structureId: structure.structure_id, // 323-5 — 벽은 id 로 고름
};
if (structure.kind === "pipe") {
const mesh = structurePipeMesh(structure, origin);
if (mesh) {
@@ -86,6 +86,8 @@ export interface CorridorStructure {
* 신원표다(2026-09-04). 세월교·BOX암거 부재는 아직 비워 둔다.
*/
key?: string;
/** 구조물 id(323-5) — C군 벽(id 로 고름 · 덮는 측점 어디서 골라도 · 한 번 스윕). 관 부재는 자리로 고름. */
structure_id?: string;
/** 단면 폴리곤 [offset, elevation][] — 스윕형(벽·집수정). pipe는 없음. */
polygon?: Array<[number, number]>;
/**
@@ -288,6 +290,7 @@ export function buildCorridorStructures(
// (2026-08-28 이관 이중그리기 방지 — 횡단도 가드와 같은 규칙).
// C군 벽 = 덮는 측점 중 한 곳에서만 · 시작 ~ 끝 스윕(323-4 · 측점마다 통째로 겹치던 것)
const wallSweep = wallSweepAt(section, crossSections);
const wallId = section.revetment?.structure_id;
const revetLayout =
wallSweep &&
ownWallsDrawAt(section, culvertOwnerFor(section, crossSections)) &&
@@ -369,8 +372,10 @@ export function buildCorridorStructures(
if (revetLayout && wallSweep) {
// 다단이면 단마다 솔리드 하나 — 횡단 카드와 같은 폴리곤을 그대로 스윕한다.
const from = solids.length;
for (const tier of revetLayout.tiers)
pushSwept("revet", tier.polygon, wallSweep.beforeM, wallSweep.afterM, null, "own");
for (const solid of solids.slice(from)) solid.structure_id = wallId ?? undefined;
}
/**
+1 -1
View File
@@ -30,7 +30,7 @@ import type { FlowArrow } from "../B04_PreProcess/B04_PreProcess_UI_FlowArrows";
import { buildStrengthArray } from "../B04_PreProcess/B04_PreProcess_UI_FlowRamp";
import { resampleRoute } from "../B04_PreProcess/B04_PreProcess_UI_RouteSamples";
import { createPipeEditor } from "./B05_Profile_UI_Drainage_Pipes";
import { followMovedPipe, PIPE_MATCH_M } from "../A00_Common/design/design_selection";
import { followMovedPipe, PIPE_MATCH_M } from "../A00_Common/design/design_pipe_ids";
import { snapStructureStation } from "../common_util/common_util_structure_station";
import { createFacilityStore } from "./B05_Profile_UI_Drainage_Facility";
import { mergeFacilityOptions } from "./B05_Profile_UI_Drainage_Facility_Merge";
@@ -35,6 +35,7 @@ import { plantsStation } from "./B05_Profile_UI_Profile_Structures";
import { listPick } from "../common_util/common_util_structure_catalog";
import { planStations } from "../common_util/common_util_structure_station";
import { plannedIrregular } from "../A00_Common/design/design_structure_status";
import { pipeChainageOf, pipeIdAt, withPipeIds } from "../A00_Common/design/design_pipe_ids";
/** 관 지점 한 건 — 유효직경·시설 종류·부속 옵션까지 그대로 받는다. */
export interface PipeProjection {
@@ -119,7 +120,7 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
if (!planTypes.length) return;
const interval = deps.panel().values().stationInterval || 20;
applyIrregularStations(
plannedIrregular(planStations(planPipes, ownStructures, planTypes), interval),
plannedIrregular(planStations(withPipeIds(planPipes), ownStructures, planTypes), interval),
);
}
@@ -138,9 +139,7 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
/** 알약 id가 계곡 통과 시설(관 정본)이면 그 누가거리, 아니면 null. */
function pipeMarkChainage(structureId: string | null): number | null {
if (!structureId?.startsWith("pipe-")) return null;
const value = Number(structureId.slice("pipe-".length));
return Number.isFinite(value) ? value : null;
return structureId ? pipeChainageOf(structureId) : null; // 관 세션 id 장부(323-5)
}
/** 그래프 알약 레인에 올릴 목록 — 구조물 정본 + 계곡 통과 시설(관 정본)을 합친다.
@@ -161,7 +160,7 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
}>,
): StructureInstance[] {
return pipes.map((pipe) => ({
structure_id: `pipe-${pipe.chainage_m.toFixed(2)}`,
structure_id: pipeIdAt(pipe.chainage_m), // 관 세션 id(323-5)
type_id: pipe.facility,
placement: "point",
chainage_m: pipe.chainage_m,
@@ -10,6 +10,8 @@
* ========================================================================== */
import * as THREE from "three";
import { pickLights } from "../A00_Common/design/design_pick_match";
import { isNear } from "../A00_Common/design/design_selection";
/** 3D에서 고른 부재의 신원표 — 메쉬 `userData`에 실려 있는 그 값이다. */
export interface StructurePick {
@@ -17,6 +19,8 @@ export interface StructurePick {
kind: string;
/** 부재키(`inlet`·`outlet`·`extra{i}`·`bextra{i}`·`own`·`basin`·`pipe`). 없을 수 있다. */
key?: string;
/** 구조물 id(323-5 · C군 벽) — 있으면 id 로 맞춤(고름 강조 한 규칙 `pickLights`). */
structureId?: string | null;
}
export interface StructurePickControls {
@@ -64,18 +68,31 @@ export function bindStructurePick(options: {
while (node) {
const data = node.userData as Partial<StructurePick>;
if (typeof data.chainageM === "number") {
return { chainageM: data.chainageM, kind: String(data.kind ?? ""), key: data.key };
return {
chainageM: data.chainageM,
kind: String(data.kind ?? ""),
key: data.key,
structureId: data.structureId,
};
}
node = node.parent;
}
return null;
}
/** 이 부재가 지금 고른 것인가 — 부재키가 없는 선택(측점만)은 그 측점 전부를 켠다. */
/** 이 부재가 지금 고른 것인가 — 고름 강조 한 규칙(카드 형상과 같음 · 부재키가 없는 선택 = 그 자리 전부). */
const asSelection = (pick: StructurePick) => ({
chainageM: pick.chainageM,
structureId: pick.structureId ?? null,
partKey: pick.key,
});
const asTag = (pick: StructurePick) => ({
chainageM: pick.chainageM,
structureId: pick.structureId ?? null,
part: pick.key,
});
function matches(tag: StructurePick): boolean {
if (!selected) return false;
if (Math.abs(tag.chainageM - selected.chainageM) >= 0.01) return false;
return selected.key === undefined || tag.key === selected.key;
return !!selected && pickLights(asSelection(selected), asTag(tag));
}
function applyHighlight(): void {
@@ -138,8 +155,8 @@ export function bindStructurePick(options: {
if (!pick) return;
const same =
selected !== null &&
Math.abs(selected.chainageM - pick.chainageM) < 0.01 &&
selected.key === pick.key;
selected.key === pick.key &&
pickLights(asSelection(selected), asTag(pick));
selected = same ? null : pick;
applyHighlight();
controls.onPick?.(selected);
@@ -162,7 +179,7 @@ export function bindStructurePick(options: {
selectAtChainage(chainageM) {
// 3D에서 부재를 집어 이미 그 측점을 고른 상태면 그대로 둔다 — 되돌아온 동기화가
// 부재 하나짜리 강조를 그 측점 전체로 넓히면 안 된다(2026-09-04).
if (chainageM !== null && selected && Math.abs(selected.chainageM - chainageM) < 0.01) return;
if (chainageM !== null && selected && isNear(selected.chainageM, chainageM)) return;
selected = chainageM === null ? null : { chainageM, kind: "" };
applyHighlight();
},