#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PatternRoute {
SdfMarchingCubes,
SdfDualContouring,
}
impl PatternRoute {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::SdfMarchingCubes => "SDF+MC (marching cubes)",
Self::SdfDualContouring => "SDF+DC (dual contouring)",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CertificationSource {
None,
SimulationOnly,
UserFieldTest,
Both,
}
impl CertificationSource {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "uncertified",
Self::SimulationOnly => "pipeline simulation only",
Self::UserFieldTest => "user field test",
Self::Both => "user field test + pipeline simulation",
}
}
#[must_use]
pub const fn includes_field_test(self) -> bool {
matches!(self, Self::UserFieldTest | Self::Both)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FieldTestRecord {
pub date: &'static str,
pub material: &'static str,
pub printer: &'static str,
pub notes: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub struct LolPattern {
pub name: &'static str,
pub description: &'static str,
pub route: PatternRoute,
pub certified_by: CertificationSource,
pub printability_score: Option<u8>,
pub field_test: Option<FieldTestRecord>,
pub source_crate: &'static str,
pub source_version: &'static str,
pub canonical_kind: Option<&'static str>,
}
pub mod registry {
use super::{CertificationSource, LolPattern, PatternRoute};
pub const SHOPPING_CART_COIN_100YEN: LolPattern = LolPattern {
name: "shopping_cart_coin_100yen",
description: "Φ22.8 × 1.7mm 100 円硬貨型キーホルダーコイン (pipeline 実プリント合格 + Sim Excellent 88)",
route: PatternRoute::SdfDualContouring,
certified_by: CertificationSource::Both,
printability_score: Some(88),
field_test: None, source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("python generator"),
};
pub const SKADIS_PANEL_300X300: LolPattern = LolPattern {
name: "skadis_panel_300x300",
description:
"IKEA SKADIS 互換 300×300×5mm ペグボード + 千鳥ペグ穴 98 個 (Sim Excellent 88、tight_aabb 修正 + RepeatFinite→Union で watertight 保証)",
route: PatternRoute::SdfDualContouring,
certified_by: CertificationSource::Both,
printability_score: Some(88),
field_test: None,
source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("python generator"),
};
pub const SHELF_DIVIDER_560X250X120: LolPattern = LolPattern {
name: "shelf_divider_560x250x120",
description:
"U 字棚仕切り 560×250×120mm + hex 抜き穴 (板反り対策、30lbs 実荷重合格、Sim 60)",
route: PatternRoute::SdfMarchingCubes,
certified_by: CertificationSource::UserFieldTest,
printability_score: Some(60),
field_test: None,
source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("rust generator"),
};
pub const SKADIS_HOOK_J: LolPattern = LolPattern {
name: "skadis_hook_j",
description: "IKEA SKADIS 互換 J 型 hook (pipeline Python 2D+extrude canonical、Sim 79)",
route: PatternRoute::SdfDualContouring,
certified_by: CertificationSource::UserFieldTest,
printability_score: Some(79),
field_test: None,
source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("python generator"),
};
pub const SKADIS_HOOK_L: LolPattern = LolPattern {
name: "skadis_hook_l",
description:
"IKEA SKADIS 互換 L 型 hook (直角曲げ、pipeline Python 2D+extrude canonical、Sim 79)",
route: PatternRoute::SdfDualContouring,
certified_by: CertificationSource::UserFieldTest,
printability_score: Some(79),
field_test: None,
source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("python generator"),
};
pub const SKADIS_HOOK_S: LolPattern = LolPattern {
name: "skadis_hook_s",
description:
"IKEA SKADIS 互換 S 型 hook (S 字曲げ、pipeline Python 2D+extrude canonical、Sim 88)",
route: PatternRoute::SdfDualContouring,
certified_by: CertificationSource::Both,
printability_score: Some(88),
field_test: None,
source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("python generator"),
};
pub const SKADIS_CONTAINER: LolPattern = LolPattern {
name: "skadis_container",
description: "IKEA SKADIS 互換 container (2 peg、gusset ribs で補強、Sim 76)",
route: PatternRoute::SdfDualContouring,
certified_by: CertificationSource::UserFieldTest,
printability_score: Some(76),
field_test: None,
source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("python generator"),
};
pub const SKADIS_CLIP: LolPattern = LolPattern {
name: "skadis_clip",
description: "IKEA SKADIS 互換 clip (単 peg、細物ホルダー、Sim 88)",
route: PatternRoute::SdfDualContouring,
certified_by: CertificationSource::Both,
printability_score: Some(88),
field_test: None,
source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("python generator"),
};
pub const SKADIS_SHELF: LolPattern = LolPattern {
name: "skadis_shelf",
description: "IKEA SKADIS 互換 shelf (2 peg、rib 補強棚板、Sim 70)",
route: PatternRoute::SdfDualContouring,
certified_by: CertificationSource::UserFieldTest,
printability_score: Some(70),
field_test: None,
source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("python generator"),
};
pub const SKADIS_ELASTIC_CORD: LolPattern = LolPattern {
name: "skadis_elastic_cord",
description: "IKEA SKADIS 互換 elastic cord holder (伸縮バンドで固定、Sim 88)",
route: PatternRoute::SdfDualContouring,
certified_by: CertificationSource::Both,
printability_score: Some(88),
field_test: None,
source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("python generator"),
};
pub const WALL_HOOK: LolPattern = LolPattern {
name: "wall_hook",
description:
"壁掛けフック (荷重指定 kgf で応力逆算、pipeline Rust generator canonical、Sim 79)",
route: PatternRoute::SdfMarchingCubes,
certified_by: CertificationSource::SimulationOnly,
printability_score: Some(79),
field_test: None,
source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("rust generator"),
};
pub const GRIDFINITY_BIN: LolPattern = LolPattern {
name: "gridfinity_bin",
description: "Gridfinity 互換 bin (42mm grid、任意 units × height × dividers、Sim 79)",
route: PatternRoute::SdfMarchingCubes,
certified_by: CertificationSource::SimulationOnly,
printability_score: Some(79),
field_test: None,
source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("rust generator"),
};
pub const DRAWER_ORGANIZER: LolPattern = LolPattern {
name: "drawer_organizer",
description: "引出し仕切り (chopsticks/fork/knife/spoon/marker/pen 等 slot 定義、Sim 72)",
route: PatternRoute::SdfMarchingCubes,
certified_by: CertificationSource::SimulationOnly,
printability_score: Some(72),
field_test: None,
source_crate: "alice-lol",
source_version: env!("CARGO_PKG_VERSION"),
canonical_kind: Some("rust generator"),
};
pub const ALL: &[LolPattern] = &[
SHOPPING_CART_COIN_100YEN,
SKADIS_PANEL_300X300,
SHELF_DIVIDER_560X250X120,
SKADIS_HOOK_J,
SKADIS_HOOK_L,
SKADIS_HOOK_S,
SKADIS_CONTAINER,
SKADIS_CLIP,
SKADIS_SHELF,
SKADIS_ELASTIC_CORD,
WALL_HOOK,
GRIDFINITY_BIN,
DRAWER_ORGANIZER,
];
}
#[must_use]
pub fn pattern_passes_ci_gate(pattern: &LolPattern) -> bool {
let score_ok = pattern.printability_score.is_some_and(|s| s >= 85);
let field_tested = pattern.certified_by.includes_field_test();
score_ok || field_tested
}
#[must_use]
pub fn patterns_failing_ci_gate() -> Vec<&'static LolPattern> {
registry::ALL
.iter()
.filter(|p| !pattern_passes_ci_gate(p))
.collect()
}
#[must_use]
pub fn field_tested_patterns() -> Vec<&'static LolPattern> {
registry::ALL
.iter()
.filter(|p| p.certified_by.includes_field_test())
.collect()
}
#[must_use]
pub fn patterns_by_route(route: PatternRoute) -> Vec<&'static LolPattern> {
registry::ALL.iter().filter(|p| p.route == route).collect()
}
#[must_use]
pub fn find_by_name(name: &str) -> Option<&'static LolPattern> {
registry::ALL.iter().find(|p| p.name == name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_has_expected_pattern_count() {
assert_eq!(registry::ALL.len(), 13);
}
#[test]
fn field_tested_patterns_count() {
let ft = field_tested_patterns();
assert_eq!(ft.len(), 10);
}
#[test]
fn uncertified_patterns_count() {
let uncertified = registry::ALL
.iter()
.filter(|p| p.certified_by == CertificationSource::None)
.count();
assert_eq!(uncertified, 0);
}
#[test]
fn dual_contouring_route_count() {
let thin = patterns_by_route(PatternRoute::SdfDualContouring);
assert_eq!(thin.len(), 9);
}
#[test]
fn sdf_route_count() {
let thick = patterns_by_route(PatternRoute::SdfMarchingCubes);
assert_eq!(thick.len(), 4);
}
#[test]
fn routes_sum_matches_registry_all() {
let thin = patterns_by_route(PatternRoute::SdfDualContouring).len();
let thick = patterns_by_route(PatternRoute::SdfMarchingCubes).len();
assert_eq!(thin + thick, registry::ALL.len());
}
#[test]
fn find_shopping_cart_coin() {
let p = find_by_name("shopping_cart_coin_100yen").expect("registered");
assert_eq!(p.route, PatternRoute::SdfDualContouring);
assert_eq!(p.certified_by, CertificationSource::Both);
assert_eq!(p.printability_score, Some(88));
}
#[test]
fn find_wall_hook_simulation_certified() {
let p = find_by_name("wall_hook").expect("registered");
assert_eq!(p.certified_by, CertificationSource::SimulationOnly);
assert_eq!(p.printability_score, Some(79));
assert!(p.canonical_kind.is_some());
assert!(
!pattern_passes_ci_gate(p),
"wall_hook は Sim 79 なので CI gate 未通過"
);
}
#[test]
fn find_missing_name_returns_none() {
assert!(find_by_name("no_such_pattern").is_none());
}
#[test]
fn certification_includes_field_test_flag() {
assert!(CertificationSource::UserFieldTest.includes_field_test());
assert!(CertificationSource::Both.includes_field_test());
assert!(!CertificationSource::SimulationOnly.includes_field_test());
assert!(!CertificationSource::None.includes_field_test());
}
#[test]
fn all_patterns_have_alice_lol_source_crate() {
for p in registry::ALL {
assert_eq!(p.source_crate, "alice-lol");
}
}
#[test]
fn ci_gate_field_tested_patterns_pass() {
for p in registry::ALL {
if p.certified_by.includes_field_test() {
assert!(
pattern_passes_ci_gate(p),
"{}: field-tested but CI gate fail",
p.name
);
}
}
}
#[test]
fn ci_gate_uncertified_without_score_fail() {
for p in registry::ALL {
if p.certified_by == CertificationSource::None && p.printability_score.is_none() {
assert!(
!pattern_passes_ci_gate(p),
"{}: uncertified + score None は CI gate 失敗すべき",
p.name
);
}
}
}
#[test]
fn ci_gate_hypothetical_high_score_passes() {
let p = LolPattern {
printability_score: Some(90),
certified_by: CertificationSource::None,
..registry::WALL_HOOK
};
assert!(pattern_passes_ci_gate(&p));
}
#[test]
fn patterns_failing_ci_gate_matches_uncertified_count() {
let failing = patterns_failing_ci_gate();
assert_eq!(
failing.len(),
3,
"tight_aabb 修正で真 bbox 反映、wall_hook + gridfinity_bin + drawer_organizer が Sim < 85"
);
}
#[test]
fn all_field_tested_have_a_canonical_kind() {
for p in registry::ALL {
if p.certified_by.includes_field_test() {
assert!(
p.canonical_kind.is_some(),
"{}: field-tested pattern must have canonical_kind set",
p.name
);
}
}
}
}