chiark / gitweb /
669b393d460098fc5261f7de27ccbde51c0b7fc4
[otter.git] / src / shapelib.rs
1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 use crate::prelude::*;
6 use crate::*; // to get ambassador_impls, macro resolution trouble
7 pub use crate::shapelib_toml::*;
8
9 pub use crate::prelude::GoodItemName; // not sure why this is needed
10
11 use parking_lot::{const_rwlock, RwLock};
12 use parking_lot::RwLockReadGuard;
13
14 use ShapelibConfig1 as Config1;
15 use ShapelibExplicit1 as Explicit1;
16
17 //==================== structs and definitions ====================
18
19 // Naming convention:
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
25
26 static GLOBAL_SHAPELIBS: RwLock<Option<Registry>> = const_rwlock(None);
27
28 #[derive(Default)]
29 pub struct Registry {
30   libs: HashMap<String, Vec<shapelib::Catalogue>>,
31 }
32
33 #[derive(Debug)]
34 pub struct GroupData {
35   groupname: String,
36   d: GroupDetails,
37   #[allow(dead_code)] /*TODO*/ mformat: materials_format::Version,
38 }
39
40 #[derive(Debug,Clone,Copy)]
41 pub struct ShapeCalculable { }
42
43 #[derive(Debug)]
44 pub struct Catalogue {
45   libname: String,
46   dirname: String,
47   bundle: Option<bundles::Id>,
48   items: HashMap<SvgBaseName<GoodItemName>, CatalogueEntry>,
49 }
50
51 #[derive(Debug,Clone)]
52 #[derive(Serialize,Deserialize)]
53 struct ItemDetails {
54   desc: Html,
55 }
56
57 #[derive(Debug,Clone)]
58 enum CatalogueEntry {
59   Item(ItemData),
60   Magic { group: Arc<GroupData>, spec: Arc<dyn PieceSpec> },
61 }
62 use CatalogueEntry as CatEnt;
63
64 #[derive(Debug,Clone)]
65 struct ItemData {
66   d: Arc<ItemDetails>,
67   sort: Option<String>,
68   group: Arc<GroupData>,
69   occ: OccData,
70   shape_calculable: ShapeCalculable,
71 }
72
73 #[derive(Debug,Clone)]
74 enum OccData {
75   None,
76   Internal(Arc<OccData_Internal>),
77   Back(OccultIlkName),
78 }
79
80 #[allow(non_camel_case_types)]
81 #[derive(Debug)]
82 struct OccData_Internal {
83   item_name: SvgBaseName<GoodItemName>,
84   desc: Html,
85   loaded: lazy_init::Lazy<Result<ImageLoaded,SpecError>>,
86 }
87
88 #[allow(non_camel_case_types)]
89 #[derive(Debug,Clone)]
90 struct ImageLoaded {
91   svgd: Html,
92   xform: FaceTransform,
93   outline: Outline,
94 }
95
96 #[derive(Error,Debug)]
97 pub enum LibraryLoadError {
98   #[error(transparent)]
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),
133
134   #[error("{0}")]
135   MaterialsFormatIncompat(#[from] materials_format::Incompat<
136     LibraryLoadMFIncompat
137   >),
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),
142
143   #[error("could not parse template-expaneded TOML: {error} (in {toml:?}")]
144   TemplatedTomlError { toml: String, error: toml_de::Error },
145
146   #[error("group {group}: {error}")]
147   InGroup { group: String, error: Box<LLE> },
148
149   #[error("library {lib}: {error}")]
150   InLibrary { lib: String, error: Box<LLE> },
151 }
152
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,
159 }
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),
165 }
166
167 #[derive(Error,Clone,Debug)]
168 #[error("bad substitution: {input:?} {kind}")]
169 pub struct SubstError {
170   pub kind: SubstErrorKind,
171   pub input: String,
172 }
173
174
175 const INHERIT_DEPTH_LIMIT: u8 = 20;
176
177 type TV = toml::Value;
178
179 #[derive(Debug, Clone, Serialize, Deserialize)]
180 pub struct MultiSpec {
181   pub lib: String,
182   #[serde(default)]
183   pub prefix: String,
184   #[serde(default)]
185   pub suffix: String,
186   pub items: Vec<String>,
187 }
188
189 define_index_type! { pub struct DescId = u8; }
190 define_index_type! { pub struct SvgId = u8; }
191
192 #[derive(Copy,Clone,Debug,Serialize,Deserialize)]
193 struct ItemFace {
194   svg: SvgId,
195   desc: DescId,
196   #[serde(flatten)]
197   xform: FaceTransform,
198 }
199
200 #[derive(Copy,Clone,Debug,Serialize,Deserialize)]
201 struct FaceTransform {
202   centre: [f64; 2],
203   scale: [f64; 2],
204 }
205
206 #[derive(Debug,Serialize,Deserialize)]
207 pub struct Item {
208   itemname: String,
209   sort: Option<String>,
210   faces: IndexVec<FaceId, ItemFace>,
211   svgs: IndexVec<SvgId, Html>,
212   descs: IndexVec<DescId, Html>,
213   outline: Outline,
214   #[serde(default)]
215   back: Option<Arc<dyn InertPieceTrait>>,
216 }
217
218 #[derive(Debug,Serialize,Deserialize)]
219 struct ItemInertForOcculted {
220   itemname: GoodItemName,
221   desc: Html,
222   svgd: Html,
223   xform: FaceTransform,
224   outline: Outline,
225 }
226
227 //==================== SvgBsseName ====================
228
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)]
232 #[repr(transparent)]
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)? }
237 }
238 impl<T> Borrow<str> for SvgBaseName<T> where T: Borrow<str> {
239   fn borrow(&self) -> &str { self.0.borrow() }
240 }
241 impl<T> SvgBaseName<T> {
242   fn into_inner(self) -> T { self.0 }
243 }
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) };
249     u
250   }
251 }
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)?;
257     SvgBaseName(i)
258   }
259 }
260
261 //==================== impls for ItemInertForOcculted ====================
262
263 impl_via_ambassador!{
264   #[dyn_upcast]
265   impl OutlineTrait for ItemInertForOcculted { outline }
266 }
267 #[dyn_upcast]
268 impl PieceBaseTrait for ItemInertForOcculted {
269   fn nfaces(&self) -> RawFaceId { 1 }
270   fn itemname(&self) -> &str { self.itemname.as_str() }
271 }
272 #[typetag::serde(name="Lib")]
273 impl InertPieceTrait for ItemInertForOcculted {
274   #[throws(IE)]
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"))
279     }
280     self.xform.write_svgd(f, &self.svgd)?;
281   }
282   #[throws(IE)]
283   fn describe_html(&self, _: FaceId) -> Html { self.desc.clone() }
284 }
285
286 //---------- ItemEnquiryData, LibraryEnquiryData ----------
287
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>,
293   pub f0desc: Html,
294   pub f0bbox: Rect,
295 }
296
297 impl From<&ItemEnquiryData> for ItemSpec {
298   fn from(it: &ItemEnquiryData) -> ItemSpec {
299     ItemSpec {
300       lib: it.lib.libname.clone(),
301       item: it.itemname.as_str().to_owned(),
302     }
303   }
304 }
305
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())?;
311   }
312 }
313
314 #[derive(Debug,Clone,Serialize,Deserialize,Eq,PartialEq,Ord,PartialOrd)]
315 pub struct LibraryEnquiryData {
316   pub bundle: Option<bundles::Id>,
317   pub libname: String,
318 }
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)?;
324     }
325     if self.libname.chars().all(|c| {
326       c.is_alphanumeric() || c=='-' || c=='_' || c=='.'
327     }) {
328       Display::fmt(&self.libname, f)?;
329     } else {
330       Debug::fmt(&self.libname, f)?;
331     }
332   }
333 }
334
335 //==================== Item ====================
336
337 impl_via_ambassador!{
338   #[dyn_upcast]
339   impl OutlineTrait for Item { outline }
340 }
341
342 impl Item {
343   #[throws(IE)]
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)?;
351     } else {
352       throw!(internal_error_bydebug(&(self, face)))
353     }
354   }
355
356   #[throws(IE)]
357   fn describe_face(&self, face: FaceId) -> Html {
358     self.descs[
359       if let Some(face) = self.faces.get(face) {
360         face.desc
361       } else if let Some(back) = &self.back {
362         return back.describe_html(default())?;
363       } else {
364         self.faces[0].desc
365       }
366     ].clone()
367   }
368 }
369
370 #[dyn_upcast]
371 impl PieceBaseTrait for Item {
372   fn nfaces(&self) -> RawFaceId {
373     (self.faces.len()
374      + self.back.iter().count())
375       .try_into().unwrap()
376   }
377
378   fn itemname(&self) -> &str { &self.itemname }
379 }
380
381 #[typetag::serde(name="Lib")]
382 impl PieceTrait for Item {
383   #[throws(IE)]
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)?;
387   }
388   #[throws(IE)]
389   fn describe_html(&self, gpc: &GPiece, _goccults: &GOccults) -> Html {
390     self.describe_face(gpc.face)?
391   }
392
393   fn sortkey(&self) -> Option<&str> { self.sort.as_ref().map(AsRef::as_ref) }
394 }
395
396 #[typetag::serde(name="LibItem")]
397 impl InertPieceTrait for Item {
398   #[throws(IE)]
399   fn svg(&self, f: &mut Html, id: VisiblePieceId, face: FaceId,
400          xdata: &PieceXDataState) {
401     self.svg_face(f, face, id, xdata)?;
402   }
403   #[throws(IE)]
404   fn describe_html(&self, _: FaceId) -> Html {
405     self.describe_face(default())?
406   }
407 }
408
409 //==================== ItemSpec and item loading ====================
410
411 //---------- ItemSpec, MultiSpec ----------
412
413 type ItemSpecLoaded = (Box<Item>, PieceSpecLoadedOccultable);
414
415 impl From<ItemSpecLoaded> for SpecLoaded {
416   fn from((p, occultable):  ItemSpecLoaded) -> SpecLoaded {
417     SpecLoaded {
418       p,
419       occultable,
420       special: default(),
421     }
422   }
423 }
424
425 impl ItemSpec {
426   #[throws(SpecError)]
427   fn find_then<F,T>(&self, ig: &Instance, then: F) -> T
428   where F: FnOnce(&Catalogue, &SvgBaseName<GoodItemName>, &CatalogueEntry)
429                   -> Result<T, SpecError>
430   {
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())?))
435     )
436       .ok_or_else(|| SpE::LibraryItemNotFound(self.clone()))?;
437     then(lib, item, idata)?
438   }
439
440   #[throws(SpecError)]
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>,
445   {
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>(),
449                                ig, depth)?;
450         mundanef(loaded)?
451       },
452       CatEnt::Magic { spec,.. } => {
453         magicf(spec)?
454       },
455     }))?
456   }
457
458   #[throws(SpecError)]
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)
464     )?
465   }
466
467   fn from_strs<L,I>(lib: &L, item: &I) -> Self
468     where L: ToOwned<Owned=String> + ?Sized,
469           I: ToOwned<Owned=String> + ?Sized,
470   {
471     let lib  = lib .to_owned();
472     let item = item.to_owned();
473     ItemSpec{ lib, item }
474   }
475 }
476
477 #[typetag::serde(name="Lib")]
478 impl PieceSpec for ItemSpec {
479   #[throws(SpecError)]
480   fn load(&self, pla: PLA) -> SpecLoaded {
481     self.find_load_general(
482       pla.ig, pla.depth,
483       |loaded| Ok(loaded.into()),
484       |magic| magic.load(pla.recursing()?)
485     )?
486   }
487   #[throws(SpecError)]
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 }
491   }
492 }
493
494 #[typetag::serde(name="LibList")]
495 impl PieceSpec for MultiSpec {
496   #[throws(SpecError)]
497   fn count(&self, _pcaliases: &PieceAliases) -> usize { self.items.len() }
498
499   #[throws(SpecError)]
500   fn load(&self, pla: PLA) -> SpecLoaded
501   {
502     let PLA { i,.. } = pla;
503     let item = self.items.get(i).ok_or_else(
504       || SpE::InternalError(format!("item {:?} from {:?}", i, &self))
505     )?;
506     let item = format!("{}{}{}", &self.prefix, item, &self.suffix);
507     let lib = self.lib.clone();
508     ItemSpec { lib, item }.load(pla)?
509   }
510 }
511
512 //---------- Loading ----------
513
514 impl Catalogue {
515   #[throws(SpecError)]
516   fn load_image(&self, item_name: &SvgBaseName<str>,
517                 lib_name_for: &str, item_for: &str,
518                 group: &GroupData, shape_calculable: ShapeCalculable)
519               -> ImageLoaded {
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)
527       } else {
528         let m = "error accessing/reading library item data file";
529         error!("{}: {} {:?}: {}", &m, &svg_path, item_for, &e);
530         SpE::InternalError(m.to_string())
531       })?;
532
533     let svg_data = Html::from_html_string(svg_data);
534
535     let sz = svg_parse_size(&svg_data).map_err(|error| SpE::SVGError {
536       error,
537       item_name: item_name.as_str().into(),
538       item_for_lib: lib_name_for.into(),
539       item_for_item: item_for.into(),
540     })?;
541
542     let (xform, outline) = group.load_shape(sz)
543       .map_err(shape_calculable.err_mapper())?;
544
545     ImageLoaded {
546       svgd: svg_data,
547       outline,
548       xform,
549     }
550   }
551
552   #[throws(SpecError)]
553   fn load1(&self, idata: &ItemData, lib_name: &str,
554            name: &SvgBaseName<str>,
555            ig: &Instance, depth:SpecDepth)
556            -> ItemSpecLoaded {
557     let ImageLoaded { svgd: svg_data, outline, xform } =
558       self.load_image(name, lib_name, &**name,
559                        &idata.group, idata.shape_calculable)?;
560
561     let mut svgs = IndexVec::with_capacity(1);
562     let svg = svgs.push(svg_data);
563
564     let mut descs = index_vec![ ];
565     let desc = descs.push(idata.d.desc.clone());
566     descs.shrink_to_fit();
567
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.;
573       faces.push(face);
574     } else if let Some(back_spec) = &idata.group.d.back {
575       match back_spec.load_inert(ig, depth) {
576         Err(SpecError::AliasNotFound) => { },
577         Err(e) => throw!(e),
578         Ok(p) => {
579           let p = p.p.into();
580           back = Some(p);
581         }
582       }
583     }
584     faces.shrink_to_fit();
585
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))
592         } else {
593           None // We got AliasNotFound, ah well
594         }
595       },
596       OccData::Internal(occ) => {
597         let occ_name = occ.item_name.clone();
598         let ImageLoaded {
599           svgd, outline, xform
600         } = occ.loaded.get_or_create(
601           || self.load_image(
602             occ.item_name.unnest::<GoodItemName>().unnest(),
603             /* original: */ lib_name, name.as_str(),
604             &idata.group, idata.shape_calculable,
605           )
606         ).clone()?;
607
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))
614       },
615     };
616
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)
621   }
622 }
623
624 //==================== size handling, and outlines ====================
625
626 impl FaceTransform {
627   #[throws(LLE)]
628   fn from_group_mf1(group: &GroupData) -> Self {
629     let d = &group.d;
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
635         })
636         .cycle()
637         .take(2)
638         .collect::<ArrayVec<_,2>>()
639         .into_inner()
640         .unwrap()
641     } else {
642       let s = group.d.scale_mf1(group.mformat)?;
643       [s,s]
644     };
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)| {
649           size * 0.5 / scale
650         })
651         .collect::<ArrayVec<_,2>>()
652         .into_inner()
653         .unwrap()
654     }))?;
655     FaceTransform { centre, scale }
656   }
657
658   #[throws(IE)]
659   fn write_svgd(&self, f: &mut Html, svgd: &Html) {
660     hwrite!(f,
661            r##"<g transform="scale({} {}) translate({} {})">{}</g>"##,
662            self.scale[0], self.scale[1], -self.centre[0], -self.centre[1],
663            svgd)?;
664   }
665 }
666
667 #[throws(LLE)]
668 fn resolve_square_size<T:Copy>(size: &[T]) -> Option<PosC<T>> {
669   Some(PosC{ coords: match size {
670     [] => return None,
671     &[s] => [s,s],
672     &[w,h] => [w,h],
673     _ => throw!(LLE::WrongNumberOfSizeDimensions
674                 { got: size.len(), expected: [1,2]}),
675   } })
676 }
677
678 impl CatalogueEntry {
679   fn group(&self) -> &Arc<GroupData> { match self {
680     CatEnt::Item(item) => &item.group,
681     CatEnt::Magic{group,..} => group,
682   } }
683 }
684
685 //---------- Outlines ----------
686
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
691     ))
692   }
693 }
694
695 impl GroupData {
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 */
700     ))?;
701     ShapeCalculable{}
702   }
703
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 {
708
709       if self.d.orig_size.len() > 0 {
710         throw!(self.mformat.incompat(LLMI::OrigSizeForbidden))
711       }
712
713       let centre: PosC<f64> = self.d.centre
714         .map(|coords| PosC { coords })
715         .unwrap_or_else(|| geometry::Mean::mean(&svg_sz, &PosC::zero()));
716
717       let size = resolve_square_size(&self.d.size)?;
718
719       use ScaleDetails as SD;
720       let scale = self.d.scale.unwrap_or(SD::Fit(ScaleFitDetails::Fit));
721
722       let of_stretch = |scale| {
723         let scale = PosC { coords: scale };
724         let size = pos_zip_map!( svg_sz, scale => |(sz,sc)| sz * sc );
725         (size, scale)
726       };
727
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();
735             PosC::new(v,v)
736           };
737           let scale = match fit {
738             ScaleFitDetails::Fit => of(min),
739             ScaleFitDetails::Cover => of(max),
740             ScaleFitDetails::Stretch => scale,
741           };
742           (size, scale)
743         },
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),
750       };
751
752       let osize = {
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)
761         }
762       };
763
764       let outline = self.d.outline.shape().load(osize);
765       (FaceTransform { scale: scale.coords, centre: centre.coords }, outline)
766
767     } else {
768       let xform = FaceTransform::from_group_mf1(self)?;
769       let outline = self.d.outline.shape().load_mf1(self)?;
770       (xform, outline)
771     }
772   }
773 }
774
775 impl GroupDetails {
776   #[throws(materials_format::Incompat<LLMI>)]
777   fn scale_mf1(&self, mformat: materials_format::Version) -> f64 {
778     match self.scale {
779       None => 1.,
780       Some(ScaleDetails::Scale(s)) => s,
781       _ => throw!(mformat.incompat(LLMI::Scale)),
782     }
783   }
784 }
785
786 //---------- OutlineDefn etc. ----------
787
788 #[ambassador::delegatable_trait]
789 pub trait ShapeLoadableTrait: Debug + Sync + Send + 'static {
790   /// Success or failure must not depend on `svg_sz`
791   ///
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()
797   }
798
799   fn load_mf1(&self, group: &GroupData) -> Result<Outline,LLE>;
800 }
801
802 // We used to do shape deser via typetag and Box<dyn OutlineDefn>
803 //
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() }
810 }
811
812 //---------- RectOutline ----------
813
814 impl ShapeLoadableTrait for RectShapeIndicator {
815   fn load(&self, size: PosC<f64>) -> Outline {
816     RectOutline { xy: size }.into()
817   }
818
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))?;
823     self.load(size)
824   }
825 }
826
827 //---------- CircleOutline ----------
828
829 impl ShapeLoadableTrait for CircleShapeIndicator {
830   fn load(&self, size: PosC<f64>) -> Outline {
831     let diam = size
832       .coords.into_iter()
833       .map(OrderedFloat)
834       .max().unwrap().
835       into_inner();
836     CircleOutline {
837       diam,
838     }.into()
839   }
840
841   #[throws(LibraryLoadError)]
842   fn load_mf1(&self, group: &GroupData) -> Outline {
843     let diam = match group.d.size.as_slice() {
844       &[c] => c,
845       size => throw!(LLE::WrongNumberOfSizeDimensions
846                      { got: size.len(), expected: [1,1] }),
847     };
848     CircleOutline {
849       diam,
850     }.into()
851   }
852 }
853
854 //==================== Catalogues ====================
855
856 //---------- enquiries etc. ----------
857
858 impl Catalogue {
859   pub fn enquiry(&self) -> LibraryEnquiryData {
860     LibraryEnquiryData {
861       libname: self.libname.clone(),
862       bundle: self.bundle,
863     }
864   }
865
866   #[throws(MgmtError)]
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) => {
877           let (loaded, _) =
878             self.load1(item, &self.libname, k.unnest(),
879                        &Instance::dummy(), SpecDepth::zero())?;
880           loaded as Box<dyn PieceTrait>
881         },
882         CatEnt::Magic{spec,..} => {
883           spec.load(PieceLoadArgs {
884             i: 0,
885             gpc: &mut gpc,
886             ig: &ig_dummy,
887             depth: SpecDepth::zero(),
888           })?.p
889         }
890       }))() {
891         Err(SpecError::LibraryItemNotFound(_)) => continue,
892         e@ Err(_) => e?,
893         Ok(r) => r,
894       };
895       let f0bbox = loaded.bbox_approx()?;
896       let ier = ItemEnquiryData {
897         lib: self.enquiry(),
898         itemname: (**k).to_owned(),
899         sortkey: loaded.sortkey().map(|s| s.to_owned()),
900         f0bbox,
901         f0desc: loaded.describe_html(&gpc, &default())?,
902       };
903       out.push(ier);
904     }
905     out
906   }
907 }
908
909 pub trait LibrarySvgNoter {
910   #[throws(SubstError)]
911   fn note_svg(&mut self, _basename: &GoodItemName,
912               _src_name: Result<&str, &SubstError>) { }
913 }
914 pub trait LibrarySource: LibrarySvgNoter {
915   fn catalogue_data(&self) -> &str;
916   fn svg_dir(&self) -> String;
917   fn bundle(&self) -> Option<bundles::Id>;
918
919   fn default_materials_format(&self)
920                               -> Result<materials_format::Version, MFVE>;
921
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;
925 }
926
927 pub struct NullLibrarySvgNoter;
928 impl LibrarySvgNoter for NullLibrarySvgNoter { }
929
930 struct BuiltinLibrary<'l> {
931   catalogue_data: &'l str,
932   dirname: &'l str,
933 }
934
935 impl LibrarySvgNoter for BuiltinLibrary<'_> {
936 }
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 }
941
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!"));
945   }
946
947   fn svg_noter(&mut self) -> &mut dyn LibrarySvgNoter { self }
948 }
949
950 //---------- reading ----------
951
952 #[throws(LibraryLoadError)]
953 pub fn load_catalogue(libname: &str, src: &mut dyn LibrarySource)
954                       -> Catalogue {
955   (||{
956
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()?,
962     Some(v) => {
963       let v = v.as_integer().ok_or_else(|| MFVE::Other("not an integer"))?;
964       materials_format::Version::try_from_integer(v)?
965     },
966   };
967
968   let mut l = Catalogue {
969     bundle: src.bundle(),
970     libname: libname.to_string(),
971     items: HashMap::new(),
972     dirname: src.svg_dir(),
973   };
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 {
979     (||{
980
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)?;
986       GroupDetails {
987         size: gdefn.d.size.iter().map(|s| s * scale).collect(),
988         ..gdefn.d
989       }
990     } else {
991       gdefn.d // v2 isn't going to do this, do this right now
992     };
993     let group = Arc::new(GroupData {
994       groupname: groupname.clone(),
995       d, mformat,
996     });
997
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()?;
1002
1003     if [
1004       group.d.flip,
1005       group.d.back.is_some(),
1006     ].iter().filter(|x|**x).count() > 1 {
1007       throw!(LLE::MultipleMultipleFaceDefinitions)
1008     }
1009
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
1015       )?;
1016     }
1017
1018     Ok(())
1019     })().map_err(|error| LLE::InGroup {
1020       group: groupname.to_string(),
1021       error: Box::new(error),
1022     })?
1023   }
1024
1025   Ok(l)
1026   })().map_err(|error| LLE::InLibrary {
1027     lib: libname.into(),
1028     error: Box::new(error),
1029   })?
1030 }
1031
1032 #[derive(Debug,Copy,Clone,Eq,PartialEq)]
1033 pub enum Dollars { Text, Filename }
1034
1035 #[derive(Debug,Clone)]
1036 pub struct Substituting<'s> {
1037   s: Cow<'s, str>,
1038   mformat: materials_format::Version,
1039   dollars: Dollars,
1040 }
1041
1042 impl<'s> Substituting<'s> {
1043   pub fn new<S: Into<Cow<'s, str>>>(
1044     mformat: materials_format::Version,
1045     dollars: Dollars,
1046     s: S
1047   ) -> Self {
1048     Substituting { s: s.into(), mformat, dollars }
1049   }
1050
1051   #[throws(SubstError)]
1052   pub fn finish(self) -> String {
1053     if self.do_dollars() {
1054       self.subst_general_precisely("${$}", "$")?.0
1055     } else {
1056       self
1057     }.s.into()
1058   }
1059
1060   fn do_dollars(&self) -> bool { self.dollars.enabled(self.mformat) }
1061
1062   #[throws(SubstError)]
1063   /// Expand, but do not do final unescaping
1064   ///
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 {
1068     self.s.into()
1069   }
1070
1071   fn err(&self, kind: SubstErrorKind) -> SubstError {
1072     SubstError { kind, input: (*self.s).to_owned() }
1073   }
1074
1075   fn internal_err(&self, msg: &'static str) -> SubstError {
1076     self.err(InternalLogicError::new(msg).into())
1077   }
1078 }
1079
1080 impl Dollars {
1081   fn enabled(self, mformat: materials_format::Version) -> bool {
1082     match self {
1083       Dollars::Filename => false,
1084       Dollars::Text => mformat >= 2,
1085     }
1086   }
1087 }
1088
1089 impl<'i> Substituting<'i> {
1090 #[throws(SubstError)]
1091   fn subst_general_precisely(&self, needle: &str, replacement: &str)
1092                              -> (Substituting<'i>, usize) {
1093     let mut count = 0;
1094     let mut work = (*self.s).to_owned();
1095     for m in self.s.rmatch_indices(needle) {
1096       count += 1;
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() {
1102           lhs = lhs_trimmed;
1103         } else {
1104           rhs = rhs.trim_start();
1105         } 
1106       }
1107       work = lhs
1108         .to_owned()
1109         + replacement
1110         + rhs
1111     }
1112     (Substituting{
1113       s: work.into(),
1114       mformat: self.mformat,
1115       dollars: self.dollars,
1116     }, count)
1117   }
1118
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"))
1129       },
1130       Dollars::Text => { },
1131     }
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() }
1140       } else {
1141         return None
1142       }
1143     }))()
1144       .ok_or_else(|| self.internal_err("needle has no '_'"))?;
1145
1146     let (r, count) = self.subst_general_precisely(&needle, replacement)?;
1147     (r, count, needle)
1148   }
1149 }
1150
1151 #[throws(SubstError)]
1152 fn subst<'i,N>(before: Substituting<'i>, needle: N, replacement: &str)
1153                -> Substituting<'i>
1154 where N: Into<Cow<'static, str>>
1155 {
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))) }
1161   out
1162 }
1163
1164 #[throws(SubstError)]
1165 fn substn<'i,N>(before: Substituting<'i>, needle: N, replacement: &str)
1166               -> Substituting<'i>
1167 where N: Into<Cow<'static, str>>
1168 {
1169   before.subst_general(needle.into(), replacement)?.0
1170 }
1171
1172 #[cfg(not(miri))]
1173 #[test]
1174 fn test_subst_mf1() {
1175   use SubstErrorKind as SEK;
1176
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);
1180
1181   assert_eq!(subst(s_f("die-image-_c"), "_c", "blue")
1182              .unwrap().finish().unwrap(),
1183              "die-image-blue");
1184   assert_eq!(subst(s_t("a _colour die"), "_colour", "blue")
1185              .unwrap().finish().unwrap(),
1186              "a blue die");
1187   assert_eq!(subst(s_t("a _colour die"), "${colour}", "blue")
1188              .unwrap().finish().unwrap(),
1189              "a blue die");
1190   assert_eq!(subst(s_t("a _colour die"), "_colour", "")
1191              .unwrap().finish().unwrap(),
1192              "a die");
1193   assert!{matches!{
1194     dbg!(subst(s_t("a die"), "_colour", "")).unwrap_err().kind,
1195     SEK::MissingToken(c) if c == "_colour",
1196   }}
1197   assert!{matches!{
1198     dbg!(subst(s_t("a _colour _colour die"), "_colour", "")).unwrap_err().kind,
1199     SEK::RepeatedToken(c) if c == "_colour",
1200   }}
1201
1202   assert_eq!(substn(s_t("a _colour die being _colour"), "_colour", "blue")
1203              .unwrap().finish().unwrap(),
1204              "a blue die being blue");
1205
1206   let (s, count, needle) = s_t("a _colour _colour die")
1207     .subst_general("_colour".into(), "")
1208     .unwrap();
1209   assert_eq!(s.finish().unwrap(), "a die".to_owned());
1210   assert_eq!(count, 2);
1211   assert_eq!(needle, "_colour");
1212 }
1213
1214 #[cfg(not(miri))]
1215 #[test]
1216 fn test_subst_mf2() {
1217   use SubstErrorKind as SEK;
1218
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);
1222
1223   assert_eq!(subst(s_f("die-image-_c"), "_c", "blue")
1224              .unwrap().finish().unwrap(),
1225              "die-image-blue");
1226   assert!{matches!{
1227     dbg!(subst(s_f("die-image-_c"), "_colour", "")).unwrap_err().kind,
1228     SEK::Internal(_)
1229   }}
1230
1231   assert_eq!(subst(s_t("a ${colour} die"), "_colour", "blue")
1232              .unwrap().finish().unwrap(),
1233              "a blue die");
1234   assert_eq!(subst(s_t("a ${c} die"), "_c", "blue")
1235              .unwrap().finish().unwrap(),
1236              "a blue die");
1237   assert_eq!(subst(s_t("a ${colour} die"), "_colour", "")
1238              .unwrap().finish().unwrap(),
1239              "a die");
1240   assert_eq!(subst(s_t("a ${colour} die"), "${colour}", "")
1241              .unwrap().finish().unwrap(),
1242              "a die");
1243   assert!{matches!{
1244     dbg!(subst(s_t("a die"), "_colour", "")).unwrap_err().kind,
1245     SEK::MissingToken(c) if c == "${colour}",
1246   }}
1247   assert!{matches!{
1248     dbg!(subst(s_t("a ${colour} ${colour} die"), "_colour", ""))
1249       .unwrap_err().kind,
1250     SEK::RepeatedToken(c) if c == "${colour}",
1251   }}
1252
1253   assert_eq!(substn(s_t("a ${colour} die being ${colour}"), "_colour", "blue")
1254              .unwrap().finish().unwrap(),
1255              "a blue die being blue");
1256
1257   let (s, count, needle) = s_t("a ${colour} ${colour} die")
1258     .subst_general("_colour".into(), "")
1259     .unwrap();
1260   assert_eq!(s.finish().unwrap(), "a die".to_owned());
1261   assert_eq!(count, 2);
1262   assert_eq!(needle, "${colour}");
1263 }
1264
1265 #[throws(LibraryLoadError)]
1266 fn format_item_name(mformat: materials_format::Version,
1267                     item_prefix: &str, fe: &FileData, item_suffix: &str)
1268                     -> Substituting<'static> {
1269   Substituting::new(
1270     mformat, Dollars::Filename,
1271     format!("{}{}{}", item_prefix, fe.item_spec, item_suffix)
1272   )
1273 }
1274
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,
1280   fe: FileData
1281 ) {
1282   let mformat = group.mformat;
1283   let item_name = format_item_name(mformat, item_prefix, &fe, item_suffix)?;
1284
1285   let sort: Option<PerhapsSubst> = match (sort, fe.extra_fields.get("sort")) {
1286     ("", None) => None,
1287     (gd, None) => Some(gd.into()),
1288     ("", Some(ef)) => Some(ef.into()),
1289     (gd, Some(ef)) => {
1290       let sort = Substituting::new(mformat, Dollars::Text, gd);
1291       Some(subst(sort, "_s", ef)?.into())
1292     },
1293   };
1294
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()));
1300       }
1301       let item_name = subst(item_name.clone(), "_c", &colour.0)?;
1302       let src_name = Substituting::new(mformat, Dollars::Filename,
1303                                        &fe.src_file_spec);
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(),
1309       )?;
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 {
1313         item_name,
1314         loaded: default(),
1315         desc,
1316       }))
1317     },
1318     Some(OccultationMethod::ByBack { ilk }) => {
1319       if group.d.back.is_none() {
1320         throw!(LLE::BackMissingForOccultation)
1321       }
1322       OccData::Back(ilk.clone())
1323     },
1324   };
1325
1326   #[derive(Debug,From,Clone)]
1327   enum PerhapsSubst<'i> {
1328     Y(Substituting<'i>),
1329     N(&'i str),
1330   }
1331   impl<'i> From<&'i String> for PerhapsSubst<'i> {
1332     fn from(s: &'i String) -> Self { (&**s).into() }
1333   }
1334
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()?,
1340     } }
1341     #[throws(SubstError)]
1342     pub fn nest(self) -> String { match self {
1343       PerhapsSubst::N(s) => s.to_owned(),
1344       PerhapsSubst::Y(s) => s.nest()?,
1345     } }
1346     pub fn mky(
1347       self,
1348       mformat: materials_format::Version,
1349       dollars: Dollars,
1350     ) -> Substituting<'i> { match self {
1351       PerhapsSubst::N(s) => Substituting::new(mformat, dollars, s),
1352       PerhapsSubst::Y(s) => s,
1353     } }
1354     #[throws(SubstError)]
1355     pub fn into_of_y(
1356       self,
1357     ) -> Substituting<'i> { match self {
1358       PerhapsSubst::Y(s) => s,
1359       PerhapsSubst::N(s) => throw!(SubstError {
1360         kind: InternalLogicError::new("expected Y").into(),
1361         input: s.into(),
1362       })
1363     } }
1364   }
1365
1366   fn colour_subst_1<'s, S>(
1367     mformat: materials_format::Version,
1368     dollars: Dollars,
1369     subst: S, kv: Option<(&'static str, &'s str)>
1370   )
1371     -> impl for <'i> Fn(PerhapsSubst<'i>)
1372                         -> Result<PerhapsSubst<'i>, SubstError>
1373                      + 's
1374   where S: for <'i> Fn(Substituting<'i>, &'static str, &str)
1375               -> Result<Substituting<'i>, SubstError> + 's
1376   {
1377     move |input| Ok(
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()
1382       } else {
1383         input
1384       }
1385     )
1386   }
1387
1388   let mut add1 = |
1389     c_colour: Option<(&'static str, &str)>,
1390     c_abbrev: Option<(&'static str, &str)>,
1391     c_substs: Option<&HashMap<String, String>>,
1392   | {
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);
1397
1398     let sort = sort.clone().map(|v| c_abbrev_t(v)).transpose()?;
1399     let sort = sort.map(|s| s.finish()).transpose()?;
1400
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)
1405     };
1406     let item_name = subst_item_name(&item_name)?;
1407
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();
1411
1412     let desc = c_colour((&fe.desc).into())?;
1413
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()
1418     } else {
1419       desc.finish()?.to_html()
1420     };
1421
1422     let idata = ItemData {
1423       group: group.clone(),
1424       occ: occ.clone(),
1425       sort,
1426       shape_calculable,
1427       d: Arc::new(ItemDetails { desc }),
1428     };
1429     l.add_item(src, src_name, &item_name, CatEnt::Item(idata))?;
1430
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())
1438       );
1439
1440       let item_name = subst_item_name(&format_item_name(
1441         mformat, &magic.item_prefix, &fe, &magic.item_suffix)?)?;
1442
1443       let mut spec = Substituting::new(mformat, Dollars::Text,
1444                                        &magic.template);
1445       for (k,v) in chain!{
1446         c_substs.into_iter().map(IntoIterator::into_iter).flatten(),
1447         &magic.substs,
1448         fe.extra_fields.iter().filter(|(k,_v)| k.starts_with('x')),
1449       } {
1450         spec = substn(spec, format!("${{{}}}", k), v)?;
1451       }
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);
1456
1457       let spec: Box<dyn PieceSpec> = toml_de::from_str(&spec)
1458         .map_err(|error| LLE::TemplatedTomlError {
1459           toml: spec,
1460           error,
1461         })?;
1462
1463       l.add_item(&mut NullLibrarySvgNoter, // there's no SVG for *this* item
1464                  src_name, &item_name, CatEnt::Magic {
1465         group: group.clone(),
1466         spec: spec.into(),
1467       })?;
1468     }
1469
1470     Ok::<_,LLE>(())
1471   };
1472
1473   if group.d.colours.is_empty() {
1474     add1(None, None, None)?;
1475   } else {
1476     for (colour, recolourdata) in &group.d.colours {
1477       add1(Some(("${colour}", colour)),
1478            Some(("_c", &recolourdata.abbrev)),
1479            Some(&recolourdata.substs))?;
1480     }
1481   }
1482 }
1483
1484 impl Catalogue {
1485   #[throws(LLE)]
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>;
1491
1492     let new_item = SvgBaseName::note(
1493       src, item_name.clone(), src_name.clone()
1494     )?;
1495
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(),
1501       }),
1502       H::Vacant(ve) => {
1503         debug!("loaded shape {} {}", &self.libname, item_name.as_str());
1504         ve.insert(catent);
1505       }
1506     };
1507   }
1508 }
1509
1510 //---------- reading, support functions ----------
1511
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);
1518
1519   let group = group.as_table().ok_or_else(|| LLE::ExpectedTable(gp()))?;
1520
1521   let parent_name = match group.get("inherit") {
1522     None => { return Cow::Borrowed(group) }
1523     Some(p) => p,
1524   };
1525   let parent_name = parent_name
1526     .as_str().ok_or_else(|| LLE::ExpectedString(format!("group.{}.inherit",
1527                                                         group_name)))?;
1528   let parent = groups.get(parent_name)
1529     .ok_or_else(|| LLE::InheritMissingParent(gn(), parent_name.to_string()))?;
1530
1531   let mut build = resolve_inherit(
1532     depth.checked_sub(1).ok_or_else(|| LLE::InheritDepthLimitExceeded(gn()))?,
1533     groups, parent_name, parent
1534   )?.into_owned();
1535
1536   build.extend(group.iter().map(|(k,v)| (k.clone(), v.clone())));
1537   Cow::Owned(build)
1538 }
1539
1540 impl TryFrom<String> for FileList {
1541   type Error = LLE;
1542 //  #[throws(LLE)]
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() {
1547       let l = l.trim();
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));
1552         }
1553         xfields = xfields_spec.split_ascii_whitespace()
1554           .filter(|s| !s.is_empty())
1555           .map(|s| s.to_owned())
1556           .collect::<Vec<_>>();
1557         continue;
1558       }
1559       let mut remain = &*l;
1560       let mut n = ||{
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())
1566       };
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 });
1574     }
1575     Ok(FileList(o))
1576   }
1577 }
1578
1579 //==================== Registry ====================
1580
1581 impl Registry {
1582   pub fn add(&mut self, data: Catalogue) {
1583     self.libs
1584       .entry(data.libname.clone()).or_default()
1585       .push(data);
1586   }
1587
1588   pub fn clear(&mut self) {
1589     self.libs.clear()
1590   }
1591
1592   pub fn iter(&self) -> impl Iterator<Item=&[Catalogue]> {
1593     self.libs.values().map(|v| v.as_slice())
1594   }
1595 }
1596
1597 pub struct AllRegistries<'ig> {
1598   global: RwLockReadGuard<'static, Option<Registry>>,
1599   ig: &'ig Instance,
1600 }
1601 pub struct AllRegistriesIterator<'i> {
1602   regs: &'i AllRegistries<'i>,
1603   count: u8,
1604 }
1605
1606 impl<'i> Iterator for AllRegistriesIterator<'i> {
1607   type Item = &'i Registry;
1608   fn next(&mut self) -> Option<&'i Registry> {
1609     loop {
1610       let r = match self.count {
1611         0 => self.regs.global.as_ref(),
1612         1 => Some(&self.regs.ig.local_libs),
1613         _ => return None,
1614       };
1615       self.count += 1;
1616       if r.is_some() { return r }
1617     }
1618   }
1619 }
1620
1621 impl Instance {
1622   pub fn all_shapelibs(&self) -> AllRegistries<'_> {
1623     AllRegistries {
1624       global: GLOBAL_SHAPELIBS.read(),
1625       ig: self,
1626     }
1627   }
1628
1629 impl<'ig> AllRegistries<'ig> {
1630   pub fn iter(&'ig self) -> AllRegistriesIterator<'ig> {
1631     AllRegistriesIterator {
1632       regs: self,
1633       count: 0,
1634     }
1635   }
1636 }
1637
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()
1642 }
1643
1644 impl<'ig> AllRegistries<'ig> {
1645   pub fn all_libs(&self) -> impl Iterator<Item=&[Catalogue]> {
1646     self.iter().map(|reg| &reg.libs).flatten().map(
1647       |(_libname, lib)| lib.as_slice()
1648     )
1649   }
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) }
1653     }
1654     return Err(SpE::LibraryNotFound);
1655   }
1656 }
1657
1658 //==================== configu and loading global libs ====================
1659
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)?;
1669     s
1670   };
1671
1672   let catalogue_data = catalogue_data.as_str();
1673   let mut src = BuiltinLibrary { dirname: &l.dirname, catalogue_data };
1674
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)
1679     .add(data);
1680   info!("loaded {} shapes in library {:?} from {:?} and {:?}",
1681         count, &l.name, &l.catalogue, &l.dirname);
1682 }
1683
1684 #[ext(pub)]
1685 impl ShapelibConfig1 {
1686   fn resolve(&self) -> Result<Box<dyn ExactSizeIterator<Item=Explicit1>>, LibraryLoadError> {
1687     use Config1::*;
1688     Ok(match self {
1689       Explicit(e) => Box::new(iter::once(e.clone())),
1690       PathGlob(pat) => {
1691
1692         #[throws(LLE)]
1693         fn resolve_globresult(pat: &str, globresult: glob::GlobResult)
1694                               -> Explicit1 {
1695           let path = globresult?;
1696           let path = path.to_str().ok_or_else(
1697             || LLE::GlobNonUTF8
1698             { pat: pat.to_string(), actual: path.clone() })?
1699             .to_string();
1700
1701           let dirname = path.rsplitn(2,'.').nth(1).ok_or_else(
1702             || LLE::GlobNoExtension
1703             { pat: pat.to_string(), path: path.clone() })?;
1704
1705           let base = dirname.rsplit('/').next().unwrap();
1706
1707           Explicit1 {
1708             name: base.to_string(),
1709             dirname: dirname.to_string(),
1710             catalogue: path,
1711           }
1712         }
1713
1714         let results = glob::glob_with(pat, glob::MatchOptions {
1715           require_literal_separator: true,
1716           require_literal_leading_dot: true,
1717           ..default()
1718         })
1719           .map_err(
1720             |glob::PatternError { pos, msg, .. }|
1721             LLE::BadGlobPattern { pat: pat.clone(), pos, msg }
1722           )?
1723           .map(|globresult| resolve_globresult(pat, globresult))
1724           .collect::<Result<Vec<_>, LLE>>()?;
1725
1726         Box::new(results.into_iter())
1727
1728       }
1729     })
1730   }
1731 }
1732
1733 #[throws(LibraryLoadError)]
1734 pub fn load_global_libs(libs: &[Config1]) {
1735   for l in libs {
1736     let libs = l.resolve()?;
1737     let n = libs.len();
1738     for e in libs {
1739       load_1_global_library(&e)?;
1740     }
1741     info!("loaded {} shape libraries from {:?}", n, &l);
1742           
1743   }
1744 }