- `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
134 lines
6.5 KiB
TypeScript
134 lines
6.5 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Cross_Shape_Pick.ts
|
|
* 물넘이포장 · 맹암거 형상 고름(287-5 · 288-2 · 요구 185).
|
|
*
|
|
* 그린 형상(`data-structure-id` 단 포장층 · 돌 무늬 둘레)마다 투명 판정면 — 누르면 그 구조물을 고름
|
|
* (다른 시설 형상 고름과 같은 창구 `onPick`). 지킬 것:
|
|
* ① 보라 손잡이(설계선 판정 · 뒤에 붙음)가 위 — 판정면은 구조물 판정면 바로 앞(아래)에 꽂고
|
|
* 구조물 판정 클래스(`b06-chart__culvert-revet-hit`)를 안 씀(보라 판정이 그 앞에 꽂혀 형상 위로 옴).
|
|
* ② 누름만 고름 — 눌렀다 3 px 넘게 움직이면(끌기) 고름 아님.
|
|
* ③ 노면공 포장 고름과 안 섞임 — 노면공 범위 표식(`data-pave-type`) 없는 형상만 · 누름은 번지지 않음.
|
|
* ④ 얇은 형상(물넘이 콘크리트 · 기본 배율 4 px)은 판정면을 아래쪽으로 `SHAPE_MIN_PX` 두께까지 늘림 —
|
|
* 노면선 보라 판정 띠(굵기 10 · 위아래 5) 바깥 아래를 누르면 형상(294-7).
|
|
* ========================================================================== */
|
|
|
|
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). */
|
|
export const SHAPE_PICK_EVENT = "b06-structure-shape-pick";
|
|
/** 끌기로 볼 움직임(px) — 이보다 크면 고름 아님. */
|
|
export const SHAPE_DRAG_PX = 3;
|
|
/** 형상 판정면 최소 두께(그림 px) — 보라 판정 띠 아래 반(5)보다 넉넉히(294-7). */
|
|
export const SHAPE_MIN_PX = 12;
|
|
|
|
type XY = [number, number];
|
|
const parse = (points: string): XY[] =>
|
|
points
|
|
.trim()
|
|
.split(/\s+/)
|
|
.map((pair) => pair.split(",").map(Number) as XY)
|
|
.filter((p) => p.length === 2 && p.every(Number.isFinite));
|
|
const insideOf = (ring: XY[], [x, y]: XY): boolean =>
|
|
ring.reduce((inn, [ax, ay], i) => {
|
|
const [bx, by] = ring[(i + 1) % ring.length];
|
|
return ay > y !== by > y && x < ax + ((y - ay) * (bx - ax)) / (by - ay) ? !inn : inn;
|
|
}, false);
|
|
|
|
/** 얇은 형상 → 판정면 꼭짓점 — 바로 아래가 형상 밖인 꼭짓점(아랫변)만 내려 두께를 `minPx` 로.
|
|
* 두께 = 넓이 ÷ 가로 폭(기운 노면도 세로 두께로 잼). */
|
|
export function thickenDown(points: string, minPx = SHAPE_MIN_PX): string {
|
|
const ring = parse(points);
|
|
if (ring.length < 3) return points;
|
|
const xs = ring.map(([x]) => x);
|
|
const area = Math.abs(
|
|
ring.reduce((sum, [x0, y0], i) => {
|
|
const [x1, y1] = ring[(i + 1) % ring.length];
|
|
return sum + x0 * y1 - x1 * y0;
|
|
}, 0) / 2,
|
|
);
|
|
const drop = minPx - area / Math.max(Math.max(...xs) - Math.min(...xs), 1e-9);
|
|
if (drop <= 0) return points;
|
|
const midX = ring.reduce((sum, [x]) => sum + x, 0) / ring.length;
|
|
const e = 1e-3;
|
|
return ring
|
|
.map(([x, y]): XY => {
|
|
const probe: XY = [x + (midX > x ? e : midX < x ? -e : 0), y + e];
|
|
return insideOf(ring, probe) ? [x, y] : [x, y + drop];
|
|
})
|
|
.map((p) => p.join(","))
|
|
.join(" ");
|
|
}
|
|
|
|
/** 판정면을 꽂고 강조 setter 를 돌려줌 — 형상이 없으면 null. */
|
|
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") &&
|
|
[null, "concrete"].includes(el.getAttribute("data-part")),
|
|
);
|
|
if (!shapes.length) return null;
|
|
const kids = [...layer.children];
|
|
const firstStructureHit = kids.find((el) =>
|
|
el.classList.contains("b06-chart__culvert-revet-hit"),
|
|
);
|
|
for (const shape of onPick ? shapes : []) {
|
|
const id = shape.getAttribute("data-structure-id") ?? "";
|
|
const hit = document.createElementNS(SVG_NS, "polygon");
|
|
hit.setAttribute("points", thickenDown(shape.getAttribute("points") ?? ""));
|
|
hit.setAttribute("class", "b06-chart__shape-hit");
|
|
let down: { x: number; y: number } | null = null;
|
|
hit.addEventListener("pointerdown", (event) => {
|
|
down = { x: event.clientX, y: event.clientY };
|
|
});
|
|
hit.addEventListener("click", (event) => {
|
|
const moved =
|
|
down && Math.hypot(event.clientX - down.x, event.clientY - down.y) > SHAPE_DRAG_PX;
|
|
down = null;
|
|
if (moved) return; // 끌기 = 고름 아님(②)
|
|
event.stopPropagation();
|
|
onPick?.(id);
|
|
});
|
|
layer.insertBefore(hit, firstStructureHit ?? null);
|
|
}
|
|
return (active) => shapes.forEach((shape) => shape.classList.toggle("is-active", active));
|
|
}
|
|
|
|
/** 형상 누름 창구(287-5 · 288-2) — 관 지점(`pipe-…` · 물넘이포장) = 그 물넘이 누가거리로 고름(카드의 다른 관 ·
|
|
* 연동 관 아님 · 294-7) · 그 밖(맹암거) = id 를 실은 알림을 위로 띄움(구조물 목록이 받음). */
|
|
export function shapePickRoute(
|
|
from: Element,
|
|
pickPipe: (chainageM: number) => void,
|
|
): (structureId: string) => void {
|
|
return (structureId) => {
|
|
const pipeAt = pipeChainageOf(structureId); // 관 세션 id 장부(323-5)
|
|
if (pipeAt !== null) return pickPipe(pipeAt);
|
|
from.dispatchEvent(new CustomEvent(SHAPE_PICK_EVENT, { bubbles: true, detail: structureId }));
|
|
};
|
|
}
|
|
|
|
/** 카드 「관」 강조를 관 그림 · 물넘이 콘크리트로 가름(307) — 콘크리트를 고름(`concrete`) = 콘크리트만 ·
|
|
* 관을 고름 = 관만(관 카드에 걸친 물넘이포장 콘크리트를 눌러도 관 형상은 안 켬). */
|
|
export function splitPipePick(
|
|
base: RevetHighlightSetter,
|
|
setShape: (active: boolean) => void,
|
|
concrete: () => boolean,
|
|
): RevetHighlightSetter {
|
|
return (key) => {
|
|
const shape = key === "pipe" && concrete();
|
|
base(shape ? null : key);
|
|
setShape(shape);
|
|
};
|
|
}
|
|
|
|
/** 카드 안 형상 강조 — 고름 강조 한 규칙(`paintPicked` · 323-5) · 물넘이 콘크리트(관 부분)는 형상을 직접
|
|
* 누른 「관」 고름만 켬 — 측점 길(측점선 · 알약 · 목록)은 안 켬(307). */
|
|
export const highlightShapes = (root: ParentNode, structureId: string | null): void =>
|
|
paintPicked(root, { structureId });
|