feat(bearing): 방위각 입력 · 계산 비교와 계산값 적용 버튼
교각·교대 솔플레이트 표에 받침별 '방위각 입력 (CAD)' · '방위각 계산 (CAD)' · '차이 (″, 계산 − 입력)' · [적용] 열을 두고, 표 위에 '계산 방위각 모두 적용 / 모두 입력값으로' 버튼을 둔다 (사용자 요청 2026-11-06, Q-SH4 "입력값 유지"를 사용자가 고르게 바꿈). - 계산 방위각: 거더 짝이면 두 받침을 잇는 방향, 교대만의 현 짝이면 원곡선 접선(현 방향 ± 방위각 변화의 절반) - 적용값은 교각 input.azimuthApplied / 교대 record.azimuthApplied에 받침번호별로 저장, 엑셀 값은 그대로. 블럭 · 받침 · 솔플레이트 회전과 오르막 방향에 쓰고, 짝 찾기는 엑셀 입력값으로 - 차이 표시: 거더 짝 ≤ 10″ / WS01, 현 짝은 '현 · 참고'. 도·분·초 CAD 각(엑셀과 같은 표기) - 테스트: P1 전열 계산값 ≤ 0.5″ · 후열 없음, 적용한 받침만 블럭 회전 변화, 원곡선 현 접선 = 입력, 교대 toAbutInput 반영 - 브라우저: 운곡교AA.xlsx A1-광주 ① 적용 → 블럭 회전 4.39° → 4.36°, 모두 적용 5개 · 모두 해제, 예외 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c5f129712
commit
c7acfccd51
9 files changed
+161
-22
No files matched your search
@@ -51,6 +51,8 @@ export function resolveBearingRule(changes?: Partial<AbutBearingRule> & Record<s
|
|||||||
export interface AbutSoleContext {
|
export interface AbutSoleContext {
|
||||||
pairs?: BearingPair[];
|
pairs?: BearingPair[];
|
||||||
slopeI?: number;
|
slopeI?: number;
|
||||||
|
/** 엑셀 방위각 per bearing number (deg model 진북), when an applied 계산 방위각 is in the model. */
|
||||||
|
inputAzimuthDeg?: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Solplate of every bearing of the abutment (one row). */
|
/** Solplate of every bearing of the abutment (one row). */
|
||||||
@@ -58,7 +60,7 @@ export function abutSoles(model: AbutModel, rule: AbutBearingRule, heightMm: num
|
|||||||
const plates = Object.fromEntries(model.bearings.map((b) => [String(b.no), { aMm: rule.aMm, bMm: rule.bMm }]));
|
const plates = Object.fromEntries(model.bearings.map((b) => [String(b.no), { aMm: rule.aMm, bMm: rule.bMm }]));
|
||||||
return structureSoles(
|
return structureSoles(
|
||||||
model.name,
|
model.name,
|
||||||
model.bearings.map((b) => ({ no: b.no, row: '1열', azimuthDeg: b.azimuthDeg ?? 0 })),
|
model.bearings.map((b) => ({ no: b.no, row: '1열', azimuthDeg: b.azimuthDeg ?? 0, inputAzimuthDeg: ctx.inputAzimuthDeg?.[String(b.no)] })),
|
||||||
{
|
{
|
||||||
h3Mm: rule.h3Mm ?? heightMm - rule.tRefMm, tRefMm: rule.tRefMm, tMinMm: rule.tMinMm, marginMm: rule.marginMm, plates,
|
h3Mm: rule.h3Mm ?? heightMm - rule.tRefMm, tRefMm: rule.tRefMm, tMinMm: rule.tMinMm, marginMm: rule.marginMm, plates,
|
||||||
tByRow: rule.tMm === undefined ? undefined : { '1열': rule.tMm },
|
tByRow: rule.tMm === undefined ? undefined : { '1열': rule.tMm },
|
||||||
@@ -132,6 +134,9 @@ export interface AbutSolplateRow {
|
|||||||
/** 방위각 검사 (″): input azimuth against the pair direction; absent without a pair. */
|
/** 방위각 검사 (″): input azimuth against the pair direction; absent without a pair. */
|
||||||
azimuthSec?: number;
|
azimuthSec?: number;
|
||||||
pairKind?: 'girder' | 'chord';
|
pairKind?: 'girder' | 'chord';
|
||||||
|
/** 방위각 입력 (엑셀) and 계산 (짝 받침), deg model 진북; 계산 absent without a pair. */
|
||||||
|
azimuthInputDeg: number;
|
||||||
|
azimuthCalcDeg?: number;
|
||||||
L: number;
|
L: number;
|
||||||
W: number;
|
W: number;
|
||||||
T: number;
|
T: number;
|
||||||
@@ -146,7 +151,7 @@ export function abutSolplateRows(model: AbutModel, rule: AbutBearingRule, height
|
|||||||
return abutSoles(model, rule, heightMm, ctx).map((p) => {
|
return abutSoles(model, rule, heightMm, ctx).map((p) => {
|
||||||
const rhs = (p.L * Math.abs(p.slopePct)) / 100;
|
const rhs = (p.L * Math.abs(p.slopePct)) / 100;
|
||||||
return {
|
return {
|
||||||
no: p.no, S: p.slopePct, source: p.slopeSource, azimuthSec: p.azimuthCheckSec,
|
no: p.no, S: p.slopePct, source: p.slopeSource, azimuthSec: p.azimuthCheckSec, azimuthInputDeg: p.azimuthInputDeg, azimuthCalcDeg: p.azimuthCalcDeg,
|
||||||
pair: p.pair ? `${p.pair.structure}-${CIRCLED[p.pair.no - 1] ?? p.pair.no}${p.pairKind === 'chord' ? ' (현)' : ''}` : undefined, pairKind: p.pairKind,
|
pair: p.pair ? `${p.pair.structure}-${CIRCLED[p.pair.no - 1] ?? p.pair.no}${p.pairKind === 'chord' ? ' (현)' : ''}` : undefined, pairKind: p.pairKind,
|
||||||
L: p.L, W: p.W, T: p.T, T1: p.T1, T2: p.T2,
|
L: p.L, W: p.W, T: p.T, T1: p.T1, T2: p.T2,
|
||||||
check: `${p.T1 - p.T2} ≈ ${rhs.toFixed(1)} ${Math.abs(p.T1 - p.T2 - rhs) <= 1 + 1e-9 ? '일치' : '불일치 (W08)'}`,
|
check: `${p.T1 - p.T2} ≈ ${rhs.toFixed(1)} ${Math.abs(p.T1 - p.T2 - rhs) <= 1 + 1e-9 ? '일치' : '불일치 (W08)'}`,
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ export interface AbutRecord {
|
|||||||
shape?: Partial<AbutShape>;
|
shape?: Partial<AbutShape>;
|
||||||
/** 받침 규칙 (② 교량받침 카드: h3, 솔플레이트 T·여유, 받침 종류). Only the changed values. Absent = 교각 P1 가정값 (QA-15). */
|
/** 받침 규칙 (② 교량받침 카드: h3, 솔플레이트 T·여유, 받침 종류). Only the changed values. Absent = 교각 P1 가정값 (QA-15). */
|
||||||
bearingRule?: Partial<AbutBearingRule>;
|
bearingRule?: Partial<AbutBearingRule>;
|
||||||
|
/** 계산 방위각을 적용한 받침: 받침번호 → 방위각 (deg, model 진북). Absent = 엑셀 입력값 (2026-11-06). */
|
||||||
|
azimuthApplied?: Record<string, number>;
|
||||||
/** 날개벽 치수 (② 날개벽 카드, 측별, mm). Only the changed values. */
|
/** 날개벽 치수 (② 날개벽 카드, 측별, mm). Only the changed values. */
|
||||||
wingDims?: { left?: Partial<WingDims>; right?: Partial<WingDims> };
|
wingDims?: { left?: Partial<WingDims>; right?: Partial<WingDims> };
|
||||||
/** 접속슬래브 입력 (② 접속슬래브 카드: 형식, 경사, 치수 mm). Only the changed values. Absent = 매립형 · 하향 4 % 기본값. */
|
/** 접속슬래브 입력 (② 접속슬래브 카드: 형식, 경사, 치수 mm). Only the changed values. Absent = 매립형 · 하향 4 % 기본값. */
|
||||||
@@ -235,7 +237,8 @@ export function toAbutInput(rec: AbutRecord): AbutInput {
|
|||||||
ip1: rec.ip1,
|
ip1: rec.ip1,
|
||||||
ip2: rec.ip2,
|
ip2: rec.ip2,
|
||||||
ip3: rec.ip3,
|
ip3: rec.ip3,
|
||||||
bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl, azimuthDeg: b.azimuthDeg })),
|
// 계산 방위각을 적용한 받침 (2026-11-06) use that value; the record keeps the workbook value.
|
||||||
|
bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl, azimuthDeg: rec.azimuthApplied?.[String(b.no)] ?? b.azimuthDeg })),
|
||||||
skewDeg: rec.skewDeg,
|
skewDeg: rec.skewDeg,
|
||||||
slopeI: rec.slopeI,
|
slopeI: rec.slopeI,
|
||||||
wings: rec.wings,
|
wings: rec.wings,
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ export interface BearingPair {
|
|||||||
slopePct: number;
|
slopePct: number;
|
||||||
/** The structure on the higher end: the 오르막 쪽 where T1 sits for both bearings. */
|
/** The structure on the higher end: the 오르막 쪽 where T1 sits for both bearings. */
|
||||||
uphill: string;
|
uphill: string;
|
||||||
|
/** 계산 방위각 (deg, model 진북) of each end from the pair: the girder direction, or on a 현 the arc tangent. */
|
||||||
|
fromCalcDeg: number;
|
||||||
|
toCalcDeg: number;
|
||||||
/** girder = the two ends of one girder; chord = A1 ↔ A2 of a bridge loaded without piers (현, chordPairs). */
|
/** girder = the two ends of one girder; chord = A1 ↔ A2 of a bridge loaded without piers (현, chordPairs). */
|
||||||
kind: 'girder' | 'chord';
|
kind: 'girder' | 'chord';
|
||||||
warnings: PairWarning[];
|
warnings: PairWarning[];
|
||||||
@@ -52,6 +55,8 @@ export interface BearingPair {
|
|||||||
|
|
||||||
const DEG = Math.PI / 180;
|
const DEG = Math.PI / 180;
|
||||||
const angleDiffDeg = (a: number, b: number) => Math.abs(((a - b + 540) % 360) - 180);
|
const angleDiffDeg = (a: number, b: number) => Math.abs(((a - b + 540) % 360) - 180);
|
||||||
|
/** a − b wrapped to (−180, 180]. */
|
||||||
|
const signedDiff = (a: number, b: number) => ((a - b + 540) % 360) - 180;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Girder pairs over the bridge (Q-SH1, Q-SH4). For each bearing b the candidates on the other structures lie
|
* Girder pairs over the bridge (Q-SH1, Q-SH4). For each bearing b the candidates on the other structures lie
|
||||||
@@ -89,7 +94,7 @@ export function girderPairs(structures: PairStructure[]): BearingPair[] {
|
|||||||
pairs.push({
|
pairs.push({
|
||||||
from: { structure: here.name, no: b.no }, to: { structure: best.c.structure, no: best.c.no },
|
from: { structure: here.name, no: b.no }, to: { structure: best.c.structure, no: best.c.no },
|
||||||
distanceM, offsetM: best.offset, azimuthDiffDeg: best.dAz, directionDeg, fromCheckSec, toCheckSec,
|
distanceM, offsetM: best.offset, azimuthDiffDeg: best.dAz, directionDeg, fromCheckSec, toCheckSec,
|
||||||
slopePct, uphill: best.c.z > b.z ? best.c.structure : here.name, warnings, kind: 'girder',
|
slopePct, uphill: best.c.z > b.z ? best.c.structure : here.name, warnings, kind: 'girder', fromCalcDeg: directionDeg, toCalcDeg: directionDeg,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +124,9 @@ export function chordPairs(starts: PairStructure[], ends: PairStructure[]): Bear
|
|||||||
distanceM, offsetM: 0, azimuthDiffDeg: angleDiffDeg(best.c.azimuthDeg, b.azimuthDeg), directionDeg: best.directionDeg,
|
distanceM, offsetM: 0, azimuthDiffDeg: angleDiffDeg(best.c.azimuthDeg, b.azimuthDeg), directionDeg: best.directionDeg,
|
||||||
fromCheckSec: best.dev * 3600, toCheckSec: best.dev * 3600, slopePct: ((best.c.z - b.z) / distanceM) * 100,
|
fromCheckSec: best.dev * 3600, toCheckSec: best.dev * 3600, slopePct: ((best.c.z - b.z) / distanceM) * 100,
|
||||||
uphill: best.c.z > b.z ? best.structure : here.name, warnings: candidates.length > 1 ? ['WS04'] : [], kind: 'chord',
|
uphill: best.c.z > b.z ? best.structure : here.name, warnings: candidates.length > 1 ? ['WS04'] : [], kind: 'chord',
|
||||||
|
// Circular arc: the tangent at each end is the chord turned by half the azimuth change.
|
||||||
|
fromCalcDeg: (best.directionDeg + signedDiff(b.azimuthDeg, best.c.azimuthDeg) / 2 + 360) % 360,
|
||||||
|
toCalcDeg: (best.directionDeg + signedDiff(best.c.azimuthDeg, b.azimuthDeg) / 2 + 360) % 360,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return pairs;
|
return pairs;
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ export interface SolplateRow {
|
|||||||
/** 방위각 검사 (″): input azimuth against the pair direction; absent without a pair. */
|
/** 방위각 검사 (″): input azimuth against the pair direction; absent without a pair. */
|
||||||
azimuthSec?: number;
|
azimuthSec?: number;
|
||||||
pairKind?: 'girder' | 'chord';
|
pairKind?: 'girder' | 'chord';
|
||||||
|
/** 방위각 입력 (엑셀) and 계산 (짝 받침), deg model 진북; 계산 absent without a pair. */
|
||||||
|
azimuthInputDeg: number;
|
||||||
|
azimuthCalcDeg?: number;
|
||||||
/** Where S comes from: 계산 (짝) / 입력 (종단경사) / 가정 (다른 열) / 수정됨 / 없음. */
|
/** Where S comes from: 계산 (짝) / 입력 (종단경사) / 가정 (다른 열) / 수정됨 / 없음. */
|
||||||
source: SlopeSource;
|
source: SlopeSource;
|
||||||
/** T typed by the user (수정됨) rather than automatic. */
|
/** T typed by the user (수정됨) rather than automatic. */
|
||||||
@@ -133,7 +136,7 @@ export function solplateRows(m: PierModel): SolplateRow[] {
|
|||||||
const checkRhs = (p.L * Math.abs(p.slopePct)) / 100;
|
const checkRhs = (p.L * Math.abs(p.slopePct)) / 100;
|
||||||
return {
|
return {
|
||||||
pair: s.pair ? `${s.pair.structure}-${CIRCLED_NO[s.pair.no - 1] ?? s.pair.no}${s.pairKind === 'chord' ? ' (현)' : ''}` : undefined, pairKind: s.pairKind,
|
pair: s.pair ? `${s.pair.structure}-${CIRCLED_NO[s.pair.no - 1] ?? s.pair.no}${s.pairKind === 'chord' ? ' (현)' : ''}` : undefined, pairKind: s.pairKind,
|
||||||
azimuthSec: s.azimuthCheckSec, source: s.slopeSource, tTyped: s.tTyped, A: s.aMm, B: s.bMm, warnings: s.warnings,
|
azimuthSec: s.azimuthCheckSec, azimuthInputDeg: s.azimuthInputDeg, azimuthCalcDeg: s.azimuthCalcDeg, source: s.slopeSource, tTyped: s.tTyped, A: s.aMm, B: s.bMm, warnings: s.warnings,
|
||||||
no: b.no,
|
no: b.no,
|
||||||
S: p.slopePct,
|
S: p.slopePct,
|
||||||
L: p.L,
|
L: p.L,
|
||||||
|
|||||||
@@ -459,11 +459,13 @@ export function buildPier(input: PierInput, ctx: PierContext = {}): PierModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blocks: BlockModel[] = [];
|
const blocks: BlockModel[] = [];
|
||||||
const bearingAz = new Map(input.bearings.map((r) => [r.no, r.azimuthDeg]));
|
// 계산 방위각을 적용한 받침 (2026-11-06): that value turns the block, bearing and solplate; the workbook value stays in input.
|
||||||
|
const azimuthOf = (r: { no: number; azimuthDeg: number }) => input.azimuthApplied?.[String(r.no)] ?? r.azimuthDeg;
|
||||||
|
const bearingAz = new Map(input.bearings.map((r) => [r.no, azimuthOf(r)]));
|
||||||
const skewOf = (no: number, k: number) => rowSkewDeg(bearingAz.get(no) ?? NaN, axis.segments[k].u);
|
const skewOf = (no: number, k: number) => rowSkewDeg(bearingAz.get(no) ?? NaN, axis.segments[k].u);
|
||||||
// Solplate of every bearing (결정 2026-11-06): S from the girder pair (ctx.pairs over the whole bridge), else the
|
// Solplate of every bearing (결정 2026-11-06): S from the girder pair (ctx.pairs over the whole bridge), else the
|
||||||
// 종단경사 input, else the other row; T per row; the girder underside stays, so the block top drops by T − T_ref.
|
// 종단경사 input, else the other row; T per row; the girder underside stays, so the block top drops by T − T_ref.
|
||||||
const soles = new Map(structureSoles(input.name, input.bearings.map((b) => ({ no: b.no, row: b.row, azimuthDeg: b.azimuthDeg })),
|
const soles = new Map(structureSoles(input.name, input.bearings.map((b) => ({ no: b.no, row: b.row, azimuthDeg: azimuthOf(b), inputAzimuthDeg: b.azimuthDeg })),
|
||||||
input.sole, ctx.pairs ?? [], input.longitudinalSlope === undefined ? undefined : input.longitudinalSlope * 100).map((s) => [s.no, s]));
|
input.sole, ctx.pairs ?? [], input.longitudinalSlope === undefined ? undefined : input.longitudinalSlope * 100).map((s) => [s.no, s]));
|
||||||
const planOf = (no: number, k: number, skewDeg: number): SolplatePlan => {
|
const planOf = (no: number, k: number, skewDeg: number): SolplatePlan => {
|
||||||
const s = soles.get(no)!;
|
const s = soles.get(no)!;
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export interface BearingSole {
|
|||||||
pair?: { structure: string; no: number };
|
pair?: { structure: string; no: number };
|
||||||
/** 방위각 검사 (″): this bearing's input azimuth against the direction to its pair (Q-SH4, WS01 > 10″). Absent without a pair. */
|
/** 방위각 검사 (″): this bearing's input azimuth against the direction to its pair (Q-SH4, WS01 > 10″). Absent without a pair. */
|
||||||
azimuthCheckSec?: number;
|
azimuthCheckSec?: number;
|
||||||
|
/** 입력 방위각 and the one computed from the pair (deg, model 진북); the second is absent without a pair. */
|
||||||
|
azimuthInputDeg: number;
|
||||||
|
azimuthCalcDeg?: number;
|
||||||
/** girder (같은 거더) or chord (교대만, A1 ↔ A2 현). */
|
/** girder (같은 거더) or chord (교대만, A1 ↔ A2 현). */
|
||||||
pairKind?: 'girder' | 'chord';
|
pairKind?: 'girder' | 'chord';
|
||||||
slopePct: number;
|
slopePct: number;
|
||||||
@@ -65,7 +68,10 @@ export interface BearingSole {
|
|||||||
export interface SoleBearing {
|
export interface SoleBearing {
|
||||||
no: number;
|
no: number;
|
||||||
row: string;
|
row: string;
|
||||||
|
/** The azimuth in use (an applied 계산 방위각 when chosen) — for the 오르막 direction. */
|
||||||
azimuthDeg: number;
|
azimuthDeg: number;
|
||||||
|
/** The workbook azimuth, when it differs from the one in use. */
|
||||||
|
inputAzimuthDeg?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEG = Math.PI / 180;
|
const DEG = Math.PI / 180;
|
||||||
@@ -121,6 +127,8 @@ export function structureSoles(structure: string, bearings: SoleBearing[], setti
|
|||||||
return {
|
return {
|
||||||
no: b.no, row: b.row, pair: pair ? (pair.from.structure === structure ? pair.to : pair.from) : undefined,
|
no: b.no, row: b.row, pair: pair ? (pair.from.structure === structure ? pair.to : pair.from) : undefined,
|
||||||
pairKind: pair?.kind,
|
pairKind: pair?.kind,
|
||||||
|
azimuthInputDeg: b.inputAzimuthDeg ?? b.azimuthDeg,
|
||||||
|
azimuthCalcDeg: pair ? (pair.from.structure === structure && pair.from.no === b.no ? pair.fromCalcDeg : pair.toCalcDeg) : undefined,
|
||||||
azimuthCheckSec: pair ? (pair.from.structure === structure && pair.from.no === b.no ? pair.fromCheckSec : pair.toCheckSec) : undefined,
|
azimuthCheckSec: pair ? (pair.from.structure === structure && pair.from.no === b.no ? pair.fromCheckSec : pair.toCheckSec) : undefined,
|
||||||
slopePct, slopeSource: source, aMm, bMm, L, W, T, tTyped: typed, T1, T2, tReq: req[i], h3: s.h3Mm,
|
slopePct, slopeSource: source, aMm, bMm, L, W, T, tTyped: typed, T1, T2, tReq: req[i], h3: s.h3Mm,
|
||||||
dropMm: T - s.tRefMm, uphill: sign ? { e: sign * dir.e, n: sign * dir.n } : null, warnings,
|
dropMm: T - s.tRefMm, uphill: sign ? { e: sign * dir.e, n: sign * dir.n } : null, warnings,
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export interface PierInput {
|
|||||||
solplate?: SolplateRule;
|
solplate?: SolplateRule;
|
||||||
/** 솔플레이트 입력 (결정 2026-11-06, 시안): h3, T_ref, T_min, 여유, 받침별 A × B, 열 T·받침 S 직접 수정. Absent = 기본값. */
|
/** 솔플레이트 입력 (결정 2026-11-06, 시안): h3, T_ref, T_min, 여유, 받침별 A × B, 열 T·받침 S 직접 수정. Absent = 기본값. */
|
||||||
sole?: SoleSettings;
|
sole?: SoleSettings;
|
||||||
|
/** 계산 방위각을 적용한 받침: 받침번호 → 방위각 (deg, model 진북). Absent = 엑셀 입력값 (2026-11-06). */
|
||||||
|
azimuthApplied?: Record<string, number>;
|
||||||
/** 다주형 기둥 위치 입력 (Q-M10 미결): 끝 ~ 기둥1 중심 e_c, 기둥 간격 s_c (mm). Absent = L/5 규칙. */
|
/** 다주형 기둥 위치 입력 (Q-M10 미결): 끝 ~ 기둥1 중심 e_c, 기둥 간격 s_c (mm). Absent = L/5 규칙. */
|
||||||
columnEndOffsetMm?: number;
|
columnEndOffsetMm?: number;
|
||||||
columnSpacingMm?: number;
|
columnSpacingMm?: number;
|
||||||
|
|||||||
+80
-15
@@ -967,7 +967,7 @@ function fillAbutBearingCard(
|
|||||||
const m3 = (v: number) => v.toFixed(3);
|
const m3 = (v: number) => v.toFixed(3);
|
||||||
const H = rec.bearingHeightMm;
|
const H = rec.bearingHeightMm;
|
||||||
// 솔플레이트 S: 짝 받침(P1 전열 등) 계산, 없으면 교대 종단경사 i (2026-11-06).
|
// 솔플레이트 S: 짝 받침(P1 전열 등) 계산, 없으면 교대 종단경사 i (2026-11-06).
|
||||||
const ctx = { pairs: state.bridgePairs, slopeI: rec.slopeI };
|
const ctx = { pairs: state.bridgePairs, slopeI: rec.slopeI, inputAzimuthDeg: Object.fromEntries(rec.bearings.filter((b) => b.azimuthDeg !== undefined).map((b) => [String(b.no), b.azimuthDeg!])) };
|
||||||
const elev = abutElevationRows(model, rule, H, ctx);
|
const elev = abutElevationRows(model, rule, H, ctx);
|
||||||
const sol = abutSolplateRows(model, rule, H, ctx);
|
const sol = abutSolplateRows(model, rule, H, ctx);
|
||||||
// 상세 보기 (받침 ①–⑤): the three 삽도 draw the picked bearing.
|
// 상세 보기 (받침 ①–⑤): the three 삽도 draw the picked bearing.
|
||||||
@@ -1016,12 +1016,23 @@ function fillAbutBearingCard(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
box.append(el('p', '솔플레이트 상세 TABLE (mm) · S는 같은 거더 양끝 받침에서 계산', 'lbl'));
|
box.append(el('p', '솔플레이트 상세 TABLE (mm) · S는 같은 거더 양끝 받침에서 계산', 'lbl'));
|
||||||
box.append(
|
const setApplied = (next: Record<string, number> | undefined) => apply({ ...rec, azimuthApplied: next && Object.keys(next).length ? next : undefined });
|
||||||
table(
|
box.append(azimuthBulk(sol, rec.azimuthApplied, setApplied));
|
||||||
['No.', '짝 받침', '방위각 검사 (″)', 'S (%)', 'S 근거', 'L', 'W', 'T', 'T1 (오르막 쪽)', 'T2', '검산 T1 − T2 ≈ L·|S|/100', '검사'],
|
{
|
||||||
sol.map((r) => [String(r.no), r.pair ?? '없음', azimuthCheckText(r.azimuthSec, r.pairKind), r.S.toFixed(3), r.source, String(r.L), String(r.W), String(r.T), String(r.T1), String(r.T2), r.check, r.warnings.length ? r.warnings.join(' · ') : '—']),
|
const tbl = table(['No.', '짝 받침', ...AZIMUTH_HEADERS, 'S (%)', 'S 근거', 'L', 'W', 'T', 'T1 (오르막 쪽)', 'T2', '검산 T1 − T2 ≈ L·|S|/100', '검사'], []);
|
||||||
),
|
const body = tbl.tBodies[0];
|
||||||
);
|
for (const r of sol) {
|
||||||
|
const key = String(r.no);
|
||||||
|
const tr = body.insertRow();
|
||||||
|
tr.append(el('td', String(r.no)), el('td', r.pair ?? '없음'), ...azimuthCells(r.azimuthInputDeg, r.azimuthCalcDeg, r.pairKind, rec.azimuthApplied?.[key], (v) => {
|
||||||
|
const next = { ...(rec.azimuthApplied ?? {}) };
|
||||||
|
if (v === undefined) delete next[key]; else next[key] = v;
|
||||||
|
setApplied(next);
|
||||||
|
}));
|
||||||
|
for (const c of [r.S.toFixed(3), r.source, String(r.L), String(r.W), String(r.T), String(r.T1), String(r.T2), r.check, r.warnings.length ? r.warnings.join(' · ') : '—']) tr.append(el('td', c));
|
||||||
|
}
|
||||||
|
box.append(tbl);
|
||||||
|
}
|
||||||
// 솔플레이트 입력 (결정 2026-11-06): 교각 카드와 같은 항목, 받침이 1열이라 T 하나.
|
// 솔플레이트 입력 (결정 2026-11-06): 교각 카드와 같은 항목, 받침이 1열이라 T 하나.
|
||||||
box.append(el('p', `솔플레이트 입력 (이 교대) · T = max(T_min, ⌈T_min + L·|S|/2⌉) · 거더 하면 고정 → 블럭 상면 = z − (T − T_ref)`, 'lbl'));
|
box.append(el('p', `솔플레이트 입력 (이 교대) · T = max(T_min, ⌈T_min + L·|S|/2⌉) · 거더 하면 고정 → 블럭 상면 = z − (T − T_ref)`, 'lbl'));
|
||||||
const auto = sol[0];
|
const auto = sol[0];
|
||||||
@@ -5136,10 +5147,11 @@ function renderElevationTable(wrap: HTMLDivElement, pier: Pier): void {
|
|||||||
function renderSolplateTable(wrap: HTMLDivElement, pier: Pier): void {
|
function renderSolplateTable(wrap: HTMLDivElement, pier: Pier): void {
|
||||||
if (!pier.model) return;
|
if (!pier.model) return;
|
||||||
wrap.appendChild(el('p', '솔플레이트 TABLE (모델값 · mm) · S는 같은 거더 양끝 받침에서 계산', 'lbl'));
|
wrap.appendChild(el('p', '솔플레이트 TABLE (모델값 · mm) · S는 같은 거더 양끝 받침에서 계산', 'lbl'));
|
||||||
|
wrap.appendChild(azimuthBulk(solplateRows(pier.model), pier.draft.azimuthApplied, (next) => setAzimuthApplied(pier.input.name, next ?? {})));
|
||||||
const table = el('table', undefined, 'grid-table rebar-table');
|
const table = el('table', undefined, 'grid-table rebar-table');
|
||||||
const head = el('thead');
|
const head = el('thead');
|
||||||
const hr = el('tr');
|
const hr = el('tr');
|
||||||
for (const h of ['No', '짝 받침', '방위각 검사 (″)', 'S (%)', 'S 근거', 'A', 'B', 'L', 'W', 'T', 'T1 (오르막 쪽)', 'T2', '검산 T1 − T2 ≈ L·|S|/100', '검사']) hr.appendChild(el('th', h));
|
for (const h of ['No', '짝 받침', ...AZIMUTH_HEADERS, 'S (%)', 'S 근거', 'A', 'B', 'L', 'W', 'T', 'T1 (오르막 쪽)', 'T2', '검산 T1 − T2 ≈ L·|S|/100', '검사']) hr.appendChild(el('th', h));
|
||||||
head.appendChild(hr);
|
head.appendChild(hr);
|
||||||
table.appendChild(head);
|
table.appendChild(head);
|
||||||
const body = el('tbody');
|
const body = el('tbody');
|
||||||
@@ -5155,7 +5167,11 @@ function renderSolplateTable(wrap: HTMLDivElement, pier: Pier): void {
|
|||||||
for (const r of solplateRows(pier.model)) {
|
for (const r of solplateRows(pier.model)) {
|
||||||
const key = String(r.no);
|
const key = String(r.no);
|
||||||
const tr = el('tr');
|
const tr = el('tr');
|
||||||
tr.append(el('td', String(r.no)), el('td', r.pair ?? '없음'), el('td', azimuthCheckText(r.azimuthSec, r.pairKind)));
|
tr.append(el('td', String(r.no)), el('td', r.pair ?? '없음'), ...azimuthCells(r.azimuthInputDeg, r.azimuthCalcDeg, r.pairKind, pier.draft.azimuthApplied?.[key], (v) => {
|
||||||
|
const next = { ...(pier.draft.azimuthApplied ?? {}) };
|
||||||
|
if (v === undefined) delete next[key]; else next[key] = v;
|
||||||
|
setAzimuthApplied(pier.input.name, next);
|
||||||
|
}));
|
||||||
tr.appendChild(cellInput(Math.round(r.S * 1000) / 1000, r.source === '수정됨', `받침 ${r.no} S (%)`, (v) => {
|
tr.appendChild(cellInput(Math.round(r.S * 1000) / 1000, r.source === '수정됨', `받침 ${r.no} S (%)`, (v) => {
|
||||||
const next = { ...(settings.slopePct ?? {}) };
|
const next = { ...(settings.slopePct ?? {}) };
|
||||||
if (v === undefined) delete next[key]; else next[key] = v;
|
if (v === undefined) delete next[key]; else next[key] = v;
|
||||||
@@ -5180,12 +5196,61 @@ function renderSolplateTable(wrap: HTMLDivElement, pier: Pier): void {
|
|||||||
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table);
|
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 방위각 검사 칸 (Q-SH4): the difference to the pair direction in arc seconds, ≤ 10″ 통과 / > 10″ WS01; '—' without a pair. */
|
/** CAD 각 (엑셀과 같은 동 = 0° 반시계) as d°mm′ss.s″, from a model 진북 azimuth (deg). */
|
||||||
function azimuthCheckText(sec: number | undefined, kind?: 'girder' | 'chord'): string {
|
function cadDms(az: number): string {
|
||||||
if (sec === undefined) return '— (짝 없음)';
|
const cad = (((90 - az) % 360) + 360) % 360;
|
||||||
// 교대만 A1 ↔ A2 현: chord against the mean azimuth, for reference (the 10″ rule is for one girder).
|
let d = Math.floor(cad), m = Math.floor((cad - d) * 60), s = Math.round(((cad - d) * 60 - m) * 600) / 10;
|
||||||
if (kind === 'chord') return `현 ${sec.toFixed(0)}″ (참고)`;
|
if (s >= 60) { s -= 60; m += 1; }
|
||||||
return `${sec.toFixed(1)}″ ${sec > AZIMUTH_CHECK_SEC ? 'WS01' : '≤ 10″'}`;
|
if (m >= 60) { m -= 60; d += 1; }
|
||||||
|
return `${d}°${String(m).padStart(2, '0')}′${s.toFixed(1).padStart(4, '0')}″`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AZIMUTH_HEADERS = ['방위각 입력 (CAD)', '방위각 계산 (CAD)', '차이 (″, 계산 − 입력)', '적용'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 방위각 입력 · 계산 · 차이 · 적용 칸 (2026-11-06). 계산 = 짝 받침 방향 (현 짝이면 원곡선 접선). The button applies the 계산
|
||||||
|
* 방위각 to the block / bearing / solplate rotation or goes back to the workbook value.
|
||||||
|
*/
|
||||||
|
function azimuthCells(input: number, calc: number | undefined, kind: 'girder' | 'chord' | undefined, applied: number | undefined, onSet: (value: number | undefined) => void): HTMLTableCellElement[] {
|
||||||
|
const cells = [el('td', cadDms(input))];
|
||||||
|
if (calc === undefined) return [...cells, el('td', '— (짝 없음)'), el('td', '—'), el('td', '—')];
|
||||||
|
// CAD runs the other way round from the 진북 azimuth, so the CAD difference is −(calc − input).
|
||||||
|
const sec = -(((calc - input + 540) % 360) - 180) * 3600;
|
||||||
|
const status = kind === 'chord' ? '현 · 참고' : Math.abs(sec) > AZIMUTH_CHECK_SEC ? 'WS01' : '≤ 10″';
|
||||||
|
cells.push(el('td', `${cadDms(calc)}${kind === 'chord' ? ' (현)' : ''}`), el('td', `${sec >= 0 ? '+' : ''}${sec.toFixed(1)}″ ${status}`));
|
||||||
|
const on = applied !== undefined;
|
||||||
|
const button = el('button', on ? '적용됨 · 입력값으로' : '계산값 적용', 'btn small');
|
||||||
|
button.type = 'button';
|
||||||
|
button.setAttribute('aria-pressed', String(on));
|
||||||
|
button.title = on ? `계산 방위각 ${cadDms(applied)} 사용 중 · 누르면 엑셀 입력값으로` : '블럭 · 받침 · 솔플레이트 회전에 계산 방위각을 씁니다';
|
||||||
|
button.addEventListener('click', () => onSet(on ? undefined : calc));
|
||||||
|
const td = el('td'); td.appendChild(button);
|
||||||
|
return [...cells, td];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** '모두 적용 / 모두 입력값으로' buttons above a solplate table. */
|
||||||
|
function azimuthBulk(rows: { no: number; azimuthCalcDeg?: number }[], applied: Record<string, number> | undefined, onSet: (next: Record<string, number> | undefined) => void): HTMLDivElement {
|
||||||
|
const box = el('div', undefined, 'btn-row');
|
||||||
|
const all = el('button', '계산 방위각 모두 적용', 'btn small');
|
||||||
|
all.type = 'button';
|
||||||
|
all.disabled = !rows.some((r) => r.azimuthCalcDeg !== undefined);
|
||||||
|
all.addEventListener('click', () => onSet(Object.fromEntries(rows.filter((r) => r.azimuthCalcDeg !== undefined).map((r) => [String(r.no), r.azimuthCalcDeg!]))));
|
||||||
|
const none = el('button', '모두 입력값으로', 'btn small');
|
||||||
|
none.type = 'button';
|
||||||
|
none.disabled = !applied || Object.keys(applied).length === 0;
|
||||||
|
none.addEventListener('click', () => onSet(undefined));
|
||||||
|
box.append(all, none, el('span', `적용 ${applied ? Object.keys(applied).length : 0}개 · 짝 찾기는 엑셀 입력값으로 합니다`, 'muted small'));
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the bearings that use the 계산 방위각 (2026-11-06) and rebuilds that pier. */
|
||||||
|
function setAzimuthApplied(name: string, applied: Record<string, number>): void {
|
||||||
|
const p = state.piers.find((q) => q.input.name === name);
|
||||||
|
if (!p) return;
|
||||||
|
p.draft = { ...p.draft, azimuthApplied: Object.keys(applied).length ? applied : undefined };
|
||||||
|
state.piers = state.piers.map((q) => (q === p ? regenerateEntry(q) : q));
|
||||||
|
setupScene();
|
||||||
|
renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets one pier's solplate settings (2026-11-06) and rebuilds that pier. */
|
/** Sets one pier's solplate settings (2026-11-06) and rebuilds that pier. */
|
||||||
|
|||||||
@@ -168,3 +168,46 @@ describe('교대만 (교각 0기): A1 ↔ A2 현 짝 (검토 2026-11-06)', () =>
|
|||||||
expect(bridgePairs([p1], [a1, abuts[3]]).some((p) => p.kind === 'chord')).toBe(false);
|
expect(bridgePairs([p1], [a1, abuts[3]]).some((p) => p.kind === 'chord')).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('방위각 입력 · 계산 · 적용 (2026-11-06)', () => {
|
||||||
|
it('P1: the 계산 방위각 is the A1 girder direction (≤ 0.5″ from the input); the back row has none', async () => {
|
||||||
|
const { bridgePairs } = await import('../src/core/bridge-pairs');
|
||||||
|
const { buildPier } = await import('../src/core/geometry');
|
||||||
|
const m = buildPier(p1, { pairs: bridgePairs([p1], [a1]) });
|
||||||
|
for (const s of m.soles.filter((x) => x.row === '전열')) expect(Math.abs(s.azimuthCalcDeg! - s.azimuthInputDeg) * 3600).toBeLessThanOrEqual(0.5);
|
||||||
|
expect(m.soles.filter((x) => x.row === '후열').every((x) => x.azimuthCalcDeg === undefined)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applying a value turns that block and keeps the workbook value as the input', async () => {
|
||||||
|
const { bridgePairs } = await import('../src/core/bridge-pairs');
|
||||||
|
const { buildPier } = await import('../src/core/geometry');
|
||||||
|
const pairs = bridgePairs([p1], [a1]);
|
||||||
|
const base = buildPier(p1, { pairs });
|
||||||
|
const turned = buildPier({ ...p1, azimuthApplied: { 3: p1.bearings[2].azimuthDeg + 1 } }, { pairs });
|
||||||
|
const skew = (m: typeof base, no: number) => m.blocks.find((b) => b.no === no)!.rowSkewDeg;
|
||||||
|
expect(skew(turned, 3) - skew(base, 3)).toBeCloseTo(1, 6);
|
||||||
|
expect(skew(turned, 2)).toBe(skew(base, 2));
|
||||||
|
expect(turned.soles.find((s) => s.no === 3)!.azimuthInputDeg).toBe(p1.bearings[2].azimuthDeg);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('현 짝 (교대만): the 계산 방위각 is the arc tangent — equal to the input on a circular arc', async () => {
|
||||||
|
const { chordPairs } = await import('../src/core/bearing-pair');
|
||||||
|
const a2: PairStructure = { name: 'A2', bearings: A1.bearings.map((b) => {
|
||||||
|
const t = ((b.azimuthDeg - 4) * Math.PI) / 180;
|
||||||
|
return { ...b, x: b.x + 350 * Math.sin(t), y: b.y + 350 * Math.cos(t), azimuthDeg: b.azimuthDeg - 8, z: b.z - 6.65 };
|
||||||
|
}) };
|
||||||
|
const pairs = chordPairs([A1], [a2]);
|
||||||
|
expect(pairs).toHaveLength(5);
|
||||||
|
pairs.forEach((p, i) => {
|
||||||
|
expect(Math.abs(p.fromCalcDeg - A1.bearings[i].azimuthDeg) * 3600).toBeLessThan(0.5);
|
||||||
|
expect(Math.abs(p.toCalcDeg - a2.bearings[i].azimuthDeg) * 3600).toBeLessThan(0.5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('교대: an applied 계산 방위각 reaches the abutment model through toAbutInput', async () => {
|
||||||
|
const { toAbutInput } = await import('../src/core/abut/parse');
|
||||||
|
const input = toAbutInput({ ...a1, azimuthApplied: { 1: 10 } });
|
||||||
|
expect(input.bearings.find((b) => b.no === 1)!.azimuthDeg).toBe(10);
|
||||||
|
expect(input.bearings.find((b) => b.no === 2)!.azimuthDeg).toBe(a1.bearings[1].azimuthDeg);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user