cimery Sprint 1 — Rust 워크스페이스 + 전 계층 파이프라인

8개 크레이트 구현, cargo test 32개 전부 통과:
- core: Mm/M 단위 newtype, UnitExt 리터럴, FeatureError
- ir: GirderIR + 전 단면 파라미터(PSC-I/U/SteelBox/PlateI) serde JSON
- dsl: Girder builder + 검증 (경간 범위·count·spacing)
- kernel: GeomKernel trait + StubKernel (box mesh, AABB)
- incremental: dirty-tracking IncrementalDb (salsa 업그레이드 경로 주석)
- evaluator: 상태 없는 IR→kernel 브리지
- usd: USDA 1.0 텍스트 익스포트 (CimeryBridgeAPI·GirderAPI schema)
- viewer: wgpu 22 + winit 0.30 컬러 삼각형 (Sprint 1 proof-of-concept)

Sprint 2 다음 단계:
- opencascade-rs로 StubKernel 교체 (실제 PSC-I sweep)
- viewer에서 Girder Mesh 렌더 + 카메라 orbit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
minsung
2026-04-14 17:46:14 +09:00
parent 919855c1e8
commit 62ddf3aea6
24 changed files with 1779 additions and 3 deletions

View File

@@ -0,0 +1,10 @@
[package]
name = "cimery-usd"
version.workspace = true
edition.workspace = true
[dependencies]
cimery-ir = { workspace = true }
[dev-dependencies]
cimery-core = { workspace = true }

View File

@@ -0,0 +1,151 @@
//! cimery-usd — USDA 1.0 text export.
//!
//! Sprint 1: stub geometry (box) — real B-rep export follows in Sprint 2
//! after `GeomKernel` backends produce STEP/BREP that can be tessellated.
//!
//! ADR-002 O / ADR-003 A4:
//! - USD is the *output format*, not a DSL.
//! - All cimery-specific concepts are captured as Applied API schemas
//! (`CimeryBridgeAPI`, `CimeryGirderAPI`) using the codeless USD schema approach.
//! - IFC alias double-tagging planned for Sprint 3 (AOUSD AECO spec alignment).
use cimery_ir::GirderIR;
// ─── Single girder export ─────────────────────────────────────────────────────
/// Export one [`GirderIR`] to a self-contained USDA 1.0 string.
///
/// Sprint 1: stub box geometry (600 × 1800 × span_mm).
/// The real geometry path is: IR → Evaluator → Mesh → USD Mesh prim.
pub fn girder_to_usda(ir: &GirderIR) -> String {
let id_str = safe_id(ir);
let pts = box_points(ir.span_mm() as f32);
let mut s = String::with_capacity(1024);
s.push_str(USDA_HEADER);
s.push_str("def Xform \"Bridge\" (\n");
s.push_str(" apiSchemas = [\"CimeryBridgeAPI\"]\n");
s.push_str(")\n{\n");
write_girder_prim(&mut s, ir, &id_str, &pts);
s.push_str("}\n");
s
}
/// Export multiple girders to a single USDA file under one Bridge prim.
pub fn girders_to_usda(girders: &[GirderIR]) -> String {
let mut s = String::with_capacity(girders.len() * 512 + 256);
s.push_str(USDA_HEADER);
s.push_str("def Xform \"Bridge\" (\n");
s.push_str(" apiSchemas = [\"CimeryBridgeAPI\"]\n");
s.push_str(")\n{\n");
for ir in girders {
let id_str = safe_id(ir);
let pts = box_points(ir.span_mm() as f32);
write_girder_prim(&mut s, ir, &id_str, &pts);
}
s.push_str("}\n");
s
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
fn safe_id(ir: &GirderIR) -> String {
ir.id.to_string().replace('-', "_")
}
fn box_points(span: f32) -> String {
format!(
"(0 0 0) (600 0 0) (600 1800 0) (0 1800 0) \
(0 0 {s}) (600 0 {s}) (600 1800 {s}) (0 1800 {s})",
s = span,
)
}
const USDA_HEADER: &str =
"#usda 1.0\n(\n metersPerUnit = 0.001\n upAxis = \"Y\"\n)\n\n";
fn write_girder_prim(s: &mut String, ir: &GirderIR, id_str: &str, pts: &str) {
s.push_str(&format!(" def Xform \"Girder_{}\" (\n", id_str));
s.push_str(" apiSchemas = [\"CimeryGirderAPI\"]\n");
s.push_str(" ) {\n");
s.push_str(&format!(
" custom float cimery:stationStart = {}\n", ir.station_start
));
s.push_str(&format!(
" custom float cimery:stationEnd = {}\n", ir.station_end
));
s.push_str(&format!(" custom int cimery:count = {}\n", ir.count));
s.push_str(&format!(
" custom token cimery:sectionType = \"{:?}\"\n", ir.section_type
));
s.push_str(&format!(
" custom token cimery:material = \"{:?}\"\n", ir.material
));
s.push('\n');
s.push_str(" def Mesh \"geometry\" {\n");
s.push_str(
" int[] faceVertexCounts = \
[3 3 3 3 3 3 3 3 3 3 3 3]\n"
);
s.push_str(
" int[] faceVertexIndices = \
[0 2 1 0 3 2 4 5 6 4 6 7 0 4 7 0 7 3 1 2 6 1 6 5 0 1 5 0 5 4 3 7 6 3 6 2]\n"
);
s.push_str(&format!(
" point3f[] points = [{}]\n", pts
));
s.push_str(" }\n");
s.push_str(" }\n");
}
// ─── Tests ────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use cimery_core::{MaterialGrade, SectionType};
use cimery_ir::{FeatureId, GirderIR, PscISectionParams, SectionParams};
fn sample() -> GirderIR {
GirderIR {
id: FeatureId::new(),
station_start: 100.0,
station_end: 140.0,
offset_from_alignment: 0.0,
section_type: SectionType::PscI,
section: SectionParams::PscI(PscISectionParams::kds_standard()),
count: 5,
spacing: 2500.0,
material: MaterialGrade::C50,
}
}
#[test]
fn usda_header_present() {
let s = girder_to_usda(&sample());
assert!(s.starts_with("#usda 1.0"), "must start with USDA header");
}
#[test]
fn contains_bridge_api() {
let s = girder_to_usda(&sample());
assert!(s.contains("CimeryBridgeAPI"));
assert!(s.contains("CimeryGirderAPI"));
}
#[test]
fn contains_station_values() {
let s = girder_to_usda(&sample());
assert!(s.contains("100"), "should contain station_start");
assert!(s.contains("140"), "should contain station_end");
}
#[test]
fn multiple_girders() {
let girders = vec![sample(), sample()];
let s = girders_to_usda(&girders);
assert!(s.contains("CimeryBridgeAPI"));
// Two distinct prim blocks
assert_eq!(s.matches("CimeryGirderAPI").count(), 2);
}
}