Files
homeclaw/tests/stl-z-align.test.ts
T
kimandClaude Code 1125f3afb9 feat: 부품 바닥 z=0 규칙 — 컴파일 자동 정렬 + 편집기 정렬 버튼
- 계기: 로봇팔 부품들이 center=true 모델링이라 바닥이 z<0으로 내려가 편집기에서
  파묻혀 보이고 좌표 감각이 어긋남(2026-09-25 사용자 지적, CAD 전부 실측 확인).
- .scad 작성 규칙에 7번 추가: 인쇄용 부품 바닥은 z=0(프롬프트 규칙문+SHORT).
- scad_to_stl/compile: 컴파일 후 bbox min z<-0.05면 바이너리 z 평행이동으로
  자동 정렬하고 안내 문구 출력(translateStlZInPlace, 원자적 재기록).
- STL 편집기에 '바닥 z=0' 버튼 — 외부 STL도 처리(현재 회전·배율 상태의
  월드 bbox 최저점을 0으로). 테스트 3건 추가.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-25 18:58:14 +09:00

76 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// "부품 바닥 z=0" 기능 테스트 (2026-09-25).
// ① translateStlZInPlace — 바이너리 STL z 평행이동(원자적 재기록)
// ② compileScadToStl 자동 정렬 — center=true 소스도 컴파일하면 바닥이 z=0에 붙는지
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { spawnSync } from 'child_process';
import { OPENSCAD_BIN, readStlBBox, translateStlZInPlace } from '../src/tools/stl-cad-core';
describe('translateStlZInPlace — STL z 평행이동', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'stl-zalign-'));
const stl = path.join(tmp, 'part.stl');
test.before(() => {
// center=true 큐브(120×80×45) — z가 [-22.5, 22.5]로 내려가 있는 전형적인 케이스
const scad = path.join(tmp, 't.scad');
fs.writeFileSync(scad, 'cube([120,80,45],center=true);\n');
const r = spawnSync(OPENSCAD_BIN, ['-o', stl, '--export-format=binstl', scad], { timeout: 60_000 });
assert.equal(r.status, 0, `STL 생성 실패: ${r.stderr}`);
});
test('z=+22.5 이동 시 바닥이 0에 붙고 x/y는 불변', () => {
const before = readStlBBox(stl);
assert.ok(Math.abs(before.min[2] - (-22.5)) < 1e-4);
const r = translateStlZInPlace(stl, 22.5);
assert.ok(r.ok, `이동 실패: ${'detail' in r ? r.detail : ''}`);
const after = readStlBBox(stl);
assert.ok(Math.abs(after.min[2]) < 1e-4, `바닥이 0이 아님: ${after.min[2]}`);
assert.ok(Math.abs(after.max[2] - 45) < 1e-4);
assert.equal(after.triangleCount, before.triangleCount, '삼각형 수가 변함');
// x/y 불변
assert.ok(Math.abs(after.min[0] - before.min[0]) < 1e-4 && Math.abs(after.max[1] - before.max[1]) < 1e-4);
// 임시파일 없음(원자적 교체)
assert.deepEqual(fs.readdirSync(tmp).filter(f => f.includes('.zshift-tmp')), []);
});
test('ASCII STL은 명확한 에러', () => {
const ascii = path.join(tmp, 'ascii.stl');
fs.writeFileSync(ascii, 'solid x\n' + ' '.repeat(100) + '\nendsolid x\n');
const r = translateStlZInPlace(ascii, 1);
assert.ok(!r.ok);
assert.match('detail' in r ? r.detail : '', /ASCII/);
});
test.after(() => { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* noop */ } });
});
describe('compileScadToStl — 컴파일 후 바닥 자동 z=0 정렬', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'scad-compile-'));
const ws = tmp; // 워크스페이스 = 임시 디렉토리
test('center=true 부품도 컴파일 결과 바닥이 z=0 + 안내 문구', { timeout: 120_000 }, async () => {
const scad = path.join(ws, 'SCAD', 'bracket.scad');
fs.mkdirSync(path.dirname(scad), { recursive: true });
// 규칙을 일부러 어기는 소스(center=true) — 컴파일러가 잡아 올려야 한다
fs.writeFileSync(scad, 'PART="part";\nmodule bracket(){difference(){cube([60,40,20],center=true);translate([0,0,50])cylinder(h=20,d=6,$fn=24);}}\nif(PART=="part")bracket();\n');
const { compileScadToStl } = await import('../src/tools/stl-cad');
// compileScadToStl은 export가 안 된 내부 함수 — stl_cad 도구의 compile 액션으로 검증
const { stlCadTool } = await import('../src/tools/stl-cad');
const r = await stlCadTool.execute({
action: 'compile',
scad_path: 'SCAD/bracket.scad',
output_path: 'CAD/bracket.stl',
_workspacePath: ws,
});
assert.ok(r.success, `컴파일 실패: ${'error' in r ? r.error : ''}`);
assert.match(String(r.stdout), /z=0/, '자동 정렬 안내가 없음');
const bb = readStlBBox(path.join(ws, 'CAD', 'bracket.stl'));
assert.ok(Math.abs(bb.min[2]) < 1e-4, `바닥이 0이 아님: ${bb.min[2]}`);
assert.ok(Math.abs(bb.max[2] - 20) < 1e-4, `높이가 20이 아님: ${bb.max[2]}`);
});
test.after(() => { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* noop */ } });
});