feat(abut-rebar): ③ 교대 부재 목록 단순화 · 철근표 선택 / 전체 선택 / 선택만 보기

- 부재 목록: 교각처럼 이름 + 경고 수 배지만. 배근 타입 · 번호 · 다음 단계는 툴팁과 카드 설명 줄로
- 철근표: 선택 칸, 행 클릭 단독 선택, 전체 선택/해제, 선택만 보기 (교각과 같은 rebarSelection · rebarIsolate)
- 삽도: 여러 철근 강조, 선택만 보기면 나머지 숨김. B10 풍선은 B10-1 · B10-2를 함께 선택

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-04 15:56:57 +09:00
1 parent f4311d7f95
commit d7714f7ef2
3 files changed
+63 -41

No files matched your search

+15 -10
View File
@@ -1,6 +1,6 @@
/**
* 교대 저판 배근 삽도 (2026-11-04, 시안 ③ 확대기초 카드, 교대확대기초철근.md §10): 교대축 직각 단면 (폭 W), 교대축 단면 (길이 L),
* 상면 평면. 순수 문자열 함수. 번호 풍선(data-pick)을 누르면 그 철근만 강조한다; 강조할 번호는 `pick`으로 받는다.
* 상면 평면. 순수 문자열 함수. 번호 풍선(data-pick)을 누르면 그 철근만 강조한다; 선택한 번호는 `pick`(철근표 번호 집합)으로, 선택만 보기는 `only`로 받는다.
* 색은 역할 기준: 주철근 B1–B4 #0F5C66, 보조 B5–B8 #B4531F, 측면 B10 #2E7D32 점선, 전단 B9 #6B3FA0.
*/
import type { AbutFootingGeom, AbutFootingRebar } from '../core/abut/rebar/footing';
@@ -14,21 +14,26 @@ const WALL = '#d9dee4';
const text = (x: number, y: number, s: string, anchor = 'middle', fill = INK, size = 11) =>
`<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" text-anchor="${anchor}" font-size="${size}" fill="${fill}" font-family="JetBrains Mono, monospace">${s}</text>`;
/** Opacity of one bar group: dimmed when another bar is picked. B10 picks cover B10-1 and B10-2. */
function opacity(pick: string | null, marks: string[]): number {
if (!pick) return 1;
return marks.some((m) => m === pick || (pick === 'B10' && m.startsWith('B10'))) ? 1 : 0.15;
/** Selected bars (철근표 번호) and 선택만 보기. An empty selection shows every bar. */
export type AbutRebarPick = { marks: ReadonlySet<string>; only: boolean };
const NO_PICK: AbutRebarPick = { marks: new Set(), only: false };
/** Opacity of one bar group: dimmed (or hidden with 선택만 보기) when other bars are selected. */
function opacity(pick: AbutRebarPick, marks: string[]): number {
if (pick.marks.size === 0) return 1;
return marks.some((m) => pick.marks.has(m)) ? 1 : pick.only ? 0 : 0.15;
}
/** A clickable 번호 풍선. */
function balloon(x: number, y: number, mark: string, color: string, pick: string | null): string {
const on = pick === mark;
function balloon(x: number, y: number, mark: string, color: string, pick: AbutRebarPick): string {
// The B10 풍선 stands for B10-1 and B10-2.
const on = [...pick.marks].some((m) => m === mark || (mark === 'B10' && m.startsWith('B10')));
const r = mark.length > 3 ? 15 : 12;
return `<g data-pick="${mark}" style="cursor:pointer" role="button" tabindex="0" aria-label="${mark} 강조"><circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${r}" fill="${on ? color : '#fff'}" stroke="${color}" stroke-width="1.4"/>${text(x, y + 4, mark, 'middle', on ? '#fff' : color, 10)}</g>`;
}
/** 교대축 직각 단면 (폭 W): 왼쪽 배면(뒷굽) → 오른쪽 전면(앞굽). B1–B4 선, B5–B8 점, B10-1 점, B9 연직선. */
export function abutFootingSectionWSvg(g: AbutFootingGeom, r: AbutFootingRebar, pick: string | null = null): string {
export function abutFootingSectionWSvg(g: AbutFootingGeom, r: AbutFootingRebar, pick: AbutRebarPick = NO_PICK): string {
const k = 0.12;
const x0 = 60;
const y0 = 46;
@@ -82,7 +87,7 @@ export function abutFootingSectionWSvg(g: AbutFootingGeom, r: AbutFootingRebar,
}
/** 교대축 단면 (길이 L, 가로 축척 0.4배): B5–B8 선(이음 위치), B1–B4 점, B10-2 점. 왼쪽 IP3 쪽. */
export function abutFootingSectionLSvg(g: AbutFootingGeom, r: AbutFootingRebar, pick: string | null = null): string {
export function abutFootingSectionLSvg(g: AbutFootingGeom, r: AbutFootingRebar, pick: AbutRebarPick = NO_PICK): string {
const kx = 0.045;
const kz = 0.12;
const x0 = 50;
@@ -132,7 +137,7 @@ export function abutFootingSectionLSvg(g: AbutFootingGeom, r: AbutFootingRebar,
* 상면 평면: skew 평행사변형, 상면 B1·B3 (옵션에 따라 skew 선과 나란히 / 교대축 직각, 옵션 ② B1-S), B5·B7, B9, 벽체 밑 띠.
* `skewK` = 날개벽 방향의 s / t 비 (장면의 abutSkewK): s(x) = s + skewK · x. 위 = 전면, 왼쪽 = IP3 끝 (s = L).
*/
export function abutFootingRebarPlanSvg(g: AbutFootingGeom, r: AbutFootingRebar, skewK: number, pick: string | null = null): string {
export function abutFootingRebarPlanSvg(g: AbutFootingGeom, r: AbutFootingRebar, skewK: number, pick: AbutRebarPick = NO_PICK): string {
const k = 0.045;
const shiftMax = Math.abs(skewK) * g.W;
const sMin = Math.min(0, skewK * g.W);