diff --git a/crates/polymodel-ldraw-core/src/adapter.rs b/crates/polymodel-ldraw-core/src/adapter.rs index 6f4af4e..73f9ca6 100644 --- a/crates/polymodel-ldraw-core/src/adapter.rs +++ b/crates/polymodel-ldraw-core/src/adapter.rs @@ -1,5 +1,6 @@ -use crate::{LdrawParser, OwnedParseResult, ParseError, ParseOptions, ParserProfile}; +use crate::{LdrawParser, ParseError, ParseOptions, ParseResult, ParserProfile}; use polymodel_renderer_ledger::{ReservationLedger, ReservationOwner}; +use std::borrow::Cow; pub struct InProcessRustAdapter; #[derive(Clone)] @@ -14,13 +15,13 @@ pub struct AdapterRequest<'a> { pub root_name: &'a str, } impl InProcessRustAdapter { - pub fn parse(request: AdapterRequest<'_>) -> Result { + pub fn parse(request: AdapterRequest<'_>) -> Result, ParseError> { let mut options = ParseOptions::new(request.owner, request.ledger); options.profile = request.profile; options.semantic_budget = request.semantic_budget; options.root_name = request.root_name.into(); let mut result = LdrawParser.parse_bytes(request.bytes, options)?; - result.provenance_id = request.provenance_id.into(); + result.provenance_id = Cow::Owned(request.provenance_id.to_owned()); let _ = request.fixture_id; Ok(result) } @@ -30,7 +31,7 @@ impl InProcessRustAdapter { profile: ParserProfile, owner: ReservationOwner, ledger: &'a ReservationLedger, - ) -> Result { + ) -> Result, ParseError> { Self::parse(AdapterRequest { fixture_id, bytes, diff --git a/crates/polymodel-ldraw-core/src/bfc.rs b/crates/polymodel-ldraw-core/src/bfc.rs index b76c1d9..8084a33 100644 --- a/crates/polymodel-ldraw-core/src/bfc.rs +++ b/crates/polymodel-ldraw-core/src/bfc.rs @@ -1,9 +1,8 @@ use crate::model::ModelData; -use crate::parser::{add_diag, limit_error}; use crate::scanner::{ScannedLine, TokenKind}; use crate::types::{ - Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, LimitKind, ParseError, ParserProfile, - Severity, + add_diag, limit_error, Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, LimitKind, + ParseError, ParserProfile, Severity, }; use serde::{Deserialize, Serialize}; @@ -38,7 +37,7 @@ impl Default for BfcFrame { } pub(crate) fn bfc( - model: &mut ModelData, + model: &mut ModelData<'_>, line: &ScannedLine, profile: ParserProfile, diagnostics: &mut Vec, @@ -90,10 +89,10 @@ pub(crate) fn bfc( model.bfc.clipping = true; model.bfc.winding = Winding::Ccw; if line.tokens.len() == 4 { - model.bfc.winding = if line.tokens[3].text == "CW" { - Winding::Cw - } else { - Winding::Ccw + model.bfc.winding = match line.tokens[3].text { + "CW" => Winding::Cw, + "CCW" => Winding::Ccw, + _ => unreachable!("scanner classifies only CW/CCW as orientation"), }; } model.bfc.invert_next = false; @@ -160,10 +159,10 @@ pub(crate) fn bfc( } else if directive == TokenKind::NoClip { model.bfc.clipping = false; } else { - model.bfc.winding = if line.tokens[2].text == "CW" { - Winding::Cw - } else { - Winding::Ccw + model.bfc.winding = match line.tokens[2].text { + "CW" => Winding::Cw, + "CCW" => Winding::Ccw, + _ => unreachable!("scanner classifies only CW/CCW as orientation"), }; } model.bfc.invert_next = false; diff --git a/crates/polymodel-ldraw-core/src/geom.rs b/crates/polymodel-ldraw-core/src/geom.rs index 94f9ed4..4743639 100644 --- a/crates/polymodel-ldraw-core/src/geom.rs +++ b/crates/polymodel-ldraw-core/src/geom.rs @@ -1,12 +1,13 @@ use crate::bfc::BfcState; use crate::model::{Include, ModelData}; -use crate::parser::{add_diag, limit_error}; -use crate::scanner::{ScannedLine, Token, TokenKind}; +use crate::scanner::{LineType, ScannedLine, Token, TokenKind}; use crate::types::{ - Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, LimitKind, ParseError, ParserProfile, + add_diag, limit_error, Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, LimitKind, + ParseError, ParserProfile, }; use crate::util::fmt_num; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct Transform { @@ -126,33 +127,43 @@ fn parse_points(tokens: &[Token], count: usize) -> Option> { Some(values.chunks_exact(3).map(|v| [v[0], v[1], v[2]]).collect()) } -pub(crate) fn primitive_is_valid(line: &ScannedLine) -> bool { +pub(crate) enum ParsedPrimitive { + Include(Transform), + Points(Vec<[f64; 3]>), +} + +pub(crate) fn parse_primitive(line: &ScannedLine) -> Option { match line.line_type { - Some(1) => { - line.tokens.len() >= 15 - && parse_number(line.tokens.get(1)).is_some() - && (2..14).all(|i| parse_number(line.tokens.get(i)).is_some()) - && line.tokens.len() > 14 + Some(LineType::One) => { + if line.tokens.len() < 15 || parse_number(line.tokens.get(1)).is_none() { + return None; + } + let values = (2..14) + .map(|index| parse_number(line.tokens.get(index))) + .collect::>>()?; + Some(ParsedPrimitive::Include(Transform::from_type1(&values)?)) + } + Some(LineType::Two) => Some(ParsedPrimitive::Points(parse_points(&line.tokens, 2)?)), + Some(LineType::Three) => Some(ParsedPrimitive::Points(parse_points(&line.tokens, 3)?)), + Some(LineType::Four | LineType::Five) => { + Some(ParsedPrimitive::Points(parse_points(&line.tokens, 4)?)) } - Some(2) => parse_points(&line.tokens, 2).is_some(), - Some(3) => parse_points(&line.tokens, 3).is_some(), - Some(4 | 5) => parse_points(&line.tokens, 4).is_some(), - _ => false, + _ => None, } } -pub(crate) fn process_geometry( - model: &mut ModelData, - line: &ScannedLine, +pub(crate) fn process_geometry<'src>( + model: &mut ModelData<'src>, + line: &ScannedLine<'src>, + primitive: Option, profile: ParserProfile, diagnostics: &mut Vec, counters: &mut LimitCounters, limits: &LdrawLimits, ) -> Result<(), ParseError> { match line.line_type { - Some(1) => { - let valid = primitive_is_valid(line); - if !valid { + Some(LineType::One) => { + let Some(ParsedPrimitive::Include(transform)) = primitive else { return add_diag( profile, diagnostics, @@ -163,16 +174,17 @@ pub(crate) fn process_geometry( "malformed type-1 include", true, ); - } - let values = (2..14) - .map(|i| parse_number(line.tokens.get(i)).unwrap()) - .collect::>(); - let transform = Transform::from_type1(&values).unwrap(); - let name = line.tokens[14..] - .iter() - .map(|t| t.text.as_str()) - .collect::>() - .join(" "); + }; + let name = match line.tokens[14..] { + [token] => Cow::Borrowed(token.text), + ref tokens => Cow::Owned( + tokens + .iter() + .map(|token| token.text) + .collect::>() + .join(" "), + ), + }; let inverted = model.bfc.invert_next; model.bfc.invert_next = false; model.includes.push(Include { @@ -186,16 +198,12 @@ pub(crate) fn process_geometry( model.bfc.state = BfcState::Uncertified; } } - Some(2) | Some(3) | Some(4) | Some(5) => { + Some(LineType::Two) + | Some(LineType::Three) + | Some(LineType::Four) + | Some(LineType::Five) => { let typ = line.line_type.unwrap(); - let count = if typ == 2 { - 2 - } else if typ == 3 { - 3 - } else { - 4 - }; - let Some(points) = parse_points(&line.tokens, count) else { + let Some(ParsedPrimitive::Points(points)) = primitive else { model.bfc.invert_next = false; return add_diag( profile, @@ -203,10 +211,11 @@ pub(crate) fn process_geometry( counters, limits, match typ { - 2 => DiagnosticCode::InvalidType2, - 3 => DiagnosticCode::InvalidType3, - 4 => DiagnosticCode::InvalidType4, - _ => DiagnosticCode::InvalidType5, + LineType::Two => DiagnosticCode::InvalidType2, + LineType::Three => DiagnosticCode::InvalidType3, + LineType::Four => DiagnosticCode::InvalidType4, + LineType::Five => DiagnosticCode::InvalidType5, + _ => unreachable!("geometry branch only matches primitive line types"), }, line.span, "malformed primitive", @@ -216,7 +225,7 @@ pub(crate) fn process_geometry( for point in points { model.bounds.add(point); } - if typ == 2 || typ == 5 { + if matches!(typ, LineType::Two | LineType::Five) { model.lines = model .lines .checked_add(1) @@ -225,7 +234,7 @@ pub(crate) fn process_geometry( .add(LimitKind::Lines, 1, limits) .map_err(|name| limit_error(name, Some(line.span)))?; } else { - let delta = if typ == 4 { 2 } else { 1 }; + let delta = if typ == LineType::Four { 2 } else { 1 }; model.triangles = model .triangles .checked_add(delta) diff --git a/crates/polymodel-ldraw-core/src/model.rs b/crates/polymodel-ldraw-core/src/model.rs index d184e9a..7132967 100644 --- a/crates/polymodel-ldraw-core/src/model.rs +++ b/crates/polymodel-ldraw-core/src/model.rs @@ -1,30 +1,40 @@ use crate::bfc::{BfcFrame, BfcState}; use crate::cache::{CacheKey, NormalizedPath}; use crate::geom::{Bounds, Transform}; -use crate::mpd::VirtualFile; +use crate::mpd::OwnedVirtualFile; +use crate::scanner::LineType; use crate::texmap::{TexmapEvent, TexmapState, TextureDescriptor}; use crate::types::{Diagnostic, LimitCounters, SCHEMA_VERSION, SemanticArenas, Span}; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct SyntaxRecord { - pub line_type: u8, - pub raw_tail: String, +pub struct SyntaxRecord<'src> { + pub line_type: Option, + #[serde(borrow)] + pub raw_tail: Cow<'src, str>, pub span: Span, - pub line_ending: String, - pub fields: Vec, + #[serde(borrow)] + pub line_ending: Cow<'src, str>, + #[serde(borrow)] + pub fields: Vec>, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct SemanticRecord { - pub canonical_path: Option, - pub root_identity: Option, - pub target_identity: Option, - pub cache_identity: Option, - pub bfc_state: String, - pub colour_state: String, +pub struct SemanticRecord<'src> { + #[serde(borrow)] + pub canonical_path: Option>, + #[serde(borrow)] + pub root_identity: Option>, + #[serde(borrow)] + pub target_identity: Option>, + #[serde(borrow)] + pub cache_identity: Option>, + pub bfc_state: BfcState, + pub colour_state: u16, pub steps: Vec, - pub limits: Vec, - pub texmap_events: Vec, + #[serde(borrow)] + pub limits: Vec>, + pub texmap_events: Vec>, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct SceneRecord { @@ -36,54 +46,55 @@ pub struct SceneRecord { pub reflection: bool, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct CanonicalRecord { - pub schema_version: String, - pub syntax: Vec, - pub semantic: SemanticRecord, +pub struct CanonicalRecord<'src> { + #[serde(borrow)] + pub schema_version: Cow<'src, str>, + pub syntax: Vec>, + pub semantic: SemanticRecord<'src>, pub scene: SceneRecord, pub diagnostics: Vec, - pub provenance_id: String, + pub provenance_id: Cow<'src, str>, } #[derive(Clone, Debug)] -pub(crate) struct Include { - pub(crate) name: String, +pub(crate) struct Include<'src> { + pub(crate) name: Cow<'src, str>, pub(crate) transform: Transform, pub(crate) span: Span, pub(crate) valid: bool, pub(crate) inverted: bool, } #[derive(Clone, Debug)] -pub(crate) struct ModelData { - pub(crate) file: VirtualFile, +pub(crate) struct ModelData<'src> { + pub(crate) file_id: u32, pub(crate) path: NormalizedPath, pub(crate) key: CacheKey, pub(crate) bfc: BfcFrame, - pub(crate) colour: String, + pub(crate) colour: u16, pub(crate) steps: Vec, pub(crate) texmap_state: TexmapState, - pub(crate) texmap_descriptor: Option, - pub(crate) texmap_stack: Vec<(TexmapState, Option, bool)>, + pub(crate) texmap_descriptor: Option>, + pub(crate) texmap_stack: Vec<(TexmapState, Option>, bool)>, pub(crate) texmap_next_span: Option, pub(crate) texmap_fallback_seen: bool, - pub(crate) texmap_events: Vec, - pub(crate) includes: Vec, + pub(crate) texmap_events: Vec>, + pub(crate) includes: Vec>, pub(crate) triangles: u64, pub(crate) lines: u64, pub(crate) bounds: Bounds, pub(crate) reflection: bool, } -pub struct OwnedParseResult { - pub syntax: Vec, - pub semantic: SemanticRecord, +pub struct ParseResult<'src> { + pub syntax: Vec>, + pub semantic: SemanticRecord<'src>, pub scene: SceneRecord, pub diagnostics: Vec, pub counters: LimitCounters, - pub files: Vec, + pub files: Vec, pub models: Vec, pub arenas: SemanticArenas, - pub provenance_id: String, + pub provenance_id: Cow<'src, str>, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct ModelSummary { @@ -94,10 +105,10 @@ pub struct ModelSummary { pub lines: u64, pub bfc: BfcState, } -impl OwnedParseResult { - pub fn project(&self) -> CanonicalRecord { +impl<'src> ParseResult<'src> { + pub fn project(&self) -> CanonicalRecord<'src> { CanonicalRecord { - schema_version: SCHEMA_VERSION.into(), + schema_version: Cow::Borrowed(SCHEMA_VERSION), syntax: self.syntax.clone(), semantic: self.semantic.clone(), scene: self.scene.clone(), diff --git a/crates/polymodel-ldraw-core/src/mpd.rs b/crates/polymodel-ldraw-core/src/mpd.rs index b750c52..2189a53 100644 --- a/crates/polymodel-ldraw-core/src/mpd.rs +++ b/crates/polymodel-ldraw-core/src/mpd.rs @@ -1,28 +1,56 @@ -use crate::scanner::{ScannedLine, TokenKind}; -use crate::types::{Diagnostic, DiagnosticCode, LdrawLimits, ParseError, Severity, Span}; +use crate::scanner::{LineType, ScannedLine, TokenKind}; +use crate::types::{ + Diagnostic, DiagnosticCode, LdrawLimits, ParseError, Severity, Span, DEFAULT_ROOT_NAME, +}; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VirtualFile<'src> { + pub id: u32, + pub name: Cow<'src, str>, + pub lines: Vec>, + pub source_span: Span, +} #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct VirtualFile { +pub struct OwnedVirtualFile { pub id: u32, pub name: String, - pub lines: Vec, + pub lines: Vec>, pub source_span: Span, } -pub fn split_mpd( - lines: &[ScannedLine], +impl<'src> From> for OwnedVirtualFile { + fn from(file: VirtualFile<'src>) -> Self { + Self { + id: file.id, + name: file.name.into_owned(), + lines: file + .lines + .into_iter() + .map(|line| line.raw.to_vec()) + .collect(), + source_span: file.source_span, + } + } +} + +pub type MpdDocument<'src> = Vec>; + +pub fn split_mpd<'src>( + lines: &[ScannedLine<'src>], limits: &LdrawLimits, -) -> Result, ParseError> { +) -> Result, ParseError> { let has_file = lines.iter().any(|line| { - line.line_type == Some(0) + line.line_type == Some(LineType::Zero) && line.tokens.get(1).map(|t| t.kind) == Some(TokenKind::File) && line.tokens.len() >= 3 }); if !has_file { return Ok(vec![VirtualFile { id: 0, - name: "model.ldr".into(), + name: Cow::Borrowed(DEFAULT_ROOT_NAME), lines: lines.to_vec(), source_span: lines.first().map(|l| l.span).unwrap_or(Span { start: 0, @@ -36,7 +64,7 @@ pub fn split_mpd( let mut active: Option = None; let mut discarded = true; for line in lines { - if line.line_type == Some(0) + if line.line_type == Some(LineType::Zero) && line.tokens.get(1).map(|t| t.kind) == Some(TokenKind::File) && line.tokens.len() >= 3 { @@ -50,11 +78,13 @@ pub fn split_mpd( span: Some(line.span), })); } - let name = line.tokens[2..] - .iter() - .map(|t| t.text.as_str()) - .collect::>() - .join(" "); + let name = Cow::Owned( + line.tokens[2..] + .iter() + .map(|t| t.text) + .collect::>() + .join(" "), + ); files.push(VirtualFile { id: u32::try_from(files.len()) .map_err(|_| ParseError::Overflow("virtual file id"))?, @@ -66,7 +96,7 @@ pub fn split_mpd( discarded = false; continue; } - if line.line_type == Some(0) + if line.line_type == Some(LineType::Zero) && line.tokens.get(1).map(|t| t.kind) == Some(TokenKind::NoFile) { active = None; @@ -81,7 +111,7 @@ pub fn split_mpd( if files.is_empty() { return Ok(vec![VirtualFile { id: 0, - name: "model.ldr".into(), + name: Cow::Borrowed(DEFAULT_ROOT_NAME), lines: lines.to_vec(), source_span: lines.first().map(|l| l.span).unwrap_or(Span { start: 0, diff --git a/crates/polymodel-ldraw-core/src/parser.rs b/crates/polymodel-ldraw-core/src/parser.rs index 733cf92..81465ab 100644 --- a/crates/polymodel-ldraw-core/src/parser.rs +++ b/crates/polymodel-ldraw-core/src/parser.rs @@ -1,15 +1,16 @@ -use crate::bfc::{BfcFrame, bfc}; +use crate::bfc::{BfcFrame, BfcState, bfc}; use crate::cache::{CacheKey, NormalizedPath}; -use crate::geom::{Bounds, primitive_is_valid, process_geometry}; -use crate::model::{ModelData, OwnedParseResult, SemanticRecord, SyntaxRecord}; -use crate::mpd::split_mpd; -use crate::scanner::{ScannedLine, TokenKind, scan_lines}; +use crate::geom::{Bounds, parse_primitive, process_geometry}; +use crate::model::{ModelData, ParseResult, SemanticRecord, SyntaxRecord}; +use crate::mpd::{OwnedVirtualFile, split_mpd}; +use crate::scanner::{LineType, ScannedLine, TokenKind, scan_lines}; use crate::texmap::{TexmapState, TextureDescriptor, texmap}; use crate::traversal::traverse; use crate::types::{ - Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, LimitKind, ParseError, ParseOptions, - ParserProfile, SemanticArenas, Severity, Span, + DEFAULT_COLOUR, DEFAULT_PROVENANCE, Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, + LimitKind, ParseError, ParseOptions, ParserProfile, SemanticArenas, add_diag, limit_error, }; +use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; pub struct LdrawParser; @@ -19,11 +20,11 @@ impl Default for LdrawParser { } } impl LdrawParser { - pub fn parse_bytes<'a>( + pub fn parse_bytes<'src, 'a>( &self, - bytes: &[u8], + bytes: &'src [u8], options: ParseOptions<'a>, - ) -> Result { + ) -> Result, ParseError> { let mut counters = LimitCounters::default(); let input_len = u64::try_from(bytes.len()).map_err(|_| ParseError::Overflow("resource bytes"))?; @@ -51,12 +52,12 @@ impl LdrawParser { } let mut files = split_mpd(&scanned, &options.limits)?; let has_mpd_file = scanned.iter().any(|line| { - line.line_type == Some(0) + line.line_type == Some(LineType::Zero) && line.tokens.get(1).map(|token| token.kind) == Some(TokenKind::File) && line.tokens.len() >= 3 }); if !has_mpd_file { - files[0].name = options.root_name.clone(); + files[0].name = Cow::Owned(options.root_name.clone()); } if options.cancellation.cancelled { return Err(ParseError::Cancelled); @@ -72,9 +73,9 @@ impl LdrawParser { for line in &scanned { syntax.push(syntax_record(line)); } - let mut local_colours = BTreeMap::new(); + let mut local_colours: BTreeMap<&'src str, u16> = BTreeMap::new(); let mut texture_ids = BTreeSet::new(); - for file in files.iter().cloned() { + for file in &files { let colours_before = local_colours.len(); if options.cancellation.cancelled { return Err(ParseError::Cancelled); @@ -92,11 +93,11 @@ impl LdrawParser { .collect::>(); let key = CacheKey::new(path.clone(), options.resolved_root, &file_bytes); let mut model = ModelData { - file: file.clone(), + file_id: file.id, path, key, bfc: BfcFrame::default(), - colour: "16".into(), + colour: DEFAULT_COLOUR, steps: Vec::new(), texmap_state: TexmapState::Inactive, texmap_descriptor: None, @@ -180,98 +181,64 @@ impl LdrawParser { let (scene, summaries) = traverse(&models, &options, &mut counters, &mut diagnostics)?; let root = models.first(); let semantic = SemanticRecord { - canonical_path: root.map(|m| m.path.to_string()), - root_identity: Some(format!("{:?}", options.resolved_root).to_ascii_lowercase()), - target_identity: root.map(|m| m.path.to_string()), - cache_identity: root.map(|m| m.key.content_hash.to_string()), - bfc_state: root - .map(|m| format!("{:?}", m.bfc.state).to_ascii_lowercase()) - .unwrap_or_else(|| "unknown".into()), - colour_state: root - .map(|m| m.colour.clone()) - .unwrap_or_else(|| "16".into()), + canonical_path: root.map(|m| Cow::Owned(m.path.to_string())), + root_identity: Some(Cow::Owned( + format!("{:?}", options.resolved_root).to_ascii_lowercase(), + )), + target_identity: root.map(|m| Cow::Owned(m.path.to_string())), + cache_identity: root.map(|m| Cow::Owned(m.key.content_hash.to_string())), + bfc_state: root.map(|m| m.bfc.state).unwrap_or(BfcState::Unknown), + colour_state: root.map(|m| m.colour).unwrap_or(DEFAULT_COLOUR), steps: root.map(|m| m.steps.clone()).unwrap_or_default(), limits: LdrawLimits::NAMES .iter() - .map(|name| (*name).into()) + .map(|name| Cow::Borrowed(*name)) .collect(), texmap_events: root.map(|m| m.texmap_events.clone()).unwrap_or_default(), }; - Ok(OwnedParseResult { + Ok(ParseResult { syntax, semantic, scene, diagnostics, counters, - files, + files: files.into_iter().map(OwnedVirtualFile::from).collect(), models: summaries, arenas, - provenance_id: "local-parse".into(), + provenance_id: Cow::Borrowed(DEFAULT_PROVENANCE), }) } } -pub(crate) fn limit_error(kind: LimitKind, span: Option) -> ParseError { - ParseError::Diagnostic(Diagnostic { - code: kind.diagnostic_code(), - severity: Severity::Error, - message: format!("{kind} limit exceeded"), - span, - }) -} -fn syntax_record(line: &ScannedLine) -> SyntaxRecord { +fn syntax_record<'src>(line: &ScannedLine<'src>) -> SyntaxRecord<'src> { + let raw_tail = std::str::from_utf8(line.raw_tail) + .expect("scanner validates UTF-8 before producing a scanned line"); SyntaxRecord { - line_type: line.line_type.unwrap_or(255), - raw_tail: String::from_utf8_lossy(&line.raw_tail).into_owned(), + line_type: line.line_type, + raw_tail: Cow::Borrowed(raw_tail), span: line.span, - line_ending: line.ending.as_str().into(), - fields: line.tokens.iter().map(|t| t.text.clone()).collect(), - } -} -pub(crate) fn add_diag( - profile: ParserProfile, - diagnostics: &mut Vec, - counters: &mut LimitCounters, - limits: &LdrawLimits, - code: DiagnosticCode, - span: Span, - message: &str, - strict_reject: bool, -) -> Result<(), ParseError> { - let severity = if strict_reject && profile == ParserProfile::Strict { - Severity::Error - } else if profile == ParserProfile::Strict { - Severity::Error - } else { - Severity::Warning - }; - counters - .add(LimitKind::Diagnostics, 1, limits) - .map_err(|kind| limit_error(kind, Some(span)))?; - diagnostics.push(Diagnostic { - code, - severity, - message: message.into(), - span: Some(span), - }); - if strict_reject && profile == ParserProfile::Strict { - return Err(ParseError::Diagnostic(diagnostics.last().cloned().unwrap())); + line_ending: Cow::Owned(line.ending.as_str().to_owned()), + fields: line + .tokens + .iter() + .map(|token| Cow::Borrowed(token.text)) + .collect(), } - Ok(()) } -fn process_line( - model: &mut ModelData, - line: &ScannedLine, +fn process_line<'src>( + model: &mut ModelData<'src>, + line: &ScannedLine<'src>, profile: ParserProfile, diagnostics: &mut Vec, counters: &mut LimitCounters, limits: &LdrawLimits, - local_colours: &mut BTreeMap, - texture_ids: &mut BTreeSet, + local_colours: &mut BTreeMap<&'src str, u16>, + texture_ids: &mut BTreeSet>, ) -> Result<(), ParseError> { + let primitive = parse_primitive(line); if model.texmap_state == TexmapState::AwaitingNext && !line.blank { - if line.line_type == Some(0) { + if line.line_type == Some(LineType::Zero) { let span = model.texmap_next_span.take().unwrap_or(line.span); model.texmap_state = TexmapState::Inactive; model.texmap_descriptor = None; @@ -285,11 +252,22 @@ fn process_line( "NEXT was cancelled by a type-0 line", true, )?; - } else if matches!(line.line_type, Some(1..=5)) { - if primitive_is_valid(line) { + } else if matches!( + line.line_type, + Some(LineType::One | LineType::Two | LineType::Three | LineType::Four | LineType::Five) + ) { + if primitive.is_some() { let descriptor = model.texmap_descriptor.take(); model.texmap_state = TexmapState::Active; - process_geometry(model, line, profile, diagnostics, counters, limits)?; + process_geometry( + model, + line, + primitive, + profile, + diagnostics, + counters, + limits, + )?; model.texmap_state = TexmapState::Inactive; if let Some(descriptor) = descriptor { let _ = texture_ids.insert(descriptor); @@ -299,7 +277,7 @@ fn process_line( } } } - if line.line_type == Some(0) { + if line.line_type == Some(LineType::Zero) { let preserves_invert = line.tokens.get(1).map(|token| token.kind) == Some(TokenKind::Bfc) && line.tokens.get(2).map(|token| token.kind) == Some(TokenKind::InvertNext) && line.tokens.len() == 3; @@ -317,18 +295,26 @@ fn process_line( texture_ids, ); } - process_geometry(model, line, profile, diagnostics, counters, limits) + process_geometry( + model, + line, + primitive, + profile, + diagnostics, + counters, + limits, + ) } -fn process_meta( - model: &mut ModelData, - line: &ScannedLine, +fn process_meta<'src>( + model: &mut ModelData<'src>, + line: &ScannedLine<'src>, profile: ParserProfile, diagnostics: &mut Vec, counters: &mut LimitCounters, limits: &LdrawLimits, - local_colours: &mut BTreeMap, - texture_ids: &mut BTreeSet, + local_colours: &mut BTreeMap<&'src str, u16>, + texture_ids: &mut BTreeSet>, ) -> Result<(), ParseError> { let tokens = &line.tokens; if tokens.len() < 2 { @@ -398,8 +384,8 @@ fn process_meta( true, ); } - local_colours.insert(tokens[2].text.clone(), slot); - model.colour = code.to_string(); + local_colours.insert(tokens[2].text, slot); + model.colour = code; } } model.bfc.invert_next = false; diff --git a/crates/polymodel-ldraw-core/src/scanner.rs b/crates/polymodel-ldraw-core/src/scanner.rs index 4b5aab2..8867b49 100644 --- a/crates/polymodel-ldraw-core/src/scanner.rs +++ b/crates/polymodel-ldraw-core/src/scanner.rs @@ -48,10 +48,10 @@ pub enum TokenKind { Garbage, } -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct Token { +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Token<'src> { pub kind: TokenKind, - pub text: String, + pub text: &'src str, pub span: Span, } @@ -73,12 +73,23 @@ impl LineEnding { } } -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct ScannedLine { - pub line_type: Option, - pub raw: Vec, - pub raw_tail: Vec, - pub tokens: Vec, +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum LineType { + Zero, + One, + Two, + Three, + Four, + Five, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ScannedLine<'src> { + pub line_type: Option, + pub raw: &'src [u8], + pub raw_tail: &'src [u8], + pub tokens: Vec>, pub span: Span, pub ending: LineEnding, pub blank: bool, @@ -129,7 +140,10 @@ fn keyword(text: &str) -> Option { }) } -pub fn scan_lines(bytes: &[u8], limits: &LdrawLimits) -> Result, ParseError> { +pub fn scan_lines<'src>( + bytes: &'src [u8], + limits: &LdrawLimits, +) -> Result>, ParseError> { let mut out = Vec::new(); let mut offset = 0usize; let mut line_no = 1u32; @@ -196,13 +210,13 @@ pub fn scan_lines(bytes: &[u8], limits: &LdrawLimits) -> Result Ok(out) } -fn scan_one( - text: &str, - raw: &[u8], +fn scan_one<'src>( + text: &'src str, + raw: &'src [u8], start: u32, line: u32, ending: LineEnding, -) -> Result { +) -> Result, ParseError> { let trimmed = text.trim(); let blank = trimmed.is_empty(); let mut tokens = Vec::new(); @@ -244,7 +258,7 @@ fn scan_one( }; tokens.push(Token { kind: TokenKind::QuotedIdentifier, - text: value.into(), + text: value, span, }); continue; @@ -271,7 +285,7 @@ fn scan_one( .ok_or(ParseError::Overflow("token column"))?; tokens.push(Token { kind, - text: value.into(), + text: value, span: Span { start: start .checked_add(begin_u32) @@ -285,22 +299,22 @@ fn scan_one( }); } let line_type = tokens.first().and_then(|t| match t.kind { - TokenKind::Zero => Some(0), - TokenKind::One => Some(1), - TokenKind::Two => Some(2), - TokenKind::Three => Some(3), - TokenKind::Four => Some(4), - TokenKind::Five => Some(5), + TokenKind::Zero => Some(LineType::Zero), + TokenKind::One => Some(LineType::One), + TokenKind::Two => Some(LineType::Two), + TokenKind::Three => Some(LineType::Three), + TokenKind::Four => Some(LineType::Four), + TokenKind::Five => Some(LineType::Five), _ => None, }); let tail_start = tokens .first() .and_then(|t| usize::try_from(t.span.end.checked_sub(start)?).ok()) .unwrap_or(0); - let raw_tail = raw.get(tail_start..).unwrap_or_default().to_vec(); + let raw_tail = raw.get(tail_start..).unwrap_or_default(); Ok(ScannedLine { line_type, - raw: raw.to_vec(), + raw, raw_tail, tokens, span: Span { diff --git a/crates/polymodel-ldraw-core/src/texmap.rs b/crates/polymodel-ldraw-core/src/texmap.rs index 607f47d..627b9db 100644 --- a/crates/polymodel-ldraw-core/src/texmap.rs +++ b/crates/polymodel-ldraw-core/src/texmap.rs @@ -1,5 +1,6 @@ use crate::types::Span; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -9,12 +10,42 @@ pub enum TexmapState { Active, Fallback, } +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TexmapProjectionMode { + Planar, + Cylindrical, + Spherical, +} + +impl TexmapProjectionMode { + fn parse(text: &str) -> Option { + match text { + "PLANAR" => Some(Self::Planar), + "CYLINDRICAL" => Some(Self::Cylindrical), + "SPHERICAL" => Some(Self::Spherical), + _ => None, + } + } + + fn parameter_count(self) -> usize { + match self { + Self::Planar => 9, + Self::Cylindrical => 7, + Self::Spherical => 5, + } + } +} + #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] -pub struct TextureDescriptor { - pub mode: String, - pub parameters: Vec, - pub pngfile: String, - pub glossmap: Option, +pub struct TextureDescriptor<'src> { + pub mode: TexmapProjectionMode, + #[serde(borrow)] + pub parameters: Vec>, + #[serde(borrow)] + pub pngfile: Cow<'src, str>, + #[serde(borrow)] + pub glossmap: Option>, } #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -25,32 +56,34 @@ pub enum TexmapKind { End, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct TexmapEvent { +pub struct TexmapEvent<'src> { pub kind: TexmapKind, pub span: Span, pub file_id: u32, - pub mode: Option, - pub parameters: Vec, - pub reference: Option, + pub mode: Option, + #[serde(borrow)] + pub parameters: Vec>, + #[serde(borrow)] + pub reference: Option>, pub state: TexmapState, } use crate::model::ModelData; -use crate::parser::{add_diag, limit_error}; use crate::scanner::{ScannedLine, TokenKind}; use crate::types::{ - Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, LimitKind, ParseError, ParserProfile, + add_diag, limit_error, Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, LimitKind, + ParseError, ParserProfile, }; use std::collections::BTreeSet; -pub(crate) fn texmap( - model: &mut ModelData, - line: &ScannedLine, +pub(crate) fn texmap<'src>( + model: &mut ModelData<'src>, + line: &ScannedLine<'src>, profile: ParserProfile, diagnostics: &mut Vec, counters: &mut LimitCounters, limits: &LdrawLimits, - texture_ids: &mut BTreeSet, + texture_ids: &mut BTreeSet>, ) -> Result<(), ParseError> { if line.tokens.len() < 3 { return add_diag( @@ -67,7 +100,7 @@ pub(crate) fn texmap( let kind = line.tokens[2].kind; match kind { TokenKind::Start | TokenKind::Next => { - let mode = line.tokens.get(3).map(|token| token.text.as_str()); + let mode = line.tokens.get(3).map(|token| token.text); let Some(mode) = mode else { return add_diag( profile, @@ -80,23 +113,19 @@ pub(crate) fn texmap( true, ); }; - let parameter_count = match mode { - "PLANAR" => 9, - "CYLINDRICAL" => 7, - "SPHERICAL" => 5, - _ => { - return add_diag( - profile, - diagnostics, - counters, - limits, - DiagnosticCode::TexmapInvalidMethod, - line.span, - "unsupported TEXMAP mapping method", - true, - ); - } + let Some(mode) = TexmapProjectionMode::parse(mode) else { + return add_diag( + profile, + diagnostics, + counters, + limits, + DiagnosticCode::TexmapInvalidMethod, + line.span, + "unsupported TEXMAP mapping method", + true, + ); }; + let parameter_count = mode.parameter_count(); if kind == TokenKind::Next && model.texmap_state != TexmapState::Inactive { return add_diag( profile, @@ -114,7 +143,7 @@ pub(crate) fn texmap( .ok_or(ParseError::Overflow("TEXMAP arity"))?; let has_gloss = line.tokens.get(base).map(|token| token.kind) == Some(TokenKind::Identifier) - && line.tokens.get(base).map(|token| token.text.as_str()) == Some("GLOSSMAP"); + && line.tokens.get(base).map(|token| token.text) == Some("GLOSSMAP"); let expected = base .checked_add(if has_gloss { 2 } else { 0 }) .ok_or(ParseError::Overflow("TEXMAP arity"))?; @@ -137,7 +166,7 @@ pub(crate) fn texmap( return None; } let value = token.text.parse::().ok()?; - value.is_finite().then(|| token.text.clone()) + value.is_finite().then(|| Cow::Borrowed(token.text)) }) .collect::>>(); let Some(parameters) = parameters else { @@ -175,7 +204,7 @@ pub(crate) fn texmap( let glossmap = if has_gloss { line.tokens.get(png_index + 1).and_then(|token| { (!matches!(token.kind, TokenKind::Number | TokenKind::Garbage)) - .then(|| token.text.clone()) + .then(|| Cow::Borrowed(token.text)) }) } else { None @@ -193,9 +222,9 @@ pub(crate) fn texmap( ); } let descriptor = TextureDescriptor { - mode: mode.into(), + mode, parameters: parameters.clone(), - pngfile: reference.text.clone(), + pngfile: Cow::Borrowed(reference.text), glossmap, }; if texture_ids.insert(descriptor.clone()) { @@ -227,10 +256,10 @@ pub(crate) fn texmap( TexmapKind::Next }, span: line.span, - file_id: model.file.id, - mode: Some(mode.into()), + file_id: model.file_id, + mode: Some(mode), parameters, - reference: Some(reference.text.clone()), + reference: Some(Cow::Borrowed(reference.text)), state: if kind == TokenKind::Start { TexmapState::Active } else { @@ -271,7 +300,7 @@ pub(crate) fn texmap( model.texmap_events.push(TexmapEvent { kind: TexmapKind::Fallback, span: line.span, - file_id: model.file.id, + file_id: model.file_id, mode: None, parameters: Vec::new(), reference: None, @@ -312,7 +341,7 @@ pub(crate) fn texmap( model.texmap_events.push(TexmapEvent { kind: TexmapKind::End, span: line.span, - file_id: model.file.id, + file_id: model.file_id, mode: None, parameters: Vec::new(), reference: None, diff --git a/crates/polymodel-ldraw-core/src/traversal.rs b/crates/polymodel-ldraw-core/src/traversal.rs index d9e5d57..9fc796b 100644 --- a/crates/polymodel-ldraw-core/src/traversal.rs +++ b/crates/polymodel-ldraw-core/src/traversal.rs @@ -1,14 +1,14 @@ use crate::cache::CacheKey; use crate::geom::{Bounds, Transform}; use crate::model::{ModelData, ModelSummary, SceneRecord}; -use crate::parser::{add_diag, limit_error}; use crate::types::{ - Diagnostic, DiagnosticCode, LimitCounters, LimitKind, ParseError, ParseOptions, + add_diag, limit_error, Diagnostic, DiagnosticCode, LimitCounters, LimitKind, ParseError, + ParseOptions, }; use std::collections::{HashMap, HashSet, VecDeque}; pub(crate) fn traverse( - models: &[ModelData], + models: &[ModelData<'_>], options: &ParseOptions<'_>, counters: &mut LimitCounters, diagnostics: &mut Vec, @@ -92,7 +92,11 @@ pub(crate) fn traverse( summaries.push(ModelSummary { id: model_id, path: model.path.to_string(), - includes: model.includes.iter().map(|i| i.name.clone()).collect(), + includes: model + .includes + .iter() + .map(|include| include.name.as_ref().to_owned()) + .collect(), triangles: model.triangles, lines: model.lines, bfc: model.bfc.state, diff --git a/crates/polymodel-ldraw-core/src/types.rs b/crates/polymodel-ldraw-core/src/types.rs index e5a4931..c0f916c 100644 --- a/crates/polymodel-ldraw-core/src/types.rs +++ b/crates/polymodel-ldraw-core/src/types.rs @@ -8,6 +8,10 @@ use std::fmt; use thiserror::Error; pub const SCHEMA_VERSION: &str = "ldraw-canonical-v1"; +pub const DEFAULT_ROOT_NAME: &str = "model.ldr"; +pub const DEFAULT_COLOUR: u16 = 16; +pub const DEFAULT_PROVENANCE: &str = "local-parse"; +pub const ROOT_MODEL_ID: &str = "model-00000001"; pub const DEFAULT_RESOURCE_BYTES: u64 = 100 * 1024 * 1024; pub const DEFAULT_LINE_BYTES: u64 = 1024 * 1024; pub const DEFAULT_FILES: u64 = 16_384; @@ -306,15 +310,6 @@ impl LdrawLimits { } } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum Checkpoint { - LineScan, - FilePartition, - IncludeTraversal, - TexmapPayload, - Projection, -} - #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct CancellationPolicy { pub cancelled: bool, @@ -345,7 +340,7 @@ impl<'a> ParseOptions<'a> { owner, ledger, semantic_budget: None, - root_name: "model.ldr".into(), + root_name: DEFAULT_ROOT_NAME.into(), resolved_root: RootId::UploadedManifest, fetch_count: 0, cancellation: CancellationPolicy::default(), @@ -419,6 +414,45 @@ impl LimitCounters { } } +pub(crate) fn limit_error(kind: LimitKind, span: Option) -> ParseError { + ParseError::Diagnostic(Diagnostic { + code: kind.diagnostic_code(), + severity: Severity::Error, + message: format!("{kind} limit exceeded"), + span, + }) +} + +pub(crate) fn add_diag( + profile: ParserProfile, + diagnostics: &mut Vec, + counters: &mut LimitCounters, + limits: &LdrawLimits, + code: DiagnosticCode, + span: Span, + message: &str, + strict_reject: bool, +) -> Result<(), ParseError> { + let severity = match profile { + ParserProfile::Strict => Severity::Error, + ParserProfile::Compatibility | ParserProfile::Lossless => Severity::Warning, + }; + counters + .add(LimitKind::Diagnostics, 1, limits) + .map_err(|kind| limit_error(kind, Some(span)))?; + let diagnostic = Diagnostic { + code, + severity, + message: message.into(), + span: Some(span), + }; + diagnostics.push(diagnostic.clone()); + if strict_reject && profile == ParserProfile::Strict { + return Err(ParseError::Diagnostic(diagnostic)); + } + Ok(()) +} + pub(crate) fn reservation_error(error: AdmissionError) -> ParseError { let message = match error.reason { RejectionReason::ParentCapacity | RejectionReason::Overflow => { diff --git a/crates/polymodel-renderer-worker/src/worker.rs b/crates/polymodel-renderer-worker/src/worker.rs index 31e308a..4443b8d 100644 --- a/crates/polymodel-renderer-worker/src/worker.rs +++ b/crates/polymodel-renderer-worker/src/worker.rs @@ -155,20 +155,20 @@ impl WorkerState { return; } }; - let canvas_reservation = match self.ledger.reserve( - owner, - ResourceClass::CanvasBackbuffers, - canvas_bytes, - ) { - Ok(reservation) => reservation, - Err(error) => { - self.post_event(&RendererEvent::Error { - fatal: true, - message: format!("canvas admission: {error}"), - }); - return; - } - }; + let canvas_reservation = + match self + .ledger + .reserve(owner, ResourceClass::CanvasBackbuffers, canvas_bytes) + { + Ok(reservation) => reservation, + Err(error) => { + self.post_event(&RendererEvent::Error { + fatal: true, + message: format!("canvas admission: {error}"), + }); + return; + } + }; let opts = js_sys::Object::new(); let _ = js_sys::Reflect::set(&opts, &"desynchronized".into(), &JsValue::from_bool(true)); @@ -326,15 +326,33 @@ fn scene_cpu_bytes(model: &ModelMesh) -> Result { match &primitive.geometry { Geometry::Triangles(mesh) => { add_bytes(&mut bytes, (mesh.positions.len() as u64).saturating_mul(12))?; - add_bytes(&mut bytes, mesh.normals.as_ref().map_or(0, |v| (v.len() as u64) * 12))?; - add_bytes(&mut bytes, mesh.tangents.as_ref().map_or(0, |v| (v.len() as u64) * 16))?; - add_bytes(&mut bytes, mesh.uvs.as_ref().map_or(0, |v| (v.len() as u64) * 8))?; - add_bytes(&mut bytes, mesh.colors.as_ref().map_or(0, |v| (v.len() as u64) * 16))?; + add_bytes( + &mut bytes, + mesh.normals.as_ref().map_or(0, |v| (v.len() as u64) * 12), + )?; + add_bytes( + &mut bytes, + mesh.tangents.as_ref().map_or(0, |v| (v.len() as u64) * 16), + )?; + add_bytes( + &mut bytes, + mesh.uvs.as_ref().map_or(0, |v| (v.len() as u64) * 8), + )?; + add_bytes( + &mut bytes, + mesh.colors.as_ref().map_or(0, |v| (v.len() as u64) * 16), + )?; add_bytes(&mut bytes, mesh.indices.len().unwrap_or(0) as u64 * 4)?; } Geometry::Points(points) => { - add_bytes(&mut bytes, (points.positions.len() as u64).saturating_mul(12))?; - add_bytes(&mut bytes, points.colors.as_ref().map_or(0, |v| (v.len() as u64) * 16))?; + add_bytes( + &mut bytes, + (points.positions.len() as u64).saturating_mul(12), + )?; + add_bytes( + &mut bytes, + points.colors.as_ref().map_or(0, |v| (v.len() as u64) * 16), + )?; } } } @@ -389,8 +407,8 @@ fn admit_scene( let mut gpu_texture_bytes = 0; let mut textures = Vec::with_capacity(texture_inputs.len()); for input in texture_inputs { - let decoded = texture_base_bytes(input) - .map_err(|error| format!("texture base charge: {error}"))?; + let decoded = + texture_base_bytes(input).map_err(|error| format!("texture base charge: {error}"))?; let gpu = texture_charge(input) .map_err(|error| format!("texture charge: {error}"))? .checked_add(GPU_TEXTURE_OVERHEAD_BYTES) @@ -550,7 +568,9 @@ mod tests { .outcome, polymodel_renderer_protocol::TransitionOutcome::Committed ); - assert!(matches!(reducer.state(), polymodel_renderer_protocol::LoadState::Loading(id) if *id == identity)); + assert!( + matches!(reducer.state(), polymodel_renderer_protocol::LoadState::Loading(id) if *id == identity) + ); assert!(matches!( reducer .reduce(LoadEvent::LoadSucceeded { @@ -561,7 +581,10 @@ mod tests { .state, polymodel_renderer_protocol::LoadState::Installing(id) if id == identity )); - assert!(!matches!(reducer.state(), polymodel_renderer_protocol::LoadState::Ready(_))); + assert!(!matches!( + reducer.state(), + polymodel_renderer_protocol::LoadState::Ready(_) + )); assert!(matches!( reducer .reduce(LoadEvent::InstallSucceeded { @@ -633,7 +656,10 @@ mod tests { }) .unwrap_or_else(|error| panic!("{format:?} fixture should parse: {error:?}")); assert_eq!(mesh.format, format); - assert!(mesh.triangle_count() > 0, "{format:?} must contain triangles"); + assert!( + mesh.triangle_count() > 0, + "{format:?} must contain triangles" + ); } } @@ -995,17 +1021,14 @@ fn handle_load_mesh( }; st.scene_generation = st.scene_generation.saturating_add(1); let scene_owner = owner(st.load_epoch, st.scene_generation); - let (scene_reservations, texture_reservations) = match admit_scene( - &st.ledger, - scene_owner, - &model, - ) { - Ok(reservations) => reservations, - Err(message) => { - st.post_event(&RendererEvent::MeshError { message }); - return; - } - }; + let (scene_reservations, texture_reservations) = + match admit_scene(&st.ledger, scene_owner, &model) { + Ok(reservations) => reservations, + Err(message) => { + st.post_event(&RendererEvent::MeshError { message }); + return; + } + }; let candidate = match build_scene(&renderer.context, &model, pw, ph) { Ok(candidate) => candidate, Err(detail) => {