Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
39 kB · 1073 lines
Rust
at epic-86
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074#![allow(clippy::too_many_arguments)]
use crate::BfcFrame;use crate::bfc::{BfcState, bfc};use crate::cache::{CacheKey, NormalizedPath};use crate::geom::{parse_primitive, process_geometry};use crate::model::{ ColourScope, ColourTable, DataPayload, DataPayloadBuilder, ModelData, ModelEvent, ParseResult, SemanticRecord, SyntaxRecord, parse_colour,};use crate::mpd::{OwnedVirtualFile, VirtualFile, split_mpd};use crate::scanner::{LineType, ScannedLine, TokenKind, scan_lines};use crate::semantic::{SemanticEvent, SemanticSink, TraversalContext};use crate::texmap::{TexmapState, TextureDescriptor, texmap};use crate::traversal::traverse;use crate::types::{ DEFAULT_COLOUR, DEFAULT_PROVENANCE, Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, LimitKind, Materialization, ParseError, ParseOptions, ParserProfile, Span, add_diag, limit_error,};use std::borrow::Cow;use std::collections::{BTreeSet, HashMap};use web_time::Instant;
#[derive(Clone, Debug, Default)]pub(crate) struct ParseMetrics { pub(crate) unique_materializations_scanned: u64, pub(crate) materialization_dedup_hits: u64, pub(crate) scan_cache_hits: u64, pub(crate) scan_cache_misses: u64, pub(crate) include_edges: u64, pub(crate) selection_lookups: u64, pub(crate) selection_hits: u64, pub(crate) unique_models_parsed: u64, pub(crate) subtree_occurrences_traversed: u64, pub(crate) definition_visits: u64, pub(crate) geometry_records_emitted: u64, pub(crate) geometry_records_merged: u64, pub(crate) bytes_hashed: u64, pub(crate) bytes_cloned: u64, pub(crate) bytes_read: u64, pub(crate) traversal_bounds_ns: u128, pub(crate) traversal_instances_ns: u128, pub(crate) traversal_children_ns: u128,}
impl ParseMetrics { fn report(&self, started: &Instant, phase: &str) { if std::env::var_os("POLYMODEL_LDRAW_TIMING").is_some() { eprintln!( "[ldraw-metrics] phase={phase} elapsed_ms={} unique_materializations_scanned={} materialization_dedup_hits={} scan_cache_hits={} scan_cache_misses={} include_edges={} selection_lookups={} selection_hits={} unique_models_parsed={} subtree_occurrences_traversed={} definition_visits={} geometry_records_emitted={} geometry_records_merged={} bytes_hashed={} bytes_cloned={} bytes_read={} traversal_bounds_ms={} traversal_instances_ms={} traversal_children_ms={}", started.elapsed().as_millis(), self.unique_materializations_scanned, self.materialization_dedup_hits, self.scan_cache_hits, self.scan_cache_misses, self.include_edges, self.selection_lookups, self.selection_hits, self.unique_models_parsed, self.subtree_occurrences_traversed, self.definition_visits, self.geometry_records_emitted, self.geometry_records_merged, self.bytes_hashed, self.bytes_cloned, self.bytes_read, self.traversal_bounds_ns / 1_000_000, self.traversal_instances_ns / 1_000_000, self.traversal_children_ns / 1_000_000, ); } }}
pub struct LdrawParser;impl Default for LdrawParser { fn default() -> Self { Self }}impl LdrawParser { pub fn parse_bytes_with_sink<'src, 'a>( &self, bytes: &'src [u8], options: ParseOptions<'a>, materializations: &[Materialization], sink: &mut impl SemanticSink, ) -> Result<crate::OwnedParseResult, ParseError> { let started = Instant::now(); let mut metrics = ParseMetrics::default(); let mut counters = LimitCounters::default(); let input_len = u64::try_from(bytes.len()).map_err(|_| ParseError::Overflow("resource bytes"))?; counters .add(LimitKind::ResourceBytes, input_len, &options.limits) .map_err(|name| limit_error(name, None))?; if options.fetch_count > options.limits.fetches { return Err(limit_error(LimitKind::Fetches, None)); } if options.cancellation.checkpoint() { return Err(ParseError::Cancelled); } let scanned = scan_lines(bytes, &options.limits)?; for line in &scanned { if options.cancellation.checkpoint() { return Err(ParseError::Cancelled); } let line_bytes = u64::try_from(line.raw.len()).map_err(|_| ParseError::Overflow("line bytes"))?; if line_bytes > options.limits.line_bytes { return Err(limit_error(LimitKind::LineBytes, Some(line.span))); } } metrics.report(&started, "scanned-root"); let mut files = split_mpd(&scanned, &options.limits)?; let has_mpd_file = scanned.iter().any(|line| { 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 { for line in &scanned { let is_file = line.line_type == Some(LineType::Zero) && line.tokens.get(1).map(|token| token.kind) == Some(TokenKind::File) && line.tokens.len() >= 3; let is_nofile = line.line_type == Some(LineType::Zero) && line.tokens.get(1).map(|token| token.kind) == Some(TokenKind::NoFile); if is_file || is_nofile { counters .add(LimitKind::Commands, 1, &options.limits) .map_err(|name| limit_error(name, Some(line.span)))?; } } } else { files[0].name = Cow::Owned(options.root_name.clone()); } if options.cancellation.checkpoint() { return Err(ParseError::Cancelled); } let file_count = u64::try_from(files.len()) .map_err(|_| ParseError::Overflow("file count conversion"))?; counters .add(LimitKind::Files, file_count, &options.limits) .map_err(|name| limit_error(name, None))?; let mut diagnostics = Vec::new(); for span in crate::mpd::duplicate_file_name_spans(&files) { add_diag( options.profile, &mut diagnostics, &mut counters, &options.limits, DiagnosticCode::DuplicateFileName, span, "MPD contains a duplicate FILE name", false, )?; } let mut models = Vec::new(); let mut scanned_cache = HashMap::<CacheKey, Vec<ScannedLine<'src>>>::new(); let mut syntax = Vec::new(); for line in &scanned { syntax.push(syntax_record(line)); } let mut colours = ColourTable::new(); let mut texture_ids = BTreeSet::new(); metrics.report(&started, "split-root"); for file in &files { let mut scope = ColourScope::with_first_local_slot(colours.next_local_slot()); let model_result = parse_model( file, &options, &mut counters, &mut diagnostics, &mut scope, &mut texture_ids, None, ); let model = match model_result { Ok(model) => model, Err(error) => { scope.rollback(); return Err(error); } }; scope.commit(); colours.merge_scope(scope); metrics.unique_models_parsed += 1; metrics.geometry_records_emitted += model.geometry.len() as u64; models.push(model); } metrics.report(&started, "parsed-root-models");
let source_model_count = models.len(); let mut materialized_keys = BTreeSet::new(); for materialization in materializations { metrics.unique_materializations_scanned += 1; metrics.bytes_read = metrics .bytes_read .saturating_add(materialization.bytes.len() as u64); if !materialization.hash_is_verified() && !materialization.has_valid_hash() { return Err(ParseError::Diagnostic(Diagnostic { code: DiagnosticCode::MaterializationHashMismatch, severity: crate::types::Severity::Error, message: format!("materialization hash mismatch for {}", materialization.path), span: None, })); } if !materialization.available || materialization.bytes.is_empty() { continue; } let normalized_path = materialization.path.clone(); let normalized_name = normalized_path.to_string(); let materialization_key = ( normalized_path.clone(), materialization.root, materialization.content_hash.clone(), ); if !materialized_keys.insert(materialization_key) { metrics.materialization_dedup_hits += 1; continue; } metrics.bytes_cloned = metrics .bytes_cloned .saturating_add(materialization.bytes.len() as u64); counters .add( LimitKind::ResourceBytes, u64::try_from(materialization.bytes.len()) .map_err(|_| ParseError::Overflow("resource bytes"))?, &options.limits, ) .map_err(|name| limit_error(name, None))?; let materialization_key = materialization.key(); let resolved_scanned = if let Some(scanned) = scanned_cache.get(&materialization_key) { metrics.scan_cache_hits += 1; scanned.clone() } else { metrics.scan_cache_misses += 1; let scanned = scan_lines(&materialization.bytes, &options.limits)?; scanned_cache.insert(materialization_key.clone(), scanned.clone()); scanned }; let file = VirtualFile { id: u32::try_from(files.len()) .map_err(|_| ParseError::Overflow("virtual file id"))?, name: Cow::Owned(normalized_name), lines: resolved_scanned, source_span: Span { start: 0, end: u32::try_from(materialization.bytes.len()) .map_err(|_| ParseError::Overflow("source span"))?, line: 1, column: 1, }, }; counters .add(LimitKind::Files, 1, &options.limits) .map_err(|name| limit_error(name, None))?; files.push(file.clone()); let mut scope = ColourScope::with_first_local_slot(colours.next_local_slot()); let mut model = match parse_model( &file, &options, &mut counters, &mut diagnostics, &mut scope, &mut texture_ids, Some(materialization_key.clone()), ) { Ok(model) => model, Err(error) => { scope.rollback(); return Err(error); } }; scope.commit(); colours.merge_scope(scope); metrics.unique_models_parsed += 1; metrics.geometry_records_emitted += model.geometry.len() as u64; for geometry in &mut model.geometry { geometry.source = model.key.clone(); } models.push(model); } metrics.report(&started, "parsed-materializations"); project_ldparse_step_ends(&mut models, source_model_count);
for diagnostic in &diagnostics { sink.on_event( &SemanticEvent::Diagnostic(diagnostic.clone()), &TraversalContext::for_model( crate::Transform::default(), DEFAULT_COLOUR, BfcFrame::default(), TexmapState::Inactive, None, 0, diagnostic.span, ), ); } for model in &models { if let Some(colour) = colours.get(model.colour) { sink.on_event( &SemanticEvent::ColourDefined { code: colour.code, data: colour.data, }, &TraversalContext::for_model( crate::Transform::default(), model.colour, model.bfc, model.texmap_state, model.texmap_descriptor.as_ref(), model.file_id, None, ), ); } for texmap in &model.texmap_events { let descriptor = texmap.mode.map(|mode| TextureDescriptor { mode, parameters: texmap .parameters .iter() .map(|value| Cow::Owned(value.to_string())) .collect(), pngfile: texmap .reference .as_ref() .map(|value| Cow::Owned(value.to_string())) .unwrap_or_else(|| Cow::Borrowed("")), glossmap: None, }); sink.on_event( &SemanticEvent::TexmapTransition { kind: match texmap.kind { crate::TexmapKind::Start => crate::TexmapKind::Start, crate::TexmapKind::Next => crate::TexmapKind::Next, crate::TexmapKind::Fallback => crate::TexmapKind::Fallback, crate::TexmapKind::End => crate::TexmapKind::End, }, descriptor, }, &TraversalContext::for_model( crate::Transform::default(), model.colour, model.bfc, texmap.state, model.texmap_descriptor.as_ref(), model.file_id, Some(texmap.span), ), ); } for payload in &model.data_payloads { sink.on_event( &SemanticEvent::DataPayload { filename: payload.filename.as_str().into(), bytes: payload.bytes.clone(), }, &TraversalContext::for_model( crate::Transform::default(), model.colour, model.bfc, model.texmap_state, model.texmap_descriptor.as_ref(), model.file_id, None, ), ); } } let (scene, summaries) = traverse( &models, &options, &mut counters, &mut diagnostics, &mut metrics, sink, )?; metrics.report(&started, "traversed"); let root = models.first(); let semantic = SemanticRecord { 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.iter().map(|s| s.to_string()).collect()) .unwrap_or_default(), limits: LdrawLimits::NAMES .iter() .map(|name| Cow::Borrowed(*name)) .collect(), texmap_events: root .map(|m| { m.texmap_events .clone() .into_iter() .map(|event| event.into_owned()) .collect() }) .unwrap_or_default(), texmap_geometry: root.map(|m| m.texmap_geometry.clone()).unwrap_or_default(), data_payloads: root.map(|m| m.data_payloads.clone()).unwrap_or_default(), }; Ok(crate::OwnedParseResult::from_parse_result(ParseResult { syntax, semantic, scene, diagnostics, counters, files: files.into_iter().map(OwnedVirtualFile::from).collect(), models: summaries, colours: colours.into_owned(), provenance_id: Cow::Borrowed(DEFAULT_PROVENANCE), })) }}
fn parse_model<'src>( file: &VirtualFile<'src>, options: &ParseOptions<'_>, counters: &mut LimitCounters, diagnostics: &mut Vec<Diagnostic>, colours: &mut ColourScope<'src>, texture_ids: &mut BTreeSet<TextureDescriptor<'src>>, known_key: Option<CacheKey>,) -> Result<ModelData<'src>, ParseError> { if options.cancellation.checkpoint() { return Err(ParseError::Cancelled); } let path = NormalizedPath::new(&file.name)?; let key = known_key.unwrap_or_else(|| { CacheKey::new_from_parts( path.clone(), options.resolved_root, file.lines .iter() .flat_map(|line| [line.raw, line.ending.as_str().as_bytes()]), ) }); let line_count = u64::try_from(file.lines.len()).map_err(|_| ParseError::Overflow("model line count"))?; let _ = line_count; let mut model = ModelData { file_id: file.id, path, key, bfc: BfcFrame::default(), colour: DEFAULT_COLOUR, steps: Vec::new(), step_ends: Vec::new(), mesh_index_count: 0, events: Vec::new(), texmap_state: TexmapState::Inactive, texmap_descriptor: None, texmap_stack: Vec::new(), texmap_next_span: None, texmap_fallback_seen: false, texmap_events: Vec::new(), texmap_geometry: Vec::new(), data_payload: None, data_payloads: Vec::new(), includes: Vec::new(), triangles: 0, quads: 0, lines: 0, conditional_lines: 0, geometry: Vec::new(), bounds: None, }; for line in &file.lines { if line.blank { continue; } if options.cancellation.checkpoint() { return Err(ParseError::Cancelled); } if counters .add(LimitKind::Commands, 1, &options.limits) .is_err() { return Err(limit_error(LimitKind::Commands, Some(line.span))); } let before = diagnostics.len(); let result = process_line( &mut model, line, options, diagnostics, counters, colours, texture_ids, ); if let Err(error) = result { let continue_diagnostic = match &error { ParseError::Diagnostic(diagnostic) => { options.profile != ParserProfile::Strict && !matches!( diagnostic.code, DiagnosticCode::LineBytesLimit | DiagnosticCode::FilesLimit | DiagnosticCode::IncludeDepthLimit | DiagnosticCode::CommandsLimit | DiagnosticCode::InstancesLimit | DiagnosticCode::TrianglesLimit | DiagnosticCode::LinesLimit | DiagnosticCode::TexturesLimit | DiagnosticCode::FetchesLimit | DiagnosticCode::DiagnosticsLimit | DiagnosticCode::ResourceBytesLimit ) } _ => false, }; if !continue_diagnostic { return Err(error); } } let diagnostic_count = u64::try_from(diagnostics.len()) .map_err(|_| ParseError::Overflow("diagnostic count conversion"))?; if diagnostics.len() > before && diagnostic_count > options.limits.diagnostics { return Err(limit_error(LimitKind::Diagnostics, Some(line.span))); } } if model.data_payload.is_some() { finalize_data(&mut model, options, diagnostics, counters, file.source_span)?; } Ok(model)}
fn project_ldparse_step_ends(models: &mut [ModelData<'_>], source_model_count: usize) { #[derive(Clone, Copy)] struct ScanFrame { source: usize, next_event: usize, }
let source_limit = source_model_count.min(models.len()); let source_index = models .iter() .take(source_limit) .enumerate() .map(|(index, model)| (model.path.clone(), index)) .collect::<std::collections::HashMap<_, _>>(); for model in models.iter_mut() { model.step_ends.clear(); }
// LDParse's callbacks are stateful pointers, not a tree-fold over model // contents. Keep the parser's source cursor stack separate from the // callback target: EOF always retargets callbacks to the root, while the // saved include cursor is replayed against the now-cached child. let mut callback_indices = vec![0_u64; models.len()]; let mut cached = BTreeSet::new(); let mut completed = vec![false; source_limit]; let mut scan_stack = Vec::new();
for outer in 0..source_limit { if completed[outer] { continue; } cached.insert(outer); let mut frame = ScanFrame { source: outer, next_event: 0, }; let mut callback_target = outer;
loop { if frame.next_event >= models[frame.source].events.len() { completed[frame.source] = true; // ModelBuilder::handleEOF calls recordTo(&target), where the // EOF callback was retained on the root model. callback_target = 0; let Some(parent) = scan_stack.pop() else { break; }; frame = parent; continue; }
let event = models[frame.source].events[frame.next_event].clone(); match event { ModelEvent::Step => { let offset = callback_indices[callback_target]; models[callback_target].step_ends.push(offset); frame.next_event += 1; } ModelEvent::Indices(count) => { callback_indices[callback_target] = callback_indices[callback_target].saturating_add(count); frame.next_event += 1; } ModelEvent::Include(name) => { let child = NormalizedPath::new(name.as_ref()) .ok() .and_then(|path| source_index.get(&path).copied()); if let Some(child) = child && !cached.contains(&child) { // Do not advance the include cursor. The real // parser saves this line and reprocesses it after // the child reaches EOF. scan_stack.push(frame); cached.insert(child); frame = ScanFrame { source: child, next_event: 0, }; callback_target = child; continue; } // A cached include is an ordinary callback and consumes // its source line without changing the callback target. frame.next_event += 1; } } } }
// Caller-admitted materializations are parsed independently of the MPD // stream and therefore retain their file-local callback target. for model in models.iter_mut().skip(source_limit) { let mut indices = 0_u64; let mut step_ends = Vec::new(); for event in &model.events { match event { ModelEvent::Step => step_ends.push(indices), ModelEvent::Indices(count) => indices = indices.saturating_add(*count), ModelEvent::Include(_) => {} } } model.step_ends.extend(step_ends); }}
fn finalize_data<'src>( model: &mut ModelData<'src>, options: &ParseOptions<'_>, diagnostics: &mut Vec<Diagnostic>, counters: &mut LimitCounters, _span: Span,) -> Result<(), ParseError> { let Some(payload) = model.data_payload.take() else { return Ok(()); }; let decoded = match base64::Engine::decode( &base64::engine::general_purpose::STANDARD, payload.encoded.as_bytes(), ) { Ok(decoded) if !decoded.is_empty() => decoded, Ok(_) => { add_diag( options.profile, diagnostics, counters, &options.limits, DiagnosticCode::DataTruncated, payload.span, "DATA payload is empty", true, )?; return Ok(()); } Err(_) => { add_diag( options.profile, diagnostics, counters, &options.limits, DiagnosticCode::DataInvalid, payload.span, "DATA payload is not valid base64", true, )?; return Ok(()); } }; if options.cancellation.checkpoint() { return Err(ParseError::Cancelled); } let decoded_len = u64::try_from(decoded.len()).map_err(|_| ParseError::Overflow("DATA resource bytes"))?; counters .add(LimitKind::ResourceBytes, decoded_len, &options.limits) .map_err(|kind| limit_error(kind, Some(payload.span)))?; model.data_payloads.push(DataPayload { filename: payload.filename, bytes: decoded, }); Ok(())}
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, raw_tail: Cow::Borrowed(raw_tail), span: line.span, line_ending: Cow::Owned(line.ending.as_str().to_owned()), fields: line .tokens .iter() .map(|token| Cow::Borrowed(token.text)) .collect(), }}
fn process_line<'src>( model: &mut ModelData<'src>, line: &ScannedLine<'src>, options: &ParseOptions<'_>, diagnostics: &mut Vec<Diagnostic>, counters: &mut LimitCounters, colours: &mut ColourScope<'src>, texture_ids: &mut BTreeSet<TextureDescriptor<'src>>,) -> Result<(), ParseError> { let profile = options.profile; let limits = &options.limits; let primitive = parse_primitive(line); let bang_colon = line.line_type == Some(LineType::Zero) && line.tokens.get(1).map(|token| token.text) == Some("!:"); if let Some(payload) = model.data_payload.as_mut() { let continuation = bang_colon && model.texmap_state == TexmapState::Inactive; if continuation { let encoded = line .tokens .get(2..) .unwrap_or_default() .iter() .map(|token| token.text) .collect::<String>(); payload.encoded.push_str(&encoded); return Ok(()); } finalize_data(model, options, diagnostics, counters, line.span)?; } if model.texmap_state == TexmapState::AwaitingNext && !line.blank { 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; add_diag( profile, diagnostics, counters, limits, DiagnosticCode::TexmapNextType0, span, "NEXT was cancelled by a type-0 line", true, )?; } else if matches!( line.line_type, Some(LineType::One | LineType::Two | LineType::Three | LineType::Four | LineType::Five) ) && primitive.is_some() { let descriptor = model.texmap_descriptor.clone(); model.texmap_state = TexmapState::Active; model.texmap_descriptor = descriptor.clone(); let diagnostic_count = diagnostics.len(); process_geometry( model, line, primitive, profile, diagnostics, counters, limits, )?; if diagnostics.len() != diagnostic_count { model.texmap_state = TexmapState::AwaitingNext; model.texmap_descriptor = descriptor; return Ok(()); } model.texmap_state = TexmapState::Inactive; model.texmap_descriptor = None; if let Some(descriptor) = descriptor { let _ = texture_ids.insert(descriptor); } model.texmap_next_span = None; return Ok(()); } } if bang_colon { return process_bang_colon(model, line, profile, diagnostics, counters, limits); } 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; if !preserves_invert { model.bfc.invert_next = false; } return process_meta( model, line, options, diagnostics, counters, colours, texture_ids, ); } process_geometry( model, line, primitive, profile, diagnostics, counters, limits, )}
fn process_bang_colon<'src>( model: &mut ModelData<'src>, line: &ScannedLine<'src>, profile: ParserProfile, diagnostics: &mut Vec<Diagnostic>, counters: &mut LimitCounters, limits: &LdrawLimits,) -> Result<(), ParseError> { if !matches!( model.texmap_state, TexmapState::Active | TexmapState::Fallback ) { return add_diag( profile, diagnostics, counters, limits, DiagnosticCode::TexmapBangColonOutOfScope, line.span, "bang-colon geometry requires an active TEXMAP frame", true, ); } let Some(embedded_type) = line.tokens.get(2).and_then(|token| match token.kind { 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, }) else { return add_diag( profile, diagnostics, counters, limits, DiagnosticCode::TexmapInvalidBangColonGeometry, line.span, "bang-colon requires an embedded line type 1 through 5", true, ); }; if matches!( model.texmap_state, TexmapState::Active | TexmapState::Fallback ) && line.tokens.iter().skip(2).any(|token| token.text == "!:") { return add_diag( profile, diagnostics, counters, limits, DiagnosticCode::TexmapNestedBangColon, line.span, "bang-colon geometry cannot be nested", true, ); } let embedded_line = ScannedLine { line_type: Some(embedded_type), raw: line.raw, raw_tail: line.raw_tail, tokens: line.tokens[2..].to_vec(), span: line.span, ending: line.ending.clone(), blank: false, }; let embedded_primitive = crate::geom::parse_primitive(&embedded_line); if embedded_primitive.is_none() || embedded_primitive.as_ref().is_some_and(|primitive| { matches!( (embedded_type, primitive), (LineType::One, crate::geom::ParsedPrimitive::Points(_)) | ( LineType::Two | LineType::Three | LineType::Four | LineType::Five, crate::geom::ParsedPrimitive::Include(_) ) ) }) { return add_diag( profile, diagnostics, counters, limits, DiagnosticCode::TexmapInvalidBangColonGeometry, line.span, "malformed bang-colon geometry", true, ); } process_geometry( model, &embedded_line, embedded_primitive, profile, diagnostics, counters, limits, )}
fn process_meta<'src>( model: &mut ModelData<'src>, line: &ScannedLine<'src>, options: &ParseOptions<'_>, diagnostics: &mut Vec<Diagnostic>, counters: &mut LimitCounters, colours: &mut ColourScope<'src>, texture_ids: &mut BTreeSet<TextureDescriptor<'src>>,) -> Result<(), ParseError> { let profile = options.profile; let limits = &options.limits; let tokens = &line.tokens; if tokens.len() < 2 { if profile == ParserProfile::Strict { return add_diag( profile, diagnostics, counters, limits, DiagnosticCode::MetaEmpty, line.span, "empty META command", true, ); } return Ok(()); } if tokens.get(1).map(|t| t.kind) == Some(TokenKind::Bfc) { return bfc(model, line, profile, diagnostics, counters, limits); } if tokens.get(1).map(|t| t.kind) == Some(TokenKind::Texmap) { return texmap( model, line, profile, diagnostics, counters, limits, texture_ids, ); } if tokens.get(1).map(|t| t.text) == Some("!DATA") { let Some(filename) = tokens.get(2).filter(|token| !token.text.is_empty()) else { add_diag( profile, diagnostics, counters, limits, DiagnosticCode::DataInvalid, line.span, "DATA requires a filename", true, )?; return Ok(()); }; if tokens.len() != 3 { add_diag( profile, diagnostics, counters, limits, DiagnosticCode::DataInvalid, line.span, "DATA accepts exactly one filename", true, )?; return Ok(()); } model.data_payload = Some(DataPayloadBuilder { filename: filename.text.to_owned(), encoded: String::new(), span: line.span, }); return Ok(()); } match tokens[1].kind { TokenKind::Step | TokenKind::Rotstep => { let step = model .steps .len() .checked_add(1) .ok_or(ParseError::Overflow("step number"))?; model.steps.push(format!("step-{step:08}").into()); model.step_ends.push(model.mesh_index_count); model.events.push(ModelEvent::Step); model.bfc.invert_next = false; model.texmap_state = TexmapState::Inactive; model.texmap_descriptor = None; model.texmap_stack.clear(); model.texmap_next_span = None; model.texmap_fallback_seen = false; } TokenKind::Colour => { if tokens.get(2).is_some_and(|token| token.text.ends_with(';')) { model.bfc.invert_next = false; return Ok(()); } if let Some(colour) = parse_colour(tokens) { let code = colour.code; if colours.record(colour).is_err() { add_diag( profile, diagnostics, counters, limits, DiagnosticCode::ColourSlotsLimit, line.span, "local colour slot table is exhausted", true, )?; } else { model.colour = code; } } else { add_diag( profile, diagnostics, counters, limits, DiagnosticCode::InvalidColour, line.span, "malformed !COLOUR directive", true, )?; } model.bfc.invert_next = false; } TokenKind::Stop => { add_diag( profile, diagnostics, counters, limits, DiagnosticCode::TexmapUnknownMetaStop, line.span, "literal STOP is unknown META, not END", true, )?; } _ => { if model.bfc.invert_next { model.bfc.invert_next = false; } } } Ok(())}