feat(profile): 종단 알약 글 — 기타 = 손 이름 머리 + 번호 · 빈 이름 = 기타 N(54-9)
- 기타 알약 = 손 이름 앞 3글자(빈칸 뺌 · 다른 손 이름과 머리가 같으면 갈릴 때까지 늘림) + 목록과 같은 번호 - 빈 이름 = 「기타 N」 · 다른 종류 알약 · 툴팁 · 목록 이름 그대로 - structurePillTexts 한 곳이 B05 · B06 알약을 함께 그림 · 시험 test_54_9_etc_pill Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPcuokNm1ZR2kqgGGDfaJA
This commit is contained in:
@@ -38,6 +38,8 @@ const ROW_GAP_PX = 2;
|
||||
const LANE_PADDING_PX = 2;
|
||||
/** 두 알약이 겹친다고 볼 가로 거리(px). 3자 라벨 폭 기준. */
|
||||
const OVERLAP_PX = 52;
|
||||
/** 기타 알약 머리 글자 수 — 약호(3자 이하)와 같은 폭(54-9). */
|
||||
const ETC_PILL_CHARS = 3;
|
||||
|
||||
/** 레인 전체 높이(px) — 2행 구조를 항상 확보한다(항목이 하나여도 높이는 같다).
|
||||
* 큰(혼자) 알약도 이 안에 들어간다. 아래 서브패널 손잡이와의 사이는 CSS 여백을
|
||||
@@ -115,6 +117,53 @@ function rowAssignments(
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 이름마다 남과 갈리는 가장 짧은 머리 — 빈칸 뺌 · 적어도 3글자 · 이름보다 길지 않음. */
|
||||
function distinctHeads(names: ReadonlyArray<string>): Map<string, string> {
|
||||
const letters = names.map((name) => [...name.replace(/\s+/g, "")]);
|
||||
return new Map(
|
||||
names.map((name, index) => {
|
||||
const own = letters[index];
|
||||
let length = ETC_PILL_CHARS;
|
||||
letters.forEach((other, otherIndex) => {
|
||||
if (otherIndex === index) return;
|
||||
let common = 0;
|
||||
while (common < own.length && own[common] === other[common]) common += 1;
|
||||
length = Math.max(length, common + 1);
|
||||
});
|
||||
return [name, own.slice(0, length).join("")];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** 표시 이름 · 알약 글(구조물 id 별). 표시 이름 = 좌측 목록과 같은 중복 번호(시점에 가까운 순
|
||||
* "이름 1/2…", 2026-08-19) — 툴팁 · 벌룬 제목이 씀. 알약 = 약호 + 그 번호. 기타는 약호가 모두
|
||||
* 「기타」라 손 이름 머리(distinctHeads) + 번호 · 빈 이름 = 「기타 N」(54-9). */
|
||||
export function structurePillTexts(
|
||||
structures: ReadonlyArray<StructureInstance>,
|
||||
typeMap: ReadonlyMap<string, StructureType>,
|
||||
): Map<string, { name: string; pill: string }> {
|
||||
const sorted = [...structures].sort(
|
||||
(left, right) => structureAnchorM(left) - structureAnchorM(right),
|
||||
);
|
||||
const rawNames = sorted.map(
|
||||
(entry) => etcName(entry) ?? typeMap.get(entry.type_id)?.name ?? entry.type_id,
|
||||
);
|
||||
const numbered = numberDuplicateNames(rawNames);
|
||||
const heads = distinctHeads([
|
||||
...new Set(rawNames.filter((_name, index) => sorted[index].type_id === "etc")),
|
||||
]);
|
||||
return new Map(
|
||||
sorted.map((entry, index) => {
|
||||
const name = numbered[index];
|
||||
const suffix = name.slice(rawNames[index].length); // " 1" 또는 ""
|
||||
const head = etcName(entry)
|
||||
? heads.get(rawNames[index])
|
||||
: typeMap.get(entry.type_id)?.style?.abbr;
|
||||
return [entry.structure_id ?? "", { name, pill: `${head ?? "?"}${suffix}` }];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 그래프 아래에 구조물 알약 레인을 만들어 돌려준다(호출부가 원하는 위치에 붙인다).
|
||||
*/
|
||||
@@ -127,28 +176,15 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement
|
||||
const typeMap = new Map(options.types.map((type) => [type.type_id, type]));
|
||||
const rows = rowAssignments(options.structures, options.x);
|
||||
|
||||
// 좌측 목록과 같은 중복 이름 번호(2026-08-19 사용자 지시) — 시점에 가까운 순
|
||||
// "이름 1/2…". 알약 라벨(약호)·툴팁·벌룬 제목이 모두 이 번호를 따른다.
|
||||
const anchorSorted = [...options.structures].sort(
|
||||
(left, right) => structureAnchorM(left) - structureAnchorM(right),
|
||||
);
|
||||
const numberedNames = numberDuplicateNames(
|
||||
anchorSorted.map(
|
||||
(entry) => etcName(entry) ?? typeMap.get(entry.type_id)?.name ?? entry.type_id,
|
||||
),
|
||||
);
|
||||
const displayNames = new Map(
|
||||
anchorSorted.map((entry, index) => [entry.structure_id ?? "", numberedNames[index]]),
|
||||
);
|
||||
// 좌측 목록과 같은 중복 이름 번호(2026-08-19 사용자 지시) — 알약 글·툴팁·벌룬 제목이
|
||||
// 모두 이 번호를 따른다.
|
||||
const texts = structurePillTexts(options.structures, typeMap);
|
||||
|
||||
options.structures.forEach((structure) => {
|
||||
const id = structure.structure_id;
|
||||
if (!id) return;
|
||||
const type = typeMap.get(structure.type_id);
|
||||
const rawName = etcName(structure) ?? type?.name ?? structure.type_id; // 기타 = 손 이름(목록과 같게)
|
||||
const displayName = displayNames.get(id) ?? rawName;
|
||||
// "옹벽 1"에서 붙은 번호만 떼어 약호 뒤에 잇는다(알약은 약호 표기 유지).
|
||||
const numberSuffix = displayName.startsWith(rawName) ? displayName.slice(rawName.length) : "";
|
||||
const { name: displayName, pill } = texts.get(id) ?? { name: structure.type_id, pill: "?" };
|
||||
const anchorPx = options.x(structureAnchorM(structure));
|
||||
const selected = options.selectedId === id;
|
||||
// 이웃과 겹치는 알약만 위·아래 두 줄로 가르고, 혼자면 레인 세로 가운데에 크게 둔다.
|
||||
@@ -185,8 +221,9 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement
|
||||
mark.style.top = `${top}px`;
|
||||
mark.style.borderColor = type?.style?.color ?? "#888";
|
||||
mark.style.color = type?.style?.color ?? "#888";
|
||||
// 라벨은 3자 이하 짧은 이름(레지스트리 style.abbr, 2026-08-17 지시 4) + 중복 번호.
|
||||
mark.textContent = `${type?.style?.abbr ?? "?"}${numberSuffix}`;
|
||||
// 라벨은 3자 이하 짧은 이름(레지스트리 style.abbr, 2026-08-17 지시 4) + 중복 번호 ·
|
||||
// 기타는 손 이름 머리(54-9).
|
||||
mark.textContent = pill;
|
||||
mark.title = `${displayName} ${positionText(structure, options.stationIntervalM)}`;
|
||||
mark.dataset.structureId = id;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user