fix(abut): 교대 받침 방위각을 읽어 받침블럭 회전에 반영
- 원인: 교대 파서가 받침 시트의 방위각 열(6번째)을 읽지 않아, 블럭이 항상 교대축에 맞춰 그려졌음 - 방위각을 교각과 같은 변환(swapXYAzimuth)으로 읽고, 교대축 직각 기준 블럭 회전각(rotDeg)을 계산해 블럭·몰탈패드를 블럭 중심에서 회전 - 운곡교 A1: 회전 약 4.42° (교각 규칙과 같은 값). 날개벽(skew) 선과의 차이는 0.63°로 시안 AW01 값과 같음 - ② 받침 ELEVATION TABLE에 방위각 CAD / 진북, 블럭 회전 열 추가. 읽을 수 없는 방위각은 AP08 경고 후 축 방향 유지 - 테스트 5건 추가, 전체 647건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7e3ba306f3
commit
4e3950eb67
7 files changed
+106
-6
No files matched your search
@@ -55,6 +55,11 @@ export interface AbutElevationRow {
|
||||
B: number;
|
||||
/** 블럭 끝에서 가장 가까운 계단 경계까지 (m). 경계가 없으면 null. */
|
||||
stepGapM: number | null;
|
||||
/** 받침 방위각 (deg): CAD (엑셀 값, 동 = 0° 반시계) / 진북 (북에서 시계). 입력이 없으면 null. */
|
||||
azimuthCadDeg: number | null;
|
||||
azimuthNorthDeg: number | null;
|
||||
/** 블럭 평면 회전 (deg), 교대축 직각 기준. */
|
||||
rotDeg: number;
|
||||
}
|
||||
|
||||
export function abutElevationRows(model: AbutModel, rule: AbutBearingRule): AbutElevationRow[] {
|
||||
@@ -78,6 +83,9 @@ export function abutElevationRows(model: AbutModel, rule: AbutBearingRule): Abut
|
||||
A: ABUT_BEARING_A_MM,
|
||||
B: ABUT_BEARING_B_MM,
|
||||
stepGapM: gaps.length ? Math.min(...gaps) : null,
|
||||
azimuthCadDeg: b.azimuthDeg === undefined ? null : (((90 - b.azimuthDeg) % 360) + 360) % 360,
|
||||
azimuthNorthDeg: b.azimuthDeg === undefined ? null : ((b.azimuthDeg % 360) + 360) % 360,
|
||||
rotDeg: b.rotDeg,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface AbutBearing {
|
||||
z: number;
|
||||
/** 이 받침의 교좌면 EL (m). 없으면 z에서 교대 블럭 높이를 뺀 값을 쓴다. */
|
||||
seatEl?: number;
|
||||
/** 받침 방위각 (deg, model-xy: 북에서 시계방향, 교각과 같은 변환 swapXYAzimuth). 없으면 블럭은 교대축에 맞춘다. */
|
||||
azimuthDeg?: number;
|
||||
}
|
||||
|
||||
/** 날개벽 형식, 측별(spec_abut §4.7): 일반형 / 단축형 / 없음. */
|
||||
@@ -79,6 +81,10 @@ export interface AbutBearingModel {
|
||||
seatEl: number;
|
||||
/** 교좌면에서 패드 상면까지 블럭 높이 (mm), 패드 포함. */
|
||||
blockHeightMm: number;
|
||||
/** 받침 방위각 (deg, model-xy). 입력이 없으면 undefined. */
|
||||
azimuthDeg?: number;
|
||||
/** 블럭 평면 회전 (deg, −90 … 90): 받침 방위각과 교대축 직각(전면) 방향 사이의 각. 장면에서는 rotateY(이 값). 입력이 없으면 0. */
|
||||
rotDeg: number;
|
||||
}
|
||||
|
||||
export interface AbutModel {
|
||||
@@ -112,6 +118,20 @@ const AW01_TOLERANCE_DEG = 1;
|
||||
export const ABUT_PAD_MM = 50;
|
||||
|
||||
const sub = (a: AbutPoint, b: AbutPoint): AbutPoint => ({ e: a.e - b.e, n: a.n - b.n });
|
||||
|
||||
/**
|
||||
* 블럭 평면 회전 (deg): the bearing azimuth direction d = (sin az, cos az) seen in the axis frame (s along u, t along the front),
|
||||
* folded to −90 … 90 because the block is symmetric. 0 when the azimuth is missing.
|
||||
*/
|
||||
export function bearingRotDeg(azimuthDeg: number | undefined, u: AbutPoint, front: AbutPoint): number {
|
||||
if (azimuthDeg === undefined || !Number.isFinite(azimuthDeg)) return 0;
|
||||
const r = (azimuthDeg * Math.PI) / 180;
|
||||
const d = { e: Math.sin(r), n: Math.cos(r) };
|
||||
let phi = (Math.atan2(d.e * u.e + d.n * u.n, d.e * front.e + d.n * front.n) * 180) / Math.PI;
|
||||
if (phi > 90) phi -= 180;
|
||||
if (phi < -90) phi += 180;
|
||||
return phi;
|
||||
}
|
||||
const dot = (a: AbutPoint, b: AbutPoint) => a.e * b.e + a.n * b.n;
|
||||
const cross = (a: AbutPoint, b: AbutPoint) => a.e * b.n - a.n * b.e;
|
||||
|
||||
@@ -140,7 +160,7 @@ export function buildAbut(input: AbutInput): AbutModel {
|
||||
const s = dot(sub(b.pos, input.ip1), u);
|
||||
const seatEl = b.seatEl ?? input.seatEl + (b.z - minZ);
|
||||
const blockHeightMm = Math.round((b.z - seatEl) * 1000);
|
||||
return { no: b.no, s, lateral: lateral[i], z: b.z, seatEl, blockHeightMm };
|
||||
return { no: b.no, s, lateral: lateral[i], z: b.z, seatEl, blockHeightMm, azimuthDeg: b.azimuthDeg, rotDeg: bearingRotDeg(b.azimuthDeg, u, frontVec) };
|
||||
});
|
||||
|
||||
const sorted = [...bearings].sort((a, b) => a.s - b.s);
|
||||
|
||||
+11
-1
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
import * as XLSX from 'xlsx';
|
||||
import { parseDms } from '../dms';
|
||||
import { swapXYAzimuth } from '../coordinate-frame';
|
||||
import type { Issue } from '../types';
|
||||
import type { AbutInput, AbutPoint, AbutType, WingForm } from './geometry';
|
||||
import type { AbutShape } from './shape';
|
||||
@@ -24,6 +25,8 @@ export interface AbutRecordBearing {
|
||||
seatEl?: number;
|
||||
/** 이 받침에 적힌 받침높이 (mm). */
|
||||
bearingHeightMm: number;
|
||||
/** 받침 방위각 (deg, model-xy, 교각과 같은 swapXYAzimuth 변환). 비었거나 읽을 수 없으면 없음. */
|
||||
azimuthDeg?: number;
|
||||
row: string;
|
||||
}
|
||||
|
||||
@@ -162,12 +165,19 @@ export function parseAbutments(wb: XLSX.WorkBook): AbutParseResult {
|
||||
continue;
|
||||
}
|
||||
const h = num(c[8]);
|
||||
if (!isBlank(c[5]) && parseDms(String(c[5])) === null) {
|
||||
issues.push({ code: 'AP08', level: 'W', pier: ab.name, sheet: rec.sheet, message: `${rec.sheet} ${rec.row}행: 받침 방위각을 읽을 수 없습니다. 예: 75°56'55.9" 형식. 블럭은 교대축에 맞춥니다.` });
|
||||
}
|
||||
ab.bearings.push({
|
||||
no: num(c[1]),
|
||||
pos: { e: num(c[3]), n: num(c[2]) },
|
||||
z: num(c[4]),
|
||||
seatEl: isBlank(c[7]) ? undefined : num(c[7]),
|
||||
bearingHeightMm: h,
|
||||
azimuthDeg: (() => {
|
||||
const deg = isBlank(c[5]) ? null : parseDms(String(c[5]));
|
||||
return deg === null ? undefined : swapXYAzimuth(deg);
|
||||
})(),
|
||||
row: `${rec.sheet} ${rec.row}행`,
|
||||
});
|
||||
}
|
||||
@@ -193,7 +203,7 @@ export function toAbutInput(rec: AbutRecord): AbutInput {
|
||||
ip1: rec.ip1,
|
||||
ip2: rec.ip2,
|
||||
ip3: rec.ip3,
|
||||
bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })),
|
||||
bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl, azimuthDeg: b.azimuthDeg })),
|
||||
skewDeg: rec.skewDeg,
|
||||
slopeI: rec.slopeI,
|
||||
wings: rec.wings,
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ export function checkStoredAbutments(list: unknown): void {
|
||||
if (!forms.includes(a.wings.left) || !forms.includes(a.wings.right)) throw new Error(`${who}: 날개벽 형식이 맞지 않습니다.`);
|
||||
}
|
||||
for (const b of a.bearings) {
|
||||
if (!b || !finite(b.no) || !isPoint(b.pos) || !finite(b.z) || (b.seatEl !== undefined && !finite(b.seatEl))) {
|
||||
if (!b || !finite(b.no) || !isPoint(b.pos) || !finite(b.z) || (b.seatEl !== undefined && !finite(b.seatEl)) || (b.azimuthDeg !== undefined && !finite(b.azimuthDeg))) {
|
||||
throw new Error(`${who}: 받침 ${String(b?.no)}의 값이 맞지 않습니다.`);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -793,10 +793,12 @@ function fillAbutBearingCard(
|
||||
box.append(el('p', '받침 ELEVATION TABLE (mm, EL·위치는 m) · 거리는 IP3(① 쪽)에서 · h3·h4는 가정 (QA-15)', 'lbl'));
|
||||
box.append(
|
||||
table(
|
||||
['No.', '위치 s', '구간', '교좌면 EL', 'h1 블럭', 'h2 패드', 'z = EL.X', 'h3 받침', 'h4 솔플', 'E', '솔플 상면 EL', 'A', 'B', '블럭 ~ 계단 경계'],
|
||||
['No.', '위치 s', '구간', '교좌면 EL', 'h1 블럭', 'h2 패드', 'z = EL.X', 'h3 받침', 'h4 솔플', 'E', '솔플 상면 EL', 'A', 'B', '블럭 ~ 계단 경계', '방위각 CAD / 진북 (°)', '블럭 회전 (°)'],
|
||||
abutElevationRows(model, rule).map((r) => [
|
||||
String(r.no), m3(r.sFromIp3M), r.seg, m3(r.seatEl), String(r.h1), String(r.h2), m3(r.z), String(r.h3), String(r.h4),
|
||||
String(r.E), m3(r.solplateTopEl), String(r.A), String(r.B), r.stepGapM === null ? '—' : m3(r.stepGapM),
|
||||
r.azimuthCadDeg === null || r.azimuthNorthDeg === null ? '입력 없음' : `${r.azimuthCadDeg.toFixed(3)} / ${r.azimuthNorthDeg.toFixed(3)}`,
|
||||
r.rotDeg.toFixed(2),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -224,11 +224,14 @@ export function buildAbutGroup(
|
||||
|
||||
// 받침블럭 (spec §4.2, §6.6): concrete 1200 × 1200 from the 교좌면 to 50 below z, and the 몰탈패드 1000 × 1000 × 50 up to z.
|
||||
// The plan follows the 교대축 (받침 방위각 = 축 방향). t is the 전면 +, so the bearing's lateral offset is taken with the front side.
|
||||
// The block turns by the bearing azimuth (rotDeg, from the 방위각 input) about its own centre.
|
||||
for (const b of model.bearings) {
|
||||
const t = model.frontSide * b.lateral;
|
||||
const blockTop = b.z - 0.05;
|
||||
add('blocks', prism([[b.s - 0.6, b.seatEl], [b.s + 0.6, b.seatEl], [b.s + 0.6, blockTop], [b.s - 0.6, blockTop]], 1.2, new THREE.Vector3(0, 0, t - 0.6), ex, ey, ez), 0xa9b4bf);
|
||||
add('blocks', prism([[b.s - 0.5, blockTop], [b.s + 0.5, blockTop], [b.s + 0.5, b.z], [b.s - 0.5, b.z]], 1.0, new THREE.Vector3(0, 0, t - 0.5), ex, ey, ez), 0x6e7a86);
|
||||
const turn = (b.rotDeg * Math.PI) / 180;
|
||||
const placed = (g: THREE.BufferGeometry) => g.rotateY(turn).translate(b.s, 0, t);
|
||||
add('blocks', placed(prism([[-0.6, b.seatEl], [0.6, b.seatEl], [0.6, blockTop], [-0.6, blockTop]], 1.2, new THREE.Vector3(0, 0, -0.6), ex, ey, ez)), 0xa9b4bf);
|
||||
add('blocks', placed(prism([[-0.5, blockTop], [0.5, blockTop], [0.5, b.z], [-0.5, b.z]], 1.0, new THREE.Vector3(0, 0, -0.5), ex, ey, ez)), 0x6e7a86);
|
||||
}
|
||||
|
||||
// 접속슬래브 (spec §4.8): bottom on the 브라켓 top (흉벽 상단 − 0.85), 5 mm behind the 흉벽 back face, 7 m back, falling 4 % away from the wall.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as XLSX from 'xlsx';
|
||||
import * as THREE from 'three';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { bearingRotDeg, buildAbut } from '../src/core/abut/geometry';
|
||||
import { parseAbutments, toAbutInput } from '../src/core/abut/parse';
|
||||
import { buildMembers } from '../src/core/abut/members';
|
||||
import { buildWings } from '../src/core/abut/wings';
|
||||
import { buildAbutGroup } from '../src/view/abut-scene';
|
||||
import { abutElevationRows, resolveBearingRule } from '../src/core/abut/bearing-table';
|
||||
|
||||
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
|
||||
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
|
||||
const model = buildAbut(toAbutInput(rec));
|
||||
|
||||
describe('교대 받침 방위각 (엑셀 받침 시트 6번째 열)', () => {
|
||||
it('reads the 방위각 of every bearing (75°56′55.9″ … 75°58′08.4″ in the workbook frame)', () => {
|
||||
expect(rec.bearings.every((b) => b.azimuthDeg !== undefined && Number.isFinite(b.azimuthDeg))).toBe(true);
|
||||
// model-xy = 90 − workbook value: 75.94886° → 14.05114°
|
||||
expect(rec.bearings.find((b) => b.no === 1)!.azimuthDeg).toBeCloseTo(90 - (75 + 56 / 60 + 55.9 / 3600), 6);
|
||||
});
|
||||
|
||||
it('turns each block like the pier rule (≈ 4.42° from the axis normal); against the wing (skew) line the rest is 0.63° (시안 AW01)', () => {
|
||||
const front = { e: model.nLeft.e * model.frontSide, n: model.nLeft.n * model.frontSide };
|
||||
// 날개벽 방향: CAD 각 lineDeg → 진북 = 90 − CAD (시안: 진북 194.68°, CAD 255.32°).
|
||||
const wingRot = bearingRotDeg(90 - model.wing!.lineDeg, model.u, front);
|
||||
for (const b of model.bearings) {
|
||||
expect(Math.abs(b.rotDeg)).toBeGreaterThan(4.3);
|
||||
expect(Math.abs(b.rotDeg)).toBeLessThan(4.5);
|
||||
}
|
||||
const first = model.bearings.find((b) => b.no === 1)!;
|
||||
expect(Math.abs(first.rotDeg - wingRot)).toBeCloseTo(0.63, 1);
|
||||
});
|
||||
|
||||
it('without the 방위각 the blocks stay on the axis (rotation 0)', () => {
|
||||
const flat = buildAbut({ ...toAbutInput(rec), bearings: toAbutInput(rec).bearings.map((b) => ({ ...b, azimuthDeg: undefined })) });
|
||||
expect(flat.bearings.every((b) => b.rotDeg === 0)).toBe(true);
|
||||
});
|
||||
|
||||
it('the 3D block turns with the bearing: its footprint is no longer axis-aligned', () => {
|
||||
const input = toAbutInput(rec);
|
||||
const members = buildMembers(model, { bottomEl: input.foundationBottomEl, pileLengthMm: input.pileLengthMm });
|
||||
const group = buildAbutGroup(model, members, buildWings(model, members));
|
||||
const block = (group.getObjectByName('blocks') as THREE.Group).children[0] as THREE.Mesh;
|
||||
const box = new THREE.Box3().setFromObject(block);
|
||||
// a turned 1.2 m square is wider than 1.2 m along s
|
||||
expect(box.max.x - box.min.x).toBeGreaterThan(1.2 + 1e-4);
|
||||
});
|
||||
|
||||
it('the ELEVATION table shows the 방위각 (CAD = the workbook value, 진북) and the block turn', () => {
|
||||
const [r] = abutElevationRows(model, resolveBearingRule()).filter((x) => x.no === 1);
|
||||
expect(r.azimuthCadDeg).toBeCloseTo(75 + 56 / 60 + 55.9 / 3600, 6);
|
||||
expect(r.azimuthNorthDeg).toBeCloseTo(90 - (75 + 56 / 60 + 55.9 / 3600), 6);
|
||||
expect(r.rotDeg).toBeCloseTo(model.bearings.find((b) => b.no === 1)!.rotDeg, 9);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user