use crate::bfc::{BfcFrame, BfcState}; use crate::cache::{CacheKey, NormalizedPath}; use crate::geom::{Bounds3, ColourCode, GeometryRecord, Transform, deserialize_optional_bounds3}; use crate::mpd::OwnedVirtualFile; use crate::scanner::{LineType, Token, TokenKind}; use crate::texmap::{TexmapAssociation, TexmapEvent, TexmapState, TextureDescriptor}; use crate::types::{Diagnostic, LimitCounters, SCHEMA_VERSION, Span}; use serde::{Deserialize, Serialize}; use smol_str::SmolStr; use std::borrow::Cow; use std::collections::BTreeMap; use std::fmt; #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct ColourData { pub r: u8, pub g: u8, pub b: u8, pub alpha: Option, pub luminance: Option, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum ColourEdge { Code(ColourCode), Data(ColourData), } #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ColourFinish { Chrome, Pearlescent, Rubber, MatteMetallic, Metal, } #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ColourFabricKind { Velvet, Canvas, String, Fur, } #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ColourMaterialKind { Glitter, Speckle, Fabric { variant: Option }, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ColourMaterial<'src> { /// The source spelling of the material kind, retained for compatibility. #[serde(borrow)] pub kind: Cow<'src, str>, pub typed_kind: ColourMaterialKind, pub value: Option, pub fraction: Option, pub vfraction: Option, pub size: Option, pub min_size: Option, pub max_size: Option, #[serde(borrow)] pub extra: Vec>, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Colour<'src> { pub code: ColourCode, #[serde(borrow)] pub name: Option>, pub data: ColourData, pub edge: ColourEdge, #[serde(default)] pub finishes: Vec, #[serde(default)] pub material: Option>, } const FIRST_LOCAL_COLOUR_SLOT: u32 = 512; const LAST_LOCAL_COLOUR_SLOT: u32 = 65_534; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct ColourSlotError; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ColourTable<'src> { #[serde(borrow)] entries: BTreeMap>, local_slots: BTreeMap, next_local_slot: u32, } impl<'src> Default for ColourTable<'src> { fn default() -> Self { Self::new() } } impl<'src> ColourTable<'src> { pub fn new() -> Self { Self { entries: BTreeMap::new(), local_slots: BTreeMap::new(), next_local_slot: FIRST_LOCAL_COLOUR_SLOT, } } pub fn record(&mut self, colour: Colour<'src>) { self.entries.insert(colour.code, colour); } pub fn get(&self, code: impl Into) -> Option<&Colour<'src>> { self.entries.get(&code.into()) } pub fn find(&self, code: impl Into, find_edge: bool) -> Option<&ColourData> { let code = code.into(); let colour = self.entries.get(&code)?; if !find_edge { return Some(&colour.data); } match &colour.edge { ColourEdge::Data(data) => Some(data), ColourEdge::Code(edge_code) => self.entries.get(edge_code).map(|edge| &edge.data), } } pub fn local_slot(&self, code: impl Into) -> Option { self.local_slots.get(&code.into()).copied() } pub fn len(&self) -> usize { self.entries.len() } pub fn is_empty(&self) -> bool { self.entries.is_empty() } pub(crate) fn merge_scope(&mut self, scope: ColourScope<'src>) { self.entries.extend(scope.committed.into_iter().flatten()); self.local_slots.extend(scope.local_slots); self.next_local_slot = scope.next_local_slot; } pub(crate) fn next_local_slot(&self) -> u32 { self.next_local_slot } pub(crate) fn into_owned(self) -> ColourTable<'static> { ColourTable { entries: self .entries .into_iter() .map(|(code, colour)| { ( code, Colour { code: colour.code, name: colour.name.map(|name| Cow::Owned(name.into_owned())), data: colour.data, edge: colour.edge, finishes: colour.finishes, material: colour.material.map(|material| ColourMaterial { kind: Cow::Owned(material.kind.into_owned()), typed_kind: material.typed_kind, value: material.value, fraction: material.fraction, vfraction: material.vfraction, size: material.size, min_size: material.min_size, max_size: material.max_size, extra: material .extra .into_iter() .map(|value| Cow::Owned(value.into_owned())) .collect(), }), }, ) }) .collect(), local_slots: self.local_slots, next_local_slot: self.next_local_slot, } } } /// File-local transactional colour definitions. /// /// Records are invisible until `commit`. Dropping or rolling back a scope /// discards both definitions and provisional local slots, so failed parses do /// not affect later virtual files or parses. #[derive(Clone, Debug, PartialEq)] pub struct ColourScope<'src> { committed: Vec>>, active: BTreeMap>, local_slots: BTreeMap, active_slot_start: u32, next_local_slot: u32, } impl<'src> ColourScope<'src> { pub fn new() -> Self { Self::with_first_local_slot(FIRST_LOCAL_COLOUR_SLOT) } pub(crate) fn with_first_local_slot(first_local_slot: u32) -> Self { Self { committed: Vec::new(), active: BTreeMap::new(), local_slots: BTreeMap::new(), active_slot_start: first_local_slot, next_local_slot: first_local_slot, } } pub fn record(&mut self, colour: Colour<'src>) -> Result { let code = colour.code; if let Some(slot) = self.local_slots.get(&code).copied() { self.active.insert(code, colour); return Ok(slot); } let slot = self.allocate_local_slot()?; self.local_slots.insert(code, slot); self.active.insert(code, colour); Ok(slot) } pub fn commit(&mut self) { if !self.active.is_empty() { self.committed.push(std::mem::take(&mut self.active)); } self.active_slot_start = self.next_local_slot; } pub fn rollback(&mut self) { self.active.clear(); self.local_slots .retain(|_, slot| u32::from(*slot) < self.active_slot_start); self.next_local_slot = self.active_slot_start; } pub fn has_uncommitted(&self) -> bool { !self.active.is_empty() } pub fn get(&self, code: impl Into) -> Option<&Colour<'src>> { if !self.active.is_empty() { return None; } let code = code.into(); self.committed.iter().rev().find_map(|map| map.get(&code)) } pub fn find(&self, code: impl Into, find_edge: bool) -> Option<&ColourData> { let mut code = code.into(); for (map_index, map) in self.committed.iter().enumerate().rev() { let Some(colour) = map.get(&code) else { continue; }; if !find_edge { return Some(&colour.data); } match &colour.edge { ColourEdge::Data(data) => return Some(data), ColourEdge::Code(edge_code) => { if let Some(edge) = map.get(edge_code) { return Some(&edge.data); } code = *edge_code; for older_map in self.committed[..map_index].iter().rev() { if let Some(edge) = older_map.get(&code) { return Some(&edge.data); } } return None; } } } None } pub fn local_slot(&self, code: impl Into) -> Option { self.local_slots.get(&code.into()).copied() } fn allocate_local_slot(&mut self) -> Result { if self.next_local_slot > LAST_LOCAL_COLOUR_SLOT { return Err(ColourSlotError); } let slot = u16::try_from(self.next_local_slot).map_err(|_| ColourSlotError)?; self.next_local_slot += 1; Ok(slot) } } impl Default for ColourScope<'_> { fn default() -> Self { Self::new() } } pub(crate) fn parse_colour<'src>(tokens: &[Token<'src>]) -> Option> { if tokens.len() < 9 || tokens.first()?.kind != TokenKind::Zero || tokens.get(1)?.kind != TokenKind::Colour || tokens.get(2)?.kind != TokenKind::Identifier { return None; } let mut index = 3; if !matches_keyword(tokens.get(index), TokenKind::Code) { return None; } let code = parse_colour_code(tokens.get(index + 1)?.text)?; index += 2; if !matches_keyword(tokens.get(index), TokenKind::Value) { return None; } let (r, g, b) = parse_rgb(tokens.get(index + 1)?.text)?; index += 2; if !matches_keyword(tokens.get(index), TokenKind::Edge) { return None; } let edge_token = tokens.get(index + 1)?.text; let edge = parse_rgb(edge_token) .map(|(r, g, b)| { ColourEdge::Data(ColourData { r, g, b, alpha: None, luminance: None, }) }) .or_else(|| parse_colour_code(edge_token).map(ColourEdge::Code))?; index += 2; let mut data = ColourData { r, g, b, alpha: None, luminance: None, }; let mut finishes = Vec::new(); let mut material = None; while index < tokens.len() { match tokens[index].kind { TokenKind::Alpha => { if data.alpha.is_some() { return None; } data.alpha = Some(parse_byte(tokens.get(index + 1)?.text)?); index += 2; } TokenKind::Luminance => { if data.luminance.is_some() { return None; } data.luminance = Some(parse_byte(tokens.get(index + 1)?.text)?); index += 2; } TokenKind::Chrome | TokenKind::Pearlescent | TokenKind::Rubber | TokenKind::MatteMetallic | TokenKind::Metal => { if !finishes.is_empty() || material.is_some() { return None; } finishes.push(match tokens[index].kind { TokenKind::Chrome => ColourFinish::Chrome, TokenKind::Pearlescent => ColourFinish::Pearlescent, TokenKind::Rubber => ColourFinish::Rubber, TokenKind::MatteMetallic => ColourFinish::MatteMetallic, TokenKind::Metal => ColourFinish::Metal, _ => unreachable!(), }); index += 1; } TokenKind::Material => { if !finishes.is_empty() || material.is_some() { return None; } material = Some(parse_material(&tokens[index + 1..])?); index = tokens.len(); } _ => return None, } } Some(Colour { code, name: Some(Cow::Borrowed(tokens[2].text)), data, edge, finishes, material, }) } fn matches_keyword(token: Option<&Token<'_>>, expected: TokenKind) -> bool { token.is_some_and(|token| token.kind == expected) } fn parse_material<'src>(tokens: &[Token<'src>]) -> Option> { let kind_token = tokens.first()?; let typed_kind = match kind_token.text { "GLITTER" => ColourMaterialKind::Glitter, "SPECKLE" => ColourMaterialKind::Speckle, "FABRIC" => ColourMaterialKind::Fabric { variant: None }, _ => return None, }; if matches!(typed_kind, ColourMaterialKind::Fabric { .. }) { let variant = tokens.get(1).map(|token| match token.text { "VELVET" => Some(ColourFabricKind::Velvet), "CANVAS" => Some(ColourFabricKind::Canvas), "STRING" => Some(ColourFabricKind::String), "FUR" => Some(ColourFabricKind::Fur), _ => None, }); if tokens.len() > 2 || variant.is_some_and(|variant| variant.is_none()) { return None; } return Some(ColourMaterial { kind: Cow::Borrowed(kind_token.text), typed_kind: ColourMaterialKind::Fabric { variant: variant.flatten(), }, value: None, fraction: None, vfraction: None, size: None, min_size: None, max_size: None, extra: Vec::new(), }); } let mut value = None; let mut fraction = None; let mut vfraction = None; let mut size = None; let mut min_size = None; let mut max_size = None; let mut index = 1; while index < tokens.len() { let key = tokens[index].text; if index + 1 >= tokens.len() { return None; } let value_token = &tokens[index + 1]; match key { "VALUE" if value.is_none() => { let (r, g, b) = parse_rgb(value_token.text)?; value = Some(ColourData { r, g, b, alpha: None, luminance: None, }); } "ALPHA" if value.is_some() && value.as_ref()?.alpha.is_none() => { value.as_mut()?.alpha = Some(parse_byte(value_token.text)?); } "LUMINANCE" if value.is_some() && value.as_ref()?.luminance.is_none() => { value.as_mut()?.luminance = Some(parse_byte(value_token.text)?); } "FRACTION" if fraction.is_none() => fraction = Some(parse_fraction(value_token.text)?), "VFRACTION" if vfraction.is_none() => { vfraction = Some(parse_fraction(value_token.text)?) } "SIZE" if size.is_none() => size = Some(parse_positive(value_token.text)?), "MINSIZE" if min_size.is_none() => min_size = Some(parse_positive(value_token.text)?), "MAXSIZE" if max_size.is_none() => max_size = Some(parse_positive(value_token.text)?), _ => return None, } index += 2; } let is_glitter = matches!(typed_kind, ColourMaterialKind::Glitter); let is_speckle = matches!(typed_kind, ColourMaterialKind::Speckle); if value.is_none() || fraction.is_none() { return None; } if is_glitter { if vfraction.is_none() || size.is_none() || min_size.is_some() || max_size.is_some() { return None; } } else if is_speckle && (vfraction.is_some() || size.is_some() || min_size.is_none() || max_size.is_none()) { return None; } if size.is_some() == (min_size.is_some() || max_size.is_some()) || min_size.is_some() != max_size.is_some() { return None; } Some(ColourMaterial { kind: Cow::Borrowed(kind_token.text), typed_kind, value, fraction, vfraction, size, min_size, max_size, extra: Vec::new(), }) } fn parse_colour_code(text: &str) -> Option { (!text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit())) .then(|| text.parse::().ok()) .flatten() .map(ColourCode) } fn parse_byte(text: &str) -> Option { (!text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit())) .then(|| text.parse::().ok()) .flatten() .and_then(|value| u8::try_from(value).ok()) } fn parse_fraction(text: &str) -> Option { let value = text.parse::().ok()?; (value > 0.0 && value < 1.0 && value.is_finite()).then_some(value) } fn parse_positive(text: &str) -> Option { let value = text.parse::().ok()?; (value > 0.0 && value.is_finite()).then_some(value) } fn parse_rgb(text: &str) -> Option<(u8, u8, u8)> { let hex = text.strip_prefix('#').or_else(|| text.strip_prefix("0x"))?; if hex.len() != 6 { return None; } Some(( u8::from_str_radix(&hex[0..2], 16).ok()?, u8::from_str_radix(&hex[2..4], 16).ok()?, u8::from_str_radix(&hex[4..6], 16).ok()?, )) } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct SyntaxRecord<'src> { pub line_type: Option, #[serde(borrow)] pub raw_tail: Cow<'src, str>, pub span: Span, #[serde(borrow)] pub line_ending: Cow<'src, str>, #[serde(borrow)] pub fields: Vec>, } impl<'src> SyntaxRecord<'src> { pub(crate) fn into_owned(self) -> SyntaxRecord<'static> { SyntaxRecord { line_type: self.line_type, raw_tail: Cow::Owned(self.raw_tail.into_owned()), span: self.span, line_ending: Cow::Owned(self.line_ending.into_owned()), fields: self .fields .into_iter() .map(|field| Cow::Owned(field.into_owned())) .collect(), } } } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct DataPayload { pub filename: String, pub bytes: Vec, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] 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: ColourCode, pub steps: Vec, #[serde(borrow)] pub limits: Vec>, pub texmap_events: Vec>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub texmap_geometry: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub data_payloads: Vec, } impl<'src> SemanticRecord<'src> { pub(crate) fn into_owned(self) -> SemanticRecord<'static> { SemanticRecord { canonical_path: self .canonical_path .map(|value| Cow::Owned(value.into_owned())), root_identity: self .root_identity .map(|value| Cow::Owned(value.into_owned())), target_identity: self .target_identity .map(|value| Cow::Owned(value.into_owned())), cache_identity: self .cache_identity .map(|value| Cow::Owned(value.into_owned())), bfc_state: self.bfc_state, colour_state: self.colour_state, steps: self.steps, limits: self .limits .into_iter() .map(|value| Cow::Owned(value.into_owned())) .collect(), texmap_events: self .texmap_events .into_iter() .map(TexmapEvent::into_owned) .collect(), texmap_geometry: self.texmap_geometry, data_payloads: self.data_payloads, } } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] pub struct ModelId(pub u32); #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct InstanceRecord { pub model: ModelId, pub instance_id: SmolStr, #[serde(default)] pub parent: Option, pub source_model: SmolStr, pub target_model: SmolStr, #[serde(default)] pub source_span: Option, #[serde(default)] pub target_key: Option, pub transform: Transform, pub inverted: bool, #[serde(default)] pub bfc: Option, #[serde(default)] pub direct_colour: Option, #[serde(default)] pub colour: Option, #[serde(default)] pub texmap_state: Option, #[serde(default)] pub texmap_reference: Option, #[serde(default)] pub texmap: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct SceneRecord { pub model_id: SmolStr, pub instance_ids: Vec, #[serde(default)] pub instances: Vec, pub triangles: u64, pub quads: u64, pub lines: u64, pub conditional_lines: u64, #[serde(deserialize_with = "deserialize_optional_bounds3")] pub bounds: Option, pub reflection: bool, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 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: Cow<'src, str>, } impl<'src> CanonicalRecord<'src> { pub fn into_owned(self) -> CanonicalRecord<'static> { CanonicalRecord { schema_version: Cow::Owned(self.schema_version.into_owned()), syntax: self .syntax .into_iter() .map(SyntaxRecord::into_owned) .collect(), semantic: self.semantic.into_owned(), scene: self.scene, diagnostics: self.diagnostics, provenance_id: Cow::Owned(self.provenance_id.into_owned()), } } } #[derive(Clone, Debug)] pub(crate) struct DataPayloadBuilder { pub(crate) filename: String, pub(crate) encoded: String, pub(crate) span: Span, } #[derive(Clone, Debug)] pub(crate) struct Include<'src> { pub(crate) name: Cow<'src, str>, pub transform: Transform, pub(crate) span: Span, pub(crate) inverted: bool, pub(crate) bfc: BfcFrame, pub(crate) colour: ColourCode, pub(crate) texmap_state: TexmapState, pub(crate) texmap: Option>, pub(crate) texmap_association: Option, } #[derive(Clone, Debug)] pub(crate) enum ModelEvent<'src> { Step, Indices(u64), Include(Cow<'src, str>), } #[derive(Clone, Debug)] 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: ColourCode, pub(crate) steps: Vec>, /// LDParse-compatible direct-mesh index offsets recorded at each STEP. /// This is distinct from the root semantic step labels used by policy. pub(crate) step_ends: Vec, pub(crate) mesh_index_count: u64, pub(crate) events: Vec>, pub(crate) texmap_state: TexmapState, 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) texmap_geometry: Vec, pub(crate) data_payload: Option, pub(crate) data_payloads: Vec, pub(crate) includes: Vec>, pub(crate) triangles: u64, pub(crate) quads: u64, pub(crate) lines: u64, pub(crate) conditional_lines: u64, pub(crate) geometry: Vec, pub(crate) bounds: Option, } pub struct ParseResult<'src> { pub syntax: Vec>, pub semantic: SemanticRecord<'src>, pub scene: SceneRecord, pub diagnostics: Vec, pub counters: LimitCounters, pub files: Vec, pub models: Vec, pub colours: ColourTable<'src>, pub provenance_id: Cow<'src, str>, } /// An owned parse result. The retained semantic result and its reservation are /// one object: projection borrows or clones data while this owner remains live. pub struct OwnedParseResult { pub result: ParseResult<'static>, } impl fmt::Debug for OwnedParseResult { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("OwnedParseResult") .field("schema_version", &SCHEMA_VERSION) .finish() } } impl std::ops::Deref for OwnedParseResult { type Target = ParseResult<'static>; fn deref(&self) -> &Self::Target { &self.result } } impl OwnedParseResult { pub(crate) fn from_parse_result(result: ParseResult<'_>) -> Self { Self { result: result.into_owned(), } } pub fn project(&self) -> CanonicalRecord<'static> { self.result.project() } /// Move both the canonical projection and its retained arena owner together. pub fn into_projection(self) -> OwnedProjection { OwnedProjection { canonical: self.result.project(), } } } pub struct OwnedProjection { pub canonical: CanonicalRecord<'static>, } impl OwnedProjection { pub fn project(&self) -> &CanonicalRecord<'static> { &self.canonical } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct LdMeshSemantic { pub positions: Vec<[f64; 3]>, pub normals: Option>, pub indices: Vec, pub bf_indices: Vec, pub step_offsets: Vec, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ModelSummary { pub model: ModelId, pub id: SmolStr, pub path: SmolStr, pub cache_key: CacheKey, pub depth: u64, pub include_spans: Vec, pub includes: Vec, pub triangles: u64, pub quads: u64, pub lines: u64, pub conditional_lines: u64, /// LDParse-authoritative direct-mesh index offsets for this model's steps. pub step_ends: Vec, pub bfc: BfcState, } impl<'src> ParseResult<'src> { pub fn project(&self) -> CanonicalRecord<'src> { CanonicalRecord { schema_version: Cow::Borrowed(SCHEMA_VERSION), syntax: self.syntax.clone(), semantic: self.semantic.clone(), scene: self.scene.clone(), diagnostics: self.diagnostics.clone(), provenance_id: self.provenance_id.clone(), } } pub(crate) fn into_owned(self) -> ParseResult<'static> { ParseResult { syntax: self .syntax .into_iter() .map(SyntaxRecord::into_owned) .collect(), semantic: self.semantic.into_owned(), scene: self.scene, diagnostics: self.diagnostics, counters: self.counters, files: self.files, models: self.models, colours: self.colours.into_owned(), provenance_id: Cow::Owned(self.provenance_id.into_owned()), } } }