1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
6 use crate::*; // to get ambassador_impls, macro resolution trouble
7 pub use crate::shapelib_toml::*;
9 pub use crate::prelude::GoodItemName; // not sure why this is needed
11 use parking_lot::{const_rwlock, RwLock};
12 use parking_lot::RwLockReadGuard;
14 use ShapelibConfig1 as Config1;
15 use ShapelibExplicit1 as Explicit1;
17 //==================== structs and definitions ====================
20 // *Data, *List from toml etc. (processed if need be)
21 // *Defn raw read from library toml file (where different from Info)
22 // *Details some shared structure
23 // Item } once loaded and part of a game,
24 // Outline } no Arc's as we serialise/deserialize during save/load
26 static GLOBAL_SHAPELIBS: RwLock<Option<Registry>> = const_rwlock(None);
30 libs: HashMap<String, Vec<shapelib::Catalogue>>,
34 pub struct GroupData {
37 #[allow(dead_code)] /*TODO*/ mformat: materials_format::Version,
40 #[derive(Debug,Clone,Copy)]
41 pub struct ShapeCalculable { }
44 pub struct Catalogue {
47 bundle: Option<bundles::Id>,
48 items: HashMap<SvgBaseName<GoodItemName>, CatalogueEntry>,
51 #[derive(Debug,Clone)]
52 #[derive(Serialize,Deserialize)]
57 #[derive(Debug,Clone)]
60 Magic { group: Arc<GroupData>, spec: Arc<dyn PieceSpec> },
62 use CatalogueEntry as CatEnt;
64 #[derive(Debug,Clone)]
68 group: Arc<GroupData>,
70 shape_calculable: ShapeCalculable,
73 #[derive(Debug,Clone)]
76 Internal(Arc<OccData_Internal>),
80 #[allow(non_camel_case_types)]
82 struct OccData_Internal {
83 item_name: SvgBaseName<GoodItemName>,
85 loaded: lazy_init::Lazy<Result<ImageLoaded,SpecError>>,
88 #[allow(non_camel_case_types)]
89 #[derive(Debug,Clone)]
96 #[derive(Error,Debug)]
97 pub enum LibraryLoadError {
99 TomlParseError(#[from] toml::de::Error),
100 #[error("error reading/opening library file: {0}: {1}")]
101 FileError(String, io::Error),
102 #[error("OS error globbing for files: {0}")]
103 GlobFileError(#[from] glob::GlobError),
104 #[error("internal error: {0}")] InternalError(#[from] InternalError),
105 #[error("bad glob pattern: {pat:?} (near char {pos}): {msg}")]
106 BadGlobPattern { pat: String, msg: &'static str, pos: usize },
107 #[error("glob pattern {pat:?} matched non-utf-8 filename {actual:?}")]
108 GlobNonUTF8 { pat: String, actual: PathBuf },
109 #[error("glob pattern {pat:?} matched filename with no extension {path:?}")]
110 GlobNoExtension { pat: String, path: String },
111 #[error("occultation colour missing: {0:?}")]
112 OccultationColourMissing(String),
113 #[error("back missing for occultation")] BackMissingForOccultation,
114 #[error("expected TOML table: {0:?}")] ExpectedTable(String),
115 #[error("expected TOML string: {0:?}")] ExpectedString(String),
116 #[error("wrong number of size dimensions {got}, expected {expected:?}")]
117 WrongNumberOfSizeDimensions { got: usize, expected: [usize;2] },
118 #[error("group {0:?} inherits from nonexistent parent {1:?}")]
119 InheritMissingParent(String, String),
120 #[error("inheritance depth limit exceeded: {0:?}")]
121 InheritDepthLimitExceeded(String),
122 #[error("duplicate item {item:?} in groups {group1:?} and {group2:?}")]
123 DuplicateItem { item: String, group1: String, group2: String },
124 #[error("files list line {0} missing whitespace")]
125 FilesListLineMissingWhitespace(usize),
126 #[error("files list line {0}, field must be at start")]
127 FilesListFieldsMustBeAtStart(usize),
128 #[error("piece defines multiple faces in multiple ways")]
129 MultipleMultipleFaceDefinitions,
130 #[error("outline specified both size and numeric scale")]
131 OutlineContradictoryScale,
132 #[error("{0}")] CoordinateOverflow(#[from] CoordinateOverflow),
135 MaterialsFormatIncompat(#[from] materials_format::Incompat<
136 LibraryLoadMFIncompat
138 #[error("{0}")] BadSubstitution(#[from] SubstError),
139 #[error("{0}")] UnsupportedColourSpec(#[from] UnsupportedColourSpec),
140 #[error("bad item name (invalid characters) in {0:?}")] BadItemName(String),
141 #[error("{0}")] MaterialsFormatVersionError(#[from] MFVE),
143 #[error("could not parse template-expaneded TOML: {error} (in {toml:?}")]
144 TemplatedTomlError { toml: String, error: toml_de::Error },
146 #[error("group {group}: {error}")]
147 InGroup { group: String, error: Box<LLE> },
149 #[error("library {lib}: {error}")]
150 InLibrary { lib: String, error: Box<LLE> },
153 #[derive(Error,Debug,Clone,Copy,Serialize,Deserialize)]
154 pub enum LibraryLoadMFIncompat {
155 #[error("bad scale definition")] Scale,
156 #[error("size not specified")] SizeRequired,
157 #[error("orig_size no longer supported")] OrigSizeForbidden,
158 #[error("specified both size and numeric scale")] ContradictoryScale,
160 #[derive(Error,Clone,Debug)]
161 pub enum SubstErrorKind {
162 #[error("missing or unrecognised token {0}")] MissingToken(Cow<'static,str>),
163 #[error("repeated token {0}")] RepeatedToken(Cow<'static,str>),
164 #[error("internal logic error {0}")] Internal(#[from] InternalLogicError),
167 #[derive(Error,Clone,Debug)]
168 #[error("bad substitution: {input:?} {kind}")]
169 pub struct SubstError {
170 pub kind: SubstErrorKind,
175 const INHERIT_DEPTH_LIMIT: u8 = 20;
177 type TV = toml::Value;
179 #[derive(Debug, Clone, Serialize, Deserialize)]
180 pub struct MultiSpec {
186 pub items: Vec<String>,
189 define_index_type! { pub struct DescId = u8; }
190 define_index_type! { pub struct SvgId = u8; }
192 #[derive(Copy,Clone,Debug,Serialize,Deserialize)]
197 xform: FaceTransform,
200 #[derive(Copy,Clone,Debug,Serialize,Deserialize)]
201 struct FaceTransform {
206 #[derive(Debug,Serialize,Deserialize)]
209 sort: Option<String>,
210 faces: IndexVec<FaceId, ItemFace>,
211 svgs: IndexVec<SvgId, Html>,
212 descs: IndexVec<DescId, Html>,
215 back: Option<Arc<dyn InertPieceTrait>>,
218 #[derive(Debug,Serialize,Deserialize)]
219 struct ItemInertForOcculted {
220 itemname: GoodItemName,
223 xform: FaceTransform,
227 //==================== SvgBsseName ====================
229 /// Represents a `T` which is an SVG basename which has been noted
230 /// for processing during bundle load.
231 #[derive(Debug,Copy,Clone,Hash,Eq,PartialEq,Ord,PartialOrd,Deref)]
233 struct SvgBaseName<T:?Sized>(T);
234 impl<T> Display for SvgBaseName<T> where T: Display + ?Sized {
235 #[throws(fmt::Error)]
236 fn fmt(&self, f: &mut fmt::Formatter) { write!(f, "{}", &self.0)? }
238 impl<T> Borrow<str> for SvgBaseName<T> where T: Borrow<str> {
239 fn borrow(&self) -> &str { self.0.borrow() }
241 impl<T> SvgBaseName<T> {
242 fn into_inner(self) -> T { self.0 }
244 impl<T> SvgBaseName<T> where T: ?Sized {
245 fn as_str(&self) -> &str where T: Borrow<str> { self.0.borrow() }
246 fn unnest<'l,U>(&'l self) -> &SvgBaseName<U> where U: ?Sized, T: Borrow<U> {
247 let s: &'l U = self.0.borrow();
248 let u: &'l SvgBaseName<U> = unsafe { mem::transmute(s) };
252 impl<T> SvgBaseName<T> where T: Borrow<GoodItemName> {
253 #[throws(SubstError)]
254 fn note(src: &mut dyn LibrarySvgNoter, i: T,
255 src_name: Result<&str, &SubstError>) -> Self {
256 src.note_svg(i.borrow(), src_name)?;
261 //==================== impls for ItemInertForOcculted ====================
263 impl_via_ambassador!{
265 impl OutlineTrait for ItemInertForOcculted { outline }
268 impl PieceBaseTrait for ItemInertForOcculted {
269 fn nfaces(&self) -> RawFaceId { 1 }
270 fn itemname(&self) -> &str { self.itemname.as_str() }
272 #[typetag::serde(name="Lib")]
273 impl InertPieceTrait for ItemInertForOcculted {
275 fn svg(&self, f: &mut Html, _: VisiblePieceId, face: FaceId,
276 _: &PieceXDataState) {
277 if face != FaceId::default() {
278 throw!(internal_logic_error("ItemInertForOcculted non-default face"))
280 self.xform.write_svgd(f, &self.svgd)?;
283 fn describe_html(&self, _: FaceId) -> Html { self.desc.clone() }
286 //---------- ItemEnquiryData, LibraryEnquiryData ----------
288 #[derive(Debug,Clone,Serialize,Deserialize,Eq,PartialEq,Ord,PartialOrd)]
289 pub struct ItemEnquiryData {
290 pub lib: LibraryEnquiryData,
291 pub itemname: GoodItemName,
292 pub sortkey: Option<String>,
297 impl From<&ItemEnquiryData> for ItemSpec {
298 fn from(it: &ItemEnquiryData) -> ItemSpec {
300 lib: it.lib.libname.clone(),
301 item: it.itemname.as_str().to_owned(),
306 impl Display for ItemEnquiryData {
307 #[throws(fmt::Error)]
308 fn fmt(&self, f: &mut Formatter) {
309 write!(f, "{:<10} {:20} {}", &self.lib, &self.itemname,
310 self.f0desc.as_html_str())?;
314 #[derive(Debug,Clone,Serialize,Deserialize,Eq,PartialEq,Ord,PartialOrd)]
315 pub struct LibraryEnquiryData {
316 pub bundle: Option<bundles::Id>,
319 impl Display for LibraryEnquiryData {
320 #[throws(fmt::Error)]
321 fn fmt(&self, f: &mut Formatter) {
322 if let Some(id) = self.bundle.as_ref() {
323 write!(f, "[{}] ", id)?;
325 if self.libname.chars().all(|c| {
326 c.is_alphanumeric() || c=='-' || c=='_' || c=='.'
328 Display::fmt(&self.libname, f)?;
330 Debug::fmt(&self.libname, f)?;
335 //==================== Item ====================
337 impl_via_ambassador!{
339 impl OutlineTrait for Item { outline }
344 fn svg_face(&self, f: &mut Html, face: FaceId, vpid: VisiblePieceId,
345 xdata: &PieceXDataState) {
346 if let Some(face) = self.faces.get(face) {
347 let svgd = &self.svgs[face.svg];
348 face.xform.write_svgd(f, svgd)?;
349 } else if let Some(back) = &self.back {
350 back.svg(f, vpid, default(), xdata)?;
352 throw!(internal_error_bydebug(&(self, face)))
357 fn describe_face(&self, face: FaceId) -> Html {
359 if let Some(face) = self.faces.get(face) {
361 } else if let Some(back) = &self.back {
362 return back.describe_html(default())?;
371 impl PieceBaseTrait for Item {
372 fn nfaces(&self) -> RawFaceId {
374 + self.back.iter().count())
378 fn itemname(&self) -> &str { &self.itemname }
381 #[typetag::serde(name="Lib")]
382 impl PieceTrait for Item {
384 fn svg_piece(&self, f: &mut Html, gpc: &GPiece,
385 _gs: &GameState, vpid: VisiblePieceId) {
386 self.svg_face(f, gpc.face, vpid, &gpc.xdata)?;
389 fn describe_html(&self, gpc: &GPiece, _goccults: &GOccults) -> Html {
390 self.describe_face(gpc.face)?
393 fn sortkey(&self) -> Option<&str> { self.sort.as_ref().map(AsRef::as_ref) }
396 #[typetag::serde(name="LibItem")]
397 impl InertPieceTrait for Item {
399 fn svg(&self, f: &mut Html, id: VisiblePieceId, face: FaceId,
400 xdata: &PieceXDataState) {
401 self.svg_face(f, face, id, xdata)?;
404 fn describe_html(&self, _: FaceId) -> Html {
405 self.describe_face(default())?
409 //==================== ItemSpec and item loading ====================
411 //---------- ItemSpec, MultiSpec ----------
413 type ItemSpecLoaded = (Box<Item>, PieceSpecLoadedOccultable);
415 impl From<ItemSpecLoaded> for SpecLoaded {
416 fn from((p, occultable): ItemSpecLoaded) -> SpecLoaded {
427 fn find_then<F,T>(&self, ig: &Instance, then: F) -> T
428 where F: FnOnce(&Catalogue, &SvgBaseName<GoodItemName>, &CatalogueEntry)
429 -> Result<T, SpecError>
431 let regs = ig.all_shapelibs();
432 let libs = regs.lib_name_lookup(&self.lib)?;
433 let (lib, (item, idata)) = libs.iter().rev().find_map(
434 |lib| Some((lib, lib.items.get_key_value(self.item.as_str())?))
436 .ok_or_else(|| SpE::LibraryItemNotFound(self.clone()))?;
437 then(lib, item, idata)?
441 fn find_load_general<MAG,MUN,T>(&self, ig: &Instance, depth: SpecDepth,
442 mundanef: MUN, magicf: MAG) -> T
443 where MUN: FnOnce(ItemSpecLoaded) -> Result<T, SpE>,
444 MAG: FnOnce(&Arc<dyn PieceSpec>) -> Result<T, SpE>,
446 self.find_then(ig, |lib, item, catent| Ok(match catent {
447 CatEnt::Item(idata) => {
448 let loaded = lib.load1(idata, &self.lib, item.unnest::<str>(),
452 CatEnt::Magic { spec,.. } => {
459 pub fn find_load_mundane(&self, ig: &Instance,
460 depth: SpecDepth) -> ItemSpecLoaded {
461 self.find_load_general(
462 ig, depth, |loaded| Ok(loaded),
463 |_| Err(SpE::ComplexPieceWhereInertRequired)
467 fn from_strs<L,I>(lib: &L, item: &I) -> Self
468 where L: ToOwned<Owned=String> + ?Sized,
469 I: ToOwned<Owned=String> + ?Sized,
471 let lib = lib .to_owned();
472 let item = item.to_owned();
473 ItemSpec{ lib, item }
477 #[typetag::serde(name="Lib")]
478 impl PieceSpec for ItemSpec {
480 fn load(&self, pla: PLA) -> SpecLoaded {
481 self.find_load_general(
483 |loaded| Ok(loaded.into()),
484 |magic| magic.load(pla.recursing()?)
488 fn load_inert(&self, ig: &Instance, depth: SpecDepth) -> SpecLoadedInert {
489 let (p, occultable) = self.find_load_mundane(ig,depth)?;
490 SpecLoadedInert { p: p as _, occultable }
494 #[typetag::serde(name="LibList")]
495 impl PieceSpec for MultiSpec {
497 fn count(&self, _pcaliases: &PieceAliases) -> usize { self.items.len() }
500 fn load(&self, pla: PLA) -> SpecLoaded
502 let PLA { i,.. } = pla;
503 let item = self.items.get(i).ok_or_else(
504 || SpE::InternalError(format!("item {:?} from {:?}", i, &self))
506 let item = format!("{}{}{}", &self.prefix, item, &self.suffix);
507 let lib = self.lib.clone();
508 ItemSpec { lib, item }.load(pla)?
512 //---------- Loading ----------
516 fn load_image(&self, item_name: &SvgBaseName<str>,
517 lib_name_for: &str, item_for: &str,
518 group: &GroupData, shape_calculable: ShapeCalculable)
520 let svg_path = format!("{}/{}.usvg", self.dirname, item_name);
521 let svg_data = fs::read_to_string(&svg_path)
522 .map_err(|e| if e.kind() == ErrorKind::NotFound {
523 warn!("library item lib={} itme={} for={:?} data file {:?} not found",
524 &self.libname, item_name, item_for, &svg_path);
525 let spec_for = ItemSpec::from_strs(lib_name_for, item_for);
526 SpE::LibraryItemNotFound(spec_for)
528 let m = "error accessing/reading library item data file";
529 error!("{}: {} {:?}: {}", &m, &svg_path, item_for, &e);
530 SpE::InternalError(m.to_string())
533 let svg_data = Html::from_html_string(svg_data);
535 let sz = svg_parse_size(&svg_data).map_err(|error| SpE::SVGError {
537 item_name: item_name.as_str().into(),
538 item_for_lib: lib_name_for.into(),
539 item_for_item: item_for.into(),
542 let (xform, outline) = group.load_shape(sz)
543 .map_err(shape_calculable.err_mapper())?;
553 fn load1(&self, idata: &ItemData, lib_name: &str,
554 name: &SvgBaseName<str>,
555 ig: &Instance, depth:SpecDepth)
557 let ImageLoaded { svgd: svg_data, outline, xform } =
558 self.load_image(name, lib_name, &**name,
559 &idata.group, idata.shape_calculable)?;
561 let mut svgs = IndexVec::with_capacity(1);
562 let svg = svgs.push(svg_data);
564 let mut descs = index_vec![ ];
565 let desc = descs.push(idata.d.desc.clone());
566 descs.shrink_to_fit();
568 let mut face = ItemFace { svg, desc, xform };
569 let mut faces = index_vec![ face ];
570 let mut back = None::<Arc<dyn InertPieceTrait>>;
571 if idata.group.d.flip {
572 face.xform.scale[0] *= -1.;
574 } else if let Some(back_spec) = &idata.group.d.back {
575 match back_spec.load_inert(ig, depth) {
576 Err(SpecError::AliasNotFound) => { },
584 faces.shrink_to_fit();
586 let occultable = match &idata.occ {
587 OccData::None => None,
588 OccData::Back(ilk) => {
589 if let Some(back) = &back {
590 let back = back.clone();
591 Some((LOI::Mix(ilk.clone()), back))
593 None // We got AliasNotFound, ah well
596 OccData::Internal(occ) => {
597 let occ_name = occ.item_name.clone();
600 } = occ.loaded.get_or_create(
602 occ.item_name.unnest::<GoodItemName>().unnest(),
603 /* original: */ lib_name, name.as_str(),
604 &idata.group, idata.shape_calculable,
608 let it = Arc::new(ItemInertForOcculted {
609 svgd, outline, xform,
610 itemname: occ_name.clone().into_inner(),
611 desc: occ.desc.clone(),
612 }) as Arc<dyn InertPieceTrait>;
613 Some((LOI::Mix(occ_name.into_inner()), it))
617 let sort = idata.sort.clone();
618 let it = Item { faces, sort, descs, svgs, outline, back,
619 itemname: name.to_string() };
620 (Box::new(it), occultable)
624 //==================== size handling, and outlines ====================
628 fn from_group_mf1(group: &GroupData) -> Self {
630 // by this point d.size has already been scaled by scale
631 let scale = if ! d.orig_size.is_empty() && ! d.size.is_empty() {
632 izip!(&d.orig_size, &d.size)
633 .map(|(&orig_size, &target_size)| {
634 target_size / orig_size
638 .collect::<ArrayVec<_,2>>()
642 let s = group.d.scale_mf1(group.mformat)?;
645 let centre = d.centre.map(Ok).unwrap_or_else(|| Ok::<_,LLE>({
646 resolve_square_size(&d.size)?
647 .ok_or_else(|| group.mformat.incompat(LLMI::SizeRequired))?
648 .coords.iter().cloned().zip(&scale).map(|(size,scale)| {
651 .collect::<ArrayVec<_,2>>()
655 FaceTransform { centre, scale }
659 fn write_svgd(&self, f: &mut Html, svgd: &Html) {
661 r##"<g transform="scale({} {}) translate({} {})">{}</g>"##,
662 self.scale[0], self.scale[1], -self.centre[0], -self.centre[1],
668 fn resolve_square_size<T:Copy>(size: &[T]) -> Option<PosC<T>> {
669 Some(PosC{ coords: match size {
673 _ => throw!(LLE::WrongNumberOfSizeDimensions
674 { got: size.len(), expected: [1,2]}),
678 impl CatalogueEntry {
679 fn group(&self) -> &Arc<GroupData> { match self {
680 CatEnt::Item(item) => &item.group,
681 CatEnt::Magic{group,..} => group,
685 //---------- Outlines ----------
687 impl ShapeCalculable {
688 pub fn err_mapper(&self) -> impl Fn(LLE) -> IE + Copy {
689 |e| internal_logic_error(format!(
690 "outline calculable but failed {} {:?}",&e,&e
696 #[throws(LibraryLoadError)]
697 fn check_shape(&self) -> ShapeCalculable {
698 let _ = self.load_shape(PosC::new(
699 1.,1. /* dummy value, suffices for error check */
704 #[throws(LibraryLoadError)]
705 /// As with OutlineDefn::load, success must not depend on svg_sz value
706 fn load_shape(&self, svg_sz: PosC<f64>) -> (FaceTransform, Outline) {
707 if self.mformat >= 2 {
709 if self.d.orig_size.len() > 0 {
710 throw!(self.mformat.incompat(LLMI::OrigSizeForbidden))
713 let centre: PosC<f64> = self.d.centre
714 .map(|coords| PosC { coords })
715 .unwrap_or_else(|| geometry::Mean::mean(&svg_sz, &PosC::zero()));
717 let size = resolve_square_size(&self.d.size)?;
719 use ScaleDetails as SD;
720 let scale = self.d.scale.unwrap_or(SD::Fit(ScaleFitDetails::Fit));
722 let of_stretch = |scale| {
723 let scale = PosC { coords: scale };
724 let size = pos_zip_map!( svg_sz, scale => |(sz,sc)| sz * sc );
728 let (size, scale) = match (size, scale) {
729 (Some(size), SD::Fit(fit)) => {
730 let scale = pos_zip_map!( size, svg_sz => |(a,b)| a/b );
731 type Of = OrderedFloat<f64>;
732 let of = |minmax: fn(Of,Of) -> Of| {
733 let v = minmax(scale.coords[0].into(),
734 scale.coords[1].into()).into_inner();
737 let scale = match fit {
738 ScaleFitDetails::Fit => of(min),
739 ScaleFitDetails::Cover => of(max),
740 ScaleFitDetails::Stretch => scale,
744 (Some(_), SD::Scale(_)) |
745 (Some(_), SD::Stretch(_))
746 => throw!(self.mformat.incompat(LLMI::ContradictoryScale)),
747 (None, SD::Fit(_)) => (svg_sz, PosC::new(1.,1.)),
748 (None, SD::Scale(s)) => of_stretch([s,s]),
749 (None, SD::Stretch(s)) => of_stretch(s),
753 let (osize, oscale) = self.d.outline.size_scale();
754 let osize = resolve_square_size(osize)?;
755 match (osize, oscale) {
756 (Some(osize), None ) => osize,
757 (None, Some(&oscale)) => (size * oscale)?,
758 (None, None ) => size,
759 (Some(_), Some(_) ) =>
760 throw!(LLE::OutlineContradictoryScale)
764 let outline = self.d.outline.shape().load(osize);
765 (FaceTransform { scale: scale.coords, centre: centre.coords }, outline)
768 let xform = FaceTransform::from_group_mf1(self)?;
769 let outline = self.d.outline.shape().load_mf1(self)?;
776 #[throws(materials_format::Incompat<LLMI>)]
777 fn scale_mf1(&self, mformat: materials_format::Version) -> f64 {
780 Some(ScaleDetails::Scale(s)) => s,
781 _ => throw!(mformat.incompat(LLMI::Scale)),
786 //---------- OutlineDefn etc. ----------
788 #[ambassador::delegatable_trait]
789 pub trait ShapeLoadableTrait: Debug + Sync + Send + 'static {
790 /// Success or failure must not depend on `svg_sz`
792 /// Called to *check* the group configuration before load, but
793 /// with a dummy svg_gz of `[1,1]`. That must correctly predict
794 /// success with other sizes.
795 fn load(&self, size: PosC<f64>) -> Outline {
796 RectOutline { xy: size }.into()
799 fn load_mf1(&self, group: &GroupData) -> Result<Outline,LLE>;
802 // We used to do shape deser via typetag and Box<dyn OutlineDefn>
804 // But I didnt manage to get typetag to deserialise the way I wanted.
805 // Instead, we have the Shape enum and a cheesy macro to impl OutlineDefn
806 // by delegating to a freshly made (static) unit struct value,
807 // - see outline_defn in mod outline in spec.rs.
808 impl_via_ambassador!{
809 impl ShapeLoadableTrait for Shape { shapelib_loadable() }
812 //---------- RectOutline ----------
814 impl ShapeLoadableTrait for RectShapeIndicator {
815 fn load(&self, size: PosC<f64>) -> Outline {
816 RectOutline { xy: size }.into()
819 #[throws(LibraryLoadError)]
820 fn load_mf1(&self, group: &GroupData) -> Outline {
821 let size = resolve_square_size(&group.d.size)?
822 .ok_or_else(|| group.mformat.incompat(LLMI::SizeRequired))?;
827 //---------- CircleOutline ----------
829 impl ShapeLoadableTrait for CircleShapeIndicator {
830 fn load(&self, size: PosC<f64>) -> Outline {
841 #[throws(LibraryLoadError)]
842 fn load_mf1(&self, group: &GroupData) -> Outline {
843 let diam = match group.d.size.as_slice() {
845 size => throw!(LLE::WrongNumberOfSizeDimensions
846 { got: size.len(), expected: [1,1] }),
854 //==================== Catalogues ====================
856 //---------- enquiries etc. ----------
859 pub fn enquiry(&self) -> LibraryEnquiryData {
861 libname: self.libname.clone(),
867 pub fn list_glob(&self, pat: &str) -> Vec<ItemEnquiryData> {
868 let pat = glob::Pattern::new(pat).map_err(|pe| ME::BadGlob {
869 pat: pat.to_string(), msg: pe.msg.to_string() })?;
870 let mut out = vec![];
871 let ig_dummy = Instance::dummy();
872 for (k,v) in &self.items {
873 if !pat.matches(k.as_str()) { continue }
874 let mut gpc = GPiece::dummy();
875 let loaded = match (|| Ok(match v {
876 CatEnt::Item(item) => {
878 self.load1(item, &self.libname, k.unnest(),
879 &Instance::dummy(), SpecDepth::zero())?;
880 loaded as Box<dyn PieceTrait>
882 CatEnt::Magic{spec,..} => {
883 spec.load(PieceLoadArgs {
887 depth: SpecDepth::zero(),
891 Err(SpecError::LibraryItemNotFound(_)) => continue,
895 let f0bbox = loaded.bbox_approx()?;
896 let ier = ItemEnquiryData {
898 itemname: (**k).to_owned(),
899 sortkey: loaded.sortkey().map(|s| s.to_owned()),
901 f0desc: loaded.describe_html(&gpc, &default())?,
909 pub trait LibrarySvgNoter {
910 #[throws(SubstError)]
911 fn note_svg(&mut self, _basename: &GoodItemName,
912 _src_name: Result<&str, &SubstError>) { }
914 pub trait LibrarySource: LibrarySvgNoter {
915 fn catalogue_data(&self) -> &str;
916 fn svg_dir(&self) -> String;
917 fn bundle(&self) -> Option<bundles::Id>;
919 fn default_materials_format(&self)
920 -> Result<materials_format::Version, MFVE>;
922 // Sadly dyn_upcast doesn't work because it doesn't support the
923 // non-'static lifetime on BuiltinLibrary
924 fn svg_noter(&mut self) -> &mut dyn LibrarySvgNoter;
927 pub struct NullLibrarySvgNoter;
928 impl LibrarySvgNoter for NullLibrarySvgNoter { }
930 struct BuiltinLibrary<'l> {
931 catalogue_data: &'l str,
935 impl LibrarySvgNoter for BuiltinLibrary<'_> {
937 impl<'l> LibrarySource for BuiltinLibrary<'l> {
938 fn catalogue_data(&self) -> &str { self.catalogue_data }
939 fn svg_dir(&self) -> String { self.dirname.to_string() }
940 fn bundle(&self) -> Option<bundles::Id> { None }
942 #[throws(materials_format::VersionError)]
943 fn default_materials_format(&self) -> materials_format::Version {
944 throw!(MFVE::Other("builtin libraries must have explicit version now!"));
947 fn svg_noter(&mut self) -> &mut dyn LibrarySvgNoter { self }
950 //---------- reading ----------
952 #[throws(LibraryLoadError)]
953 pub fn load_catalogue(libname: &str, src: &mut dyn LibrarySource)
957 let toplevel: toml::Value = src.catalogue_data().parse()?;
958 let toplevel = toplevel
959 .as_table().ok_or_else(|| LLE::ExpectedTable(format!("toplevel")))?;
960 let mformat = match toplevel.get("format") {
961 None => src.default_materials_format()?,
963 let v = v.as_integer().ok_or_else(|| MFVE::Other("not an integer"))?;
964 materials_format::Version::try_from_integer(v)?
968 let mut l = Catalogue {
969 bundle: src.bundle(),
970 libname: libname.to_string(),
971 items: HashMap::new(),
972 dirname: src.svg_dir(),
974 let empty_table = toml::value::Value::Table(default());
975 let groups = toplevel
976 .get("group").unwrap_or(&empty_table)
977 .as_table().ok_or_else(|| LLE::ExpectedTable(format!("group")))?;
978 for (groupname, gdefn) in groups {
981 let gdefn = resolve_inherit(INHERIT_DEPTH_LIMIT,
982 groups, groupname, gdefn)?;
983 let gdefn: GroupDefn = TV::Table(gdefn.into_owned()).try_into()?;
984 let d = if mformat == 1 {
985 let scale = gdefn.d.scale_mf1(mformat)?;
987 size: gdefn.d.size.iter().map(|s| s * scale).collect(),
991 gdefn.d // v2 isn't going to do this, do this right now
993 let group = Arc::new(GroupData {
994 groupname: groupname.clone(),
998 // We do this here rather than in the files loop because
999 // 1. we want to check it even if there are no files specified
1000 // 2. this is OK because the group doesn't change from here on
1001 let shape_calculable = group.check_shape()?;
1005 group.d.back.is_some(),
1006 ].iter().filter(|x|**x).count() > 1 {
1007 throw!(LLE::MultipleMultipleFaceDefinitions)
1010 for fe in gdefn.files.0 {
1011 process_files_entry(
1012 src.svg_noter(), &mut l,
1013 &gdefn.item_prefix, &gdefn.item_suffix, &gdefn.sort,
1014 &group, shape_calculable, fe
1019 })().map_err(|error| LLE::InGroup {
1020 group: groupname.to_string(),
1021 error: Box::new(error),
1026 })().map_err(|error| LLE::InLibrary {
1027 lib: libname.into(),
1028 error: Box::new(error),
1032 #[derive(Debug,Copy,Clone,Eq,PartialEq)]
1033 pub enum Dollars { Text, Filename }
1035 #[derive(Debug,Clone)]
1036 pub struct Substituting<'s> {
1038 mformat: materials_format::Version,
1042 impl<'s> Substituting<'s> {
1043 pub fn new<S: Into<Cow<'s, str>>>(
1044 mformat: materials_format::Version,
1048 Substituting { s: s.into(), mformat, dollars }
1051 #[throws(SubstError)]
1052 pub fn finish(self) -> String {
1053 if self.do_dollars() {
1054 self.subst_general_precisely("${$}", "$")?.0
1060 fn do_dollars(&self) -> bool { self.dollars.enabled(self.mformat) }
1062 #[throws(SubstError)]
1063 /// Expand, but do not do final unescaping
1065 /// Used when we are expanding something that is going to be used
1066 /// as a replacement in a further expansion, which will do final unescaping.
1067 pub fn nest(self) -> String {
1071 fn err(&self, kind: SubstErrorKind) -> SubstError {
1072 SubstError { kind, input: (*self.s).to_owned() }
1075 fn internal_err(&self, msg: &'static str) -> SubstError {
1076 self.err(InternalLogicError::new(msg).into())
1081 fn enabled(self, mformat: materials_format::Version) -> bool {
1083 Dollars::Filename => false,
1084 Dollars::Text => mformat >= 2,
1089 impl<'i> Substituting<'i> {
1090 #[throws(SubstError)]
1091 fn subst_general_precisely(&self, needle: &str, replacement: &str)
1092 -> (Substituting<'i>, usize) {
1094 let mut work = (*self.s).to_owned();
1095 for m in self.s.rmatch_indices(needle) {
1097 let mut lhs = &work[0.. m.0];
1098 let mut rhs = &work[m.0 + m.1.len() ..];
1099 if replacement.is_empty() {
1100 let lhs_trimmed = lhs.trim_end();
1101 if lhs_trimmed.len() != lhs.len() {
1104 rhs = rhs.trim_start();
1114 mformat: self.mformat,
1115 dollars: self.dollars,
1119 #[throws(SubstError)]
1120 // This takes &Substituting. The rest of the code uses subst or
1121 // substn, which takes Substituting, thus ensuring that at some future
1122 // time we might be able to accumulate all the substitutions in
1123 // Substituting and do them all at once.
1124 fn subst_general(&self, needle: Cow<'static, str>, replacement: &str)
1125 -> (Substituting<'i>, usize, Cow<'static, str>) {
1126 match self.dollars {
1127 Dollars::Filename => if needle != "_c" {
1128 throw!(self.internal_err("long subst in filename"))
1130 Dollars::Text => { },
1132 let needle: Cow<str> = (move || Some({
1133 if let Some(rhs) = needle.strip_prefix("${") {
1134 let token = rhs.strip_suffix('}')?;
1135 if self.do_dollars() { needle }
1136 else { format!("_{}", token).into() }
1137 } else if let Some(token) = needle.strip_prefix('_') {
1138 if ! self.do_dollars() { needle }
1139 else { format!("${{{}}}", token).into() }
1144 .ok_or_else(|| self.internal_err("needle has no '_'"))?;
1146 let (r, count) = self.subst_general_precisely(&needle, replacement)?;
1151 #[throws(SubstError)]
1152 fn subst<'i,N>(before: Substituting<'i>, needle: N, replacement: &str)
1154 where N: Into<Cow<'static, str>>
1156 use SubstErrorKind as SEK;
1157 let needle = needle.into();
1158 let (out, count, needle) = before.subst_general(needle, replacement)?;
1159 if count == 0 { throw!(before.err(SEK::MissingToken(needle))) }
1160 if count > 1 { throw!(before.err(SEK::RepeatedToken(needle))) }
1164 #[throws(SubstError)]
1165 fn substn<'i,N>(before: Substituting<'i>, needle: N, replacement: &str)
1167 where N: Into<Cow<'static, str>>
1169 before.subst_general(needle.into(), replacement)?.0
1174 fn test_subst_mf1() {
1175 use SubstErrorKind as SEK;
1177 let mformat = materials_format::Version::try_from_integer(1).unwrap();
1178 let s_t = |s| Substituting::new(mformat, Dollars::Text, s);
1179 let s_f = |s| Substituting::new(mformat, Dollars::Filename, s);
1181 assert_eq!(subst(s_f("die-image-_c"), "_c", "blue")
1182 .unwrap().finish().unwrap(),
1184 assert_eq!(subst(s_t("a _colour die"), "_colour", "blue")
1185 .unwrap().finish().unwrap(),
1187 assert_eq!(subst(s_t("a _colour die"), "${colour}", "blue")
1188 .unwrap().finish().unwrap(),
1190 assert_eq!(subst(s_t("a _colour die"), "_colour", "")
1191 .unwrap().finish().unwrap(),
1194 dbg!(subst(s_t("a die"), "_colour", "")).unwrap_err().kind,
1195 SEK::MissingToken(c) if c == "_colour",
1198 dbg!(subst(s_t("a _colour _colour die"), "_colour", "")).unwrap_err().kind,
1199 SEK::RepeatedToken(c) if c == "_colour",
1202 assert_eq!(substn(s_t("a _colour die being _colour"), "_colour", "blue")
1203 .unwrap().finish().unwrap(),
1204 "a blue die being blue");
1206 let (s, count, needle) = s_t("a _colour _colour die")
1207 .subst_general("_colour".into(), "")
1209 assert_eq!(s.finish().unwrap(), "a die".to_owned());
1210 assert_eq!(count, 2);
1211 assert_eq!(needle, "_colour");
1216 fn test_subst_mf2() {
1217 use SubstErrorKind as SEK;
1219 let mformat = materials_format::Version::try_from_integer(2).unwrap();
1220 let s_t = |s| Substituting::new(mformat, Dollars::Text, s);
1221 let s_f = |s| Substituting::new(mformat, Dollars::Filename, s);
1223 assert_eq!(subst(s_f("die-image-_c"), "_c", "blue")
1224 .unwrap().finish().unwrap(),
1227 dbg!(subst(s_f("die-image-_c"), "_colour", "")).unwrap_err().kind,
1231 assert_eq!(subst(s_t("a ${colour} die"), "_colour", "blue")
1232 .unwrap().finish().unwrap(),
1234 assert_eq!(subst(s_t("a ${c} die"), "_c", "blue")
1235 .unwrap().finish().unwrap(),
1237 assert_eq!(subst(s_t("a ${colour} die"), "_colour", "")
1238 .unwrap().finish().unwrap(),
1240 assert_eq!(subst(s_t("a ${colour} die"), "${colour}", "")
1241 .unwrap().finish().unwrap(),
1244 dbg!(subst(s_t("a die"), "_colour", "")).unwrap_err().kind,
1245 SEK::MissingToken(c) if c == "${colour}",
1248 dbg!(subst(s_t("a ${colour} ${colour} die"), "_colour", ""))
1250 SEK::RepeatedToken(c) if c == "${colour}",
1253 assert_eq!(substn(s_t("a ${colour} die being ${colour}"), "_colour", "blue")
1254 .unwrap().finish().unwrap(),
1255 "a blue die being blue");
1257 let (s, count, needle) = s_t("a ${colour} ${colour} die")
1258 .subst_general("_colour".into(), "")
1260 assert_eq!(s.finish().unwrap(), "a die".to_owned());
1261 assert_eq!(count, 2);
1262 assert_eq!(needle, "${colour}");
1265 #[throws(LibraryLoadError)]
1266 fn format_item_name(mformat: materials_format::Version,
1267 item_prefix: &str, fe: &FileData, item_suffix: &str)
1268 -> Substituting<'static> {
1270 mformat, Dollars::Filename,
1271 format!("{}{}{}", item_prefix, fe.item_spec, item_suffix)
1275 #[throws(LibraryLoadError)]
1276 fn process_files_entry(
1277 src: &mut dyn LibrarySvgNoter, l: &mut Catalogue,
1278 item_prefix: &str, item_suffix: &str, sort: &str,
1279 group: &Arc<GroupData>, shape_calculable: ShapeCalculable,
1282 let mformat = group.mformat;
1283 let item_name = format_item_name(mformat, item_prefix, &fe, item_suffix)?;
1285 let sort: Option<PerhapsSubst> = match (sort, fe.extra_fields.get("sort")) {
1287 (gd, None) => Some(gd.into()),
1288 ("", Some(ef)) => Some(ef.into()),
1290 let sort = Substituting::new(mformat, Dollars::Text, gd);
1291 Some(subst(sort, "_s", ef)?.into())
1295 let occ = match &group.d.occulted {
1296 None => OccData::None,
1297 Some(OccultationMethod::ByColour { colour }) => {
1298 if ! group.d.colours.contains_key(colour.0.as_str()) {
1299 throw!(LLE::OccultationColourMissing(colour.0.clone()));
1301 let item_name = subst(item_name.clone(), "_c", &colour.0)?;
1302 let src_name = Substituting::new(mformat, Dollars::Filename,
1304 let src_name = subst(src_name, "_c", &colour.0)
1305 .and_then(|s| s.finish());
1306 let item_name: GoodItemName = item_name.finish()?.try_into()?;
1307 let item_name = SvgBaseName::note(
1308 src, item_name, src_name.as_deref(),
1310 let desc = Substituting::new(mformat, Dollars::Text, &fe.desc);
1311 let desc = subst(desc, "${colour}", "")?.finish()?.to_html();
1312 OccData::Internal(Arc::new(OccData_Internal {
1318 Some(OccultationMethod::ByBack { ilk }) => {
1319 if group.d.back.is_none() {
1320 throw!(LLE::BackMissingForOccultation)
1322 OccData::Back(ilk.clone())
1326 #[derive(Debug,From,Clone)]
1327 enum PerhapsSubst<'i> {
1328 Y(Substituting<'i>),
1331 impl<'i> From<&'i String> for PerhapsSubst<'i> {
1332 fn from(s: &'i String) -> Self { (&**s).into() }
1335 impl<'i> PerhapsSubst<'i> {
1336 #[throws(SubstError)]
1337 pub fn finish(self) -> String { match self {
1338 PerhapsSubst::N(s) => s.to_owned(),
1339 PerhapsSubst::Y(s) => s.finish()?,
1341 #[throws(SubstError)]
1342 pub fn nest(self) -> String { match self {
1343 PerhapsSubst::N(s) => s.to_owned(),
1344 PerhapsSubst::Y(s) => s.nest()?,
1348 mformat: materials_format::Version,
1350 ) -> Substituting<'i> { match self {
1351 PerhapsSubst::N(s) => Substituting::new(mformat, dollars, s),
1352 PerhapsSubst::Y(s) => s,
1354 #[throws(SubstError)]
1357 ) -> Substituting<'i> { match self {
1358 PerhapsSubst::Y(s) => s,
1359 PerhapsSubst::N(s) => throw!(SubstError {
1360 kind: InternalLogicError::new("expected Y").into(),
1366 fn colour_subst_1<'s, S>(
1367 mformat: materials_format::Version,
1369 subst: S, kv: Option<(&'static str, &'s str)>
1371 -> impl for <'i> Fn(PerhapsSubst<'i>)
1372 -> Result<PerhapsSubst<'i>, SubstError>
1374 where S: for <'i> Fn(Substituting<'i>, &'static str, &str)
1375 -> Result<Substituting<'i>, SubstError> + 's
1378 if let Some((keyword, val)) = kv {
1379 subst(input.mky(mformat, dollars), keyword, val)?.into()
1380 } else if dollars.enabled(mformat) {
1381 input.mky(mformat, dollars).into()
1389 c_colour: Option<(&'static str, &str)>,
1390 c_abbrev: Option<(&'static str, &str)>,
1391 c_substs: Option<&HashMap<String, String>>,
1393 let c_colour_all =colour_subst_1(mformat,Dollars::Text, substn, c_colour);
1394 let c_colour = colour_subst_1(mformat,Dollars::Text, subst, c_colour);
1395 let c_abbrev_t = colour_subst_1(mformat,Dollars::Text, subst, c_abbrev);
1396 let c_abbrev_f =colour_subst_1(mformat,Dollars::Filename,subst, c_abbrev);
1398 let sort = sort.clone().map(|v| c_abbrev_t(v)).transpose()?;
1399 let sort = sort.map(|s| s.finish()).transpose()?;
1401 let subst_item_name = |item_name: &Substituting| {
1402 let item_name = c_abbrev_f(item_name.clone().into())?;
1403 let item_name = item_name.finish()?.try_into()?;
1404 Ok::<_,LLE>(item_name)
1406 let item_name = subst_item_name(&item_name)?;
1408 let src_name = c_abbrev_f((&fe.src_file_spec).into())
1409 .and_then(|s| s.finish());
1410 let src_name = src_name.as_deref();
1412 let desc = c_colour((&fe.desc).into())?;
1414 let desc = if let Some(desc_template) = &group.d.desc_template {
1415 let desc_template = Substituting::new(
1416 mformat, Dollars::Text, desc_template);
1417 subst(desc_template, "${desc}", &desc.nest()?)?.finish()?.to_html()
1419 desc.finish()?.to_html()
1422 let idata = ItemData {
1423 group: group.clone(),
1427 d: Arc::new(ItemDetails { desc }),
1429 l.add_item(src, src_name, &item_name, CatEnt::Item(idata))?;
1431 if let Some(magic) = &group.d.magic {
1432 // Ideally the toml crate would have had let us build an inline
1433 // table. But in fact it won't even toml-escape the strings without
1434 // a fuss, so we bodge it with strings:
1435 let image_table = format!(
1436 r#"{{ type="Lib", lib="{}", item="{}" }}"#,
1437 TomlQuote(&l.libname), TomlQuote(item_name.as_str())
1440 let item_name = subst_item_name(&format_item_name(
1441 mformat, &magic.item_prefix, &fe, &magic.item_suffix)?)?;
1443 let mut spec = Substituting::new(mformat, Dollars::Text,
1445 for (k,v) in chain!{
1446 c_substs.into_iter().map(IntoIterator::into_iter).flatten(),
1448 fe.extra_fields.iter().filter(|(k,_v)| k.starts_with('x')),
1450 spec = substn(spec, format!("${{{}}}", k), v)?;
1452 let spec = substn(spec, "${image}", &image_table)?;
1453 let spec = c_colour_all(spec.into())?.into_of_y()?;
1454 let spec = spec.finish()?;
1455 trace!("magic item {}\n\n{}\n", &item_name, &spec);
1457 let spec: Box<dyn PieceSpec> = toml_de::from_str(&spec)
1458 .map_err(|error| LLE::TemplatedTomlError {
1463 l.add_item(&mut NullLibrarySvgNoter, // there's no SVG for *this* item
1464 src_name, &item_name, CatEnt::Magic {
1465 group: group.clone(),
1473 if group.d.colours.is_empty() {
1474 add1(None, None, None)?;
1476 for (colour, recolourdata) in &group.d.colours {
1477 add1(Some(("${colour}", colour)),
1478 Some(("_c", &recolourdata.abbrev)),
1479 Some(&recolourdata.substs))?;
1486 fn add_item(&mut self,
1487 src: &mut dyn LibrarySvgNoter,
1488 src_name: Result<&str,&SubstError>,
1489 item_name: &GoodItemName, catent: CatalogueEntry) {
1490 type H<'e,X,Y> = hash_map::Entry<'e,X,Y>;
1492 let new_item = SvgBaseName::note(
1493 src, item_name.clone(), src_name.clone()
1496 match self.items.entry(new_item) {
1497 H::Occupied(oe) => throw!(LLE::DuplicateItem {
1498 item: item_name.as_str().to_owned(),
1499 group1: oe.get().group().groupname.clone(),
1500 group2: catent.group().groupname.clone(),
1503 debug!("loaded shape {} {}", &self.libname, item_name.as_str());
1510 //---------- reading, support functions ----------
1512 #[throws(LibraryLoadError)]
1513 fn resolve_inherit<'r>(depth: u8, groups: &toml::value::Table,
1514 group_name: &str, group: &'r toml::Value)
1515 -> Cow<'r, toml::value::Table> {
1516 let gn = || format!("{}", group_name);
1517 let gp = || format!("group.{}", group_name);
1519 let group = group.as_table().ok_or_else(|| LLE::ExpectedTable(gp()))?;
1521 let parent_name = match group.get("inherit") {
1522 None => { return Cow::Borrowed(group) }
1525 let parent_name = parent_name
1526 .as_str().ok_or_else(|| LLE::ExpectedString(format!("group.{}.inherit",
1528 let parent = groups.get(parent_name)
1529 .ok_or_else(|| LLE::InheritMissingParent(gn(), parent_name.to_string()))?;
1531 let mut build = resolve_inherit(
1532 depth.checked_sub(1).ok_or_else(|| LLE::InheritDepthLimitExceeded(gn()))?,
1533 groups, parent_name, parent
1536 build.extend(group.iter().map(|(k,v)| (k.clone(), v.clone())));
1540 impl TryFrom<String> for FileList {
1543 fn try_from(s: String) -> Result<FileList,LLE> {
1544 let mut o = Vec::new();
1545 let mut xfields = Vec::new();
1546 for (lno,l) in s.lines().enumerate() {
1548 if l=="" || l.starts_with('#') { continue }
1549 if let Some(xfields_spec) = l.strip_prefix(':') {
1550 if ! (o.is_empty() && xfields.is_empty()) {
1551 throw!(LLE::FilesListFieldsMustBeAtStart(lno));
1553 xfields = xfields_spec.split_ascii_whitespace()
1554 .filter(|s| !s.is_empty())
1555 .map(|s| s.to_owned())
1556 .collect::<Vec<_>>();
1559 let mut remain = &*l;
1561 let ws = remain.find(char::is_whitespace)
1562 .ok_or(LLE::FilesListLineMissingWhitespace(lno))?;
1563 let (l, r) = remain.split_at(ws);
1564 remain = r.trim_start();
1565 Ok::<_,LLE>(l.to_owned())
1567 let item_spec = n()?;
1568 let src_file_spec = n()?;
1569 let extra_fields = xfields.iter()
1570 .map(|field| Ok::<_,LLE>((field.to_owned(), n()?)))
1571 .collect::<Result<_,_>>()?;
1572 let desc = remain.to_owned();
1573 o.push(FileData{ item_spec, src_file_spec, extra_fields, desc });
1579 //==================== Registry ====================
1582 pub fn add(&mut self, data: Catalogue) {
1584 .entry(data.libname.clone()).or_default()
1588 pub fn clear(&mut self) {
1592 pub fn iter(&self) -> impl Iterator<Item=&[Catalogue]> {
1593 self.libs.values().map(|v| v.as_slice())
1597 pub struct AllRegistries<'ig> {
1598 global: RwLockReadGuard<'static, Option<Registry>>,
1601 pub struct AllRegistriesIterator<'i> {
1602 regs: &'i AllRegistries<'i>,
1606 impl<'i> Iterator for AllRegistriesIterator<'i> {
1607 type Item = &'i Registry;
1608 fn next(&mut self) -> Option<&'i Registry> {
1610 let r = match self.count {
1611 0 => self.regs.global.as_ref(),
1612 1 => Some(&self.regs.ig.local_libs),
1616 if r.is_some() { return r }
1622 pub fn all_shapelibs(&self) -> AllRegistries<'_> {
1624 global: GLOBAL_SHAPELIBS.read(),
1629 impl<'ig> AllRegistries<'ig> {
1630 pub fn iter(&'ig self) -> AllRegistriesIterator<'ig> {
1631 AllRegistriesIterator {
1638 pub fn lib_name_list(ig: &Instance) -> Vec<String> {
1639 ig.all_shapelibs().iter().map(
1640 |reg| reg.libs.keys().cloned()
1641 ).flatten().collect()
1644 impl<'ig> AllRegistries<'ig> {
1645 pub fn all_libs(&self) -> impl Iterator<Item=&[Catalogue]> {
1646 self.iter().map(|reg| ®.libs).flatten().map(
1647 |(_libname, lib)| lib.as_slice()
1650 pub fn lib_name_lookup(&self, libname: &str) -> Result<&[Catalogue], SpE> {
1651 for reg in self.iter() {
1652 if let Some(r) = reg.libs.get(libname) { return Ok(r) }
1654 return Err(SpE::LibraryNotFound);
1658 //==================== configu and loading global libs ====================
1660 #[throws(LibraryLoadError)]
1661 pub fn load_1_global_library(l: &Explicit1) {
1662 let toml_path = &l.catalogue;
1663 let catalogue_data = {
1664 let ioe = |io| LLE::FileError(toml_path.to_string(), io);
1665 let f = File::open(toml_path).map_err(ioe)?;
1666 let mut f = BufReader::new(f);
1667 let mut s = String::new();
1668 f.read_to_string(&mut s).map_err(ioe)?;
1672 let catalogue_data = catalogue_data.as_str();
1673 let mut src = BuiltinLibrary { dirname: &l.dirname, catalogue_data };
1675 let data = load_catalogue(&l.name, &mut src)?;
1676 let count = data.items.len();
1677 GLOBAL_SHAPELIBS.write()
1678 .get_or_insert_with(default)
1680 info!("loaded {} shapes in library {:?} from {:?} and {:?}",
1681 count, &l.name, &l.catalogue, &l.dirname);
1685 impl ShapelibConfig1 {
1686 fn resolve(&self) -> Result<Box<dyn ExactSizeIterator<Item=Explicit1>>, LibraryLoadError> {
1689 Explicit(e) => Box::new(iter::once(e.clone())),
1693 fn resolve_globresult(pat: &str, globresult: glob::GlobResult)
1695 let path = globresult?;
1696 let path = path.to_str().ok_or_else(
1698 { pat: pat.to_string(), actual: path.clone() })?
1701 let dirname = path.rsplitn(2,'.').nth(1).ok_or_else(
1702 || LLE::GlobNoExtension
1703 { pat: pat.to_string(), path: path.clone() })?;
1705 let base = dirname.rsplit('/').next().unwrap();
1708 name: base.to_string(),
1709 dirname: dirname.to_string(),
1714 let results = glob::glob_with(pat, glob::MatchOptions {
1715 require_literal_separator: true,
1716 require_literal_leading_dot: true,
1720 |glob::PatternError { pos, msg, .. }|
1721 LLE::BadGlobPattern { pat: pat.clone(), pos, msg }
1723 .map(|globresult| resolve_globresult(pat, globresult))
1724 .collect::<Result<Vec<_>, LLE>>()?;
1726 Box::new(results.into_iter())
1733 #[throws(LibraryLoadError)]
1734 pub fn load_global_libs(libs: &[Config1]) {
1736 let libs = l.resolve()?;
1739 load_1_global_library(&e)?;
1741 info!("loaded {} shape libraries from {:?}", n, &l);