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 #[serde(default="dummy_item_name")] 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 fn dummy_item_name() -> GoodItemName {
287 "".to_string().try_into().unwrap()
291 fn test_dummy_item_name() {
292 let _ = dummy_item_name();
295 //---------- ItemEnquiryData, LibraryEnquiryData ----------
297 #[derive(Debug,Clone,Serialize,Deserialize,Eq,PartialEq,Ord,PartialOrd)]
298 pub struct ItemEnquiryData {
299 pub lib: LibraryEnquiryData,
300 pub itemname: GoodItemName,
301 pub sortkey: Option<String>,
306 impl From<&ItemEnquiryData> for ItemSpec {
307 fn from(it: &ItemEnquiryData) -> ItemSpec {
309 lib: it.lib.libname.clone(),
310 item: it.itemname.as_str().to_owned(),
315 impl Display for ItemEnquiryData {
316 #[throws(fmt::Error)]
317 fn fmt(&self, f: &mut Formatter) {
318 write!(f, "{:<10} {:20} {}", &self.lib, &self.itemname,
319 self.f0desc.as_html_str())?;
323 #[derive(Debug,Clone,Serialize,Deserialize,Eq,PartialEq,Ord,PartialOrd)]
324 pub struct LibraryEnquiryData {
325 pub bundle: Option<bundles::Id>,
328 impl Display for LibraryEnquiryData {
329 #[throws(fmt::Error)]
330 fn fmt(&self, f: &mut Formatter) {
331 if let Some(id) = self.bundle.as_ref() {
332 write!(f, "[{}] ", id)?;
334 if self.libname.chars().all(|c| {
335 c.is_alphanumeric() || c=='-' || c=='_' || c=='.'
337 Display::fmt(&self.libname, f)?;
339 Debug::fmt(&self.libname, f)?;
344 //==================== Item ====================
346 impl_via_ambassador!{
348 impl OutlineTrait for Item { outline }
353 fn svg_face(&self, f: &mut Html, face: FaceId, vpid: VisiblePieceId,
354 xdata: &PieceXDataState) {
355 if let Some(face) = self.faces.get(face) {
356 let svgd = &self.svgs[face.svg];
357 face.xform.write_svgd(f, svgd)?;
358 } else if let Some(back) = &self.back {
359 back.svg(f, vpid, default(), xdata)?;
361 throw!(internal_error_bydebug(&(self, face)))
366 fn describe_face(&self, face: FaceId) -> Html {
368 if let Some(face) = self.faces.get(face) {
370 } else if let Some(back) = &self.back {
371 return back.describe_html(default())?;
380 impl PieceBaseTrait for Item {
381 fn nfaces(&self) -> RawFaceId {
383 + self.back.iter().count())
387 fn itemname(&self) -> &str { &self.itemname }
390 #[typetag::serde(name="Lib")]
391 impl PieceTrait for Item {
393 fn svg_piece(&self, f: &mut Html, gpc: &GPiece,
394 _gs: &GameState, vpid: VisiblePieceId) {
395 self.svg_face(f, gpc.face, vpid, &gpc.xdata)?;
398 fn describe_html(&self, gpc: &GPiece, _goccults: &GOccults) -> Html {
399 self.describe_face(gpc.face)?
402 fn sortkey(&self) -> Option<&str> { self.sort.as_ref().map(AsRef::as_ref) }
405 #[typetag::serde(name="LibItem")]
406 impl InertPieceTrait for Item {
408 fn svg(&self, f: &mut Html, id: VisiblePieceId, face: FaceId,
409 xdata: &PieceXDataState) {
410 self.svg_face(f, face, id, xdata)?;
413 fn describe_html(&self, _: FaceId) -> Html {
414 self.describe_face(default())?
418 //==================== ItemSpec and item loading ====================
420 //---------- ItemSpec, MultiSpec ----------
422 type ItemSpecLoaded = (Box<Item>, PieceSpecLoadedOccultable);
424 impl From<ItemSpecLoaded> for SpecLoaded {
425 fn from((p, occultable): ItemSpecLoaded) -> SpecLoaded {
436 fn find_then<F,T>(&self, ig: &Instance, then: F) -> T
437 where F: FnOnce(&Catalogue, &SvgBaseName<GoodItemName>, &CatalogueEntry)
438 -> Result<T, SpecError>
440 let regs = ig.all_shapelibs();
441 let libs = regs.lib_name_lookup(&self.lib)?;
442 let (lib, (item, idata)) = libs.iter().rev().find_map(
443 |lib| Some((lib, lib.items.get_key_value(self.item.as_str())?))
445 .ok_or_else(|| SpE::LibraryItemNotFound(self.clone()))?;
446 then(lib, item, idata)?
450 fn find_load_general<MAG,MUN,T>(&self, ig: &Instance, depth: SpecDepth,
451 mundanef: MUN, magicf: MAG) -> T
452 where MUN: FnOnce(ItemSpecLoaded) -> Result<T, SpE>,
453 MAG: FnOnce(&Arc<dyn PieceSpec>) -> Result<T, SpE>,
455 self.find_then(ig, |lib, item, catent| Ok(match catent {
456 CatEnt::Item(idata) => {
457 let loaded = lib.load1(idata, &self.lib, item.unnest::<str>(),
461 CatEnt::Magic { spec,.. } => {
468 pub fn find_load_mundane(&self, ig: &Instance,
469 depth: SpecDepth) -> ItemSpecLoaded {
470 self.find_load_general(
471 ig, depth, |loaded| Ok(loaded),
472 |_| Err(SpE::ComplexPieceWhereInertRequired)
476 fn from_strs<L,I>(lib: &L, item: &I) -> Self
477 where L: ToOwned<Owned=String> + ?Sized,
478 I: ToOwned<Owned=String> + ?Sized,
480 let lib = lib .to_owned();
481 let item = item.to_owned();
482 ItemSpec{ lib, item }
486 #[typetag::serde(name="Lib")]
487 impl PieceSpec for ItemSpec {
489 fn load(&self, pla: PLA) -> SpecLoaded {
490 self.find_load_general(
492 |loaded| Ok(loaded.into()),
493 |magic| magic.load(pla.recursing()?)
497 fn load_inert(&self, ig: &Instance, depth: SpecDepth) -> SpecLoadedInert {
498 let (p, occultable) = self.find_load_mundane(ig,depth)?;
499 SpecLoadedInert { p: p as _, occultable }
503 #[typetag::serde(name="LibList")]
504 impl PieceSpec for MultiSpec {
506 fn count(&self, _pcaliases: &PieceAliases) -> usize { self.items.len() }
509 fn load(&self, pla: PLA) -> SpecLoaded
511 let PLA { i,.. } = pla;
512 let item = self.items.get(i).ok_or_else(
513 || SpE::InternalError(format!("item {:?} from {:?}", i, &self))
515 let item = format!("{}{}{}", &self.prefix, item, &self.suffix);
516 let lib = self.lib.clone();
517 ItemSpec { lib, item }.load(pla)?
521 //---------- Loading ----------
525 fn load_image(&self, item_name: &SvgBaseName<str>,
526 lib_name_for: &str, item_for: &str,
527 group: &GroupData, shape_calculable: ShapeCalculable)
529 let svg_path = format!("{}/{}.usvg", self.dirname, item_name);
530 let svg_data = fs::read_to_string(&svg_path)
531 .map_err(|e| if e.kind() == ErrorKind::NotFound {
532 warn!("library item lib={} itme={} for={:?} data file {:?} not found",
533 &self.libname, item_name, item_for, &svg_path);
534 let spec_for = ItemSpec::from_strs(lib_name_for, item_for);
535 SpE::LibraryItemNotFound(spec_for)
537 let m = "error accessing/reading library item data file";
538 error!("{}: {} {:?}: {}", &m, &svg_path, item_for, &e);
539 SpE::InternalError(m.to_string())
542 let svg_data = Html::from_html_string(svg_data);
544 let sz = svg_parse_size(&svg_data).map_err(|error| SpE::SVGError {
546 item_name: item_name.as_str().into(),
547 item_for_lib: lib_name_for.into(),
548 item_for_item: item_for.into(),
551 let (xform, outline) = group.load_shape(sz)
552 .map_err(shape_calculable.err_mapper())?;
562 fn load1(&self, idata: &ItemData, lib_name: &str,
563 name: &SvgBaseName<str>,
564 ig: &Instance, depth:SpecDepth)
566 let ImageLoaded { svgd: svg_data, outline, xform } =
567 self.load_image(name, lib_name, &**name,
568 &idata.group, idata.shape_calculable)?;
570 let mut svgs = IndexVec::with_capacity(1);
571 let svg = svgs.push(svg_data);
573 let mut descs = index_vec![ ];
574 let desc = descs.push(idata.d.desc.clone());
575 descs.shrink_to_fit();
577 let mut face = ItemFace { svg, desc, xform };
578 let mut faces = index_vec![ face ];
579 let mut back = None::<Arc<dyn InertPieceTrait>>;
580 if idata.group.d.flip {
581 face.xform.scale[0] *= -1.;
583 } else if let Some(back_spec) = &idata.group.d.back {
584 match back_spec.load_inert(ig, depth) {
585 Err(SpecError::AliasNotFound) => { },
593 faces.shrink_to_fit();
595 let occultable = match &idata.occ {
596 OccData::None => None,
597 OccData::Back(ilk) => {
598 if let Some(back) = &back {
599 let back = back.clone();
600 Some((LOI::Mix(ilk.clone()), back))
602 None // We got AliasNotFound, ah well
605 OccData::Internal(occ) => {
606 let occ_name = occ.item_name.clone();
609 } = occ.loaded.get_or_create(
611 occ.item_name.unnest::<GoodItemName>().unnest(),
612 /* original: */ lib_name, name.as_str(),
613 &idata.group, idata.shape_calculable,
617 let it = Arc::new(ItemInertForOcculted {
618 svgd, outline, xform,
619 itemname: occ_name.clone().into_inner(),
620 desc: occ.desc.clone(),
621 }) as Arc<dyn InertPieceTrait>;
622 Some((LOI::Mix(occ_name.into_inner()), it))
626 let sort = idata.sort.clone();
627 let it = Item { faces, sort, descs, svgs, outline, back,
628 itemname: name.to_string() };
629 (Box::new(it), occultable)
633 //==================== size handling, and outlines ====================
637 fn from_group_mf1(group: &GroupData) -> Self {
639 // by this point d.size has already been scaled by scale
640 let scale = if ! d.orig_size.is_empty() && ! d.size.is_empty() {
641 izip!(&d.orig_size, &d.size)
642 .map(|(&orig_size, &target_size)| {
643 target_size / orig_size
647 .collect::<ArrayVec<_,2>>()
651 let s = group.d.scale_mf1(group.mformat)?;
654 let centre = d.centre.map(Ok).unwrap_or_else(|| Ok::<_,LLE>({
655 resolve_square_size(&d.size)?
656 .ok_or_else(|| group.mformat.incompat(LLMI::SizeRequired))?
657 .coords.iter().cloned().zip(&scale).map(|(size,scale)| {
660 .collect::<ArrayVec<_,2>>()
664 FaceTransform { centre, scale }
668 fn write_svgd(&self, f: &mut Html, svgd: &Html) {
670 r##"<g transform="scale({} {}) translate({} {})">{}</g>"##,
671 self.scale[0], self.scale[1], -self.centre[0], -self.centre[1],
677 fn resolve_square_size<T:Copy>(size: &[T]) -> Option<PosC<T>> {
678 Some(PosC{ coords: match size {
682 _ => throw!(LLE::WrongNumberOfSizeDimensions
683 { got: size.len(), expected: [1,2]}),
687 impl CatalogueEntry {
688 fn group(&self) -> &Arc<GroupData> { match self {
689 CatEnt::Item(item) => &item.group,
690 CatEnt::Magic{group,..} => group,
694 //---------- Outlines ----------
696 impl ShapeCalculable {
697 pub fn err_mapper(&self) -> impl Fn(LLE) -> IE + Copy {
698 |e| internal_logic_error(format!(
699 "outline calculable but failed {} {:?}",&e,&e
705 #[throws(LibraryLoadError)]
706 fn check_shape(&self) -> ShapeCalculable {
707 let _ = self.load_shape(PosC::new(
708 1.,1. /* dummy value, suffices for error check */
713 #[throws(LibraryLoadError)]
714 /// As with OutlineDefn::load, success must not depend on svg_sz value
715 fn load_shape(&self, svg_sz: PosC<f64>) -> (FaceTransform, Outline) {
716 if self.mformat >= 2 {
718 if self.d.orig_size.len() > 0 {
719 throw!(self.mformat.incompat(LLMI::OrigSizeForbidden))
722 let centre: PosC<f64> = self.d.centre
723 .map(|coords| PosC { coords })
724 .unwrap_or_else(|| geometry::Mean::mean(&svg_sz, &PosC::zero()));
726 let size = resolve_square_size(&self.d.size)?;
728 use ScaleDetails as SD;
729 let scale = self.d.scale.unwrap_or(SD::Fit(ScaleFitDetails::Fit));
731 let of_stretch = |scale| {
732 let scale = PosC { coords: scale };
733 let size = pos_zip_map!( svg_sz, scale => |(sz,sc)| sz * sc );
737 let (size, scale) = match (size, scale) {
738 (Some(size), SD::Fit(fit)) => {
739 let scale = pos_zip_map!( size, svg_sz => |(a,b)| a/b );
740 type Of = OrderedFloat<f64>;
741 let of = |minmax: fn(Of,Of) -> Of| {
742 let v = minmax(scale.coords[0].into(),
743 scale.coords[1].into()).into_inner();
746 let scale = match fit {
747 ScaleFitDetails::Fit => of(min),
748 ScaleFitDetails::Cover => of(max),
749 ScaleFitDetails::Stretch => scale,
753 (Some(_), SD::Scale(_)) |
754 (Some(_), SD::Stretch(_))
755 => throw!(self.mformat.incompat(LLMI::ContradictoryScale)),
756 (None, SD::Fit(_)) => (svg_sz, PosC::new(1.,1.)),
757 (None, SD::Scale(s)) => of_stretch([s,s]),
758 (None, SD::Stretch(s)) => of_stretch(s),
762 let (osize, oscale) = self.d.outline.size_scale();
763 let osize = resolve_square_size(osize)?;
764 match (osize, oscale) {
765 (Some(osize), None ) => osize,
766 (None, Some(&oscale)) => (size * oscale)?,
767 (None, None ) => size,
768 (Some(_), Some(_) ) =>
769 throw!(LLE::OutlineContradictoryScale)
773 let outline = self.d.outline.shape().load(osize);
774 (FaceTransform { scale: scale.coords, centre: centre.coords }, outline)
777 let xform = FaceTransform::from_group_mf1(self)?;
778 let outline = self.d.outline.shape().load_mf1(self)?;
785 #[throws(materials_format::Incompat<LLMI>)]
786 fn scale_mf1(&self, mformat: materials_format::Version) -> f64 {
789 Some(ScaleDetails::Scale(s)) => s,
790 _ => throw!(mformat.incompat(LLMI::Scale)),
795 //---------- OutlineDefn etc. ----------
797 #[ambassador::delegatable_trait]
798 pub trait ShapeLoadableTrait: Debug + Sync + Send + 'static {
799 /// Success or failure must not depend on `svg_sz`
801 /// Called to *check* the group configuration before load, but
802 /// with a dummy svg_gz of `[1,1]`. That must correctly predict
803 /// success with other sizes.
804 fn load(&self, size: PosC<f64>) -> Outline {
805 RectOutline { xy: size }.into()
808 fn load_mf1(&self, group: &GroupData) -> Result<Outline,LLE>;
811 // We used to do shape deser via typetag and Box<dyn OutlineDefn>
813 // But I didnt manage to get typetag to deserialise the way I wanted.
814 // Instead, we have the Shape enum and a cheesy macro to impl OutlineDefn
815 // by delegating to a freshly made (static) unit struct value,
816 // - see outline_defn in mod outline in spec.rs.
817 impl_via_ambassador!{
818 impl ShapeLoadableTrait for Shape { shapelib_loadable() }
821 //---------- RectOutline ----------
823 impl ShapeLoadableTrait for RectShapeIndicator {
824 fn load(&self, size: PosC<f64>) -> Outline {
825 RectOutline { xy: size }.into()
828 #[throws(LibraryLoadError)]
829 fn load_mf1(&self, group: &GroupData) -> Outline {
830 let size = resolve_square_size(&group.d.size)?
831 .ok_or_else(|| group.mformat.incompat(LLMI::SizeRequired))?;
836 //---------- CircleOutline ----------
838 impl ShapeLoadableTrait for CircleShapeIndicator {
839 fn load(&self, size: PosC<f64>) -> Outline {
850 #[throws(LibraryLoadError)]
851 fn load_mf1(&self, group: &GroupData) -> Outline {
852 let diam = match group.d.size.as_slice() {
854 size => throw!(LLE::WrongNumberOfSizeDimensions
855 { got: size.len(), expected: [1,1] }),
863 //==================== Catalogues ====================
865 //---------- enquiries etc. ----------
868 pub fn enquiry(&self) -> LibraryEnquiryData {
870 libname: self.libname.clone(),
876 pub fn list_glob(&self, pat: &str) -> Vec<ItemEnquiryData> {
877 let pat = glob::Pattern::new(pat).map_err(|pe| ME::BadGlob {
878 pat: pat.to_string(), msg: pe.msg.to_string() })?;
879 let mut out = vec![];
880 let ig_dummy = Instance::dummy();
881 for (k,v) in &self.items {
882 if !pat.matches(k.as_str()) { continue }
883 let mut gpc = GPiece::dummy();
884 let loaded = match (|| Ok(match v {
885 CatEnt::Item(item) => {
887 self.load1(item, &self.libname, k.unnest(),
888 &Instance::dummy(), SpecDepth::zero())?;
889 loaded as Box<dyn PieceTrait>
891 CatEnt::Magic{spec,..} => {
892 spec.load(PieceLoadArgs {
896 depth: SpecDepth::zero(),
900 Err(SpecError::LibraryItemNotFound(_)) => continue,
904 let f0bbox = loaded.bbox_approx()?;
905 let ier = ItemEnquiryData {
907 itemname: (**k).to_owned(),
908 sortkey: loaded.sortkey().map(|s| s.to_owned()),
910 f0desc: loaded.describe_html(&gpc, &default())?,
918 pub trait LibrarySvgNoter {
919 #[throws(SubstError)]
920 fn note_svg(&mut self, _basename: &GoodItemName,
921 _src_name: Result<&str, &SubstError>) { }
923 pub trait LibrarySource: LibrarySvgNoter {
924 fn catalogue_data(&self) -> &str;
925 fn svg_dir(&self) -> String;
926 fn bundle(&self) -> Option<bundles::Id>;
928 fn default_materials_format(&self)
929 -> Result<materials_format::Version, MFVE>;
931 // Sadly dyn_upcast doesn't work because it doesn't support the
932 // non-'static lifetime on BuiltinLibrary
933 fn svg_noter(&mut self) -> &mut dyn LibrarySvgNoter;
936 pub struct NullLibrarySvgNoter;
937 impl LibrarySvgNoter for NullLibrarySvgNoter { }
939 struct BuiltinLibrary<'l> {
940 catalogue_data: &'l str,
944 impl LibrarySvgNoter for BuiltinLibrary<'_> {
946 impl<'l> LibrarySource for BuiltinLibrary<'l> {
947 fn catalogue_data(&self) -> &str { self.catalogue_data }
948 fn svg_dir(&self) -> String { self.dirname.to_string() }
949 fn bundle(&self) -> Option<bundles::Id> { None }
951 #[throws(materials_format::VersionError)]
952 fn default_materials_format(&self) -> materials_format::Version {
953 throw!(MFVE::Other("builtin libraries must have explicit version now!"));
956 fn svg_noter(&mut self) -> &mut dyn LibrarySvgNoter { self }
959 //---------- reading ----------
961 #[throws(LibraryLoadError)]
962 pub fn load_catalogue(libname: &str, src: &mut dyn LibrarySource)
966 let toplevel: toml::Value = src.catalogue_data().parse()?;
967 let toplevel = toplevel
968 .as_table().ok_or_else(|| LLE::ExpectedTable(format!("toplevel")))?;
969 let mformat = match toplevel.get("format") {
970 None => src.default_materials_format()?,
972 let v = v.as_integer().ok_or_else(|| MFVE::Other("not an integer"))?;
973 materials_format::Version::try_from_integer(v)?
977 let mut l = Catalogue {
978 bundle: src.bundle(),
979 libname: libname.to_string(),
980 items: HashMap::new(),
981 dirname: src.svg_dir(),
983 let empty_table = toml::value::Value::Table(default());
984 let groups = toplevel
985 .get("group").unwrap_or(&empty_table)
986 .as_table().ok_or_else(|| LLE::ExpectedTable(format!("group")))?;
987 for (groupname, gdefn) in groups {
990 let gdefn = resolve_inherit(INHERIT_DEPTH_LIMIT,
991 groups, groupname, gdefn)?;
992 let gdefn: GroupDefn = TV::Table(gdefn.into_owned()).try_into()?;
993 let d = if mformat == 1 {
994 let scale = gdefn.d.scale_mf1(mformat)?;
996 size: gdefn.d.size.iter().map(|s| s * scale).collect(),
1000 gdefn.d // v2 isn't going to do this, do this right now
1002 let group = Arc::new(GroupData {
1003 groupname: groupname.clone(),
1007 // We do this here rather than in the files loop because
1008 // 1. we want to check it even if there are no files specified
1009 // 2. this is OK because the group doesn't change from here on
1010 let shape_calculable = group.check_shape()?;
1014 group.d.back.is_some(),
1015 ].iter().filter(|x|**x).count() > 1 {
1016 throw!(LLE::MultipleMultipleFaceDefinitions)
1019 for fe in gdefn.files.0 {
1020 process_files_entry(
1021 src.svg_noter(), &mut l,
1022 &gdefn.item_prefix, &gdefn.item_suffix, &gdefn.sort,
1023 &group, shape_calculable, fe
1028 })().map_err(|error| LLE::InGroup {
1029 group: groupname.to_string(),
1030 error: Box::new(error),
1035 })().map_err(|error| LLE::InLibrary {
1036 lib: libname.into(),
1037 error: Box::new(error),
1041 #[derive(Debug,Copy,Clone,Eq,PartialEq)]
1042 pub enum Dollars { Text, Filename }
1044 #[derive(Debug,Clone)]
1045 pub struct Substituting<'s> {
1047 mformat: materials_format::Version,
1051 impl<'s> Substituting<'s> {
1052 pub fn new<S: Into<Cow<'s, str>>>(
1053 mformat: materials_format::Version,
1057 Substituting { s: s.into(), mformat, dollars }
1060 #[throws(SubstError)]
1061 pub fn finish(self) -> String {
1062 if self.do_dollars() {
1063 self.subst_general_precisely("${$}", "$")?.0
1069 fn do_dollars(&self) -> bool { self.dollars.enabled(self.mformat) }
1071 #[throws(SubstError)]
1072 /// Expand, but do not do final unescaping
1074 /// Used when we are expanding something that is going to be used
1075 /// as a replacement in a further expansion, which will do final unescaping.
1076 pub fn nest(self) -> String {
1080 fn err(&self, kind: SubstErrorKind) -> SubstError {
1081 SubstError { kind, input: (*self.s).to_owned() }
1084 fn internal_err(&self, msg: &'static str) -> SubstError {
1085 self.err(InternalLogicError::new(msg).into())
1090 fn enabled(self, mformat: materials_format::Version) -> bool {
1092 Dollars::Filename => false,
1093 Dollars::Text => mformat >= 2,
1098 impl<'i> Substituting<'i> {
1099 #[throws(SubstError)]
1100 fn subst_general_precisely(&self, needle: &str, replacement: &str)
1101 -> (Substituting<'i>, usize) {
1103 let mut work = (*self.s).to_owned();
1104 for m in self.s.rmatch_indices(needle) {
1106 let mut lhs = &work[0.. m.0];
1107 let mut rhs = &work[m.0 + m.1.len() ..];
1108 if replacement.is_empty() {
1109 let lhs_trimmed = lhs.trim_end();
1110 if lhs_trimmed.len() != lhs.len() {
1113 rhs = rhs.trim_start();
1123 mformat: self.mformat,
1124 dollars: self.dollars,
1128 #[throws(SubstError)]
1129 // This takes &Substituting. The rest of the code uses subst or
1130 // substn, which takes Substituting, thus ensuring that at some future
1131 // time we might be able to accumulate all the substitutions in
1132 // Substituting and do them all at once.
1133 fn subst_general(&self, needle: Cow<'static, str>, replacement: &str)
1134 -> (Substituting<'i>, usize, Cow<'static, str>) {
1135 match self.dollars {
1136 Dollars::Filename => if needle != "_c" {
1137 throw!(self.internal_err("long subst in filename"))
1139 Dollars::Text => { },
1141 let needle: Cow<str> = (move || Some({
1142 if let Some(rhs) = needle.strip_prefix("${") {
1143 let token = rhs.strip_suffix('}')?;
1144 if self.do_dollars() { needle }
1145 else { format!("_{}", token).into() }
1146 } else if let Some(token) = needle.strip_prefix('_') {
1147 if ! self.do_dollars() { needle }
1148 else { format!("${{{}}}", token).into() }
1153 .ok_or_else(|| self.internal_err("needle has no '_'"))?;
1155 let (r, count) = self.subst_general_precisely(&needle, replacement)?;
1160 #[throws(SubstError)]
1161 fn subst<'i,N>(before: Substituting<'i>, needle: N, replacement: &str)
1163 where N: Into<Cow<'static, str>>
1165 use SubstErrorKind as SEK;
1166 let needle = needle.into();
1167 let (out, count, needle) = before.subst_general(needle, replacement)?;
1168 if count == 0 { throw!(before.err(SEK::MissingToken(needle))) }
1169 if count > 1 { throw!(before.err(SEK::RepeatedToken(needle))) }
1173 #[throws(SubstError)]
1174 fn substn<'i,N>(before: Substituting<'i>, needle: N, replacement: &str)
1176 where N: Into<Cow<'static, str>>
1178 before.subst_general(needle.into(), replacement)?.0
1183 fn test_subst_mf1() {
1184 use SubstErrorKind as SEK;
1186 let mformat = materials_format::Version::try_from_integer(1).unwrap();
1187 let s_t = |s| Substituting::new(mformat, Dollars::Text, s);
1188 let s_f = |s| Substituting::new(mformat, Dollars::Filename, s);
1190 assert_eq!(subst(s_f("die-image-_c"), "_c", "blue")
1191 .unwrap().finish().unwrap(),
1193 assert_eq!(subst(s_t("a _colour die"), "_colour", "blue")
1194 .unwrap().finish().unwrap(),
1196 assert_eq!(subst(s_t("a _colour die"), "${colour}", "blue")
1197 .unwrap().finish().unwrap(),
1199 assert_eq!(subst(s_t("a _colour die"), "_colour", "")
1200 .unwrap().finish().unwrap(),
1203 dbg!(subst(s_t("a die"), "_colour", "")).unwrap_err().kind,
1204 SEK::MissingToken(c) if c == "_colour",
1207 dbg!(subst(s_t("a _colour _colour die"), "_colour", "")).unwrap_err().kind,
1208 SEK::RepeatedToken(c) if c == "_colour",
1211 assert_eq!(substn(s_t("a _colour die being _colour"), "_colour", "blue")
1212 .unwrap().finish().unwrap(),
1213 "a blue die being blue");
1215 let (s, count, needle) = s_t("a _colour _colour die")
1216 .subst_general("_colour".into(), "")
1218 assert_eq!(s.finish().unwrap(), "a die".to_owned());
1219 assert_eq!(count, 2);
1220 assert_eq!(needle, "_colour");
1225 fn test_subst_mf2() {
1226 use SubstErrorKind as SEK;
1228 let mformat = materials_format::Version::try_from_integer(2).unwrap();
1229 let s_t = |s| Substituting::new(mformat, Dollars::Text, s);
1230 let s_f = |s| Substituting::new(mformat, Dollars::Filename, s);
1232 assert_eq!(subst(s_f("die-image-_c"), "_c", "blue")
1233 .unwrap().finish().unwrap(),
1236 dbg!(subst(s_f("die-image-_c"), "_colour", "")).unwrap_err().kind,
1240 assert_eq!(subst(s_t("a ${colour} die"), "_colour", "blue")
1241 .unwrap().finish().unwrap(),
1243 assert_eq!(subst(s_t("a ${c} die"), "_c", "blue")
1244 .unwrap().finish().unwrap(),
1246 assert_eq!(subst(s_t("a ${colour} die"), "_colour", "")
1247 .unwrap().finish().unwrap(),
1249 assert_eq!(subst(s_t("a ${colour} die"), "${colour}", "")
1250 .unwrap().finish().unwrap(),
1253 dbg!(subst(s_t("a die"), "_colour", "")).unwrap_err().kind,
1254 SEK::MissingToken(c) if c == "${colour}",
1257 dbg!(subst(s_t("a ${colour} ${colour} die"), "_colour", ""))
1259 SEK::RepeatedToken(c) if c == "${colour}",
1262 assert_eq!(substn(s_t("a ${colour} die being ${colour}"), "_colour", "blue")
1263 .unwrap().finish().unwrap(),
1264 "a blue die being blue");
1266 let (s, count, needle) = s_t("a ${colour} ${colour} die")
1267 .subst_general("_colour".into(), "")
1269 assert_eq!(s.finish().unwrap(), "a die".to_owned());
1270 assert_eq!(count, 2);
1271 assert_eq!(needle, "${colour}");
1274 #[throws(LibraryLoadError)]
1275 fn format_item_name(mformat: materials_format::Version,
1276 item_prefix: &str, fe: &FileData, item_suffix: &str)
1277 -> Substituting<'static> {
1279 mformat, Dollars::Filename,
1280 format!("{}{}{}", item_prefix, fe.item_spec, item_suffix)
1284 #[throws(LibraryLoadError)]
1285 fn process_files_entry(
1286 src: &mut dyn LibrarySvgNoter, l: &mut Catalogue,
1287 item_prefix: &str, item_suffix: &str, sort: &str,
1288 group: &Arc<GroupData>, shape_calculable: ShapeCalculable,
1291 let mformat = group.mformat;
1292 let item_name = format_item_name(mformat, item_prefix, &fe, item_suffix)?;
1294 let sort: Option<PerhapsSubst> = match (sort, fe.extra_fields.get("sort")) {
1296 (gd, None) => Some(gd.into()),
1297 ("", Some(ef)) => Some(ef.into()),
1299 let sort = Substituting::new(mformat, Dollars::Text, gd);
1300 Some(subst(sort, "_s", ef)?.into())
1304 let occ = match &group.d.occulted {
1305 None => OccData::None,
1306 Some(OccultationMethod::ByColour { colour }) => {
1307 if ! group.d.colours.contains_key(colour.0.as_str()) {
1308 throw!(LLE::OccultationColourMissing(colour.0.clone()));
1310 let item_name = subst(item_name.clone(), "_c", &colour.0)?;
1311 let src_name = Substituting::new(mformat, Dollars::Filename,
1313 let src_name = subst(src_name, "_c", &colour.0)
1314 .and_then(|s| s.finish());
1315 let item_name: GoodItemName = item_name.finish()?.try_into()?;
1316 let item_name = SvgBaseName::note(
1317 src, item_name, src_name.as_deref(),
1319 let desc = Substituting::new(mformat, Dollars::Text, &fe.desc);
1320 let desc = subst(desc, "${colour}", "")?.finish()?.to_html();
1321 OccData::Internal(Arc::new(OccData_Internal {
1327 Some(OccultationMethod::ByBack { ilk }) => {
1328 if group.d.back.is_none() {
1329 throw!(LLE::BackMissingForOccultation)
1331 OccData::Back(ilk.clone())
1335 #[derive(Debug,From,Clone)]
1336 enum PerhapsSubst<'i> {
1337 Y(Substituting<'i>),
1340 impl<'i> From<&'i String> for PerhapsSubst<'i> {
1341 fn from(s: &'i String) -> Self { (&**s).into() }
1344 impl<'i> PerhapsSubst<'i> {
1345 #[throws(SubstError)]
1346 pub fn finish(self) -> String { match self {
1347 PerhapsSubst::N(s) => s.to_owned(),
1348 PerhapsSubst::Y(s) => s.finish()?,
1350 #[throws(SubstError)]
1351 pub fn nest(self) -> String { match self {
1352 PerhapsSubst::N(s) => s.to_owned(),
1353 PerhapsSubst::Y(s) => s.nest()?,
1357 mformat: materials_format::Version,
1359 ) -> Substituting<'i> { match self {
1360 PerhapsSubst::N(s) => Substituting::new(mformat, dollars, s),
1361 PerhapsSubst::Y(s) => s,
1363 #[throws(SubstError)]
1366 ) -> Substituting<'i> { match self {
1367 PerhapsSubst::Y(s) => s,
1368 PerhapsSubst::N(s) => throw!(SubstError {
1369 kind: InternalLogicError::new("expected Y").into(),
1375 fn colour_subst_1<'s, S>(
1376 mformat: materials_format::Version,
1378 subst: S, kv: Option<(&'static str, &'s str)>
1380 -> impl for <'i> Fn(PerhapsSubst<'i>)
1381 -> Result<PerhapsSubst<'i>, SubstError>
1383 where S: for <'i> Fn(Substituting<'i>, &'static str, &str)
1384 -> Result<Substituting<'i>, SubstError> + 's
1387 if let Some((keyword, val)) = kv {
1388 subst(input.mky(mformat, dollars), keyword, val)?.into()
1389 } else if dollars.enabled(mformat) {
1390 input.mky(mformat, dollars).into()
1398 c_colour: Option<(&'static str, &str)>,
1399 c_abbrev: Option<(&'static str, &str)>,
1400 c_substs: Option<&HashMap<String, String>>,
1402 let c_colour_all =colour_subst_1(mformat,Dollars::Text, substn, c_colour);
1403 let c_colour = colour_subst_1(mformat,Dollars::Text, subst, c_colour);
1404 let c_abbrev_t = colour_subst_1(mformat,Dollars::Text, subst, c_abbrev);
1405 let c_abbrev_f =colour_subst_1(mformat,Dollars::Filename,subst, c_abbrev);
1407 let sort = sort.clone().map(|v| c_abbrev_t(v)).transpose()?;
1408 let sort = sort.map(|s| s.finish()).transpose()?;
1410 let subst_item_name = |item_name: &Substituting| {
1411 let item_name = c_abbrev_f(item_name.clone().into())?;
1412 let item_name = item_name.finish()?.try_into()?;
1413 Ok::<_,LLE>(item_name)
1415 let item_name = subst_item_name(&item_name)?;
1417 let src_name = c_abbrev_f((&fe.src_file_spec).into())
1418 .and_then(|s| s.finish());
1419 let src_name = src_name.as_deref();
1421 let desc = c_colour((&fe.desc).into())?;
1423 let desc = if let Some(desc_template) = &group.d.desc_template {
1424 let desc_template = Substituting::new(
1425 mformat, Dollars::Text, desc_template);
1426 subst(desc_template, "${desc}", &desc.nest()?)?.finish()?.to_html()
1428 desc.finish()?.to_html()
1431 let idata = ItemData {
1432 group: group.clone(),
1436 d: Arc::new(ItemDetails { desc }),
1438 l.add_item(src, src_name, &item_name, CatEnt::Item(idata))?;
1440 if let Some(magic) = &group.d.magic {
1441 // Ideally the toml crate would have had let us build an inline
1442 // table. But in fact it won't even toml-escape the strings without
1443 // a fuss, so we bodge it with strings:
1444 let image_table = format!(
1445 r#"{{ type="Lib", lib="{}", item="{}" }}"#,
1446 TomlQuote(&l.libname), TomlQuote(item_name.as_str())
1449 let item_name = subst_item_name(&format_item_name(
1450 mformat, &magic.item_prefix, &fe, &magic.item_suffix)?)?;
1452 let mut spec = Substituting::new(mformat, Dollars::Text,
1454 for (k,v) in chain!{
1455 c_substs.into_iter().map(IntoIterator::into_iter).flatten(),
1457 fe.extra_fields.iter().filter(|(k,_v)| k.starts_with('x')),
1459 spec = substn(spec, format!("${{{}}}", k), v)?;
1461 let spec = substn(spec, "${image}", &image_table)?;
1462 let spec = c_colour_all(spec.into())?.into_of_y()?;
1463 let spec = spec.finish()?;
1464 trace!("magic item {}\n\n{}\n", &item_name, &spec);
1466 let spec: Box<dyn PieceSpec> = toml_de::from_str(&spec)
1467 .map_err(|error| LLE::TemplatedTomlError {
1472 l.add_item(&mut NullLibrarySvgNoter, // there's no SVG for *this* item
1473 src_name, &item_name, CatEnt::Magic {
1474 group: group.clone(),
1482 if group.d.colours.is_empty() {
1483 add1(None, None, None)?;
1485 for (colour, recolourdata) in &group.d.colours {
1486 add1(Some(("${colour}", colour)),
1487 Some(("_c", &recolourdata.abbrev)),
1488 Some(&recolourdata.substs))?;
1495 fn add_item(&mut self,
1496 src: &mut dyn LibrarySvgNoter,
1497 src_name: Result<&str,&SubstError>,
1498 item_name: &GoodItemName, catent: CatalogueEntry) {
1499 type H<'e,X,Y> = hash_map::Entry<'e,X,Y>;
1501 let new_item = SvgBaseName::note(
1502 src, item_name.clone(), src_name.clone()
1505 match self.items.entry(new_item) {
1506 H::Occupied(oe) => throw!(LLE::DuplicateItem {
1507 item: item_name.as_str().to_owned(),
1508 group1: oe.get().group().groupname.clone(),
1509 group2: catent.group().groupname.clone(),
1512 debug!("loaded shape {} {}", &self.libname, item_name.as_str());
1519 //---------- reading, support functions ----------
1521 #[throws(LibraryLoadError)]
1522 fn resolve_inherit<'r>(depth: u8, groups: &toml::value::Table,
1523 group_name: &str, group: &'r toml::Value)
1524 -> Cow<'r, toml::value::Table> {
1525 let gn = || format!("{}", group_name);
1526 let gp = || format!("group.{}", group_name);
1528 let group = group.as_table().ok_or_else(|| LLE::ExpectedTable(gp()))?;
1530 let parent_name = match group.get("inherit") {
1531 None => { return Cow::Borrowed(group) }
1534 let parent_name = parent_name
1535 .as_str().ok_or_else(|| LLE::ExpectedString(format!("group.{}.inherit",
1537 let parent = groups.get(parent_name)
1538 .ok_or_else(|| LLE::InheritMissingParent(gn(), parent_name.to_string()))?;
1540 let mut build = resolve_inherit(
1541 depth.checked_sub(1).ok_or_else(|| LLE::InheritDepthLimitExceeded(gn()))?,
1542 groups, parent_name, parent
1545 build.extend(group.iter().map(|(k,v)| (k.clone(), v.clone())));
1549 impl TryFrom<String> for FileList {
1552 fn try_from(s: String) -> Result<FileList,LLE> {
1553 let mut o = Vec::new();
1554 let mut xfields = Vec::new();
1555 for (lno,l) in s.lines().enumerate() {
1557 if l=="" || l.starts_with('#') { continue }
1558 if let Some(xfields_spec) = l.strip_prefix(':') {
1559 if ! (o.is_empty() && xfields.is_empty()) {
1560 throw!(LLE::FilesListFieldsMustBeAtStart(lno));
1562 xfields = xfields_spec.split_ascii_whitespace()
1563 .filter(|s| !s.is_empty())
1564 .map(|s| s.to_owned())
1565 .collect::<Vec<_>>();
1568 let mut remain = &*l;
1570 let ws = remain.find(char::is_whitespace)
1571 .ok_or(LLE::FilesListLineMissingWhitespace(lno))?;
1572 let (l, r) = remain.split_at(ws);
1573 remain = r.trim_start();
1574 Ok::<_,LLE>(l.to_owned())
1576 let item_spec = n()?;
1577 let src_file_spec = n()?;
1578 let extra_fields = xfields.iter()
1579 .map(|field| Ok::<_,LLE>((field.to_owned(), n()?)))
1580 .collect::<Result<_,_>>()?;
1581 let desc = remain.to_owned();
1582 o.push(FileData{ item_spec, src_file_spec, extra_fields, desc });
1588 //==================== Registry ====================
1591 pub fn add(&mut self, data: Catalogue) {
1593 .entry(data.libname.clone()).or_default()
1597 pub fn clear(&mut self) {
1601 pub fn iter(&self) -> impl Iterator<Item=&[Catalogue]> {
1602 self.libs.values().map(|v| v.as_slice())
1606 pub struct AllRegistries<'ig> {
1607 global: RwLockReadGuard<'static, Option<Registry>>,
1610 pub struct AllRegistriesIterator<'i> {
1611 regs: &'i AllRegistries<'i>,
1615 impl<'i> Iterator for AllRegistriesIterator<'i> {
1616 type Item = &'i Registry;
1617 fn next(&mut self) -> Option<&'i Registry> {
1619 let r = match self.count {
1620 0 => self.regs.global.as_ref(),
1621 1 => Some(&self.regs.ig.local_libs),
1625 if r.is_some() { return r }
1631 pub fn all_shapelibs(&self) -> AllRegistries<'_> {
1633 global: GLOBAL_SHAPELIBS.read(),
1638 impl<'ig> AllRegistries<'ig> {
1639 pub fn iter(&'ig self) -> AllRegistriesIterator<'ig> {
1640 AllRegistriesIterator {
1647 pub fn lib_name_list(ig: &Instance) -> Vec<String> {
1648 ig.all_shapelibs().iter().map(
1649 |reg| reg.libs.keys().cloned()
1650 ).flatten().collect()
1653 impl<'ig> AllRegistries<'ig> {
1654 pub fn all_libs(&self) -> impl Iterator<Item=&[Catalogue]> {
1655 self.iter().map(|reg| ®.libs).flatten().map(
1656 |(_libname, lib)| lib.as_slice()
1659 pub fn lib_name_lookup(&self, libname: &str) -> Result<&[Catalogue], SpE> {
1660 for reg in self.iter() {
1661 if let Some(r) = reg.libs.get(libname) { return Ok(r) }
1663 return Err(SpE::LibraryNotFound);
1667 //==================== configu and loading global libs ====================
1669 #[throws(LibraryLoadError)]
1670 pub fn load_1_global_library(l: &Explicit1) {
1671 let toml_path = &l.catalogue;
1672 let catalogue_data = {
1673 let ioe = |io| LLE::FileError(toml_path.to_string(), io);
1674 let f = File::open(toml_path).map_err(ioe)?;
1675 let mut f = BufReader::new(f);
1676 let mut s = String::new();
1677 f.read_to_string(&mut s).map_err(ioe)?;
1681 let catalogue_data = catalogue_data.as_str();
1682 let mut src = BuiltinLibrary { dirname: &l.dirname, catalogue_data };
1684 let data = load_catalogue(&l.name, &mut src)?;
1685 let count = data.items.len();
1686 GLOBAL_SHAPELIBS.write()
1687 .get_or_insert_with(default)
1689 info!("loaded {} shapes in library {:?} from {:?} and {:?}",
1690 count, &l.name, &l.catalogue, &l.dirname);
1694 impl ShapelibConfig1 {
1695 fn resolve(&self) -> Result<Box<dyn ExactSizeIterator<Item=Explicit1>>, LibraryLoadError> {
1698 Explicit(e) => Box::new(iter::once(e.clone())),
1702 fn resolve_globresult(pat: &str, globresult: glob::GlobResult)
1704 let path = globresult?;
1705 let path = path.to_str().ok_or_else(
1707 { pat: pat.to_string(), actual: path.clone() })?
1710 let dirname = path.rsplitn(2,'.').nth(1).ok_or_else(
1711 || LLE::GlobNoExtension
1712 { pat: pat.to_string(), path: path.clone() })?;
1714 let base = dirname.rsplit('/').next().unwrap();
1717 name: base.to_string(),
1718 dirname: dirname.to_string(),
1723 let results = glob::glob_with(pat, glob::MatchOptions {
1724 require_literal_separator: true,
1725 require_literal_leading_dot: true,
1729 |glob::PatternError { pos, msg, .. }|
1730 LLE::BadGlobPattern { pat: pat.clone(), pos, msg }
1732 .map(|globresult| resolve_globresult(pat, globresult))
1733 .collect::<Result<Vec<_>, LLE>>()?;
1735 Box::new(results.into_iter())
1742 #[throws(LibraryLoadError)]
1743 pub fn load_global_libs(libs: &[Config1]) {
1745 let libs = l.resolve()?;
1748 load_1_global_library(&e)?;
1750 info!("loaded {} shape libraries from {:?}", n, &l);