use crate::{
args::CensusArgs,
load::{Loaded, from_bytes, load_path},
materials::Catalogue,
outcome::CliError,
};
use indicatrix_cut_core::{
ConstraintTier, Design, MaterialSelection, PreformSpec, ScheduleMeta,
native::{DesignExtras, design_to_string},
templates::TEMPLATES,
};
use indicatrix_editor::{loading::design_from_asc_text, templates::gallery_design};
use indicatrix_vault::{
db::sqlite::{Database, DisplayFilters},
local::{
design_attachment_position, design_file_to_asc_text, native_design_attachment_position,
},
model::{entry::FullDiagramRecord, filter::RangeFilter, frosting::StoredTierFrosting},
};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Fixture {
Concave,
Brilliant,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum SourceRef {
Template(i32),
Fixture(Fixture),
File(PathBuf),
Library(i64),
}
pub(super) struct Subject {
pub source: &'static str,
pub key: String,
pub title: String,
pub design: Result<Design, String>,
pub loaded: Option<Loaded>,
pub mast_decimals: Option<usize>,
}
pub(super) fn open_library(path: &Path) -> Result<Database, CliError> {
Database::open_read_only(&path.to_string_lossy()).map_err(|error| {
CliError::io(format!(
"cannot open the design library {}: {error:#}",
path.display()
))
})
}
pub(super) fn plan_sources(
args: &CensusArgs,
library: Option<&Database>,
) -> Result<Vec<SourceRef>, CliError> {
let mut sources = Vec::new();
if args.builtins.templates {
for number in 1..=TEMPLATES.len() {
if let Ok(number) = i32::try_from(number) {
sources.push(SourceRef::Template(number));
}
}
}
if args.builtins.fixtures {
sources.push(SourceRef::Fixture(Fixture::Concave));
sources.push(SourceRef::Fixture(Fixture::Brilliant));
}
sources.extend(args.files.iter().cloned().map(SourceRef::File));
if let Some(database) = library {
let mut ids = if args.all {
database
.matching_entry_ids(
"",
"All",
"All",
&RangeFilter::default(),
DisplayFilters::default(),
)
.map_err(|error| {
CliError::io(format!("cannot list the design library: {error:#}"))
})?
} else {
args.ids.clone()
};
ids.sort_unstable();
ids.dedup();
let wanted = ids.into_iter().skip(args.offset);
match args.limit {
Some(limit) => sources.extend(wanted.take(limit).map(SourceRef::Library)),
None => sources.extend(wanted.map(SourceRef::Library)),
}
}
Ok(sources)
}
pub(super) fn open_subject(source: &SourceRef, library: Option<&Database>) -> Subject {
match source {
SourceRef::Template(number) => template_subject(*number),
SourceRef::Fixture(fixture) => fixture_subject(*fixture),
SourceRef::File(path) => file_subject(path),
SourceRef::Library(id) => library_subject(*id, library),
}
}
fn template_subject(number: i32) -> Subject {
let name = usize::try_from(number - 1)
.ok()
.and_then(|index| TEMPLATES.get(index))
.map_or("template", |spec| spec.name);
Subject {
source: "template",
key: number.to_string(),
title: name.to_string(),
design: Ok(gallery_design(number)),
loaded: None,
mast_decimals: None,
}
}
fn brilliant_at_172() -> Design {
let mut design = Design::new(
PreformSpec::block(2.0, 1.0, 4.0),
ScheduleMeta::standard_round_brilliant(),
ConstraintTier::standard_round_brilliant(),
);
design.material = MaterialSelection {
name: None,
specific_gravity_override: None,
refractive_index_override: Some(1.72),
body_color_override: None,
body_color_bands_override: None,
absorption_path_scale_override: None,
};
design.ensure_tier_ids();
design
}
fn fixture_subject(fixture: Fixture) -> Subject {
let (key, title, design) = match fixture {
Fixture::Concave => (
"concave_fixture",
"Concave Fixture",
Design::concave_fixture(),
),
Fixture::Brilliant => ("brilliant_172", "Brilliant at 1.72", brilliant_at_172()),
};
Subject {
source: "fixture",
key: key.to_string(),
title: title.to_string(),
design: Ok(design),
loaded: None,
mast_decimals: None,
}
}
fn file_subject(path: &Path) -> Subject {
let key = path.file_name().map_or_else(
|| path.display().to_string(),
|name| name.to_string_lossy().into_owned(),
);
match load_path(path) {
Ok(loaded) => Subject {
source: "file",
title: loaded.name.clone(),
key,
design: Ok(loaded.design.clone()),
mast_decimals: loaded.mast_decimals,
loaded: Some(loaded),
},
Err(error) => Subject {
source: "file",
title: key.clone(),
key,
design: Err(error.message),
loaded: None,
mast_decimals: None,
},
}
}
fn library_subject(id: i64, library: Option<&Database>) -> Subject {
let failed = |title: String, why: String| Subject {
source: "library",
key: id.to_string(),
title,
design: Err(why),
loaded: None,
mast_decimals: None,
};
let Some(database) = library else {
return failed(String::new(), "no design library".to_string());
};
let full = match database.get_diagram_full(id) {
Ok(Some(full)) => full,
Ok(None) => return failed(String::new(), "no such library entry".to_string()),
Err(error) => return failed(String::new(), format!("cannot read the entry: {error:#}")),
};
match design_from_record(&full) {
Ok((design, loaded, mast_decimals)) => Subject {
source: "library",
key: id.to_string(),
title: full.title,
design: Ok(design),
loaded,
mast_decimals,
},
Err(why) => failed(full.title, why),
}
}
pub(super) fn design_from_record(
full: &FullDiagramRecord,
) -> Result<(Design, Option<Loaded>, Option<usize>), String> {
let names = || full.attached_files.iter().map(|file| file.name.as_str());
if let Some(position) = native_design_attachment_position(names()) {
let file = &full.attached_files[position];
let loaded = from_bytes(&file.name, &file.content).map_err(|error| error.message)?;
return Ok((loaded.design.clone(), Some(loaded), None));
}
let (position, kind) =
design_attachment_position(names()).ok_or_else(|| "no design file".to_string())?;
let file = &full.attached_files[position];
let text = design_file_to_asc_text(&file.name, kind, &file.content)
.map_err(|error| format!("{}: {error}", file.name))?;
let frosting = if full.frosting.is_empty() {
text.frosting
} else {
StoredTierFrosting::to_gcs(&full.frosting)
};
let mut loaded = design_from_asc_text(
&text.asc_file_name,
&text.asc_text,
full.lw_ratio.as_deref(),
)
.map_err(|error| format!("{}: {error}", file.name))?;
loaded.design.apply_imported_frosting(&frosting);
let decimals = crate::load::mast_decimals(&text.asc_text);
Ok((loaded.design, None, decimals))
}
pub(super) fn design_text(
subject: &Subject,
design: &Design,
catalogue: &Catalogue,
) -> Result<String, CliError> {
if let Some(loaded) = &subject.loaded {
return loaded.design_text(design, catalogue);
}
let snapshot = design
.material
.name
.as_deref()
.and_then(|name| catalogue.library_snapshot(name));
let extras = DesignExtras {
custom_material: snapshot,
history_entries: &[],
metadata: None,
attachments: &[],
};
design_to_string(design, None, &extras).map_err(|error| {
CliError::design(format!("the design cannot be written as a file: {error}"))
})
}