fix(B05): 종단 · 배수유역도 · 3D · B06 우클릭 구조물군 · 종류 목록 = 좌측 패널 드롭다운 목록 한 벌(129-1)
- 패널이 자기 드롭다운 목록(pickGroups — 군 차례 · 정의 이름 · 켜진 종류 · 옛 대피 뺌 · 숨김)을 내고 우클릭 메뉴가 그것을 그대로 씀 - 옛 메뉴 = 꺼진 종류(F 야생동물 이동통로 · 비탈면 녹화 · G 쇄석 포설) · 옛 대피 종류(대피소 · 정차작업장 · 차돌림곳) 더 보임 · 군 이름은 정의(48) 대신 고정 이름 - 시험 test_129(실제 레지스트리 + 정의로 패널 = 메뉴 · B05 · B06) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
This commit is contained in:
@@ -219,10 +219,9 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
},
|
||||
onBasinSelected: (chainageM) => callbacks?.onBasinSelected?.(chainageM),
|
||||
onPipeSelected: (chainageM) => callbacks?.onPipeSelected?.(chainageM),
|
||||
// 배수유역도 우클릭 빈 자리 메뉴 = 종단그래프와 같은 구조물군 → 종류 2단 목록
|
||||
// (2026-08-18 일원화). 선택하면 사이드 폼 자동 지정(addAt 경로).
|
||||
// 배수유역도 우클릭 빈 자리 = 사이드 패널과 같은 군 → 종류 목록(129-1 · addAt 경로).
|
||||
structureMenuItems: (chainage) =>
|
||||
structureGroupMenuItems(structureTypes, (typeId) =>
|
||||
structureGroupMenuItems(callbacks?.structureGroups?.() ?? [], (typeId) =>
|
||||
callbacks?.onStructureTypeAdd?.(chainage, typeId),
|
||||
),
|
||||
}) ?? null;
|
||||
@@ -608,7 +607,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
irregularStations = snapToMasterStations(alignment, stations);
|
||||
draw();
|
||||
},
|
||||
/** 구조물 타입 레지스트리를 받아 마크 색·약호·우클릭 메뉴에 쓴다(최초 1회). */
|
||||
/** 구조물 타입 레지스트리를 받아 마크 색·약호에 쓴다(최초 1회 · 우클릭 목록은 사이드 패널 몫). */
|
||||
setStructureTypes(types: StructureType[]) {
|
||||
structureTypes = types;
|
||||
syncDrainageSpans();
|
||||
|
||||
@@ -172,6 +172,7 @@ export function sharedProfile(shared: DesignPageShared): DesignProfile {
|
||||
onStructureSelect: toView(shared, "onStructureSelect"),
|
||||
onStructureMarkMove: toView(shared, "onStructureMarkMove"),
|
||||
onStructureTypeAdd: toView(shared, "onStructureTypeAdd"),
|
||||
structureGroups: () => sharedStructures(shared).pickGroups(),
|
||||
onBasinSelected: toView(shared, "onBasinSelected"),
|
||||
onPipeSelected: toView(shared, "onPipeSelected"),
|
||||
},
|
||||
|
||||
@@ -343,7 +343,7 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
|
||||
return {
|
||||
/** 3D 노선 우클릭 → 구조물 배치 메뉴(그래프·배수유역도와 동일, 2026-08-19).
|
||||
* 타입 목록은 레지스트리 로드 후 채워지는 structureTypeMap을 그대로 쓴다. */
|
||||
* 목록은 사이드 패널 군 · 종류 드롭다운 그대로(129-1). */
|
||||
mountViewerMenu(
|
||||
viewer: {
|
||||
root: HTMLElement;
|
||||
@@ -355,8 +355,7 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
root: viewer.root,
|
||||
modelPointAt: viewer.modelPointAt,
|
||||
routePoints,
|
||||
structureTypes: () =>
|
||||
[...structureTypeMap].map(([type_id, info]) => ({ type_id, ...info })),
|
||||
structureGroups: () => deps.panel().structures.pickGroups(),
|
||||
onPick: (chainage, typeId) => deps.panel().structures.addAt(chainage, typeId),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import type { GroupItems, SummaryItem } from "../common_util/common_util_summary_item";
|
||||
import type { PipeFacility, PipeSource } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import type { IrregularStation } from "./B05_Profile_UI_IrregularStations";
|
||||
import type { PickGroup } from "./B05_Profile_UI_Structures_Panel_Types";
|
||||
|
||||
/** 종단 테이블 구조물 라인·배수유역도가 Page로 올려 보내는 알림. */
|
||||
export interface RouteProfilePanelCallbacks {
|
||||
@@ -47,6 +48,8 @@ export interface RouteProfilePanelCallbacks {
|
||||
onStructureMarkMove?: (structureId: string, toChainageM: number) => void;
|
||||
/** 종단 그래프 우클릭으로 레지스트리 타입을 지정해 구조물을 넣음. */
|
||||
onStructureTypeAdd?: (chainageM: number, typeId: string) => void;
|
||||
/** 우클릭 군 · 종류 목록 = 사이드 「구조물 배치」 드롭다운 그대로(129-1 · B05 · B06 한 패널). */
|
||||
structureGroups?: () => PickGroup[];
|
||||
/** 배수유역도에서 유역을 고름 — 그 관의 누가거리(해제면 null). 그래프·사이드 패널을 맞춘다. */
|
||||
onBasinSelected?: (chainageM: number | null) => void;
|
||||
/** 배수유역도에서 관 마커를 고름(유역 없는 관 포함) — 전 화면 동기화용(2026-08-17). */
|
||||
|
||||
@@ -305,13 +305,8 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
onRemove: (station) => callbacks?.onStructureRemove?.(station),
|
||||
onAddPipe: (chainage) => callbacks?.onPipeAdd?.(chainage),
|
||||
onAddStructure: (chainage, type) => callbacks?.onStructureAdd?.(chainage, type),
|
||||
// 사이드 「구조물 배치」와 같은 전체 목록(A군 포함) — 선택 = 즉시 추가가 아니라
|
||||
// 폼 자동 지정이므로 필수 옵션·관리 주체로 거를 이유가 없어졌다(2026-08-18 일원화).
|
||||
structureTypes: structureTypes.map((type) => ({
|
||||
type_id: type.type_id,
|
||||
group: type.group,
|
||||
name: type.name,
|
||||
})),
|
||||
// 사이드 「구조물 배치」 군 · 종류 드롭다운 그대로(129-1 — 패널이 가진 목록).
|
||||
structureGroups: callbacks?.structureGroups,
|
||||
onAddStructureType: (chainage, typeId) => callbacks?.onStructureTypeAdd?.(chainage, typeId),
|
||||
});
|
||||
// 구조물 알약 레인 — 그래프 아래 별도 줄(2026-08-17 사용자 지시 1·3). 자동 배수관도
|
||||
|
||||
@@ -16,31 +16,23 @@ import {
|
||||
type IrregularStation,
|
||||
} from "./B05_Profile_UI_IrregularStations";
|
||||
import { structureAnchorM, type StructureInstance } from "./B05_Profile_Api_Structures";
|
||||
import { GROUP_LABELS } from "./B05_Profile_UI_Structures_Panel";
|
||||
import { isPickHidden } from "./B05_Profile_UI_Structures_Panel_Types";
|
||||
import type { PickGroup } from "./B05_Profile_UI_Structures_Panel_Types";
|
||||
|
||||
/** 우클릭 메뉴용 최소 타입 정보 — 사이드 「구조물 배치」 종류 목록과 같은 원천. */
|
||||
export interface StructureMenuType {
|
||||
type_id: string;
|
||||
group: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** 사이드 「구조물 배치」와 같은 구조물군 → 종류 2단 메뉴 항목을 만든다
|
||||
* (2026-08-18 우클릭 메뉴 일원화 — 종단그래프·배수유역도 공용). 항목 선택 =
|
||||
* 즉시 추가가 아니라 폼 자동 지정(addAt 경로). */
|
||||
/** 사이드 「구조물 배치」 군 · 종류 드롭다운 목록(`section.pickGroups()`)을 그대로 구조물군 → 종류
|
||||
* 2단 메뉴 항목으로(129-1 — 종단그래프 · 배수유역도 · 3D · B06 종단 공용 · 가린 군 · 종류는 뺌).
|
||||
* 항목 선택 = addAt 경로. */
|
||||
export function structureGroupMenuItems(
|
||||
types: ReadonlyArray<StructureMenuType>,
|
||||
groups: ReadonlyArray<PickGroup>,
|
||||
onPick: (typeId: string) => void,
|
||||
): MapContextMenuItem[] {
|
||||
const pickable = types.filter((type) => !isPickHidden(type));
|
||||
const groups = [...new Set(pickable.map((type) => type.group))];
|
||||
return groups.map((group) => ({
|
||||
label: GROUP_LABELS[group] ?? group,
|
||||
children: pickable
|
||||
.filter((type) => type.group === group)
|
||||
.map((type): [string, () => void] => [type.name, () => onPick(type.type_id)]),
|
||||
}));
|
||||
return groups
|
||||
.filter((group) => !group.hidden)
|
||||
.map((group) => ({
|
||||
label: group.label,
|
||||
children: group.types
|
||||
.filter((member) => !member.hidden)
|
||||
.map(({ type }): [string, () => void] => [type.name, () => onPick(type.type_id)]),
|
||||
}));
|
||||
}
|
||||
|
||||
/** 선을 잡았다고 볼 좌우 여유(px). 선 자체는 얇아 그대로는 집기 어렵다. */
|
||||
@@ -61,8 +53,8 @@ export interface StructureLineOptions {
|
||||
onAddPipe: (chainageM: number) => void;
|
||||
/** 빈 자리에서 우클릭해 배관 외 구조물(기성막이/대피로/기타)을 넣을 때. */
|
||||
onAddStructure?: (chainageM: number, structureType: "기성막이" | "대피로" | "기타") => void;
|
||||
/** 레지스트리 타입 목록(구조물군·이름). 우클릭 메뉴의 "구조물 추가" 항목이 된다. */
|
||||
structureTypes?: ReadonlyArray<{ type_id: string; group: string; name: string }>;
|
||||
/** 사이드 패널 군 · 종류 고르기 목록(129-1). 우클릭 메뉴의 "구조물 추가" 항목이 된다. */
|
||||
structureGroups?: () => ReadonlyArray<PickGroup>;
|
||||
/** 레지스트리 타입을 골라 넣을 때. */
|
||||
onAddStructureType?: (chainageM: number, typeId: string) => void;
|
||||
}
|
||||
@@ -97,9 +89,9 @@ export function mountStructureMenu(host: HTMLElement, options: StructureLineOpti
|
||||
// 빈 자리 — 사이드 「구조물 배치」와 같은 구조물군 → 종류 2단 메뉴(2026-08-18
|
||||
// 일원화). 선택 = 폼 자동 지정(addAt 경로, A군은 임시 배치까지). 레지스트리를
|
||||
// 아직 못 받았으면 구 항목(배관·기성막이 등)으로 대신한다.
|
||||
const types = options.structureTypes ?? [];
|
||||
const addItems: MapContextMenuItem[] = types.length
|
||||
? structureGroupMenuItems(types, (typeId) => options.onAddStructureType?.(at, typeId))
|
||||
const groups = options.structureGroups?.() ?? [];
|
||||
const addItems: MapContextMenuItem[] = groups.length
|
||||
? structureGroupMenuItems(groups, (typeId) => options.onAddStructureType?.(at, typeId))
|
||||
: [
|
||||
["배관 추가", () => options.onAddPipe(at)],
|
||||
...(["기성막이", "대피로", "기타"] as const).map((type): [string, () => void] => [
|
||||
|
||||
@@ -56,6 +56,7 @@ export type { PipeFacilityItem, StructuresSection } from "./B05_Profile_UI_Struc
|
||||
import {
|
||||
DEFAULT_INTERVAL_LENGTH_M,
|
||||
groupOptions,
|
||||
pickGroups,
|
||||
hasComputedRange,
|
||||
isRangeOnly,
|
||||
typeOptions,
|
||||
@@ -543,6 +544,7 @@ export function createStructuresSection(
|
||||
facility: { ...facilityOptions, recount: () => (facilityOptions.recount(), renderList()) }, // 단 수 → 목록 램프(54-15)
|
||||
listRoot: list,
|
||||
hasSelection: () => editingId !== null || selectedPipeChainage !== null,
|
||||
pickGroups: () => pickGroups(types, canPick), // 우클릭 메뉴 = 이 드롭다운 목록(129-1)
|
||||
setTypes(next, pick = () => true) {
|
||||
types = next;
|
||||
canPick = pick;
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
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";
|
||||
import { pickable } from "./B05_Profile_UI_Structures_Define";
|
||||
|
||||
/** 구조물군 표시 이름. 리스트 기호(A~G)만으로는 무엇인지 알기 어렵다.
|
||||
* 우클릭 메뉴(종단그래프·배수유역도)도 같은 이름을 쓴다(2026-08-18 일원화). */
|
||||
@@ -59,19 +60,41 @@ export function pickOption(text: string, value: string, hidden: boolean): HTMLOp
|
||||
return option;
|
||||
}
|
||||
|
||||
/** 군 드롭다운 항목들 — 이름은 정의(48) 먼저 · 뺄 군과 고를 종류가 없는 군은 가림. */
|
||||
/** 고르기 목록에서 가릴 종류인가 — 꺼진(pick) 종류 · 뺄 군 · 뺄 종류(드롭다운 · 메뉴 공용). */
|
||||
const pickHidden = (type: StructureType, pick: (type: StructureType) => boolean): boolean =>
|
||||
!pick(type) || isPickHidden(type);
|
||||
|
||||
/** 고르기 목록 한 군 — 가린 군 · 종류도 담음(옛 항목을 열 때 값으로 고름). */
|
||||
export interface PickGroup {
|
||||
group: string;
|
||||
label: string;
|
||||
hidden: boolean;
|
||||
types: { type: StructureType; hidden: boolean }[];
|
||||
}
|
||||
|
||||
/** 군 → 종류 고르기 목록 한 벌 — 패널 드롭다운과 우클릭 메뉴(종단 · 배수유역도 · 3D · B06)가
|
||||
* 같은 자료를 봄(129-1). 군 이름은 정의(48) 먼저 · 옛 대피 종류는 뺌 · 꺼진(pick) 종류 · 뺄 군 ·
|
||||
* 뺄 종류는 가림 · 보일 종류가 없는 군도 가림. */
|
||||
export function pickGroups(
|
||||
types: ReadonlyArray<StructureType>,
|
||||
pick: (type: StructureType) => boolean,
|
||||
): PickGroup[] {
|
||||
const groups = new Map(types.map((t) => [t.group, t.group_name] as const));
|
||||
return [...groups].map(([group, name]) => {
|
||||
const members = types
|
||||
.filter((t) => t.group === group && pickable(t))
|
||||
.map((type) => ({ type, hidden: pickHidden(type, pick) }));
|
||||
const hidden = members.every((member) => member.hidden);
|
||||
return { group, label: name ?? GROUP_LABELS[group] ?? group, hidden, types: members };
|
||||
});
|
||||
}
|
||||
|
||||
/** 군 드롭다운 항목들 — `pickGroups` 그대로. */
|
||||
export function groupOptions(
|
||||
types: StructureType[],
|
||||
pick: (type: StructureType) => boolean,
|
||||
): HTMLOptionElement[] {
|
||||
const groups = new Map(types.map((t) => [t.group, t.group_name] as const));
|
||||
return [...groups].map(([g, name]) =>
|
||||
pickOption(
|
||||
name ?? GROUP_LABELS[g] ?? g,
|
||||
g,
|
||||
!types.some((t) => t.group === g && pick(t) && !isPickHidden(t)),
|
||||
),
|
||||
);
|
||||
return pickGroups(types, pick).map((g) => pickOption(g.label, g.group, g.hidden));
|
||||
}
|
||||
|
||||
/** 종류 드롭다운 항목들 — 고를 수 없거나 뺄 종류는 가림. */
|
||||
@@ -79,7 +102,7 @@ export function typeOptions(
|
||||
types: StructureType[],
|
||||
pick: (type: StructureType) => boolean,
|
||||
): HTMLOptionElement[] {
|
||||
return types.map((t) => pickOption(t.name, t.type_id, !pick(t) || isPickHidden(t)));
|
||||
return types.map((t) => pickOption(t.name, t.type_id, pickHidden(t, pick)));
|
||||
}
|
||||
|
||||
/** 구간형 신규 추가 시 기본 구간 길이(m). 사용자가 종점을 바로 고칠 수 있게 짧게 잡는다. */
|
||||
@@ -134,6 +157,8 @@ export interface StructuresSection {
|
||||
/** 서버에서 받은 타입 목록을 채운다(최초 1회). 이름·폼은 넘긴 종류 전부로 풀고,
|
||||
* 고르기 목록엔 `pick` 이 고른 종류만 보임(빼면 전부). */
|
||||
setTypes: (types: StructureType[], pick?: (type: StructureType) => boolean) => void;
|
||||
/** 지금 군 · 종류 드롭다운 목록 그대로 — 우클릭 메뉴(종단 · 배수유역도 · 3D)가 같은 자료를 봄(129-1). */
|
||||
pickGroups: () => PickGroup[];
|
||||
/** 서버 정본으로 목록을 교체한다(진입·복원·저장 후). */
|
||||
setStructures: (structures: StructureInstance[]) => void;
|
||||
getStructures: () => StructureInstance[];
|
||||
|
||||
@@ -10,10 +10,8 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createMapContextMenu } from "@ui/ui_template_context_menu";
|
||||
import {
|
||||
structureGroupMenuItems,
|
||||
type StructureMenuType,
|
||||
} from "./B05_Profile_UI_Profile_Structures";
|
||||
import { structureGroupMenuItems } from "./B05_Profile_UI_Profile_Structures";
|
||||
import type { PickGroup } from "./B05_Profile_UI_Structures_Panel_Types";
|
||||
|
||||
/** 노선 위로 본다 판정 거리(m) — 이보다 멀면 노선 우클릭이 아니다. */
|
||||
const ROUTE_PICK_DISTANCE_M = 15;
|
||||
@@ -25,8 +23,8 @@ export interface ViewerMenuDeps {
|
||||
modelPointAt: (clientX: number, clientY: number) => { x: number; y: number } | null;
|
||||
/** 노선 폴리라인(모델 좌표, 시점→종점 순). 비어 있으면 메뉴를 띄우지 않는다. */
|
||||
routePoints: () => ReadonlyArray<{ x: number; y: number }>;
|
||||
/** 레지스트리 타입 목록 — 사이드 「구조물 배치」 종류와 같은 원천. */
|
||||
structureTypes: () => ReadonlyArray<StructureMenuType>;
|
||||
/** 사이드 「구조물 배치」 군 · 종류 고르기 목록 그대로(129-1). */
|
||||
structureGroups: () => ReadonlyArray<PickGroup>;
|
||||
/** 종류 선택 — 사이드 폼 자동 지정(addAt 경로, A군은 임시 배치까지). */
|
||||
onPick: (chainageM: number, typeId: string) => void;
|
||||
}
|
||||
@@ -70,8 +68,8 @@ export function mountViewerStructureMenu(deps: ViewerMenuDeps): void {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const types = deps.structureTypes();
|
||||
if (!types.length) return;
|
||||
const groups = deps.structureGroups();
|
||||
if (!groups.length) return;
|
||||
const model = deps.modelPointAt(event.clientX, event.clientY);
|
||||
if (!model) return;
|
||||
const hit = projectToRoute(deps.routePoints(), model.x, model.y);
|
||||
@@ -83,7 +81,7 @@ export function mountViewerStructureMenu(deps: ViewerMenuDeps): void {
|
||||
menu.open(
|
||||
event.clientX - rect.left,
|
||||
event.clientY - rect.top,
|
||||
structureGroupMenuItems(types, (typeId) => deps.onPick(chainage, typeId)),
|
||||
structureGroupMenuItems(groups, (typeId) => deps.onPick(chainage, typeId)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profil
|
||||
import { structureAnchorM } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import type { IrregularStation } from "../B05_Profile/B05_Profile_UI_IrregularStations";
|
||||
import { mountStructureMenu } from "../B05_Profile/B05_Profile_UI_Profile_Structures";
|
||||
import { pickGroups } from "../B05_Profile/B05_Profile_UI_Structures_Panel_Types";
|
||||
|
||||
/** 종단 그래프에서 구조물을 넣고 빼는 길 — 페이지가 물려 준다. 없으면 메뉴를 안 붙인다. */
|
||||
export interface SectionStructureEdit {
|
||||
@@ -122,11 +123,7 @@ export function mountSectionStructureMenu(
|
||||
else input.edit.removeStructure(station.id);
|
||||
},
|
||||
onAddPipe: (chainageM) => input.edit.addPipe(chainageM),
|
||||
structureTypes: input.types.map((type) => ({
|
||||
type_id: type.type_id,
|
||||
group: type.group,
|
||||
name: type.name,
|
||||
})),
|
||||
structureGroups: () => pickGroups(input.types, () => true), // B06 패널 거르기 = 전부(129-1)
|
||||
onAddStructureType: (chainageM, typeId) => input.edit.addStructureType(chainageM, typeId),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/* 129-1 우클릭 목록 = 좌측 패널 목록 헬퍼 — 종류 목록 JSON(argv[2] · 서버 응답) · 구조물 정의(argv[3])를
|
||||
* 화면처럼 `withDefinition` 으로 얹어 패널 군 · 종류 드롭다운(`groupOptions` · `typeOptions` — `syncTypeOptions` 와 같은 후보)과 우클릭 메뉴
|
||||
* (`structureGroupMenuItems(pickGroups)`)를 B05(켜진 종류) · B06(전부) 두 거르기로 뽑아 표준출력 JSON 한 벌로.
|
||||
* TS 로더는 `helper_116_right_click_add.cjs` 와 같다.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api", API_TIMEOUT_MS: 1000 } };
|
||||
// 드롭다운 항목 — 브라우저 `new Option(text, value)` 과 같은 칸만.
|
||||
globalThis.Option = class {
|
||||
constructor(text, value) {
|
||||
this.text = text;
|
||||
this.value = value;
|
||||
this.hidden = false;
|
||||
this.dataset = {};
|
||||
}
|
||||
};
|
||||
|
||||
function loadTs(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 localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
if (request.startsWith("@")) return new Proxy({}, { get: () => () => {} });
|
||||
if (request.endsWith(".css")) return {};
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const B05 = path.join(ROOT, "B05_Profile");
|
||||
const T = loadTs(path.join(B05, "B05_Profile_UI_Structures_Panel_Types.ts"));
|
||||
const M = loadTs(path.join(B05, "B05_Profile_UI_Profile_Structures.ts"));
|
||||
const D = loadTs(path.join(B05, "B05_Profile_UI_Structures_Define.ts"));
|
||||
|
||||
const read = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
const types = D.withDefinition(read(process.argv[2]), read(process.argv[3]));
|
||||
|
||||
/** 패널 드롭다운에 보이는 것 — 군(차례 · 이름) → 그 군 고르면 보이는 종류(차례 · 이름 · id). */
|
||||
function panelList(pick) {
|
||||
return T.groupOptions(types, pick)
|
||||
.filter((option) => !option.hidden)
|
||||
.map((group) => {
|
||||
const candidates = types.filter((t) => t.group === group.value && D.pickable(t));
|
||||
const kinds = T.typeOptions(candidates, pick).filter((option) => !option.hidden);
|
||||
return { label: group.text, types: kinds.map((option) => [option.text, option.value]) };
|
||||
});
|
||||
}
|
||||
|
||||
/** 우클릭 메뉴 — 군 라벨 → 종류 이름 · 누르면 넘어가는 id. */
|
||||
function menuList(pick) {
|
||||
const picked = [];
|
||||
const items = M.structureGroupMenuItems(T.pickGroups(types, pick), (id) => picked.push(id));
|
||||
return items.map((item) => ({
|
||||
label: item.label,
|
||||
types: item.children.map(([name, run]) => {
|
||||
run();
|
||||
return [name, picked.pop()];
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
const enabled = (t) => t.enabled;
|
||||
const all = () => true;
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
b05: { panel: panelList(enabled), menu: menuList(enabled) },
|
||||
b06: { panel: panelList(all), menu: menuList(all) },
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""129-1 종단 우클릭 구조물군 · 종류 목록 = 좌측 패널 「구조물 배치」 군 · 종류 드롭다운.
|
||||
|
||||
못박는 것
|
||||
- 실제 레지스트리(`with_visible`) + 구조물 정의(`구조물정의.json`)로 화면과 같은 종류 목록을 세워,
|
||||
패널 드롭다운에 보이는 군(차례 · 이름) · 종류(차례 · 이름 · id) = 우클릭 메뉴 — B05(켜진 종류) · B06(전부).
|
||||
- 메뉴 길 넷(B05 종단 · 배수유역도 · 3D · B06 종단 = 한 profile)이 모두 패널 `pickGroups()` 를 봄.
|
||||
|
||||
Node 헬퍼 `helper_129_menu_equals_panel.cjs` 가 패널 · 메뉴 목록을 뽑음. 화면은 사용자 확인.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Options import with_visible
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
||||
|
||||
|
||||
def _read(*parts: str) -> str:
|
||||
with open(os.path.join(ROOT, *parts), encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out(tmp_path_factory):
|
||||
folder = tmp_path_factory.mktemp("t129")
|
||||
types_path = folder / "types.json"
|
||||
definition_path = folder / "definition.json"
|
||||
types_path.write_text(json.dumps(with_visible().model_dump()["types"]), "utf-8")
|
||||
definition_path.write_text(json.dumps(definition.read()), "utf-8")
|
||||
proc = subprocess.run(
|
||||
[
|
||||
"node",
|
||||
os.path.join(HERE, "helper_129_menu_equals_panel.cjs"),
|
||||
str(types_path),
|
||||
str(definition_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("page", ["b05", "b06"])
|
||||
def test_우클릭_목록은_패널_드롭다운과_같음(out, page):
|
||||
panel, menu = out[page]["panel"], out[page]["menu"]
|
||||
assert panel, "패널 목록이 비면 대조가 안 됨"
|
||||
assert menu == panel # 군 차례 · 이름 · 종류 차례 · 이름 · 누르면 넘어가는 id
|
||||
|
||||
|
||||
def test_B05_는_꺼진_종류를_안_보임(out):
|
||||
shown = {type_id for group in out["b05"]["menu"] for _, type_id in group["types"]}
|
||||
off = {t["type_id"] for t in with_visible().model_dump()["types"] if not t["enabled"]}
|
||||
assert off and not shown & off
|
||||
|
||||
|
||||
def test_옛_대피_종류는_어느_쪽도_안_보임(out):
|
||||
for page in ("b05", "b06"):
|
||||
shown = {type_id for group in out[page]["menu"] for _, type_id in group["types"]}
|
||||
assert not shown & {"refuge", "work_yard", "turnaround"}
|
||||
|
||||
|
||||
def test_메뉴_길_넷이_패널_목록을_봄():
|
||||
assert "pickGroups: () => pickGroups(types, canPick)" in _read(
|
||||
"B05_Profile", "B05_Profile_UI_Structures_Panel.ts"
|
||||
)
|
||||
# B05 · B06 한 profile — 종단그래프 · 배수유역도 우클릭은 공용 콜백 하나
|
||||
assert "structureGroups: () => sharedStructures(shared).pickGroups()" in _read(
|
||||
"A00_Common", "design", "design_shared.ts"
|
||||
)
|
||||
assert "structureGroups: callbacks?.structureGroups," in _read(
|
||||
"B05_Profile", "B05_Profile_UI_Profile_Render.ts"
|
||||
)
|
||||
assert "structureGroupMenuItems(callbacks?.structureGroups?.() ?? []" in _read(
|
||||
"A00_Common", "design", "design_profile.ts"
|
||||
)
|
||||
# 3D
|
||||
assert "structureGroups: () => deps.panel().structures.pickGroups()" in _read(
|
||||
"B05_Profile", "B05_Profile_UI_Page_Structures.ts"
|
||||
)
|
||||
Reference in New Issue
Block a user