- 계기: 로봇팔 부품들이 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>
76 lines
3.9 KiB
TypeScript
76 lines
3.9 KiB
TypeScript
// "부품 바닥 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 */ } });
|
||
}); |