ui: review candidate physical material cards without assignment (#1344)
Adds a read-only review window to Validate Physical Material Data: paste a schema-1 card, see its known properties in SI units, unknown ones marked, with stated conditions and source claims. Nothing is assigned or saved. Co-authored-by: 0xlillith <0xlillith@users.noreply.github.com>
This commit is contained in:
1 parent
be985f136d
commit
a47e82ba94
11 files changed
+527
No files matched your search
@@ -85,6 +85,28 @@
|
||||
"ui.clear_recent": "Zuletzt verwendet leeren",
|
||||
"ui.clear_the_selection": "Auswahl aufheben",
|
||||
"ui.close": "Schließen",
|
||||
"ui.material_review.title": "Physikalische Materialdaten prüfen",
|
||||
"ui.material_review.scope": "Nur Kandidat: nicht zugewiesen oder gespeichert. Numerische Prüfung ist keine technische Zertifizierung.",
|
||||
"ui.material_review.input": "Materialkarte mit Schema 1 einfügen (JSON)",
|
||||
"ui.material_review.review": "Karte prüfen",
|
||||
"ui.material_review.limit": "Die Karte überschreitet die Eingabegrenze von 256 KiB.",
|
||||
"ui.material_review.unknown": "Unbekannt",
|
||||
"ui.material_review.properties": "Eigenschaften",
|
||||
"ui.material_review.conditions": "Angegebene Bedingungen",
|
||||
"ui.material_review.source": "Quelle",
|
||||
"ui.material_review.axes": "Materiallokale Achsen; keine Körperausrichtung zugewiesen.",
|
||||
"ui.material_review.derived": "Abgeleitete isotrope Moduln",
|
||||
"ui.material_review.claims": "Quellen und Lizenzen sind ungeprüfte Angaben. Fehlende Werte bleiben unbekannt.",
|
||||
"ui.material_review.density": "Dichte",
|
||||
"ui.material_review.elasticity": "Elastizität",
|
||||
"ui.material_review.tensile_strength": "Zugfestigkeit",
|
||||
"ui.material_review.compressive_strength": "Druckfestigkeit",
|
||||
"ui.material_review.yield_strength": "Streckgrenze",
|
||||
"ui.material_review.shear_strength": "Scherfestigkeit",
|
||||
"ui.material_review.thermal_conductivity": "Wärmeleitfähigkeit",
|
||||
"ui.material_review.thermal_expansion": "Wärmeausdehnung",
|
||||
"ui.material_review.specific_heat": "Spezifische Wärmekapazität",
|
||||
"ui.material_review.moisture_unit": "kg Wasser / kg Trockenmaterial",
|
||||
"ui.coil": "Schraube",
|
||||
"ui.coincident": "Koinzident",
|
||||
"ui.collapse": "Einklappen",
|
||||
|
||||
@@ -370,6 +370,28 @@
|
||||
"ui.click_selects_whole_bodies": "Click selects whole bodies",
|
||||
"ui.click_then_press_key": "Click, then press the new key",
|
||||
"ui.close": "Close",
|
||||
"ui.material_review.title": "Review Physical Material Data",
|
||||
"ui.material_review.scope": "Candidate only: not assigned or saved. Numerical validation is not engineering certification.",
|
||||
"ui.material_review.input": "Paste a schema-1 material card (JSON)",
|
||||
"ui.material_review.review": "Review card",
|
||||
"ui.material_review.limit": "The card exceeds the 256 KiB input limit.",
|
||||
"ui.material_review.unknown": "Unknown",
|
||||
"ui.material_review.properties": "Properties",
|
||||
"ui.material_review.conditions": "Stated conditions",
|
||||
"ui.material_review.source": "Source",
|
||||
"ui.material_review.axes": "Material-local axes; no body orientation is assigned.",
|
||||
"ui.material_review.derived": "Derived isotropic moduli",
|
||||
"ui.material_review.claims": "Sources and licences are unverified claims. Missing values remain unknown.",
|
||||
"ui.material_review.density": "Density",
|
||||
"ui.material_review.elasticity": "Elasticity",
|
||||
"ui.material_review.tensile_strength": "Tensile strength",
|
||||
"ui.material_review.compressive_strength": "Compressive strength",
|
||||
"ui.material_review.yield_strength": "Yield strength",
|
||||
"ui.material_review.shear_strength": "Shear strength",
|
||||
"ui.material_review.thermal_conductivity": "Thermal conductivity",
|
||||
"ui.material_review.thermal_expansion": "Thermal expansion",
|
||||
"ui.material_review.specific_heat": "Specific heat",
|
||||
"ui.material_review.moisture_unit": "kg water / kg dry material",
|
||||
"ui.coil": "Coil",
|
||||
"ui.coincident": "Coincident",
|
||||
"ui.collapse": "Collapse",
|
||||
|
||||
@@ -370,6 +370,28 @@
|
||||
"ui.click_selects_whole_bodies": "Un clic sélectionne des corps entiers",
|
||||
"ui.click_then_press_key": "Cliquer, puis appuyer sur la nouvelle touche",
|
||||
"ui.close": "Fermer",
|
||||
"ui.material_review.title": "Examiner les données physiques du matériau",
|
||||
"ui.material_review.scope": "Candidat uniquement : ni attribué ni enregistré. La validation numérique ne constitue pas une certification technique.",
|
||||
"ui.material_review.input": "Coller une fiche matériau de schéma 1 (JSON)",
|
||||
"ui.material_review.review": "Examiner la fiche",
|
||||
"ui.material_review.limit": "La fiche dépasse la limite de saisie de 256 Kio.",
|
||||
"ui.material_review.unknown": "Inconnu",
|
||||
"ui.material_review.properties": "Propriétés",
|
||||
"ui.material_review.conditions": "Conditions déclarées",
|
||||
"ui.material_review.source": "Source",
|
||||
"ui.material_review.axes": "Axes locaux au matériau ; aucune orientation du corps attribuée.",
|
||||
"ui.material_review.derived": "Modules isotropes dérivés",
|
||||
"ui.material_review.claims": "Les sources et licences sont des déclarations non vérifiées. Les valeurs absentes restent inconnues.",
|
||||
"ui.material_review.density": "Masse volumique",
|
||||
"ui.material_review.elasticity": "Élasticité",
|
||||
"ui.material_review.tensile_strength": "Résistance à la traction",
|
||||
"ui.material_review.compressive_strength": "Résistance à la compression",
|
||||
"ui.material_review.yield_strength": "Limite élastique",
|
||||
"ui.material_review.shear_strength": "Résistance au cisaillement",
|
||||
"ui.material_review.thermal_conductivity": "Conductivité thermique",
|
||||
"ui.material_review.thermal_expansion": "Dilatation thermique",
|
||||
"ui.material_review.specific_heat": "Capacité thermique massique",
|
||||
"ui.material_review.moisture_unit": "kg eau / kg matériau sec",
|
||||
"ui.coil": "Hélice",
|
||||
"ui.coincident": "Coïncidence",
|
||||
"ui.collapse": "Réduire",
|
||||
|
||||
@@ -370,6 +370,28 @@
|
||||
"ui.click_selects_whole_bodies": "クリックでボディ全体を選択",
|
||||
"ui.click_then_press_key": "クリックした後、割り当てるキーを押してください",
|
||||
"ui.close": "閉じる",
|
||||
"ui.material_review.title": "物理材料データを確認",
|
||||
"ui.material_review.scope": "候補のみ:割り当てや保存は行いません。数値検証は工学的認証ではありません。",
|
||||
"ui.material_review.input": "スキーマ1の材料カードを貼り付け(JSON)",
|
||||
"ui.material_review.review": "カードを確認",
|
||||
"ui.material_review.limit": "カードが256 KiBの入力上限を超えています。",
|
||||
"ui.material_review.unknown": "不明",
|
||||
"ui.material_review.properties": "特性",
|
||||
"ui.material_review.conditions": "記載された条件",
|
||||
"ui.material_review.source": "出典",
|
||||
"ui.material_review.axes": "材料ローカル軸。ボディの方向は割り当てません。",
|
||||
"ui.material_review.derived": "導出された等方性弾性率",
|
||||
"ui.material_review.claims": "出典とライセンスの申告は未確認です。欠落値は不明のままです。",
|
||||
"ui.material_review.density": "密度",
|
||||
"ui.material_review.elasticity": "弾性",
|
||||
"ui.material_review.tensile_strength": "引張強度",
|
||||
"ui.material_review.compressive_strength": "圧縮強度",
|
||||
"ui.material_review.yield_strength": "降伏強度",
|
||||
"ui.material_review.shear_strength": "せん断強度",
|
||||
"ui.material_review.thermal_conductivity": "熱伝導率",
|
||||
"ui.material_review.thermal_expansion": "熱膨張",
|
||||
"ui.material_review.specific_heat": "比熱",
|
||||
"ui.material_review.moisture_unit": "kg 水 / kg 乾燥材料",
|
||||
"ui.coil": "コイル",
|
||||
"ui.coincident": "一致",
|
||||
"ui.collapse": "折りたたむ",
|
||||
|
||||
@@ -85,6 +85,28 @@
|
||||
"ui.clear_recent": "최근 항목 지우기",
|
||||
"ui.clear_the_selection": "선택 해제",
|
||||
"ui.close": "닫기",
|
||||
"ui.material_review.title": "물리적 재료 데이터 검토",
|
||||
"ui.material_review.scope": "후보만 검토합니다. 지정하거나 저장하지 않습니다. 수치 검증은 공학적 인증이 아닙니다.",
|
||||
"ui.material_review.input": "스키마 1 재료 카드 붙여넣기 (JSON)",
|
||||
"ui.material_review.review": "카드 검토",
|
||||
"ui.material_review.limit": "카드가 256 KiB 입력 한도를 초과합니다.",
|
||||
"ui.material_review.unknown": "알 수 없음",
|
||||
"ui.material_review.properties": "물성",
|
||||
"ui.material_review.conditions": "명시된 조건",
|
||||
"ui.material_review.source": "출처",
|
||||
"ui.material_review.axes": "재료 로컬 축입니다. 바디 방향을 지정하지 않습니다.",
|
||||
"ui.material_review.derived": "유도된 등방성 탄성 계수",
|
||||
"ui.material_review.claims": "출처와 라이선스는 검증되지 않은 주장입니다. 누락된 값은 알 수 없는 상태로 유지됩니다.",
|
||||
"ui.material_review.density": "밀도",
|
||||
"ui.material_review.elasticity": "탄성",
|
||||
"ui.material_review.tensile_strength": "인장 강도",
|
||||
"ui.material_review.compressive_strength": "압축 강도",
|
||||
"ui.material_review.yield_strength": "항복 강도",
|
||||
"ui.material_review.shear_strength": "전단 강도",
|
||||
"ui.material_review.thermal_conductivity": "열전도율",
|
||||
"ui.material_review.thermal_expansion": "열팽창",
|
||||
"ui.material_review.specific_heat": "비열",
|
||||
"ui.material_review.moisture_unit": "kg 물 / kg 건조 재료",
|
||||
"ui.coil": "코일",
|
||||
"ui.coincident": "일치",
|
||||
"ui.collapse": "접기",
|
||||
|
||||
@@ -345,6 +345,28 @@
|
||||
"ui.click_selects_whole_bodies": "Щелчок выделяет тела целиком",
|
||||
"ui.click_then_press_key": "Щёлкните, затем нажмите новую клавишу",
|
||||
"ui.close": "Закрыть",
|
||||
"ui.material_review.title": "Проверка физических данных материала",
|
||||
"ui.material_review.scope": "Только кандидат: без назначения и сохранения. Численная проверка не является инженерной сертификацией.",
|
||||
"ui.material_review.input": "Вставьте карточку материала по схеме 1 (JSON)",
|
||||
"ui.material_review.review": "Проверить карточку",
|
||||
"ui.material_review.limit": "Карточка превышает предел ввода 256 КиБ.",
|
||||
"ui.material_review.unknown": "Неизвестно",
|
||||
"ui.material_review.properties": "Свойства",
|
||||
"ui.material_review.conditions": "Указанные условия",
|
||||
"ui.material_review.source": "Источник",
|
||||
"ui.material_review.axes": "Локальные оси материала; ориентация тела не назначается.",
|
||||
"ui.material_review.derived": "Вычисленные изотропные модули",
|
||||
"ui.material_review.claims": "Источники и лицензии указаны без проверки. Отсутствующие значения остаются неизвестными.",
|
||||
"ui.material_review.density": "Плотность",
|
||||
"ui.material_review.elasticity": "Упругость",
|
||||
"ui.material_review.tensile_strength": "Прочность при растяжении",
|
||||
"ui.material_review.compressive_strength": "Прочность при сжатии",
|
||||
"ui.material_review.yield_strength": "Предел текучести",
|
||||
"ui.material_review.shear_strength": "Прочность при сдвиге",
|
||||
"ui.material_review.thermal_conductivity": "Теплопроводность",
|
||||
"ui.material_review.thermal_expansion": "Тепловое расширение",
|
||||
"ui.material_review.specific_heat": "Удельная теплоёмкость",
|
||||
"ui.material_review.moisture_unit": "кг воды / кг сухого материала",
|
||||
"ui.coil": "Спираль",
|
||||
"ui.coincident": "Совпадение",
|
||||
"ui.collapse": "Свернуть",
|
||||
|
||||
@@ -349,6 +349,28 @@
|
||||
"ui.click_selects_whole_bodies": "Клацання виділяє тіла цілком",
|
||||
"ui.click_then_press_key": "Клацніть, а потім натисніть нову клавішу",
|
||||
"ui.close": "Закрити",
|
||||
"ui.material_review.title": "Перевірка фізичних даних матеріалу",
|
||||
"ui.material_review.scope": "Лише кандидат: без призначення та збереження. Числова перевірка не є інженерною сертифікацією.",
|
||||
"ui.material_review.input": "Вставте картку матеріалу за схемою 1 (JSON)",
|
||||
"ui.material_review.review": "Перевірити картку",
|
||||
"ui.material_review.limit": "Картка перевищує межу введення 256 КіБ.",
|
||||
"ui.material_review.unknown": "Невідомо",
|
||||
"ui.material_review.properties": "Властивості",
|
||||
"ui.material_review.conditions": "Зазначені умови",
|
||||
"ui.material_review.source": "Джерело",
|
||||
"ui.material_review.axes": "Локальні осі матеріалу; орієнтація тіла не призначається.",
|
||||
"ui.material_review.derived": "Обчислені ізотропні модулі",
|
||||
"ui.material_review.claims": "Джерела та ліцензії зазначені без перевірки. Відсутні значення залишаються невідомими.",
|
||||
"ui.material_review.density": "Густина",
|
||||
"ui.material_review.elasticity": "Пружність",
|
||||
"ui.material_review.tensile_strength": "Міцність на розтяг",
|
||||
"ui.material_review.compressive_strength": "Міцність на стиск",
|
||||
"ui.material_review.yield_strength": "Межа плинності",
|
||||
"ui.material_review.shear_strength": "Міцність на зсув",
|
||||
"ui.material_review.thermal_conductivity": "Теплопровідність",
|
||||
"ui.material_review.thermal_expansion": "Теплове розширення",
|
||||
"ui.material_review.specific_heat": "Питома теплоємність",
|
||||
"ui.material_review.moisture_unit": "кг води / кг сухого матеріалу",
|
||||
"ui.coil": "Спіраль",
|
||||
"ui.coincident": "Співпадання",
|
||||
"ui.collapse": "Згорнути",
|
||||
|
||||
@@ -369,6 +369,28 @@
|
||||
"ui.click_selects_whole_bodies": "单击选择整个实体",
|
||||
"ui.click_then_press_key": "单击,然后按新按键",
|
||||
"ui.close": "关闭",
|
||||
"ui.material_review.title": "检查物理材料数据",
|
||||
"ui.material_review.scope": "仅检查候选数据:不指定或保存。数值验证不等于工程认证。",
|
||||
"ui.material_review.input": "粘贴架构版本1的材料卡(JSON)",
|
||||
"ui.material_review.review": "检查材料卡",
|
||||
"ui.material_review.limit": "材料卡超过256 KiB输入限制。",
|
||||
"ui.material_review.unknown": "未知",
|
||||
"ui.material_review.properties": "属性",
|
||||
"ui.material_review.conditions": "声明的条件",
|
||||
"ui.material_review.source": "来源",
|
||||
"ui.material_review.axes": "材料局部坐标轴;不指定实体方向。",
|
||||
"ui.material_review.derived": "推导的各向同性模量",
|
||||
"ui.material_review.claims": "来源和许可证声明未经核实。缺失值保持未知。",
|
||||
"ui.material_review.density": "密度",
|
||||
"ui.material_review.elasticity": "弹性",
|
||||
"ui.material_review.tensile_strength": "抗拉强度",
|
||||
"ui.material_review.compressive_strength": "抗压强度",
|
||||
"ui.material_review.yield_strength": "屈服强度",
|
||||
"ui.material_review.shear_strength": "抗剪强度",
|
||||
"ui.material_review.thermal_conductivity": "导热系数",
|
||||
"ui.material_review.thermal_expansion": "热膨胀",
|
||||
"ui.material_review.specific_heat": "比热容",
|
||||
"ui.material_review.moisture_unit": "kg 水 / kg 干燥材料",
|
||||
"ui.coil": "螺旋",
|
||||
"ui.coincident": "重合",
|
||||
"ui.collapse": "折叠",
|
||||
|
||||
@@ -58,6 +58,7 @@ pub mod inference;
|
||||
pub mod install;
|
||||
pub mod issue_report;
|
||||
pub mod keymap;
|
||||
mod material_card_review;
|
||||
#[cfg(test)]
|
||||
mod menu_tests;
|
||||
#[cfg(test)]
|
||||
@@ -273,6 +274,7 @@ pub struct SolveApp {
|
||||
pub params: Option<params_dialog::ParamsDialog>,
|
||||
/// The Design Properties window, when open.
|
||||
pub design_properties: Option<design_properties::DesignProperties>,
|
||||
material_card_review: Option<material_card_review::ReviewWindow>,
|
||||
pub palette: palette::Palette,
|
||||
/// Context menus, rename box and Properties window.
|
||||
pub menu: context_menu::MenuState,
|
||||
@@ -391,6 +393,7 @@ impl SolveApp {
|
||||
dialog: None,
|
||||
params: None,
|
||||
design_properties: None,
|
||||
material_card_review: None,
|
||||
palette: palette::Palette::default(),
|
||||
menu: context_menu::MenuState::default(),
|
||||
tree: browser::TreeState::default(),
|
||||
@@ -667,6 +670,11 @@ impl SolveApp {
|
||||
self.set_status(format!("{label}: {why}"), true);
|
||||
return;
|
||||
}
|
||||
if id == "material.properties.validate" {
|
||||
self.material_card_review = Some(material_card_review::ReviewWindow::default());
|
||||
self.home.open = false;
|
||||
return;
|
||||
}
|
||||
// Another command ends Edit Text (its dialog is not a feature edit to keep).
|
||||
sketch_text_move::cancel_drag(self);
|
||||
if sketch_text::editing(self) || self.dialog.as_ref().is_some_and(|d| matches!(d.kind, dialogs::Kind::TextMove(_))) {
|
||||
@@ -1315,6 +1323,7 @@ impl SolveApp {
|
||||
self.resolve_fit(ui.ctx());
|
||||
params_dialog::show(self, ui.ctx());
|
||||
design_properties::show(self, ui.ctx());
|
||||
material_card_review::show(self, ui.ctx());
|
||||
recovery_ui::show(self, ui.ctx());
|
||||
stand_up::show(self, ui.ctx());
|
||||
prefs::show_reset_notice(self, ui.ctx());
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
//! Candidate review through the existing validator, independent of assigned-card persistence.
|
||||
use egui::RichText;
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::doc::physical_material::{Family, MaterialCard, PropertyData, admit_card};
|
||||
|
||||
use crate::{SolveApp, i18n::tr, theme::Tokens};
|
||||
|
||||
const MAX_BYTES: usize = 256 * 1024;
|
||||
const KINDS: [&str; 9] = [
|
||||
"density",
|
||||
"elasticity",
|
||||
"tensile_strength",
|
||||
"compressive_strength",
|
||||
"yield_strength",
|
||||
"shear_strength",
|
||||
"thermal_conductivity",
|
||||
"thermal_expansion",
|
||||
"specific_heat",
|
||||
];
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ReviewWindow {
|
||||
input: String,
|
||||
reviewed: Option<(MaterialCard, Value)>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl ReviewWindow {
|
||||
fn review(&mut self, app: &mut SolveApp) {
|
||||
self.reviewed = None;
|
||||
let result = (|| {
|
||||
if self.input.len() > MAX_BYTES {
|
||||
return Err(tr("ui.material_review.limit").to_string());
|
||||
}
|
||||
let value: Value = serde_json::from_str(&self.input).map_err(|e| e.to_string())?;
|
||||
let report = app.run("material.properties.validate", json!({"card":value}))?;
|
||||
let card = admit_card(&value).map_err(|e| e.to_string())?;
|
||||
Ok((card, report))
|
||||
})();
|
||||
match result {
|
||||
Ok(reviewed) => {
|
||||
self.reviewed = Some(reviewed);
|
||||
self.error = None;
|
||||
}
|
||||
Err(error) => self.error = Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Open only through ui.start; executing the validator never opens a window.
|
||||
pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
let Some(mut window) = app.material_card_review.take() else { return };
|
||||
let mut open = true;
|
||||
let mut close = false;
|
||||
crate::frame::window(ctx, tr("ui.material_review.title"), crate::frame::Width::Wide)
|
||||
.id(egui::Id::new("sc_material_card_review"))
|
||||
.open(&mut open)
|
||||
.show(ctx, |ui| {
|
||||
ui.label(tr("ui.material_review.scope"));
|
||||
egui::ScrollArea::vertical().max_height((ctx.content_rect().height() - 180.0).max(100.0)).show(ui, |ui| {
|
||||
ui.label(tr("ui.material_review.input"));
|
||||
let response = ui.add(
|
||||
egui::TextEdit::multiline(&mut window.input)
|
||||
.id(egui::Id::new("sc_material_card_input"))
|
||||
.desired_width(f32::INFINITY)
|
||||
.desired_rows(5)
|
||||
.char_limit(MAX_BYTES),
|
||||
);
|
||||
if response.changed() {
|
||||
window.reviewed = None;
|
||||
window.error = None;
|
||||
}
|
||||
if ui.button(tr("ui.material_review.review")).clicked() {
|
||||
window.review(app);
|
||||
}
|
||||
if let Some(error) = &window.error {
|
||||
ui.colored_label(Tokens::get().error, error);
|
||||
}
|
||||
if let Some((card, report)) = &window.reviewed {
|
||||
show_card(ui, card, report);
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
close = ui.button(tr("ui.close")).clicked();
|
||||
});
|
||||
if open && !close {
|
||||
app.material_card_review = Some(window);
|
||||
}
|
||||
}
|
||||
|
||||
fn value(ui: &mut egui::Ui, field: &str, value: Option<String>, unit: &str) {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.monospace(field);
|
||||
ui.label(value.unwrap_or_else(|| tr("ui.material_review.unknown").to_string()));
|
||||
ui.label(unit);
|
||||
});
|
||||
}
|
||||
|
||||
fn numbers(ui: &mut egui::Ui, field: &str, values: &[f64], unit: &str) {
|
||||
value(ui, field, Some(values.iter().map(|v| format!("{v:e}")).collect::<Vec<_>>().join(", ")), unit);
|
||||
}
|
||||
|
||||
fn show_card(ui: &mut egui::Ui, card: &MaterialCard, report: &Value) {
|
||||
ui.separator();
|
||||
ui.heading(&card.designation);
|
||||
// Schema identifiers are inert metadata, not translated source claims or assigned body axes.
|
||||
let family = serde_json::to_value(card.family).unwrap_or(Value::Null);
|
||||
value(ui, "family", family.as_str().map(str::to_string), "");
|
||||
ui.strong(tr("ui.material_review.properties"));
|
||||
for kind in KINDS {
|
||||
let label = format!("ui.material_review.{kind}");
|
||||
let Some(property) = card.properties.iter().find(|p| p.data.kind() == kind) else {
|
||||
ui.label(format!("{}: {}", tr(&label), tr("ui.material_review.unknown")));
|
||||
continue;
|
||||
};
|
||||
egui::CollapsingHeader::new(tr(&label)).id_salt(kind).default_open(true).show(ui, |ui| {
|
||||
show_data(ui, &property.data);
|
||||
if let Some(axes) = serde_json::to_value(&property.data)
|
||||
.ok()
|
||||
.and_then(|d| d.get("axes").or_else(|| d.get("model").and_then(|m| m.get("axes"))).and_then(Value::as_str).map(str::to_string))
|
||||
{
|
||||
let axes = if axes == "wood_lrt" { "wood_lrt (1=L, 2=R, 3=T)".to_string() } else { axes };
|
||||
value(ui, "axes", Some(axes), "");
|
||||
ui.label(tr("ui.material_review.axes"));
|
||||
}
|
||||
ui.strong(tr("ui.material_review.conditions"));
|
||||
let c = &property.conditions;
|
||||
value(ui, "temperature_k", c.temperature_k.map(|v| v.to_string()), "K");
|
||||
value(ui, "state", c.state.clone(), "");
|
||||
if card.family == Family::Wood || c.moisture_dry_basis_kg_kg.is_some() {
|
||||
value(ui, "moisture_dry_basis_kg_kg", c.moisture_dry_basis_kg_kg.map(|v| v.to_string()), tr("ui.material_review.moisture_unit"));
|
||||
}
|
||||
if card.family == Family::Polymer || c.strain_rate_per_s.is_some() {
|
||||
value(ui, "strain_rate_per_s", c.strain_rate_per_s.map(|v| v.to_string()), "s⁻¹");
|
||||
}
|
||||
ui.strong(tr("ui.material_review.source"));
|
||||
if let Some(source) = card.sources.iter().find(|s| s.id == property.source_id) {
|
||||
ui.label(&source.title);
|
||||
value(ui, "source_id", Some(source.id.clone()), "");
|
||||
value(ui, "kind", serde_json::to_value(source.kind).ok().and_then(|v| v.as_str().map(str::to_string)), "");
|
||||
value(ui, "license", source.license.clone(), "");
|
||||
value(ui, "reference", source.reference.clone(), "");
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some(moduli) = report.get("validation").and_then(|v| v.get("isotropic_moduli")).filter(|v| v.is_object()) {
|
||||
ui.strong(tr("ui.material_review.derived"));
|
||||
for field in ["shear_pa", "bulk_pa"] {
|
||||
value(ui, field, moduli.get(field).and_then(Value::as_f64).map(|v| format!("{v:e}")), "Pa");
|
||||
}
|
||||
}
|
||||
ui.label(RichText::new(tr("ui.material_review.claims")).small().color(Tokens::get().text_dim));
|
||||
}
|
||||
|
||||
fn show_data(ui: &mut egui::Ui, data: &PropertyData) {
|
||||
match data {
|
||||
PropertyData::Density { kg_m3 } => numbers(ui, "density", &[*kg_m3], "kg/m³"),
|
||||
PropertyData::Elasticity { model } => match model {
|
||||
solidcraft_engine::doc::physical_material::Elasticity::Isotropic { young_modulus_pa, poisson_ratio } => {
|
||||
numbers(ui, "E", &[*young_modulus_pa], "Pa");
|
||||
numbers(ui, "ν", &[*poisson_ratio], "");
|
||||
}
|
||||
solidcraft_engine::doc::physical_material::Elasticity::Orthotropic { young_moduli_pa, shear_moduli_pa, poisson_ratios, .. } => {
|
||||
numbers(ui, "E1, E2, E3", young_moduli_pa, "Pa");
|
||||
numbers(ui, "G12, G23, G31", shear_moduli_pa, "Pa");
|
||||
numbers(ui, "ν12, ν23, ν31", poisson_ratios, "");
|
||||
}
|
||||
},
|
||||
PropertyData::TensileStrength { pa } | PropertyData::CompressiveStrength { pa } | PropertyData::YieldStrength { pa } => {
|
||||
numbers(ui, "σ", &[*pa], "Pa");
|
||||
}
|
||||
PropertyData::ShearStrength { pa } => numbers(ui, "τ", &[*pa], "Pa"),
|
||||
PropertyData::ThermalConductivity { w_m_k, .. } => numbers(ui, "k1, k2, k3", w_m_k, "W/(m·K)"),
|
||||
PropertyData::ThermalExpansion { per_k, .. } => numbers(ui, "α1, α2, α3", per_k, "K⁻¹"),
|
||||
PropertyData::SpecificHeat { j_kg_k } => numbers(ui, "c", &[*j_kg_k], "J/(kg·K)"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scenario::Harness;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn candidate() -> Value {
|
||||
json!({"schema_version":1,"designation":"Original synthetic candidate","family":"wood",
|
||||
"sources":[{"id":"example","title":"Original illustrative <b>source</b>","kind":"illustrative",
|
||||
"license":"Unverified licence claim","reference":"https://invalid.example/not-fetched"}],
|
||||
"properties":[{"source_id":"example","data":{"kind":"elasticity","model":{
|
||||
"symmetry":"isotropic","young_modulus_pa":12e9,"poisson_ratio":0.25}},
|
||||
"conditions":{"temperature_k":293.15,"moisture_dry_basis_kg_kg":0.,"state":"synthetic specimen"}}]})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicked_candidate_review_and_close_preserve_design_history_and_unknowns() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("solid.box", json!({"length":10,"width":10,"height":10})).unwrap();
|
||||
h.app.run("edit.undo", json!({})).unwrap();
|
||||
let doc = h.app.session.doc.clone();
|
||||
let before = (
|
||||
h.app.session.revision,
|
||||
h.app.session.model.is_stale(),
|
||||
h.app.session.undo.len(),
|
||||
h.app.session.redo.len(),
|
||||
h.app.session.selection.clone(),
|
||||
);
|
||||
h.step(&json!({"start":"material.properties.validate"})).unwrap();
|
||||
h.app.material_card_review.as_mut().unwrap().input = candidate().to_string();
|
||||
h.frames(2);
|
||||
h.step(&json!({"widget":"Review card"})).unwrap();
|
||||
let (card, report) = h.app.material_card_review.as_ref().unwrap().reviewed.as_ref().unwrap();
|
||||
assert_eq!(card.designation, "Original synthetic candidate");
|
||||
assert_eq!(card.properties.len(), 1, "missing values were not filled with zeros");
|
||||
assert_eq!(card.sources[0].title, "Original illustrative <b>source</b>");
|
||||
assert_eq!(card.properties[0].conditions.moisture_dry_basis_kg_kg, Some(0.));
|
||||
assert_eq!(report["validation"]["isotropic_moduli"], json!({"shear_pa":4.8e9,"bulk_pa":8e9}));
|
||||
assert_eq!(report["validation"]["source_review_required"], json!(["example"]));
|
||||
assert!(h.widget("Original synthetic candidate").is_some());
|
||||
assert!(h.widget("Density: Unknown").is_some());
|
||||
assert!(h.widget("Pa").is_some());
|
||||
h.ctx.memory_mut(|m| m.request_focus(egui::Id::new("sc_material_card_input")));
|
||||
h.step(&json!({"text":" "})).unwrap();
|
||||
assert!(h.app.material_card_review.as_ref().unwrap().reviewed.is_none(), "editing invalidates the old report");
|
||||
h.step(&json!({"widget":"Close"})).unwrap();
|
||||
assert!(h.app.material_card_review.is_none());
|
||||
assert!(Arc::ptr_eq(&doc, &h.app.session.doc));
|
||||
assert_eq!(
|
||||
before,
|
||||
(
|
||||
h.app.session.revision,
|
||||
h.app.session.model.is_stale(),
|
||||
h.app.session.undo.len(),
|
||||
h.app.session.redo.len(),
|
||||
h.app.session.selection.clone()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_and_oversized_candidates_never_keep_a_previous_review() {
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
|
||||
let doc = app.session.doc.clone();
|
||||
let mut window = ReviewWindow { input: candidate().to_string(), ..Default::default() };
|
||||
window.review(&mut app);
|
||||
assert!(window.reviewed.is_some());
|
||||
for input in ["[".into(), "{\"schema_version\":2}".into(), "é".repeat(MAX_BYTES / 2 + 1)] {
|
||||
window.input = input;
|
||||
window.review(&mut app);
|
||||
assert!(window.reviewed.is_none());
|
||||
assert!(window.error.is_some());
|
||||
}
|
||||
assert!(window.error.as_deref().unwrap().contains("256 KiB"));
|
||||
assert!(Arc::ptr_eq(&doc, &app.session.doc));
|
||||
assert!(app.session.undo.is_empty() && app.session.redo.is_empty());
|
||||
assert!(!app.session.is_dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_property_variant_renders_its_declared_si_unit_and_inert_source() {
|
||||
let fixtures = [
|
||||
(json!({"kind":"density","kg_m3":600.}), "kg/m³"),
|
||||
(
|
||||
json!({"kind":"elasticity","model":{"symmetry":"orthotropic","axes":"wood_lrt",
|
||||
"young_moduli_pa":[12e9,3e9,1e9],"shear_moduli_pa":[2e9,1e9,0.5e9],"poisson_ratios":[0.,0.,0.]}}),
|
||||
"Pa",
|
||||
),
|
||||
(json!({"kind":"tensile_strength","pa":1e8}), "Pa"),
|
||||
(json!({"kind":"compressive_strength","pa":1e8}), "Pa"),
|
||||
(json!({"kind":"yield_strength","pa":1e8}), "Pa"),
|
||||
(json!({"kind":"shear_strength","pa":1e8}), "Pa"),
|
||||
(json!({"kind":"thermal_conductivity","w_m_k":[1.,2.,3.],"axes":"material123"}), "W/(m·K)"),
|
||||
(json!({"kind":"thermal_expansion","per_k":[-1e-6,0.,2e-6],"axes":"material123"}), "K⁻¹"),
|
||||
(json!({"kind":"specific_heat","j_kg_k":500.}), "J/(kg·K)"),
|
||||
];
|
||||
for (data, unit) in fixtures {
|
||||
let mut h = Harness::new();
|
||||
h.app.start("material.properties.validate");
|
||||
let mut card = candidate();
|
||||
card["properties"][0]["data"] = data;
|
||||
h.app.material_card_review.as_mut().unwrap().input = card.to_string();
|
||||
h.frames(2);
|
||||
h.step(&json!({"widget":"Review card"})).unwrap();
|
||||
assert!(h.app.material_card_review.as_ref().unwrap().error.is_none(), "{card}");
|
||||
assert!(h.widget(unit).is_some(), "expected {unit} for {card}");
|
||||
assert!(h.widget("Original illustrative <b>source</b>").is_some(), "source is displayed as literal text");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executing_json_validation_does_not_open_the_inspector() {
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
|
||||
app.run("material.properties.validate", json!({"card":candidate()})).unwrap();
|
||||
assert!(app.material_card_review.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reviewer_messages_exist_in_every_locale() {
|
||||
let keys = [
|
||||
"title",
|
||||
"scope",
|
||||
"input",
|
||||
"review",
|
||||
"limit",
|
||||
"unknown",
|
||||
"properties",
|
||||
"conditions",
|
||||
"source",
|
||||
"axes",
|
||||
"derived",
|
||||
"claims",
|
||||
"moisture_unit",
|
||||
];
|
||||
for catalog in crate::i18n::LOCALES {
|
||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/locales").join(format!("{}.json", catalog.id));
|
||||
let messages: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
|
||||
for key in keys.into_iter().chain(KINDS) {
|
||||
let key = format!("ui.material_review.{key}");
|
||||
assert!(messages.get(&key).and_then(Value::as_str).is_some_and(|s| !s.is_empty()), "{}: {key}", catalog.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,26 @@ advanced rendering. The result reports `analysis_solver_available: false` and
|
||||
|
||||
## Contract (standalone card schema 1)
|
||||
|
||||
### Candidate review window
|
||||
|
||||
Search for **Validate Physical Material Data** in the command palette (or use
|
||||
`ui.start` with `material.properties.validate`) to open the candidate reviewer.
|
||||
Paste a schema-1 card object and choose **Review card**. The existing engine
|
||||
validator checks it; the window shows known properties in SI units, unknown
|
||||
properties, per-property conditions and source claims as inert text. Material-local
|
||||
axes do not assign a frame to a body. Isotropic G/K come from the engine report.
|
||||
|
||||
Editing the input clears the previous review. Invalid input shows its refusal
|
||||
without retaining a stale valid result. Input is capped at 262,144 characters and
|
||||
256 KiB before JSON parsing; the validator's tighter structural and text budgets
|
||||
still apply. The window does not read files, fetch references, assign properties,
|
||||
replace density, modify history or save the candidate. Close discards it. Calling
|
||||
the engine's JSON command still returns its report without opening a dialog.
|
||||
|
||||
This main-based review unit is independent of assigned-card persistence, local
|
||||
file import and the design-owned inspector follow-up in #1094. No dataset or
|
||||
rendering capability is supplied by reviewing a candidate.
|
||||
|
||||
A card has `schema_version: 1`, `designation`, `family` (`wood`, `polymer`, `glass`,
|
||||
`metal`, `other`), `sources` and `properties`. Source IDs are exact, case-sensitive
|
||||
local references. Each source has `id`, `title`, `kind` (`user_supplied`,
|
||||
|
||||
Reference in new issue
Block a user