From 61260413c69585d643c15dbac93a4c2f5b240ba2 Mon Sep 17 00:00:00 2001 From: ypshim Date: Fri, 23 Oct 2026 11:01:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(view):=20turn=20the=20bearing=20blocks,=20?= =?UTF-8?q?pads=20and=20block=20rebar=20to=20the=20bearing=20row=20(?= =?UTF-8?q?=EC=82=BD=EB=8F=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each block takes the skew of its bearing row against the perpendicular of its stretch axis (rowSkewDeg, from the bearing azimuth). The blocks and pads are turned to that skew, and the S1/S2 bars are drawn in the block frame and turned with it, so their lengths and embedments do not change. ② shows the row skew (P1: front −4.42°, back −3.11°). Approved 2026-10-23 with the drawing as reference. Co-Authored-By: Claude Haiku 5.5 --- src/core/geometry.ts | 25 ++++++++++++++++++++-- src/main.ts | 6 ++++++ src/view/rebar3d.ts | 20 ++++++++++------- src/view/scene.ts | 4 ++++ tests/bearing-skew.test.ts | 44 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 tests/bearing-skew.test.ts diff --git a/src/core/geometry.ts b/src/core/geometry.ts index 2dbe05b..9f812de 100644 --- a/src/core/geometry.ts +++ b/src/core/geometry.ts @@ -66,6 +66,25 @@ export interface BlockModel { bottom: number; top: number; padTop: number; + /** Skew (deg, −90…90] of the bearing row against the perpendicular of the stretch axis; the block sides follow the row. */ + rowSkewDeg: number; +} + +/** Skew of a bearing row (deg) from the bearing azimuth and the stretch axis direction u (see {@link BlockModel.rowSkewDeg}). */ +export function rowSkewDeg(bearingAzimuthDeg: number, u: Vec2): number { + if (Number.isNaN(bearingAzimuthDeg)) return 0; + const axisAz = (Math.atan2(u.e, u.n) * 180) / Math.PI; + const d = (((bearingAzimuthDeg - (axisAz + 90)) % 180) + 540) % 180; // 0…180 + return d > 90 ? d - 180 : d; +} + +/** + * (s, n) of a point (a along the block's s side, c along its n side) measured from the block centre, after the block turns + * by its row skew. Same turn as the 3D geometry (rotateY(−skew)). + */ +export function blockFramePoint(b: Pick, a: number, c: number): [number, number] { + const d = (b.rowSkewDeg * Math.PI) / 180; + return [b.s + a * Math.cos(d) - c * Math.sin(d), b.n + a * Math.sin(d) + c * Math.cos(d)]; } export interface ColumnModel { @@ -372,13 +391,15 @@ export function buildPier(input: PierInput): PierModel { } const blocks: BlockModel[] = []; + const bearingAz = new Map(input.bearings.map((r) => [r.no, r.azimuthDeg])); + const skewOf = (no: number, k: number) => rowSkewDeg(bearingAz.get(no) ?? NaN, axis.segments[k].u); frontPlaced.forEach((b) => { const bottom = segmentTop(segments, b.g); - blocks.push({ no: b.no, row: '전열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z }); + blocks.push({ no: b.no, row: '전열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k) }); }); backPlaced.forEach((b) => { const bottom = b.z - h2; - blocks.push({ no: b.no, row: '후열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z }); + blocks.push({ no: b.no, row: '후열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k) }); }); const blockHeightFront = blocks.find((b) => b.row === '전열')!.top - blocks.find((b) => b.row === '전열')!.bottom; diff --git a/src/main.ts b/src/main.ts index 215155c..73da512 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1242,6 +1242,12 @@ function renderPlan(): void { $('member-summary').textContent = `기본: ${defaultSummary(member.id)} · 선택: ${defaultSummary(member.id)}`; renderShapeOptions(member.id); $('member-note').textContent = member.note; + if (member.id === 'bearing' && pier?.model) { + // Row skew of the bearing blocks (계산값): the block sides follow the bearing row (승인 2026-10-23). + const front = pier.model.blocks.find((b) => b.row === '전열'); + const back = pier.model.blocks.find((b) => b.row === '후열'); + $('member-note').textContent += ` 받침 열 기울기(교축 직각 기준, 계산값): 전열 ${front?.rowSkewDeg.toFixed(2) ?? '—'}° · 후열 ${back?.rowSkewDeg.toFixed(2) ?? '—'}°.`; + } const regen = $('member-regen'); const fields = $('member-fields'); if (!pier) { diff --git a/src/view/rebar3d.ts b/src/view/rebar3d.ts index 02ed773..6511dc3 100644 --- a/src/view/rebar3d.ts +++ b/src/view/rebar3d.ts @@ -5,6 +5,7 @@ import { bottomPiece, MAIN, widthLayout } from '../core/rebar/coping-main'; import { C14_INSET, c14Bars, CAP, COPING_REBAR, SHEAR_LOOP_STEP, shearBars, shearBarsMulti, shearUHeight, copingSideBars, SIDE_HOOK_LEG, stairBars, undersideAt } from '../core/rebar/coping'; import { columnRebar, COLUMN_P1 } from '../core/rebar/column'; import { barsAcross, BLOCK_REBAR } from '../core/rebar/bearing-block'; +import { blockFramePoint } from '../core/geometry'; import { bendRadius, DEFAULT_MATERIAL, FckByMember, hookLength, Material, materialFor, NOMINAL_DIAMETER } from '../core/rebar/common'; import { diameterGroupIndex, DIAMETER_GROUPS, memberOfBar, type RebarMemberKey } from '../core/rebar/color'; import { stretchMatrix } from './scene'; @@ -468,18 +469,21 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, const legM = BLOCK_REBAR.legLength / 1000; const offsets = Array.from({ length: across.count }, (_, i) => (across.firstEnd + i * across.spacing) / 1000 - BLOCK_REBAR.dim / 2000); const dBar = NOMINAL_DIAMETER.H19; + // The bars are drawn in the block's own frame (a along its s side, c along its n side) and turned with the block. for (const b of m.blocks) { const z2 = b.top - BLOCK_REBAR.depthS2 / 1000; const z1 = b.top - BLOCK_REBAR.depthS1 / 1000; + const P = (a: number, c: number, y: number): [number, number, number] => { + const [s, n] = blockFramePoint(b, a, c); + return [s, y, n]; + }; for (const off of offsets) { - const n2 = b.n + off; - addBar('S2', b.k, [b.s - half, z2, n2], [b.s + half, z2, n2], dBar, COLOR.block); - addBar('S2', b.k, [b.s - half, z2 - legM, n2], [b.s - half, z2, n2], dBar, COLOR.block); - addBar('S2', b.k, [b.s + half, z2, n2], [b.s + half, z2 - legM, n2], dBar, COLOR.block); - const s1 = b.s + off; - addBar('S1', b.k, [s1, z1, b.n - half], [s1, z1, b.n + half], dBar, COLOR.block); - addBar('S1', b.k, [s1, z1 - legM, b.n - half], [s1, z1, b.n - half], dBar, COLOR.block); - addBar('S1', b.k, [s1, z1, b.n + half], [s1, z1 - legM, b.n + half], dBar, COLOR.block); + addBar('S2', b.k, P(-half, off, z2), P(half, off, z2), dBar, COLOR.block); + addBar('S2', b.k, P(-half, off, z2 - legM), P(-half, off, z2), dBar, COLOR.block); + addBar('S2', b.k, P(half, off, z2), P(half, off, z2 - legM), dBar, COLOR.block); + addBar('S1', b.k, P(off, -half, z1), P(off, half, z1), dBar, COLOR.block); + addBar('S1', b.k, P(off, -half, z1 - legM), P(off, -half, z1), dBar, COLOR.block); + addBar('S1', b.k, P(off, half, z1), P(off, half, z1 - legM), dBar, COLOR.block); } } diff --git a/src/view/scene.ts b/src/view/scene.ts index cb8b4ff..640a2bf 100644 --- a/src/view/scene.ts +++ b/src/view/scene.ts @@ -125,11 +125,15 @@ export function buildPierScene(m: PierModel): PierScene { } for (const b of m.blocks) { + // Turn the block so its sides follow the bearing row (rowSkewDeg); the 3D turn is rotateY(−skew). + const turn = (-b.rowSkewDeg * Math.PI) / 180; const h = b.top - b.bottom; const g = new THREE.BoxGeometry(1.2, h, 1.2); + g.rotateY(turn); g.translate(b.s, b.bottom + h / 2, b.n); add('blocks', g, 1, b.k); const pad = new THREE.BoxGeometry(1.0, 0.05, 1.0); + pad.rotateY(turn); pad.translate(b.s, b.padTop - 0.025, b.n); add('pads', pad, 1, b.k); } diff --git a/tests/bearing-skew.test.ts b/tests/bearing-skew.test.ts new file mode 100644 index 0000000..d7f2ba8 --- /dev/null +++ b/tests/bearing-skew.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { buildPier, blockFramePoint } from '../src/core/geometry'; +import { loadP1 } from './helpers'; + +const model = buildPier(loadP1()); +const front = model.blocks.filter((b) => b.row === '전열'); +const back = model.blocks.filter((b) => b.row === '후열'); + +describe('받침블럭 방위 (삽도 · 승인 2026-10-23)', () => { + it('P1 rows are skewed by the bearing azimuth against the axis perpendicular: front ≈ −4.42°, back ≈ −3.11°', () => { + expect(front.every((b) => Math.abs(b.rowSkewDeg - -4.42) < 0.02)).toBe(true); + expect(back.every((b) => Math.abs(b.rowSkewDeg - -3.11) < 0.02)).toBe(true); + }); + + it('a block side follows the bearing row: the n side of a block runs along (−sinδ, cosδ)', () => { + const b = front[0]; + const [s1, n1] = blockFramePoint(b, 0, -0.6); + const [s2, n2] = blockFramePoint(b, 0, 0.6); + const ds = s2 - s1; + const dn = n2 - n1; + const len = Math.hypot(ds, dn); + const d = (b.rowSkewDeg * Math.PI) / 180; + expect(ds / len).toBeCloseTo(-Math.sin(d), 9); + expect(dn / len).toBeCloseTo(Math.cos(d), 9); + }); + + it('turning keeps the block centre and the bearing spacing (the row is unchanged)', () => { + const b = front[0]; + const [s0, n0] = blockFramePoint(b, 0, 0); + expect(s0).toBeCloseTo(b.s, 12); + expect(n0).toBeCloseTo(b.n, 12); + }); + + it('the turned block corners stay inside the coping ends', () => { + const L = model.copingL; + for (const b of model.blocks) { + for (const [a, c] of [[-0.6, -0.6], [-0.6, 0.6], [0.6, -0.6], [0.6, 0.6]] as const) { + const [s] = blockFramePoint(b, a, c); + expect(s, `block ${b.no}`).toBeGreaterThan(0); + expect(s, `block ${b.no}`).toBeLessThan(L); + } + } + }); +});