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:
@@ -133,6 +133,8 @@ export const STATE_REGISTRY = {
|
||||
scope: "project",
|
||||
legacy: (p) => `aislo:structure-pick:${p}`,
|
||||
},
|
||||
/** 관 세션 id 장부(323-5 · `design_pipe_ids`) — `{next, ids: [{id, at}]}` · 저장 안 함. */
|
||||
"pipe-ids": { bucket: "draft", scope: "project" },
|
||||
/** 설계 가게(`design/design_store`)의 고른 측점 · 구조물 — `{chainageM, structureId}`. */
|
||||
selection: { bucket: "draft", scope: "project" },
|
||||
/** 구조물 패널 종류별 캐시 — 고른 구조물 하나 × 종류마다 옛 값(54-32). [저장]·[확정] 뒤 비움. */
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { currentRoute, navigateTo, type MountedPage } from "../router";
|
||||
import type { DesignView } from "./design_dock";
|
||||
import type { DesignPageShared } from "./design_shared";
|
||||
import { usePipeIds } from "./design_pipe_ids";
|
||||
import "./design_page.css";
|
||||
|
||||
export type { DesignPageShared } from "./design_shared";
|
||||
@@ -105,6 +106,7 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
||||
page.className = "design-page";
|
||||
root.replaceChildren(page);
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
|
||||
usePipeIds(projectId || null); // 관 세션 id 장부 = 이 프로젝트(323-5 · 카드 · 알약 · 종단 · 3D 같은 id)
|
||||
|
||||
// 한 벌 틀 — 좌측 자리 · 가운데(위 칸 + 아래 종단도). 진행 단계 틀은 단계 상태가 오면 감싼다.
|
||||
const left = document.createElement("div");
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_pick_match.ts
|
||||
* 고름 강조 한 규칙(323-5) — 그린 것(카드 형상 · 3D 부재 · 알약 · 측점선)마다 신원표 `{구조물 id · 부분 · 자리}`
|
||||
* 를 달고, 고름 한 칸(`structure-pick`)과 이 함수 하나로 켜짐을 가름.
|
||||
*
|
||||
* ① 고름 · 그림 둘 다 구조물 id 가 있으면 id 가 같아야(관 = 세션 id `design_pipe_ids` · 옮겨도 그대로)
|
||||
* ② 아니면 같은 자리(`PIPE_MATCH_M` — 같은 자리 거리는 이 한 값)
|
||||
* ③ 고른 부분(부재키)이 있으면 그 부분만 · 부분을 안 골랐으면 그 구조물 그림 모두
|
||||
* DOM 은 `data-structure-id` · `data-part`(`tagShape`) — 부분 그림(관 · 벽 · 날개벽 · 물넘이 콘크리트)은
|
||||
* 조정창 손잡이가 같은 고름으로 칠함(`paintPicked` 는 부분 없는 형상만 · 307 「측점 길은 부분 안 켬」).
|
||||
* ========================================================================== */
|
||||
|
||||
import { isNear, type DesignSelection } from "./design_selection";
|
||||
|
||||
export interface PickTag {
|
||||
structureId?: string | null;
|
||||
part?: string | null;
|
||||
chainageM?: number | null;
|
||||
}
|
||||
|
||||
export type PickSelection =
|
||||
(Partial<DesignSelection> & { partKey?: string | null }) | null | undefined;
|
||||
|
||||
/** 이 그림이 고름에 켜지나 — 위 ① ② ③. */
|
||||
export function pickLights(selection: PickSelection, tag: PickTag): boolean {
|
||||
if (!selection) return false;
|
||||
let here: boolean;
|
||||
if (selection.structureId && tag.structureId) here = selection.structureId === tag.structureId;
|
||||
else if (selection.chainageM == null || tag.chainageM == null) return false;
|
||||
else here = isNear(tag.chainageM, selection.chainageM);
|
||||
if (!here) return false;
|
||||
return !selection.partKey || tag.part === selection.partKey;
|
||||
}
|
||||
|
||||
/** 그린 형상에 신원표를 닮(부분 없으면 통째 형상). */
|
||||
export function tagShape(el: Element, structureId: string | null, part?: string | null): void {
|
||||
if (structureId) el.setAttribute("data-structure-id", structureId);
|
||||
if (part) el.setAttribute("data-part", part);
|
||||
}
|
||||
|
||||
/** 카드 안 통째 형상 칠하기 — 고른 구조물 id 와 같은 형상만. 부분 그림(`data-part`) · 관 계열(`pipe-`) ·
|
||||
* 노면공 포장층은 제 손잡이(조정창 · 포장층 고름)가 칠함 — 측점 길(알약 · 목록 · 측점선)은 부분을 안 켬(307). */
|
||||
export function paintPicked(root: ParentNode, selection: PickSelection): void {
|
||||
for (const shape of root.querySelectorAll<SVGElement>("[data-structure-id]")) {
|
||||
const id = shape.getAttribute("data-structure-id") ?? "";
|
||||
if (id.startsWith("pipe-") || shape.getAttribute("data-part") !== null) continue;
|
||||
if (shape.getAttribute("data-pave-type") !== null) continue;
|
||||
shape.classList.toggle(
|
||||
"is-active",
|
||||
!!selection?.structureId && pickLights(selection, { structureId: id }),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* 고름 한 칸(`structure-pick`)이 유일 원천이다. 값이 바뀌면(구독) 그 값으로 종단도를 다시 세우고,
|
||||
* 관 · 구조물 목록이 바뀐 뒤에도 같은 값으로 다시 얹는다(`paint`) — 화면끼리 손으로 부르던 길이
|
||||
* 하나 빠지거나 다른 길이 강조를 끄더라도 종단도는 값을 따라간다. 관 알약 id(`pipe-`+측점)는
|
||||
* 그릴 때마다 지금 관 목록에서 다시 찾으므로 관이 옮겨져도 끊기지 않는다.
|
||||
* 그릴 때마다 지금 관 목록에서 다시 찾고 관 id 는 세션 id(323-5)라 관이 옮겨져도 끊기지 않는다.
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type DesignSelection,
|
||||
} from "./design_selection";
|
||||
import { bindEscapeRelease, ESCAPE_FROM } from "./design_pick_escape";
|
||||
import { pipeIdAt } from "./design_pipe_ids";
|
||||
import { graphTargetAt } from "../../B05_Profile/B05_Profile_UI_Selection";
|
||||
import { irregularStationId } from "../../B05_Profile/B05_Profile_UI_IrregularStations";
|
||||
|
||||
@@ -68,9 +69,7 @@ export function profilePickOf(
|
||||
);
|
||||
if (own) return { stationId: irregularStationId(own.id) };
|
||||
const target = graphTargetAt(null, at, source.pipes(), source.sections());
|
||||
return "pipeChainageM" in target
|
||||
? { structureId: `pipe-${target.pipeChainageM.toFixed(2)}` }
|
||||
: target;
|
||||
return "pipeChainageM" in target ? { structureId: pipeIdAt(target.pipeChainageM) } : target;
|
||||
}
|
||||
|
||||
export function paintProfilePick(target: ProfilePickTarget, pick: ProfilePick): void {
|
||||
@@ -91,7 +90,9 @@ export function paintMapPick(
|
||||
export interface ViewerPickTarget {
|
||||
markers: { selectStation: (stationId: string | null, notify?: boolean) => void };
|
||||
structurePick: {
|
||||
select: (pick: { chainageM: number; kind: string; key?: string } | null) => void;
|
||||
select: (
|
||||
pick: { chainageM: number; kind: string; key?: string; structureId?: string | null } | null,
|
||||
) => void;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,7 +101,10 @@ export interface ViewerPickTarget {
|
||||
export function viewerPickOf(
|
||||
selection: (DesignSelection & { partKey?: string }) | null,
|
||||
source: ProfilePickSource,
|
||||
): { stationId: string | null; member: { chainageM: number; kind: string; key?: string } | null } {
|
||||
): {
|
||||
stationId: string | null;
|
||||
member: { chainageM: number; kind: string; key?: string; structureId?: string | null } | null;
|
||||
} {
|
||||
if (!selection || selection.chainageM === null) return { stationId: null, member: null };
|
||||
const at = selection.chainageM;
|
||||
const pick = profilePickOf(selection, source);
|
||||
@@ -108,7 +112,10 @@ export function viewerPickOf(
|
||||
? nearestWithin(source.irregular(), at, (entry) => entry.chainage_m)
|
||||
: undefined;
|
||||
const stationId = "stationId" in pick ? pick.stationId : own ? irregularStationId(own.id) : null;
|
||||
return { stationId, member: { chainageM: at, kind: "", key: selection.partKey } };
|
||||
return {
|
||||
stationId,
|
||||
member: { chainageM: at, kind: "", key: selection.partKey, structureId: selection.structureId },
|
||||
};
|
||||
}
|
||||
|
||||
export function paintViewerPick(
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_pipe_ids.ts
|
||||
* 관 세션 id(323-5) — 관을 처음 볼 때 자리마다 붙이는 id · 옮겨도 그대로 · 저장 안 함(파일 모양 그대로).
|
||||
*
|
||||
* id = `pipe-s` + 차례(`pipe-s3`) — 머리 `pipe-` 로 관임을 가름(옛 자리 이름 `pipe-{누가거리}` 와 같은 머리) ·
|
||||
* 누가거리는 id 에서 읽지 않음(`pipeChainageOf`). 장부 = 세션 `pipe-ids`(고름 `structure-pick` 과 같은 수명 ·
|
||||
* 초안 비우기에 함께 비움). 자리 → id = 찾는 거리(`PIPE_MATCH_M`) 안 가장 가까운 것 · 없으면 새 차례 ·
|
||||
* 옮김 = `movePipeId`(고름 따라가기 `followMovedPipe` 가 부름).
|
||||
* 프로젝트 = 보기가 열 때 정함(`usePipeIds`) — 그리는 함수(카드 · 알약)가 프로젝트를 몰라도 같은 id.
|
||||
* 프로젝트가 없으면(시험 · 도움 파일) 옛 자리 이름.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readState, writeState } from "../b_page_state";
|
||||
import { isNear, nearestWithin, readSelection, selectDesign } from "./design_selection";
|
||||
|
||||
/** 관 찾는 거리 — 정의는 `design_selection` 한 곳(배수 패널이 관 옮김과 함께 가져감). */
|
||||
export { PIPE_MATCH_M } from "./design_selection";
|
||||
|
||||
interface Book {
|
||||
next: number;
|
||||
ids: Array<{ id: string; at: number }>;
|
||||
}
|
||||
|
||||
let active: string | null = null;
|
||||
|
||||
/** 지금 프로젝트 — 종단 · 횡단 보기가 열 때. */
|
||||
export function usePipeIds(projectId: string | null): void {
|
||||
active = projectId;
|
||||
}
|
||||
|
||||
const bookOf = (projectId: string): Book =>
|
||||
readState<Book>("pipe-ids", projectId) ?? { next: 1, ids: [] };
|
||||
|
||||
/** 그 자리 관의 id — 장부에 없으면 새 차례로 적음. */
|
||||
export function pipeIdAt(chainageM: number, projectId: string | null = active): string {
|
||||
if (!projectId) return `pipe-${chainageM.toFixed(2)}`;
|
||||
const book = bookOf(projectId);
|
||||
const hit = nearestWithin(book.ids, chainageM, (entry) => entry.at);
|
||||
if (hit) return hit.id;
|
||||
const id = `pipe-s${book.next}`;
|
||||
writeState(
|
||||
"pipe-ids",
|
||||
{ next: book.next + 1, ids: [...book.ids, { id, at: chainageM }] },
|
||||
projectId,
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
/** id 의 관 자리 — 장부 · 옛 자리 이름 순 · 관 id 가 아니면 null. */
|
||||
export function pipeChainageOf(id: string, projectId: string | null = active): number | null {
|
||||
if (!id.startsWith("pipe-")) return null;
|
||||
const hit = projectId ? bookOf(projectId).ids.find((entry) => entry.id === id) : undefined;
|
||||
if (hit) return hit.at;
|
||||
const legacy = Number(id.slice("pipe-".length));
|
||||
return Number.isFinite(legacy) ? legacy : null;
|
||||
}
|
||||
|
||||
/** 관을 옮김 — 옛 자리 id 가 새 자리로(새 자리에 남은 옛 기록은 뺌) · 옮긴 관의 id. */
|
||||
export function movePipeId(fromM: number, toM: number, projectId: string | null = active): string {
|
||||
if (!projectId) return `pipe-${toM.toFixed(2)}`;
|
||||
const book = bookOf(projectId);
|
||||
const moving = nearestWithin(book.ids, fromM, (entry) => entry.at);
|
||||
if (!moving) return pipeIdAt(toM, projectId);
|
||||
const stale = nearestWithin(
|
||||
book.ids.filter((entry) => entry !== moving),
|
||||
toM,
|
||||
(entry) => entry.at,
|
||||
);
|
||||
const ids = book.ids
|
||||
.filter((entry) => entry !== stale)
|
||||
.map((entry) => (entry === moving ? { ...entry, at: toM } : entry));
|
||||
writeState("pipe-ids", { next: book.next, ids }, projectId);
|
||||
return moving.id;
|
||||
}
|
||||
|
||||
/** 관 목록에 세션 id 를 얹은 사본 — 측점 계획의 덮는 구조물 id(종단 측점 id · 알약 짝)가 세션 id. */
|
||||
export function withPipeIds<T extends { chainage_m: number }>(
|
||||
pipes: ReadonlyArray<T>,
|
||||
projectId: string | null = active,
|
||||
): Array<T & { id: string }> {
|
||||
return pipes.map((pipe) => ({ ...pipe, id: pipeIdAt(pipe.chainage_m, projectId) }));
|
||||
}
|
||||
|
||||
/** 관을 옮김 — 관 세션 id 가 새 자리로(323-5 · id 그대로) · 그 관을 골라 둔 고름이면 자리만 새로(부재키
|
||||
* 그대로 · 옛 자리 이름 id 면 세션 id 로). 다른 것을 골랐으면 그대로. */
|
||||
export function followMovedPipe(projectId: string | null, fromM: number, toM: number): void {
|
||||
const id = movePipeId(fromM, toM, projectId);
|
||||
const picked = readSelection(projectId);
|
||||
if (!picked || picked.chainageM === null || !isNear(picked.chainageM, fromM)) return;
|
||||
const sid = picked.structureId;
|
||||
if (sid !== null && !sid.startsWith("pipe-")) return;
|
||||
selectDesign(
|
||||
projectId,
|
||||
{ chainageM: toM, structureId: sid === null ? null : id },
|
||||
{ partKey: picked.partKey },
|
||||
);
|
||||
}
|
||||
@@ -188,17 +188,3 @@ export function selectDesign(
|
||||
dispatching = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 관을 옮김 — 그 관을 골라 둔 고름이면 새 자리로(322-3 · 관 id = 자리 이름 `pipe-{누가거리}` 라 id 도
|
||||
* 새 자리로 · 옮겨도 고름 유지 · 부재키 그대로). 다른 것을 골랐으면 그대로. */
|
||||
export function followMovedPipe(projectId: string | null, fromM: number, toM: number): void {
|
||||
const picked = readSelection(projectId);
|
||||
if (!picked || picked.chainageM === null || !isNear(picked.chainageM, fromM)) return;
|
||||
const sid = picked.structureId;
|
||||
if (sid !== null && !sid.startsWith("pipe-")) return;
|
||||
selectDesign(
|
||||
projectId,
|
||||
{ chainageM: toM, structureId: sid === null ? null : `pipe-${toM.toFixed(2)}` },
|
||||
{ partKey: picked.partKey },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* → 한 목록(초안이 있으면 초안 · 없으면 저장분 · 종류마다 따로) · 측점 계획 한 벌(`planStations`) →
|
||||
* 종단 측점 목록(B05 종단 · 3D 막대 · B06 종단 띠가 같은 꼴).
|
||||
*
|
||||
* 구조물 id = 구조물은 structure_id · 관은 자리 이름 `pipe-{누가거리 2자리}`(파일 모양 그대로 · 저장 안 함).
|
||||
* 구조물 id = 구조물은 structure_id · 관은 세션 id(`design_pipe_ids` · 323-5 · 파일 모양 그대로 · 저장 안 함).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { IrregularStation } from "../../B05_Profile/B05_Profile_UI_IrregularStations";
|
||||
@@ -18,9 +18,6 @@ export function structureStatus<P, S>(
|
||||
return { pipes: [...(drafts[0] ?? saved[0])], structures: [...(drafts[1] ?? saved[1])] };
|
||||
}
|
||||
|
||||
/** 관 id(현황 · 알약 · 고름이 같은 값) — 자리 이름. */
|
||||
export const pipeId = (chainageM: number): string => `pipe-${chainageM.toFixed(2)}`;
|
||||
|
||||
/**
|
||||
* 측점 계획 → 종단 측점(측점번호 · 잔여거리 = 간격 기준) — 기준 측점 id = 첫 주인 id(관 = `pipe-…` ·
|
||||
* 구조물 = structure_id) · 시작 · 끝 = `{id}:{역할}`(옮겨도 계획고 편집이 따라감 · 272) · 관 기준 측점 =
|
||||
|
||||
@@ -18,6 +18,7 @@ import { readByKey, stateKey, writeByKey } from "../b_page_state";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { mountMissingStationNotice } from "../../B06_Section/B06_Section_UI_Missing_Stations";
|
||||
import { onStaleStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { pipeIdAt } from "./design_pipe_ids";
|
||||
import {
|
||||
fetchSectionContext,
|
||||
getSections,
|
||||
@@ -399,7 +400,7 @@ export async function mountCrossView(
|
||||
// 카드 고름은 그 카드 측점선으로 알약을 맞춤 — 일반 측점 구조물 · 옆 측점 이음 카드면
|
||||
// 알약이 꺼지므로 연 것으로 다시 세움(관 알약 id = pipe-측점 · 53-1)
|
||||
const pipeAt = structuresPanel.currentChainage();
|
||||
const mark = opened.structureId ?? (pipeAt === null ? null : `pipe-${pipeAt.toFixed(2)}`);
|
||||
const mark = opened.structureId ?? (pipeAt === null ? null : pipeIdAt(pipeAt, projectId));
|
||||
sectionView.selectMark(mark, false); // 측점 고름 = 카드 하나(137-4) · 덮는 것 없으면 알약 · 강조 풂(152-5)
|
||||
structureSelection.syncInletStructure();
|
||||
// 카드만 고른 경우엔 구조물(벽·구체) 선택을 비운다 — 조정창이 저절로 뜨면 안 된다
|
||||
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
type DesignPageShared,
|
||||
} from "./design_shared";
|
||||
import { bindProfileViewLife } from "../../B05_Profile/B05_Profile_UI_Page_Life";
|
||||
import { structureAnchorM } from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { createStructuresBridge } from "../../B05_Profile/B05_Profile_UI_Page_Structures";
|
||||
import { createRouteViewer } from "../../B05_Profile/B05_Profile_UI_Viewer";
|
||||
import {
|
||||
@@ -60,6 +59,7 @@ import { loadSectionDetail } from "../../B06_Section/B06_Section_Section_Store";
|
||||
import {
|
||||
migrateLegacyStations,
|
||||
onStaleStructureTypes,
|
||||
structureAnchorM,
|
||||
} from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { pipeToFacilityItem } from "../../B05_Profile/B05_Profile_UI_Structures_Panel_Types";
|
||||
import {
|
||||
@@ -80,6 +80,7 @@ import "../../B05_Profile/B05_Profile_UI_Style.css";
|
||||
import "../../B05_Profile/B05_Profile_UI_Style_Table.css";
|
||||
import "../../B05_Profile/B05_Profile_UI_Style_Drainage.css";
|
||||
import "../../B05_Profile/B05_Profile_UI_Style_Structures.css";
|
||||
import { pipeIdAt } from "./design_pipe_ids";
|
||||
import {
|
||||
DEFAULT_ROAD_WIDTHS,
|
||||
fetchRoadWidths,
|
||||
@@ -134,10 +135,8 @@ export async function mountProfileView(
|
||||
const structurePick = !!readSelection(activeProjectId)?.structureId;
|
||||
const sections = structurePick ? [] : (currentSectionDetail?.cross_sections ?? []);
|
||||
const target = graphTargetAt(id, chainageM, lastPipes ?? [], sections);
|
||||
if ("pipeChainageM" in target) {
|
||||
profilePanel.setSelectedStructure(`pipe-${target.pipeChainageM.toFixed(2)}`);
|
||||
return;
|
||||
}
|
||||
if ("pipeChainageM" in target)
|
||||
return profilePanel.setSelectedStructure(pipeIdAt(target.pipeChainageM, activeProjectId));
|
||||
profilePanel.setSelectedStation(target.stationId);
|
||||
if (id === null && target.stationId !== null) viewer.markers.selectStation(target.stationId);
|
||||
};
|
||||
@@ -552,11 +551,8 @@ export async function mountProfileView(
|
||||
});
|
||||
|
||||
// 탭이 다시 보일 때 정의 판이 바뀌었으면 종류 · 패널 다시 읽음(322-0)
|
||||
host.onDispose(
|
||||
onStaleStructureTypes(() => {
|
||||
if (host.visible() && !restoring) void bridge.load();
|
||||
}),
|
||||
);
|
||||
const reloadIfShown = (): void => void (host.visible() && !restoring && bridge.load());
|
||||
host.onDispose(onStaleStructureTypes(reloadIfShown));
|
||||
// 한 페이지 두 보기 — 숨김 · 보임 · 떠남(계획서 24-3a · 24-3b).
|
||||
bindProfileViewLife(host, {
|
||||
projectId: activeProjectId,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
* 콘크리트 색은 세월교 바닥판 클래스를 그대로 쓴다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
|
||||
import { tagShape } from "../A00_Common/design/design_pick_match";
|
||||
import type { BoxLayout, BoxSideRole } from "./B06_Section_UI_Cross_Box_Geom";
|
||||
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { appendChuteBand } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
@@ -219,6 +221,7 @@ export function appendBoxOverlay(
|
||||
event.stopPropagation();
|
||||
onSelectSide(role);
|
||||
});
|
||||
tagShape(hit, box.chainage_m != null ? pipeIdAt(box.chainage_m) : null, role); // 신원표(323-5)
|
||||
hits.set(role, hit);
|
||||
layer.append(hit);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
* 집수정) → ③ 배관 → ④ 유출구측 성토부선(보호공 삭제 — 2026-08-22)
|
||||
* ========================================================================== */
|
||||
|
||||
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
|
||||
import { tagShape } from "../A00_Common/design/design_pick_match";
|
||||
import {
|
||||
REVET_EMBED_DEPTH_M,
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
@@ -88,6 +90,8 @@ export function appendCulvertOverlay(
|
||||
linked?: boolean,
|
||||
): RevetHighlightSetter {
|
||||
const { culvert, pipe, pipeCorners } = layout;
|
||||
// 신원표(323-5) — 관 세션 id · 부분 = 조정창 부재키
|
||||
const ownerId = culvert.chainage_m != null ? pipeIdAt(culvert.chainage_m) : null;
|
||||
const diameter = culvert.diameter_m;
|
||||
const roleLabel = (role: "inlet" | "outlet" | "extra") =>
|
||||
role === "inlet" ? "유입" : role === "outlet" ? "유출" : "추가(성토부)";
|
||||
@@ -219,6 +223,7 @@ export function appendCulvertOverlay(
|
||||
);
|
||||
if (onSelectRevet) revetShape.classList.add("is-selectable");
|
||||
if (linked) revetShape.classList.add("is-linked");
|
||||
tagShape(revetShape, ownerId, wallKey);
|
||||
revetShapes.set(wallKey, revetShape);
|
||||
layer.append(revetShape);
|
||||
// 이음선 + 형태별 표현(돌쌓기 메/찰·콘크리트·돌망태·통나무·바자)은 공용 해칭이
|
||||
@@ -242,6 +247,7 @@ export function appendCulvertOverlay(
|
||||
`유입 집수정(${shapeName}) ${part.kind === "floor" ? "바닥" : "벽"}` +
|
||||
` — ${reasonText} · 내공 1.0m(실무 돌집수정) · 부재 두께 0.2m`,
|
||||
);
|
||||
tagShape(shape, ownerId, "inlet"); // 집수정 = 유입 부재(조정창 같은 키)
|
||||
basinShapes.push(shape);
|
||||
layer.append(shape);
|
||||
}
|
||||
@@ -345,6 +351,7 @@ export function appendCulvertOverlay(
|
||||
];
|
||||
if (!pipeHitPoints.length) pipeHitPoints = points; // 외경 = 판정면
|
||||
const shape = polygon(points, cls, tip);
|
||||
tagShape(shape, ownerId, "pipe");
|
||||
pipeShapes.push(shape);
|
||||
layer.append(shape);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
* ④ 계류측 성토부선·절토선. 관 위 성토 채움은 그리지 않는다(2026-08-25 사용자).
|
||||
* ========================================================================== */
|
||||
|
||||
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
|
||||
import { tagShape } from "../A00_Common/design/design_pick_match";
|
||||
import type { FordLayout, FordWallRole } from "./B06_Section_UI_Cross_Ford_Geom";
|
||||
import {
|
||||
appendPipeInfoText,
|
||||
@@ -54,6 +56,7 @@ export function appendFordOverlay(
|
||||
onSelectWall?: (key: FordPick) => void,
|
||||
): FordHighlightSetter {
|
||||
const { ford } = layout;
|
||||
const ownerId = ford.chainage_m != null ? pipeIdAt(ford.chainage_m) : null; // 신원표(323-5)
|
||||
const toXY = (point: OffsetPoint): [number, number] => [
|
||||
x(point.offset),
|
||||
toDisplayY(point.elevation),
|
||||
@@ -118,6 +121,7 @@ export function appendFordOverlay(
|
||||
onSelectWall(side.role);
|
||||
});
|
||||
}
|
||||
tagShape(shape, ownerId, side.role);
|
||||
shapes.push(shape);
|
||||
layer.append(shape);
|
||||
}
|
||||
@@ -133,6 +137,7 @@ export function appendFordOverlay(
|
||||
const pipeInner = polygon(layout.pipe.inner, "b06-chart__culvert-pipe", layout.label.text);
|
||||
layer.append(pipeOuter, pipeInner);
|
||||
const bodyShapes = [slab, pipeOuter, pipeInner];
|
||||
for (const shape of bodyShapes) tagShape(shape, ownerId, "body");
|
||||
|
||||
// 도수로 · 돌붙임 띠(90-2) — 유출 측벽 끝 ~ 원지반 성토부선 따라(흙막이 쪽은 배관 오버레이가 그림)
|
||||
if (ford.chute)
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
import type { CrossDesign } from "./B06_Section_Api_Fetch";
|
||||
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { appendPavementOverlay } from "./B06_Section_UI_Cross_Design_Surface";
|
||||
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
|
||||
import { tagShape } from "../A00_Common/design/design_pick_match";
|
||||
import { appendChuteBand, chuteRun } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
|
||||
/** 백엔드 `section.ford_pavement` 제원(치수 결정은 서버 몫 — 여기서는 좌표만 만든다). */
|
||||
@@ -117,8 +119,7 @@ export function appendFordPavementOverlay(
|
||||
.forEach((el) => {
|
||||
el.setAttribute("class", "b06-chart__pavement b06-chart__ford-pavement");
|
||||
// 형상 고름 표식(287-5) — 관 지점 알약 id 와 같은 글
|
||||
if (typeof spec.chainage_m === "number")
|
||||
el.setAttribute("data-structure-id", `pipe-${spec.chainage_m.toFixed(2)}`);
|
||||
if (typeof spec.chainage_m === "number") tagShape(el, pipeIdAt(spec.chainage_m), "concrete"); // 관 세션 id · 부분(323-5)
|
||||
});
|
||||
// 월류 높이선 — 콘크리트 윗면(설계 노면) 위로 월류 높이만큼 · 같은 각도(그림_183) · 성토 쪽 = 성토선 연장(315-2)
|
||||
const depth = spec.depth_m ?? 0;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* 3D 코리도는 `tiers[].polygon`·`span`을 그대로 스윕한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { tagShape } from "../A00_Common/design/design_pick_match";
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
@@ -90,6 +91,8 @@ export interface RevetmentLayout {
|
||||
heightM: number;
|
||||
/** 이 단면의 누가거리(m) — 부족 안내를 측점 단위로 센다(재렌더 중복 방지). */
|
||||
chainageM: number;
|
||||
/** 벽 구조물 id — 그린 벽 신원표(323-5). */
|
||||
structureId: string | null;
|
||||
/** 사용자가 요청한 단 수 — 실제로 선 단 수(`walls.length`)와 다르면 자리가 부족한 것이다. */
|
||||
requestedTiers: number;
|
||||
/** 1단 벽 이음선 상단점(성토 설계선 트림 경계). */
|
||||
@@ -315,6 +318,7 @@ export function computeRevetmentLayout(
|
||||
side,
|
||||
heightM: pureHeight,
|
||||
chainageM: Number(section.chainage_m) || 0,
|
||||
structureId: spec.structure_id ?? null,
|
||||
requestedTiers,
|
||||
top: jt,
|
||||
walls,
|
||||
@@ -376,6 +380,7 @@ export function appendRevetmentOverlay(
|
||||
const shape = drawRevetWall(svg, wall, x, y, "b06-chart__culvert-revet", tooltip, keyId);
|
||||
if (onSelect) shape.classList.add("is-selectable");
|
||||
if (layout.linked) shape.classList.add("is-linked");
|
||||
tagShape(shape, layout.structureId, keyId); // 신원표(323-5)
|
||||
drawn.push(shape);
|
||||
});
|
||||
// 클릭 판정용 투명 겹면 — **맨 위**에 얹는다. 돌 해칭이 벽 폴리곤을 덮어 직접 핸들러를
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import type { RevetHighlightSetter } from "./B06_Section_UI_Cross_Culvert";
|
||||
import { pipeChainageOf } from "../A00_Common/design/design_pipe_ids";
|
||||
import { paintPicked } from "../A00_Common/design/design_pick_match";
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
/** 관 지점이 아닌 구조물(맹암거) 형상 누름 — 카드 밖(구조물 목록)이 id 로 고름(288-2). */
|
||||
@@ -65,8 +67,11 @@ export function attachShapePick(
|
||||
layer: SVGElement,
|
||||
onPick?: (structureId: string) => void,
|
||||
): ((active: boolean) => void) | null {
|
||||
// 통째 형상(맹암거) · 물넘이 콘크리트만 — 조정창 부분(관 · 벽 · 날개벽 · 323-5 신원표)은 제 판정면이 있음
|
||||
const shapes = [...layer.querySelectorAll<SVGElement>("[data-structure-id]")].filter(
|
||||
(el) => !el.hasAttribute("data-pave-type"),
|
||||
(el) =>
|
||||
!el.hasAttribute("data-pave-type") &&
|
||||
[null, "concrete"].includes(el.getAttribute("data-part")),
|
||||
);
|
||||
if (!shapes.length) return null;
|
||||
const kids = [...layer.children];
|
||||
@@ -102,7 +107,8 @@ export function shapePickRoute(
|
||||
pickPipe: (chainageM: number) => void,
|
||||
): (structureId: string) => void {
|
||||
return (structureId) => {
|
||||
if (structureId.startsWith("pipe-")) return pickPipe(Number(structureId.slice(5)));
|
||||
const pipeAt = pipeChainageOf(structureId); // 관 세션 id 장부(323-5)
|
||||
if (pipeAt !== null) return pickPipe(pipeAt);
|
||||
from.dispatchEvent(new CustomEvent(SHAPE_PICK_EVENT, { bubbles: true, detail: structureId }));
|
||||
};
|
||||
}
|
||||
@@ -121,12 +127,7 @@ export function splitPipePick(
|
||||
};
|
||||
}
|
||||
|
||||
/** 카드 안 형상 강조 — 고른 구조물 id 와 같은 형상만(목록 · 알약 · 폼 고름이 따라옴 · 맹암거). 물넘이
|
||||
* 콘크리트(`pipe-…`)는 형상을 직접 누른 「관」 고름만 켬 — 측점 길(측점선 · 알약 · 목록)은 안 켬(307). */
|
||||
export function highlightShapes(root: ParentNode, structureId: string | null): void {
|
||||
for (const shape of root.querySelectorAll<SVGElement>("[data-structure-id]")) {
|
||||
const id = shape.getAttribute("data-structure-id") ?? "";
|
||||
if (id.startsWith("pipe-")) continue;
|
||||
shape.classList.toggle("is-active", !!structureId && id === structureId);
|
||||
}
|
||||
}
|
||||
/** 카드 안 형상 강조 — 고름 강조 한 규칙(`paintPicked` · 323-5) · 물넘이 콘크리트(관 부분)는 형상을 직접
|
||||
* 누른 「관」 고름만 켬 — 측점 길(측점선 · 알약 · 목록)은 안 켬(307). */
|
||||
export const highlightShapes = (root: ParentNode, structureId: string | null): void =>
|
||||
paintPicked(root, { structureId });
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
coversChainage,
|
||||
NO_SELECTION,
|
||||
PIPE_MATCH_M,
|
||||
followMovedPipe,
|
||||
readSelection,
|
||||
resolveSelection,
|
||||
selectDesign,
|
||||
@@ -64,6 +63,7 @@ import {
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import { withSpecDefaults } from "./B06_Section_UI_Page_Structures_Spec";
|
||||
import { listPick } from "../common_util/common_util_structure_catalog";
|
||||
import { followMovedPipe, pipeIdAt } from "../A00_Common/design/design_pipe_ids";
|
||||
|
||||
const PIPE_ADD_GUIDE = "계곡 통과 시설을 넣고 빼려면 프로젝트를 먼저 여세요.";
|
||||
const PIPE_MOVE_GUIDE = "기준 측점 이동은 배수유역을 다시 나눠야 해 B05(종단) 화면에서 합니다.";
|
||||
@@ -348,7 +348,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
);
|
||||
if (chainageM !== null) deps.focusChainage(chainageM);
|
||||
// 알약 = 그 관 알약(같은 측점 구조물 말고 · 측점선 없는 물넘이포장도) — 카드 뒤에(53-2)
|
||||
deps.markStructure?.(chainageM === null ? null : `pipe-${chainageM.toFixed(2)}`);
|
||||
deps.markStructure?.(chainageM === null ? null : pipeIdAt(chainageM, deps.projectId));
|
||||
},
|
||||
onListRelease: () => deps.releaseCard?.(),
|
||||
},
|
||||
@@ -650,7 +650,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
refreshSpecDefaults(hit.pipeChainageM);
|
||||
section.selectPipeByChainage(hit.pipeChainageM);
|
||||
// 알약은 폼 뒤 — 폼이 연 구조물을 닫으며 알약을 끄므로(165-2 · 측점선 없는 물넘이포장도 53-2)
|
||||
deps.markStructure?.(`pipe-${hit.pipeChainageM.toFixed(2)}`);
|
||||
deps.markStructure?.(pipeIdAt(hit.pipeChainageM, deps.projectId));
|
||||
} else {
|
||||
openingAtStation = preferredId === null;
|
||||
try {
|
||||
@@ -666,7 +666,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
(entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M,
|
||||
);
|
||||
if (!pipe) return;
|
||||
const id = `pipe-${pipe.chainage_m.toFixed(2)}`;
|
||||
const id = pipeIdAt(pipe.chainage_m, deps.projectId);
|
||||
deps.focusChainage(pipe.chainage_m, true); // 카드 안 고름 · 스크롤 없음 — 옛 카드 · 측점선 풂(177-4 길 · 307)
|
||||
currentChainageM = pipe.chainage_m;
|
||||
refreshSpecDefaults(pipe.chainage_m);
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
} 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;
|
||||
@@ -107,7 +108,8 @@ function planMarkStations(
|
||||
options: mark.options,
|
||||
}));
|
||||
const structures = marks.filter((mark) => !isPipeMark(mark));
|
||||
return plannedIrregular(planStations(pipeList, structures, types), intervalM);
|
||||
// 덮는 구조물 id = 관 세션 id(323-5 · 알약 id 와 같음)
|
||||
return plannedIrregular(planStations(withPipeIds(pipeList), structures, types), intervalM);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -133,6 +133,8 @@ const panelModule = loadTs(
|
||||
})),
|
||||
structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0,
|
||||
writePendingStructures() {},
|
||||
dropStaleStructureTypes: async () => {}, // 정의 판 확인(322-0)
|
||||
staleDefinitionNotice: () => null, // 판 알림(322-2)
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Pipes_Draft": {
|
||||
readPendingPipes: () => null,
|
||||
@@ -165,6 +167,15 @@ const panelModule = loadTs(
|
||||
setAdjustSideSlots() {},
|
||||
},
|
||||
"./B06_Section_UI_Page_Structures_Spec": { withSpecDefaults: (o) => o ?? {} },
|
||||
// 옛 소수 관 맞춤(322-3 고침) · 관 세션 id(323-5) — 프로젝트 없음 = 옛 자리 이름
|
||||
"../B05_Profile/B05_Profile_UI_Structures_Snap": {
|
||||
snapNotice() {},
|
||||
snapPlan: () => ({ structures: [], pipes: [], skipped: 0 }),
|
||||
},
|
||||
"../A00_Common/design/design_pipe_ids": {
|
||||
pipeIdAt: (at) => `pipe-${at.toFixed(2)}`,
|
||||
followMovedPipe() {},
|
||||
},
|
||||
// 요구 178 — 알약 측점 짝(진짜 `plantsStation` · 272 측점 심기 한 벌)
|
||||
"../B05_Profile/B05_Profile_UI_Profile_Structures": loadTs(
|
||||
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Structures.ts"),
|
||||
|
||||
@@ -152,6 +152,8 @@ const panelModule = loadTs(
|
||||
})),
|
||||
structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0,
|
||||
writePendingStructures() {},
|
||||
dropStaleStructureTypes: async () => {}, // 정의 판 확인(322-0)
|
||||
staleDefinitionNotice: () => null, // 판 알림(322-2)
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Pipes_Draft": {
|
||||
readPendingPipes: () => null,
|
||||
@@ -184,6 +186,15 @@ const panelModule = loadTs(
|
||||
setAdjustSideSlots() {},
|
||||
},
|
||||
"./B06_Section_UI_Page_Structures_Spec": { withSpecDefaults: (o) => o ?? {} },
|
||||
// 옛 소수 관 맞춤(322-3 고침) · 관 세션 id(323-5) — 프로젝트 없음 = 옛 자리 이름
|
||||
"../B05_Profile/B05_Profile_UI_Structures_Snap": {
|
||||
snapNotice() {},
|
||||
snapPlan: () => ({ structures: [], pipes: [], skipped: 0 }),
|
||||
},
|
||||
"../A00_Common/design/design_pipe_ids": {
|
||||
pipeIdAt: (at) => `pipe-${at.toFixed(2)}`,
|
||||
followMovedPipe() {},
|
||||
},
|
||||
// 요구 178 — 알약 측점 짝(진짜 `plantsStation` · 272 측점 심기 한 벌)
|
||||
"../B05_Profile/B05_Profile_UI_Profile_Structures": loadTs(
|
||||
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Structures.ts"),
|
||||
|
||||
@@ -73,7 +73,12 @@ const MK3 = loadTs("B05_Profile/B05_Profile_UI_Markers.ts", {
|
||||
const MI = loadTs("B05_Profile/B05_Profile_UI_Viewer_Marker_Input.ts", {
|
||||
"./B05_Profile_UI_Markers": MK3,
|
||||
});
|
||||
const SPK = loadTs("B05_Profile/B05_Profile_UI_Viewer_Structure_Pick.ts");
|
||||
// 고름 강조 한 규칙(323-5) — 진짜 `design_pick_match`(같은 자리 = 고름 모듈 거리)
|
||||
const PM = loadTs("A00_Common/design/design_pick_match.ts", { "./design_selection": DSel });
|
||||
const SPK = loadTs("B05_Profile/B05_Profile_UI_Viewer_Structure_Pick.ts", {
|
||||
"../A00_Common/design/design_pick_match": PM,
|
||||
"../A00_Common/design/design_selection": DSel,
|
||||
});
|
||||
const SY = loadTs("B05_Profile/B05_Profile_UI_Selection.ts", {
|
||||
"../A00_Common/design/design_selection": DSel,
|
||||
"./B05_Profile_UI_IrregularStations": {
|
||||
|
||||
@@ -39,6 +39,7 @@ const SY = loadTs("B05_Profile/B05_Profile_UI_Selection.ts", {
|
||||
});
|
||||
const PP = loadTs("A00_Common/design/design_pick_profile.ts", {
|
||||
"./design_selection": DSel,
|
||||
"./design_pipe_ids": { pipeIdAt: (at) => `pipe-${at.toFixed(2)}` }, // 프로젝트 없음 = 옛 자리 이름(323-5)
|
||||
"./design_pick_escape": ESC,
|
||||
"../../B05_Profile/B05_Profile_UI_Selection": SY,
|
||||
"../../B05_Profile/B05_Profile_UI_IrregularStations": IRR,
|
||||
|
||||
@@ -78,6 +78,7 @@ const SY = loadTs("B05_Profile/B05_Profile_UI_Selection.ts", {
|
||||
});
|
||||
const PP = loadTs("A00_Common/design/design_pick_profile.ts", {
|
||||
"./design_selection": DSel,
|
||||
"./design_pipe_ids": { pipeIdAt: (at) => `pipe-${at.toFixed(2)}` }, // 프로젝트 없음 = 옛 자리 이름(323-5)
|
||||
"./design_pick_escape": ESC,
|
||||
"../../B05_Profile/B05_Profile_UI_Selection": SY,
|
||||
"../../B05_Profile/B05_Profile_UI_IrregularStations": IRR,
|
||||
@@ -85,7 +86,12 @@ const PP = loadTs("A00_Common/design/design_pick_profile.ts", {
|
||||
const MK3 = loadTs("B05_Profile/B05_Profile_UI_Markers.ts", {
|
||||
"@util/common_util_svg": { inferStationInterval: () => 20, stationLabel: (c) => `0+${c}` },
|
||||
});
|
||||
const SPK = loadTs("B05_Profile/B05_Profile_UI_Viewer_Structure_Pick.ts");
|
||||
// 고름 강조 한 규칙(323-5) — 진짜 `design_pick_match`
|
||||
const PM = loadTs("A00_Common/design/design_pick_match.ts", { "./design_selection": DSel });
|
||||
const SPK = loadTs("B05_Profile/B05_Profile_UI_Viewer_Structure_Pick.ts", {
|
||||
"../A00_Common/design/design_pick_match": PM,
|
||||
"../A00_Common/design/design_selection": DSel,
|
||||
});
|
||||
const SYNC = loadTs("B06_Section/B06_Section_UI_Page_Pick_Sync.ts", {
|
||||
"../A00_Common/design/design_selection": DSel,
|
||||
"../A00_Common/design/design_pick_escape": ESC,
|
||||
|
||||
@@ -55,6 +55,7 @@ const SY = loadTs("B05_Profile/B05_Profile_UI_Selection.ts", {
|
||||
});
|
||||
const PP = loadTs("A00_Common/design/design_pick_profile.ts", {
|
||||
"./design_selection": DSel,
|
||||
"./design_pipe_ids": { pipeIdAt: (at) => `pipe-${at.toFixed(2)}` }, // 프로젝트 없음 = 옛 자리 이름(323-5)
|
||||
"./design_pick_escape": ESC,
|
||||
"../../B05_Profile/B05_Profile_UI_Selection": SY,
|
||||
"../../B05_Profile/B05_Profile_UI_IrregularStations": IRR,
|
||||
|
||||
@@ -42,6 +42,7 @@ const SY = loadTs("B05_Profile/B05_Profile_UI_Selection.ts", {
|
||||
});
|
||||
const PP = loadTs("A00_Common/design/design_pick_profile.ts", {
|
||||
"./design_selection": DSel,
|
||||
"./design_pipe_ids": { pipeIdAt: (at) => `pipe-${at.toFixed(2)}` }, // 프로젝트 없음 = 옛 자리 이름(323-5)
|
||||
"./design_pick_escape": ESC,
|
||||
"../../B05_Profile/B05_Profile_UI_Selection": SY,
|
||||
"../../B05_Profile/B05_Profile_UI_IrregularStations": IRR,
|
||||
|
||||
@@ -31,6 +31,14 @@ const ford = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pa
|
||||
{ "./B06_Section_UI_Cross_Culvert_Const": {} },
|
||||
),
|
||||
"./B06_Section_UI_Cross_Design_Surface": { appendPavementOverlay() {} },
|
||||
// 관 세션 id · 신원표(323-5) — 프로젝트 없음 = 옛 자리 이름
|
||||
"../A00_Common/design/design_pipe_ids": { pipeIdAt: (at) => `pipe-${at.toFixed(2)}` },
|
||||
"../A00_Common/design/design_pick_match": {
|
||||
tagShape: (el, id, part) => {
|
||||
if (id) el.setAttribute("data-structure-id", id);
|
||||
if (part) el.setAttribute("data-part", part);
|
||||
},
|
||||
},
|
||||
});
|
||||
// 286-3 도수로 · 돌붙임 길이 — 유출(좌 · 우가 높음) 성토사면 1:1.5(5 m 내려감 · 사면 길이 √(7.5²+5²) ≈ 9.01)
|
||||
const sets = loadTs(path.join(ROOT, "common_util", "common_util_culvert_sets.ts"));
|
||||
|
||||
@@ -183,13 +183,14 @@ async function run(setup, detail) {
|
||||
out.remove = await run({ savedStructures: [wall("retaining_wall")], draftStructures: [] }, kept);
|
||||
// 저장 뒤 다시 열기 — 초안 없음 = 서버 목록 그대로(빼 둔 옛 자리 돌아옴)
|
||||
out.reopen = await run({ savedStructures: [wall("retaining_wall")] }, kept);
|
||||
// 관을 옮겨도 고름 유지(322-3) — 고른 관이면 새 자리 · 다른 구조물 고름은 그대로
|
||||
// 관을 옮겨도 고름 유지(322-3) — 고른 관이면 새 자리 · id = 관 세션 id 그대로(323-5) · 다른 고름은 그대로
|
||||
const sel = load(file("A00_Common", "design", "design_selection.ts"));
|
||||
sel.selectDesign("P", { chainageM: 61, structureId: "pipe-61.00" });
|
||||
sel.followMovedPipe("P", 61, 75);
|
||||
const pipeIds = load(file("A00_Common", "design", "design_pipe_ids.ts"));
|
||||
sel.selectDesign("P", { chainageM: 61, structureId: pipeIds.pipeIdAt(61, "P") });
|
||||
pipeIds.followMovedPipe("P", 61, 75);
|
||||
const moved = sel.readSelection("P");
|
||||
sel.selectDesign("P", { chainageM: 101, structureId: "w1" });
|
||||
sel.followMovedPipe("P", 101, 120);
|
||||
pipeIds.followMovedPipe("P", 101, 120);
|
||||
out.follow = [moved, sel.readSelection("P")];
|
||||
// 옛 소수 관(검증_sub1 견본) — B06 을 바로 열어도 B05 와 같은 정수 m 초안(`snapPlan` · 요구 167)
|
||||
const snap = load(file("B05_Profile", "B05_Profile_UI_Structures_Snap.ts"));
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/* PLAN 323-5 고름 한 칸 헬퍼 — 진짜 `design_pipe_ids.ts`(관 세션 id) · `design_selection.ts`(고름 · 관 옮김 따라가기) ·
|
||||
* `design_pick_match.ts`(고름 강조 한 규칙)를 그 자리에서 트랜스파일해 Node 로 돌림(세션만 흉내) · 결과 = 표준출력 JSON.
|
||||
* 짝 = `test_323_5_pick.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
|
||||
const memory = new Map();
|
||||
const store = {
|
||||
getItem: (k) => (memory.has(k) ? memory.get(k) : null),
|
||||
setItem: (k, v) => memory.set(k, String(v)),
|
||||
removeItem: (k) => memory.delete(k),
|
||||
key: (i) => [...memory.keys()][i] ?? null,
|
||||
get length() {
|
||||
return memory.size;
|
||||
},
|
||||
};
|
||||
globalThis.window = { sessionStorage: store, localStorage: store, addEventListener: () => {} };
|
||||
globalThis.sessionStorage = store;
|
||||
globalThis.localStorage = store;
|
||||
|
||||
const cache = new Map();
|
||||
function load(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const need = (spec) => load(path.join(path.dirname(full), `${spec}.ts`));
|
||||
new Function("exports", "module", "require", js)(box.exports, box, need);
|
||||
return box.exports;
|
||||
}
|
||||
const D = (name) => path.join(ROOT, "A00_Common", "design", `${name}.ts`);
|
||||
const ids = load(D("design_pipe_ids"));
|
||||
const sel = load(D("design_selection"));
|
||||
const match = load(D("design_pick_match"));
|
||||
|
||||
const out = {};
|
||||
// ① 관 세션 id — 자리마다 차례 · 같은 자리(0.51 안)면 같은 id · 프로젝트 없으면 옛 자리 이름
|
||||
ids.usePipeIds("P");
|
||||
const a = ids.pipeIdAt(85.05);
|
||||
out.ids = [a, ids.pipeIdAt(85), ids.pipeIdAt(173), ids.pipeChainageOf(a)];
|
||||
ids.usePipeIds(null);
|
||||
out.legacy = [ids.pipeIdAt(85), ids.pipeChainageOf("pipe-85.00"), ids.pipeChainageOf("w1")];
|
||||
ids.usePipeIds("P");
|
||||
// ② 옮겨도 id 그대로 — 고름도 자리만 새로(부재키 그대로) · 새 자리 id = 그 id
|
||||
sel.selectDesign("P", { chainageM: 85.05, structureId: a }, { partKey: "inlet" });
|
||||
ids.followMovedPipe("P", 85.05, 90);
|
||||
out.moved = [sel.readSelection("P"), ids.pipeIdAt(90), ids.pipeChainageOf(a)];
|
||||
// 다른 것을 골랐으면 고름 그대로 · 관 id 는 따라감
|
||||
const b = ids.pipeIdAt(173);
|
||||
sel.selectDesign("P", { chainageM: 300, structureId: "w1" });
|
||||
ids.followMovedPipe("P", 173, 180);
|
||||
out.other = [sel.readSelection("P"), ids.pipeIdAt(180) === b];
|
||||
// ③ 강조 한 규칙
|
||||
const L = match.pickLights;
|
||||
out.lights = {
|
||||
idOverPlace: L({ chainageM: 93, structureId: "w1" }, { chainageM: 101, structureId: "w1" }),
|
||||
idDiffers: L({ chainageM: 101, structureId: "w2" }, { chainageM: 101, structureId: "w1" }),
|
||||
placeNear: L({ chainageM: 85, structureId: null }, { chainageM: 85.4 }),
|
||||
placeFar: L({ chainageM: 85, structureId: null }, { chainageM: 85.6 }),
|
||||
partOnly: L({ chainageM: 85, partKey: "inlet" }, { chainageM: 85, part: "outlet" }),
|
||||
partSame: L({ chainageM: 85, partKey: "inlet" }, { chainageM: 85, part: "inlet" }),
|
||||
partNoTag: L({ chainageM: 85, partKey: "inlet" }, { chainageM: 85 }),
|
||||
wholePick: L({ chainageM: 85 }, { chainageM: 85, part: "outlet" }),
|
||||
none: L(null, { chainageM: 85 }),
|
||||
};
|
||||
// ④ 카드 통째 형상 칠하기 — 부분 · 관 계열 · 포장층은 제 손잡이 몫
|
||||
class Shape {
|
||||
constructor(attrs) {
|
||||
this.attrs = attrs;
|
||||
this.on = false;
|
||||
this.classList = { toggle: (_c, v) => (this.on = v) };
|
||||
}
|
||||
getAttribute(k) {
|
||||
return this.attrs[k] ?? null;
|
||||
}
|
||||
}
|
||||
const shapes = [
|
||||
new Shape({ "data-structure-id": "ud-1" }),
|
||||
new Shape({ "data-structure-id": "w1", "data-part": "own" }),
|
||||
new Shape({ "data-structure-id": a, "data-part": "concrete" }),
|
||||
new Shape({ "data-structure-id": "pave-1", "data-pave-type": "pavement_concrete" }),
|
||||
];
|
||||
const root = { querySelectorAll: () => shapes };
|
||||
match.paintPicked(root, { structureId: "ud-1" });
|
||||
const lit1 = shapes.map((s) => s.on);
|
||||
match.paintPicked(root, { structureId: "w1" });
|
||||
out.paint = [lit1, shapes.map((s) => s.on)];
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -116,7 +116,9 @@ def test_배선():
|
||||
assert "openId" not in panel # 범위 안 빈 카드 = 연 노면공 지킴(165-1) 없앰 · 189-1
|
||||
# 관 알약은 폼 뒤(폼이 연 구조물을 닫으며 알약을 끔)
|
||||
at = panel.index("section.selectPipeByChainage(hit.pipeChainageM);")
|
||||
assert at < panel.index("deps.markStructure?.(`pipe-${hit.pipeChainageM.toFixed(2)}`);", at)
|
||||
assert at < panel.index(
|
||||
"deps.markStructure?.(pipeIdAt(hit.pipeChainageM, deps.projectId));", at
|
||||
)
|
||||
view = _read("B06_Section", "B06_Section_UI_Section_View.ts")
|
||||
assert "spanMarkId = span || isPavement(structureId) ? structureId : null;" in view
|
||||
assert "held" not in view # 범위 카드 재누름 = 그대로(167-3) 없앰 · 189-1
|
||||
|
||||
@@ -60,13 +60,20 @@ def test_물넘이포장_측점길은_콘크리트_안켬(out):
|
||||
view = _read("B06_Section", "B06_Section_UI_Section_View.ts")
|
||||
assert "highlightShapes(card, pickedMarkId);" in view
|
||||
shape = _read("B06_Section", "B06_Section_UI_Cross_Shape_Pick.ts")
|
||||
assert 'if (id.startsWith("pipe-")) continue;' in shape
|
||||
assert "paintPicked(root, { structureId })" in shape # 고름 강조 한 규칙(323-5)
|
||||
paint = _read("A00_Common", "design", "design_pick_match.ts")
|
||||
assert (
|
||||
'if (id.startsWith("pipe-") || shape.getAttribute("data-part") !== null) continue;' in paint
|
||||
)
|
||||
# 네 길이 모이는 자리 — 카드 고름 리스너 · 목록 관 줄 · 폼 관 열기 · 측점 없는 관
|
||||
cross = _read("A00_Common", "design", "design_view_cross.ts")
|
||||
assert "sectionView.selectMark(mark, false);" in cross
|
||||
panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts")
|
||||
assert "deps.markStructure?.(`pipe-${hit.pipeChainageM.toFixed(2)}`);" in panel # 카드 → 폼
|
||||
assert "deps.markStructure?.(chainageM === null ? null : `pipe-${" in panel # 목록 관 줄
|
||||
# 알약 id = 관 세션 id(323-5)
|
||||
assert (
|
||||
"deps.markStructure?.(pipeIdAt(hit.pipeChainageM, deps.projectId));" in panel
|
||||
) # 카드 → 폼
|
||||
assert "deps.markStructure?.(chainageM === null ? null : pipeIdAt(" in panel # 목록 관 줄
|
||||
# 측점 길은 「관」 벽 고름을 안 엶 — 카드만 고르면 벽 고름을 비움(그대로)
|
||||
assert "stationControls.revetOffset.select(target.chainage_m, null);" in cross
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ def test_cross_owner_switch_drops_pipe_drawing(flows: dict) -> None:
|
||||
|
||||
def test_pick_follows_moved_pipe(flows: dict) -> None:
|
||||
moved, other = flows["follow"]
|
||||
assert (moved["chainageM"], moved["structureId"]) == (75, "pipe-75.00")
|
||||
assert (moved["chainageM"], moved["structureId"]) == (75, "pipe-s1") # 세션 id 그대로(323-5)
|
||||
assert (other["chainageM"], other["structureId"]) == (101, "w1") # 구조물 고름은 그대로
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""PLAN 323-5 고름 한 칸 — 관 세션 id(옮겨도 그대로 · 저장 안 함) · 고름 강조 한 규칙(`pickLights`).
|
||||
|
||||
- 관 id = 자리마다 차례(`pipe-s1` …) · 같은 자리(PIPE_MATCH_M 안)면 같은 id · 프로젝트 없으면 옛 자리 이름.
|
||||
- 관을 옮기면 id 가 새 자리로 · 그 관을 고른 고름은 자리만 새로(부재키 그대로) · 다른 고름은 그대로.
|
||||
- 강조 = id 둘 다 있으면 id · 아니면 같은 자리 · 고른 부분이 있으면 그 부분만 — 카드 형상 · 3D 같은 함수.
|
||||
- 카드 통째 형상 칠하기는 부분(조정창 부재) · 관 계열 · 포장층을 안 건드림(307).
|
||||
TS 쪽 = `helper_323_5_pick.cjs`(Node).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parent.parent
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
run = subprocess.run(
|
||||
["node", str(HERE / "helper_323_5_pick.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
check=True,
|
||||
)
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def test_session_ids(out: dict) -> None:
|
||||
assert out["ids"] == ["pipe-s1", "pipe-s1", "pipe-s2", 85.05]
|
||||
assert out["legacy"] == ["pipe-85.00", 85, None]
|
||||
|
||||
|
||||
def test_move_keeps_id_and_pick(out: dict) -> None:
|
||||
picked, at90, moved_to = out["moved"]
|
||||
assert picked == {"chainageM": 90, "structureId": "pipe-s1", "partKey": "inlet"}
|
||||
assert (at90, moved_to) == ("pipe-s1", 90)
|
||||
other, same = out["other"]
|
||||
assert other["structureId"] == "w1" and other["chainageM"] == 300 and same is True
|
||||
|
||||
|
||||
def test_one_highlight_rule(out: dict) -> None:
|
||||
assert out["lights"] == {
|
||||
"idOverPlace": True,
|
||||
"idDiffers": False,
|
||||
"placeNear": True,
|
||||
"placeFar": False,
|
||||
"partOnly": False,
|
||||
"partSame": True,
|
||||
"partNoTag": False,
|
||||
"wholePick": True,
|
||||
"none": False,
|
||||
}
|
||||
|
||||
|
||||
def test_card_paint_whole_shapes_only(out: dict) -> None:
|
||||
assert out["paint"] == [[True, False, False, False], [False, False, False, False]]
|
||||
Reference in New Issue
Block a user