fix(B05): 절취 측벽에서 빗금을 뺀다

벽은 방향성을 표현하는 면이 아니다(2026-08-27 사용자). 성토면 빗금은 그대로 두고
벽만 단색으로 그린다.

- Mesh: 벽 재질에서 `map`(빗금 텍스처)과 uv 속성을 뺀다.
- Cut/Build: 벽이 UV를 안 쓰므로 `CorridorCutWall.uvs`와 수집 코드를 지운다.
- Corridor: 저장본에서 `uvsBase64` 제거, ENVELOPE_VERSION 4 → 5.

검증: 재빌드 후 벽 서피스 우 360·좌 356 삼각형 그대로, `uvs` 필드 없음.
ISO 화면에서 벽면이 단색, 주변 성토면 빗금은 유지. npm run typecheck 통과, prettier 적용.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 10:54:04 +09:00
co-authored by Claude Opus 5
parent 43804f2f5b
commit e9914af675
4 changed files with 12 additions and 34 deletions
+2 -4
View File
@@ -36,8 +36,9 @@ export interface CorridorStructureHook {
*
* 3 = 리본에 셀 마스크·축 고정 커브 조각 삼각형 추가 — 2026-08-26.
* 4 = 절취 측벽 서피스(`cutWalls`) 추가 — 2026-08-27.
* 5 = 측벽에서 빗금(UV)을 뺀다 — 벽은 방향성을 표현하지 않는다(2026-08-27 사용자).
*/
const ENVELOPE_VERSION = 4;
const ENVELOPE_VERSION = 5;
interface CorridorEnvelope {
version: number;
@@ -68,7 +69,6 @@ interface CorridorEnvelope {
cutWalls?: Array<{
side: "left" | "right";
positionsBase64: string;
uvsBase64: string;
}>;
}
@@ -200,7 +200,6 @@ function serialize(build: CorridorBuildResult, hash: string): CorridorEnvelope {
cutWalls: (build.cutWalls ?? []).map((wall) => ({
side: wall.side,
positionsBase64: base64FromFloats(wall.positions),
uvsBase64: base64FromBytes(new Uint8Array(wall.uvs.buffer.slice(0))),
})),
};
}
@@ -228,7 +227,6 @@ function deserialize(envelope: CorridorEnvelope): CorridorBuildResult {
cutWalls: (envelope.cutWalls ?? []).map((wall) => ({
side: wall.side,
positions: floatsFromBase64(wall.positionsBase64),
uvs: new Float32Array(bytesFromBase64(wall.uvsBase64).buffer),
})),
};
}
+1 -2
View File
@@ -92,12 +92,11 @@ export interface CorridorRibbon {
/**
* 잘린 자리 **측벽 서피스**(2026-08-27 사용자) — 리본 메시에 섞지 않고 구조물처럼
* 따로 그린다. 삼각형 수프(정점당 3, 삼각형당 9)와 짝 UV(정점당 2).
* 따로 그린다. 삼각형 수프(정점당 3, 삼각형당 9). 빗금이 없어 UV는 담지 않는다.
*/
export interface CorridorCutWall {
side: "left" | "right";
positions: Float64Array;
uvs: Float32Array;
}
/** 시·종점 마구리(캡) — 설계선과 지반선 사이를 세로로 봉인하는 스트립(모델 좌표). */
+8 -26
View File
@@ -293,37 +293,26 @@ function pushSkirt(
loop: ReadonlyArray<[number, number]>,
ribbon: CorridorRibbon,
tris: number[],
uvs: number[],
): void {
const ground = ribbon.groundZ;
if (!ground) return;
const cols = ribbon.colCount;
const startChainage = ribbon.chainages[0];
/** 격자점 → [x, y, 서피스 z, 지반 z, u]. */
const at = (row: number, col: number): [number, number, number, number, number] => {
/** 격자점 → [x, y, 서피스 z, 지반 z 여유]. */
const at = (row: number, col: number): [number, number, number, number] => {
const i = (row * cols + col) * 3;
return [
ribbon.positions[i],
ribbon.positions[i + 1],
ribbon.positions[i + 2],
ground[row * cols + col] - SKIRT_MARGIN_M,
(ribbon.chainages[row] - startChainage) / 2,
];
};
const push = (x: number, y: number, z: number, u: number): void => {
tris.push(x, y, z);
uvs.push(u, z / 2); // 빗금은 2m마다 한 타일 — 세로도 실척으로 이어진다.
};
for (let i = 0; i < loop.length - 1; i += 1) {
const [x1, y1, top1, bottom1, u1] = at(loop[i][0], loop[i][1]);
const [x2, y2, top2, bottom2, u2] = at(loop[i + 1][0], loop[i + 1][1]);
const [x1, y1, top1, bottom1] = at(loop[i][0], loop[i][1]);
const [x2, y2, top2, bottom2] = at(loop[i + 1][0], loop[i + 1][1]);
if (top1 - bottom1 < 1e-6 && top2 - bottom2 < 1e-6) continue; // 높이 0이면 벽이 없다.
push(x1, y1, top1, u1);
push(x2, y2, top2, u2);
push(x2, y2, bottom2, u2);
push(x1, y1, top1, u1);
push(x2, y2, bottom2, u2);
push(x1, y1, bottom1, u1);
tris.push(x1, y1, top1, x2, y2, top2, x2, y2, bottom2);
tris.push(x1, y1, top1, x2, y2, bottom2, x1, y1, bottom1);
}
}
@@ -368,7 +357,6 @@ export function maskFillByPlanCurves(
const uvs: number[] = [];
// 측벽은 리본 메시가 아니라 **별도 서피스**로 나간다(2026-08-27 사용자).
const wallTris: number[] = [];
const wallUvs: number[] = [];
const startChainage = ribbon.chainages[0];
const point = (row: number, col: number): [number, number, number] => {
const i = (row * cols + col) * 3;
@@ -427,7 +415,7 @@ export function maskFillByPlanCurves(
traceMaskLoops(rows, cols, mask).forEach((raw) => {
const loop = dropCollinear(raw);
if (loop.length < 4 || !planes.length) return;
pushSkirt(loop, ribbon, wallTris, wallUvs);
pushSkirt(loop, ribbon, wallTris);
const meanRow = loop.reduce((sum, [row]) => sum + row, 0) / loop.length;
const chainage = ribbon.chainages[Math.min(rows - 1, Math.round(meanRow))];
const plane = planes.reduce((best, item) =>
@@ -448,13 +436,7 @@ export function maskFillByPlanCurves(
],
});
});
if (wallTris.length) {
walls.push({
side,
positions: new Float64Array(wallTris),
uvs: new Float32Array(wallUvs),
});
}
if (wallTris.length) walls.push({ side, positions: new Float64Array(wallTris) });
return {
...ribbon,
cellMask: mask,
+1 -2
View File
@@ -429,13 +429,12 @@ export function createCorridorGroup(build: CorridorBuildResult, bounds: ModelBou
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("uv", new THREE.BufferAttribute(wall.uvs.slice(0), 2));
geometry.computeVertexNormals();
const mesh = new THREE.Mesh(
geometry,
new THREE.MeshLambertMaterial({
// 빗금 없음 — 벽은 방향성을 표현할 면이 아니다(2026-08-27 사용자).
color: KIND_COLORS.fill,
map: hatchTexture(HATCH_DIRECTION.fill ?? -1),
side: THREE.DoubleSide,
polygonOffset: true,
polygonOffsetFactor: POLYGON_OFFSET_FACTOR,