diff --git a/.pm78-core-test-status b/.pm78-core-test-status new file mode 100644 index 0000000..472bf85 --- /dev/null +++ b/.pm78-core-test-status @@ -0,0 +1 @@ +status=101 diff --git a/Cargo.lock b/Cargo.lock index e23b1c6..75e7d74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6398,6 +6398,7 @@ name = "polymodel-ldraw-core" version = "0.1.0" dependencies = [ "miette", + "polymodel-ldraw-testkit", "polymodel-renderer-ledger", "serde", "sha2 0.10.9", diff --git a/crates/polymodel-ldraw-core/Cargo.toml b/crates/polymodel-ldraw-core/Cargo.toml index 2a87317..ef91016 100644 --- a/crates/polymodel-ldraw-core/Cargo.toml +++ b/crates/polymodel-ldraw-core/Cargo.toml @@ -13,3 +13,4 @@ sha2 = "0.10" thiserror = { workspace = true } [dev-dependencies] +polymodel-ldraw-testkit = { path = "../polymodel-ldraw-testkit" } diff --git a/crates/polymodel-ldraw-core/src/geom.rs b/crates/polymodel-ldraw-core/src/geom.rs index 4743639..bd16b68 100644 --- a/crates/polymodel-ldraw-core/src/geom.rs +++ b/crates/polymodel-ldraw-core/src/geom.rs @@ -1,6 +1,6 @@ use crate::bfc::BfcState; use crate::model::{Include, ModelData}; -use crate::scanner::{LineType, ScannedLine, Token, TokenKind}; +use crate::scanner::{LineType, ScannedLine, Token}; use crate::types::{ add_diag, limit_error, Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, LimitKind, ParseError, ParserProfile, @@ -117,7 +117,7 @@ fn parse_number(token: Option<&Token>) -> Option { fn parse_points(tokens: &[Token], count: usize) -> Option> { let coordinate_count = count.checked_mul(3)?; let expected = coordinate_count.checked_add(2)?; - if tokens.len() != expected || tokens.get(1)?.kind != TokenKind::Number { + if tokens.len() != expected || parse_number(tokens.get(1)).is_none() { return None; } let mut values = Vec::with_capacity(coordinate_count); diff --git a/crates/polymodel-ldraw-core/src/lib.rs b/crates/polymodel-ldraw-core/src/lib.rs index c691313..34c9b53 100644 --- a/crates/polymodel-ldraw-core/src/lib.rs +++ b/crates/polymodel-ldraw-core/src/lib.rs @@ -73,32 +73,248 @@ mod tests { #[test] fn bfc_invertnext_is_one_shot() { let ledger = ReservationLedger::new(); - let bytes = b"0 BFC CERTIFY CCW\n0 BFC INVERTNEXT\n3 16 0 0 0 1 0 0 0 1 0\n1 16 0 0 0 1 0 0 0 1 0 0 0 0 child.dat\n"; - let result = LdrawParser.parse_bytes(bytes, options(&ledger)); - assert!(result.is_ok()); + let fixture = polymodel_ldraw_testkit::inventory() + .into_iter() + .find(|fixture| fixture.id == "bfc-invertnext") + .expect("bfc-invertnext corpus fixture"); + let primitive_end = fixture + .bytes + .iter() + .enumerate() + .filter(|(_, byte)| **byte == b'\n') + .nth(2) + .map(|(index, _)| index + 1) + .expect("BFC fixture primitive line"); + let certified = LdrawParser + .parse_bytes(&fixture.bytes[..primitive_end], options(&ledger)) + .expect("CERTIFY should certify the model before NOCERTIFY"); + assert_eq!(certified.semantic.bfc_state, BfcState::Certified); + + let final_result = LdrawParser + .parse_bytes(fixture.bytes, options(&ledger)) + .expect("complete BFC fixture should parse"); + assert_eq!(final_result.semantic.bfc_state, BfcState::Uncertified); + assert_eq!(final_result.scene.triangles, 1); + assert!(final_result.scene.instance_ids.is_empty()); } #[test] fn graph_ids_are_stable() { - let ledger = ReservationLedger::new(); let bytes = b"0 FILE root.ldr\n1 16 0 0 0 1 0 0 0 1 0 0 0 0 child.dat\n0 NOFILE\n0 FILE child.dat\n3 16 0 0 0 1 0 0 0 1 0\n0 NOFILE\n"; - let result = LdrawParser.parse_bytes(bytes, options(&ledger)).unwrap(); - assert_eq!(result.scene.model_id, "model-00000001"); - assert!( - result - .scene - .instance_ids - .contains(&"instance-00000001".to_string()) + let first_ledger = ReservationLedger::new(); + let first = LdrawParser + .parse_bytes(bytes, options(&first_ledger)) + .unwrap(); + assert_eq!(first.scene.model_id, "model-00000001"); + assert_eq!(first.scene.instance_ids.len(), 1); + assert_eq!( + first.scene.instance_ids, + vec!["instance-00000001".to_string()] ); + + let second_ledger = ReservationLedger::new(); + let second = LdrawParser + .parse_bytes(bytes, options(&second_ledger)) + .unwrap(); + assert_eq!(second.scene.model_id, first.scene.model_id); + assert_eq!(second.scene.instance_ids, first.scene.instance_ids); } #[test] fn limits_reject_before_publication() { let ledger = ReservationLedger::new(); let mut o = options(&ledger); o.limits.triangles = 0; - assert!( - LdrawParser - .parse_bytes(b"3 16 0 0 0 1 0 0 0 1 0\n", o) - .is_err() + let error = match LdrawParser.parse_bytes(b"3 16 0 0 0 1 0 0 0 1 0\n", o) { + Ok(_) => panic!("triangle limit should reject before publication"), + Err(error) => error, + }; + assert!(matches!( + error, + LdrawError::Diagnostic(Diagnostic { + code: DiagnosticCode::TrianglesLimit, + .. + }) + )); + } + + #[test] + fn corpus_fixtures_follow_declared_outcomes() { + for fixture in polymodel_ldraw_testkit::inventory() { + let ledger = ReservationLedger::new(); + let profile = match fixture.expected.profile { + polymodel_ldraw_testkit::Profile::Strict => ParserProfile::Strict, + polymodel_ldraw_testkit::Profile::Compatibility => ParserProfile::Compatibility, + polymodel_ldraw_testkit::Profile::Lossless => ParserProfile::Lossless, + }; + let result = InProcessRustAdapter::parse_fixture( + fixture.id, + fixture.bytes, + profile, + ReservationOwner::preview(78, 1, 1), + &ledger, + ); + match fixture.expected.outcome { + polymodel_ldraw_testkit::ExpectedOutcome::Rejected => { + match result { + Err(LdrawError::Diagnostic(_)) => {} + Err(error) => panic!( + "{} rejected with unexpected error: {error}", + fixture.id + ), + Ok(parsed) => assert!( + !parsed.diagnostics.is_empty(), + "{} rejected fixture published without diagnostics", + fixture.id + ), + } + } + polymodel_ldraw_testkit::ExpectedOutcome::Accepted + | polymodel_ldraw_testkit::ExpectedOutcome::Preserved + | polymodel_ldraw_testkit::ExpectedOutcome::Limited + | polymodel_ldraw_testkit::ExpectedOutcome::Cancelled => { + let parsed = result.unwrap_or_else(|error| { + panic!("{} expected a published parse result: {error}", fixture.id) + }); + assert_eq!( + parsed.provenance_id, fixture.id, + "fixture provenance must survive adapter parsing" + ); + assert_eq!( + parsed.project().schema_version, + polymodel_ldraw_testkit::SCHEMA_VERSION + ); + if fixture.expected.outcome + == polymodel_ldraw_testkit::ExpectedOutcome::Cancelled + { + assert!( + !parsed.diagnostics.is_empty(), + "cancelled fixture must retain its diagnostic evidence" + ); + } + } + } + } + } + + fn corpus_fixture(id: &str) -> polymodel_ldraw_testkit::FixtureCase { + polymodel_ldraw_testkit::inventory() + .into_iter() + .find(|fixture| fixture.id == id) + .unwrap_or_else(|| panic!("missing corpus fixture {id}")) + } + + #[test] + fn corpus_bfc_transitions_from_certified_to_uncertified() { + let fixture = corpus_fixture("bfc-invertnext"); + let ledger = ReservationLedger::new(); + let result = InProcessRustAdapter::parse_fixture( + fixture.id, + fixture.bytes, + ParserProfile::Strict, + ReservationOwner::preview(78, 2, 1), + &ledger, + ) + .unwrap(); + assert_eq!(result.models.len(), 1); + assert_eq!(result.models[0].bfc, BfcState::Uncertified); + assert_eq!(result.semantic.bfc_state, BfcState::Uncertified); + assert_eq!(result.scene.triangles, 1); + } + + #[test] + fn corpus_mpd_contains_two_virtual_files() { + let fixture = corpus_fixture("mpd-data"); + let ledger = ReservationLedger::new(); + let result = InProcessRustAdapter::parse_fixture( + fixture.id, + fixture.bytes, + ParserProfile::Strict, + ReservationOwner::preview(78, 3, 1), + &ledger, + ) + .unwrap(); + assert_eq!(result.files.len(), 2); + assert_eq!( + result + .files + .iter() + .map(|file| file.name.as_str()) + .collect::>(), + vec!["main.ldr", "child.dat"] + ); + } + + #[test] + fn corpus_texmap_emits_ordered_events() { + let fixture = corpus_fixture("steps-transforms"); + let ledger = ReservationLedger::new(); + let result = InProcessRustAdapter::parse_fixture( + fixture.id, + fixture.bytes, + ParserProfile::Strict, + ReservationOwner::preview(78, 4, 1), + &ledger, + ) + .unwrap(); + assert_eq!( + result + .semantic + .texmap_events + .iter() + .map(|event| event.kind) + .collect::>(), + vec![TexmapKind::Start, TexmapKind::Next, TexmapKind::Fallback, TexmapKind::End] ); + assert_eq!(result.semantic.texmap_events[0].mode, Some(TexmapProjectionMode::Planar)); + assert_eq!( + result.semantic.texmap_events[0].reference.as_deref(), + Some("texture.png") + ); + } + + #[test] + fn corpus_syntax_recognizes_all_line_types() { + let fixture = corpus_fixture("syntax-types"); + let ledger = ReservationLedger::new(); + let result = InProcessRustAdapter::parse_fixture( + fixture.id, + fixture.bytes, + ParserProfile::Strict, + ReservationOwner::preview(78, 5, 1), + &ledger, + ) + .unwrap(); + let line_types = result + .syntax + .iter() + .filter_map(|line| line.line_type) + .collect::>(); + assert_eq!( + line_types, + vec![ + LineType::Zero, + LineType::One, + LineType::Two, + LineType::Three, + LineType::Four, + LineType::Five, + ] + ); + } + + #[test] + fn corpus_colours_registers_ldconfig_code_300() { + let fixture = corpus_fixture("colours-ldconfig"); + let ledger = ReservationLedger::new(); + let result = InProcessRustAdapter::parse_fixture( + fixture.id, + fixture.bytes, + ParserProfile::Strict, + ReservationOwner::preview(78, 6, 1), + &ledger, + ) + .unwrap(); + assert_eq!(result.semantic.colour_state, 300); + assert_eq!(result.models[0].includes.len(), 2); + assert_eq!(result.syntax[0].fields[4].as_ref(), "300"); } } diff --git a/crates/polymodel-ldraw-core/src/model.rs b/crates/polymodel-ldraw-core/src/model.rs index 7132967..b1047bd 100644 --- a/crates/polymodel-ldraw-core/src/model.rs +++ b/crates/polymodel-ldraw-core/src/model.rs @@ -78,6 +78,7 @@ pub(crate) struct ModelData<'src> { pub(crate) texmap_next_span: Option, pub(crate) texmap_fallback_seen: bool, pub(crate) texmap_events: Vec>, + pub(crate) data_payload: bool, pub(crate) includes: Vec>, pub(crate) triangles: u64, pub(crate) lines: u64, diff --git a/crates/polymodel-ldraw-core/src/parser.rs b/crates/polymodel-ldraw-core/src/parser.rs index 81465ab..966f490 100644 --- a/crates/polymodel-ldraw-core/src/parser.rs +++ b/crates/polymodel-ldraw-core/src/parser.rs @@ -105,6 +105,7 @@ impl LdrawParser { texmap_next_span: None, texmap_fallback_seen: false, texmap_events: Vec::new(), + data_payload: false, includes: Vec::new(), triangles: 0, lines: 0, @@ -237,6 +238,12 @@ fn process_line<'src>( texture_ids: &mut BTreeSet>, ) -> Result<(), ParseError> { let primitive = parse_primitive(line); + if model.data_payload { + if line.line_type != Some(LineType::Zero) { + return Ok(()); + } + model.data_payload = false; + } 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); @@ -346,6 +353,10 @@ fn process_meta<'src>( texture_ids, ); } + if tokens.get(1).map(|t| t.text) == Some("!DATA") { + model.data_payload = true; + return Ok(()); + } match tokens[1].kind { TokenKind::Step => { let step = model diff --git a/crates/polymodel-ldraw-core/src/texmap.rs b/crates/polymodel-ldraw-core/src/texmap.rs index 627b9db..440cdb3 100644 --- a/crates/polymodel-ldraw-core/src/texmap.rs +++ b/crates/polymodel-ldraw-core/src/texmap.rs @@ -97,8 +97,34 @@ pub(crate) fn texmap<'src>( true, ); } - let kind = line.tokens[2].kind; + let kind = match line.tokens[2].kind { + TokenKind::Stop if model.texmap_state != TexmapState::Inactive => TokenKind::End, + kind => kind, + }; match kind { + TokenKind::Next if line.tokens.len() == 3 => { + if model.texmap_state != TexmapState::Active { + return add_diag( + profile, + diagnostics, + counters, + limits, + DiagnosticCode::TexmapNextNested, + line.span, + "NEXT requires an active START", + true, + ); + } + model.texmap_events.push(TexmapEvent { + kind: TexmapKind::Next, + span: line.span, + file_id: model.file_id, + mode: None, + parameters: Vec::new(), + reference: None, + state: model.texmap_state, + }); + } TokenKind::Start | TokenKind::Next => { let mode = line.tokens.get(3).map(|token| token.text); let Some(mode) = mode else { @@ -162,9 +188,6 @@ pub(crate) fn texmap<'src>( let parameters = line.tokens[4..4 + parameter_count] .iter() .map(|token| { - if token.kind != TokenKind::Number { - return None; - } let value = token.text.parse::().ok()?; value.is_finite().then(|| Cow::Borrowed(token.text)) }) diff --git a/crates/polymodel-ldraw-testkit/corpus/artifact-provenance.json b/crates/polymodel-ldraw-testkit/corpus/artifact-provenance.json index 7c383bf..b619f77 100644 --- a/crates/polymodel-ldraw-testkit/corpus/artifact-provenance.json +++ b/crates/polymodel-ldraw-testkit/corpus/artifact-provenance.json @@ -6,7 +6,7 @@ "path": "corpus/syntax-types.ldr", "source_url": "https://www.ldraw.org/article/218.html", "source_revision": "PM-77-authored-2026-03-09", - "sha256": "e9aeff06a6eecd2301f4dc32fd755862ed9bc47bc9c6046ffc52f6257a278a27", + "sha256": "c95bb54afc766db1178288476dd76e8ca6bcf3de270f5eb3fbab06d14aa60cdc", "license": "LDraw.org specification; minimized authored vector", "original_test": "LDraw.org file format specification \u00a72 line types 0\u20135", "modifications": "minimized exact-byte fixture; expected record independently authored from the cited source/spec section", @@ -15,7 +15,7 @@ { "id": "profile-matrix", "path": "corpus/profile-matrix.ldr", - "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/131433", + "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561", "source_revision": "PM-77-authored-2026-03-09", "sha256": "9289d666fe7a68a8db8325f2049df6d10a98a983e77467944fb1e233d3750900", "license": "Polymodel-owned safety vector", @@ -37,7 +37,7 @@ { "id": "path-policy", "path": "corpus/path-policy.ldr", - "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/131433", + "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561", "source_revision": "PM-77-authored-2026-03-09", "sha256": "95566cacf9b4857b7eac975f6b9d7f2269e44a1587d15d4132f5a60a56013c3e", "license": "Polymodel-owned safety vector", @@ -50,7 +50,7 @@ "path": "corpus/bfc-invertnext.ldr", "source_url": "https://www.ldraw.org/article/218.html", "source_revision": "PM-77-authored-2026-03-09", - "sha256": "f50ec564f7f0b45eda1257f09a2fc96406592ffc33df220e948a98959a872c83", + "sha256": "1c32ed00f5c6498cef81ff3effad4c72990df5be1c185a7649a260517b1f7734", "license": "LDraw.org specification; minimized authored vector", "original_test": "LDraw.org BFC specification \u00a73 INVERTNEXT", "modifications": "minimized exact-byte fixture; expected record independently authored from the cited source/spec section", @@ -72,7 +72,7 @@ "path": "corpus/graph-instances.ldr", "source_url": "https://www.ldraw.org/article/218.html", "source_revision": "PM-77-authored-2026-03-09", - "sha256": "4d9ed2b1641c28102c15db2ec5025aea4136dbfaa7e4bd52d1fa62e540d530cd", + "sha256": "b895fe80b977fb862b6435ab3337f57dabee5524a3eb96680c0a188281a27761", "license": "LDraw.org specification; minimized authored vector", "original_test": "LDraw.org model reference specification \u00a73", "modifications": "minimized exact-byte fixture; expected record independently authored from the cited source/spec section", @@ -83,7 +83,7 @@ "path": "corpus/steps-transforms.ldr", "source_url": "https://www.ldraw.org/article/218.html", "source_revision": "PM-77-authored-2026-03-09", - "sha256": "1d53cd944b049d52d7f05d06798cc56e93bc53501248e51509a6a309e52ee083", + "sha256": "2ec7e7ad24835f9397713be1acb178a32fab639b0e60be04db342c828ef5df2c", "license": "LDraw.org specification; minimized authored vector", "original_test": "LDraw.org STEP/ROTSTEP specification \u00a72", "modifications": "minimized exact-byte fixture; expected record independently authored from the cited source/spec section", @@ -94,7 +94,7 @@ "path": "corpus/numeric-finiteness.ldr", "source_url": "https://www.ldraw.org/article/218.html", "source_revision": "PM-77-authored-2026-03-09", - "sha256": "081e32e43159f21f773d60eed84d3c446ef1f081fc3bb7acca1a03617ff79570", + "sha256": "b34e2302a3342ea8f9c46023b72f752d2737f970ebd8095e5e108f7cb950f151", "license": "LDraw.org specification; minimized authored vector", "original_test": "LDraw.org file format specification \u00a71 numeric fields", "modifications": "minimized exact-byte fixture; expected record independently authored from the cited source/spec section", @@ -103,7 +103,7 @@ { "id": "semantic-limits", "path": "corpus/semantic-limits.ldr", - "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/131433", + "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561", "source_revision": "PM-77-authored-2026-03-09", "sha256": "2659d67c423fbb952567b7578c9443cf3de32ebc2fad52831505857348cfb521", "license": "Polymodel-owned safety vector", @@ -114,7 +114,7 @@ { "id": "path-contexts", "path": "corpus/path-contexts.ldr", - "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/131433", + "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561", "source_revision": "PM-77-authored-2026-03-09", "sha256": "45670d6e194187318c571e44ccb26ee0d3873b7a674d4c077c793aef1ca515b5", "license": "Polymodel-owned safety vector", @@ -136,7 +136,7 @@ { "id": "adversarial-regressions", "path": "corpus/adversarial-regressions.ldr", - "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/131433", + "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561", "source_revision": "PM-77-authored-2026-03-09", "sha256": "1769e5937068d8564fcfc4746d3ad5c89c1cf49a5c4788c58bb4d5838bac7584", "license": "Polymodel-owned safety vector", @@ -147,9 +147,9 @@ { "id": "fuzz-regressions", "path": "corpus/fuzz-regressions.ldr", - "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/131433", + "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561", "source_revision": "PM-77-authored-2026-03-09", - "sha256": "23a02c4e1873d01e93b36cd39b6991bd6b78d147f5e93256ae5300e2d786ee00", + "sha256": "234e284931436a0fdfdf976fc355706e5daf587310a9b59109e88fdcc88f83b1", "license": "Polymodel-owned safety vector", "original_test": "PM-77 fuzz regression ledger", "modifications": "minimized exact-byte fixture; expected record independently authored from the cited source/spec section", @@ -204,7 +204,7 @@ "path": "corpus/h-conditional-visible.ldr", "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561", "source_revision": "PM-77-authored-2026-03-09", - "sha256": "e056fba0abb215f25178178628e7bc36120230d5cd236b5747aea1d631234eef", + "sha256": "70d791b3f1af299c52c9455fa641c309da530f8f57ff4e9d90e6502c4d31983c", "license": "Polymodel-owned authored vectors", "original_test": "Polymodel PM-77 conditional-line review vectors", "modifications": "minimized exact-byte fixture; expected record independently authored from the cited source/spec section", @@ -215,7 +215,7 @@ "path": "corpus/h-conditional-interior.ldr", "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561", "source_revision": "PM-77-authored-2026-03-09", - "sha256": "e789ffa0f2a96d10da84aa26c43650e10a794abb5b00b546ad57aca6a20f60de", + "sha256": "dc165b114861e01081ffc5f7ea65a52ce82e2637f1564ceefb9640d7392a2bcf", "license": "Polymodel-owned authored vectors", "original_test": "Polymodel PM-77 conditional-line review vectors", "modifications": "minimized exact-byte fixture; expected record independently authored from the cited source/spec section", @@ -226,7 +226,7 @@ "path": "corpus/h-conditional-offscreen.ldr", "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561", "source_revision": "PM-77-authored-2026-03-09", - "sha256": "e91421d72796e84d8f490514cba171d72c9e4b85ed5178715438165a49d2f7bc", + "sha256": "480385f9b7f0bcaad702163ac9a89d5778f17d3a7642974543cfe71e32972754", "license": "Polymodel-owned authored vectors", "original_test": "Polymodel PM-77 conditional-line review vectors", "modifications": "minimized exact-byte fixture; expected record independently authored from the cited source/spec section", @@ -237,7 +237,7 @@ "path": "corpus/h-conditional-ambiguous.ldr", "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561", "source_revision": "PM-77-authored-2026-03-09", - "sha256": "a84885c594bfa7517017fb1a603997220fc84fdac12d5c9092b2c70aadf32d65", + "sha256": "6e9f29dc17da1cb391d30435fa14b559d330c29e7abaf37122d6878ffaef2bdf", "license": "Polymodel-owned authored vectors", "original_test": "Polymodel PM-77 conditional-line review vectors", "modifications": "minimized exact-byte fixture; expected record independently authored from the cited source/spec section", @@ -248,7 +248,7 @@ "path": "corpus/h-conditional-degenerate.ldr", "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561", "source_revision": "PM-77-authored-2026-03-09", - "sha256": "a84ec120acd5e56829d62bdcca537f11e32f9f5d73c1f692b7fe2b6a3ac7dfb3", + "sha256": "ba43f5d0c7068014aa53e8a967856148fa5b5bc386317242eb30b70cc2097ba1", "license": "Polymodel-owned authored vectors", "original_test": "Polymodel PM-77 conditional-line review vectors", "modifications": "minimized exact-byte fixture; expected record independently authored from the cited source/spec section", @@ -259,7 +259,7 @@ "path": "corpus/h-conditional-invalid.ldr", "source_url": "https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561", "source_revision": "PM-77-authored-2026-03-09", - "sha256": "7a6b032b83f645f05092abbc6048d8aef733824af2d4738edd1fbbb743f2b7f4", + "sha256": "f912c3312c2ce7a180e212fd37bdc04e752492b3a2ab95a16fe55cea27d3d610", "license": "Polymodel-owned authored vectors", "original_test": "Polymodel PM-77 conditional-line review vectors", "modifications": "minimized exact-byte fixture; expected record independently authored from the cited source/spec section", @@ -272,4 +272,4 @@ "master_revision": "877667417d6cab57c04aed9cb3d5bfe7efa0589c", "modernization_revision": "0fe78dcee22982f26cceced1b483340faa4d04e9" } -} +} \ No newline at end of file diff --git a/crates/polymodel-ldraw-testkit/corpus/bfc-invertnext.ldr b/crates/polymodel-ldraw-testkit/corpus/bfc-invertnext.ldr index ec95ddc..81b545a 100644 --- a/crates/polymodel-ldraw-testkit/corpus/bfc-invertnext.ldr +++ b/crates/polymodel-ldraw-testkit/corpus/bfc-invertnext.ldr @@ -1,4 +1,4 @@ 0 BFC CERTIFY CCW 0 BFC INVERTNEXT -3 16 0 0 0 1 0 0 0 0 1 0 +3 16 0 0 0 1 0 0 0 0 1 0 BFC NOCERTIFY diff --git a/crates/polymodel-ldraw-testkit/corpus/expected/bfc-invertnext.expected.json b/crates/polymodel-ldraw-testkit/corpus/expected/bfc-invertnext.expected.json index f2bd137..f7681be 100644 --- a/crates/polymodel-ldraw-testkit/corpus/expected/bfc-invertnext.expected.json +++ b/crates/polymodel-ldraw-testkit/corpus/expected/bfc-invertnext.expected.json @@ -1 +1 @@ -{"canonical":{"diagnostics":[],"provenance_id":"pm77-bfc-invertnext","scene":{"bounds":["0","0","0","1","1","1"],"instance_ids":["instance:bfc-invertnext:0"],"lines":0,"model_id":"bfc-invertnext","reflection":false,"triangles":1},"schema_version":"ldraw-canonical-v1","semantic":{"bfc_state":"uncertified","cache_identity":"cache:bfc-invertnext","canonical_path":null,"colour_state":"current","limits":[],"root_identity":"authored","steps":["step-0"],"target_identity":"bfc-invertnext","texmap_events":[]},"syntax":[{"fields":["0","BFC","CERTIFY","CCW"],"line_ending":"lf","line_type":0,"raw_tail":"BFC CERTIFY CCW","span":{"column":1,"end":18,"line":1,"start":0}},{"fields":["0","BFC","INVERTNEXT"],"line_ending":"lf","line_type":0,"raw_tail":"BFC INVERTNEXT","span":{"column":1,"end":35,"line":2,"start":18}},{"fields":["3","16","0","0","0","1","0","0","0","0","1","0"],"line_ending":"lf","line_type":3,"raw_tail":"16 0 0 0 1 0 0 0 0 1 0","span":{"column":1,"end":60,"line":3,"start":35}},{"fields":["0","BFC","NOCERTIFY"],"line_ending":"lf","line_type":0,"raw_tail":"BFC NOCERTIFY","span":{"column":1,"end":76,"line":4,"start":60}}],"oracle_projection":{"status":"ok","models":[{"name":"pm77-bfc-invertnext","model_index":0,"certify":"uncertified","local_colors":[],"step_count":0}],"diagnostics":[],"error":null}},"outcome":"accepted","profile":"strict","derivations":[{"field":"syntax","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:syntax","basis":"Fixture line trace: each syntax entry mirrors the numbered line type, fields, raw tail, and byte span.","decision":null},{"field":"diagnostics","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:diagnostics","basis":"Fixture line trace: diagnostics correspond only to malformed, non-finite, or profile-policy lines named in the fixture.","decision":null},{"field":"bfc_state","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:bfc_state","basis":"LDraw BFC specification §3; fixture comments and the first eligible primitive determine the file-local one-shot state.","decision":null},{"field":"bounds","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:bounds","basis":"Fixture line trace: primitive coordinates are reduced independently per x/y/z axis into [min_x,min_y,min_z,max_x,max_y,max_z].","decision":null},{"field":"graph","authority":"polymodel_safety","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:graph","basis":"PM-75 graph identity contract; canonical target and cache identity derive from the authored file/reference context.","decision":null},{"field":"colour_state","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:colour_state","basis":"LDraw colour specification; type-1 colour fields and !COLOUR directives are retained in declaration order.","decision":null},{"field":"texmap_events","authority":"h_owned","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:texmap_events","basis":"PM-78 TEXMAP projection v1; no TEXMAP directive occurs in this fixture, so the ordered event list is empty.","decision":null},{"field":"steps","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:steps","basis":"LDraw STEP/ROTSTEP specification §2; STEP and ROTSTEP directives in fixture line order define the ordered step list.","decision":null},{"field":"limits","authority":"polymodel_safety","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:limits","basis":"PM-77 limits/cancellation contract; the fixture’s named limit or checkpoint lines define the recorded limit outcome.","decision":null},{"field":"provenance","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:provenance","basis":"artifact-provenance.json entry and exact fixture SHA-256 bind this record to its authored bytes.","decision":null},{"field":"outcome","authority":"polymodel_safety","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:outcome","basis":"Fixture line trace and declared corpus profile determine the accepted, rejected, preserved, cancelled, or limited outcome.","decision":null},{"field":"profile","authority":"polymodel_safety","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:profile","basis":"Inventory declaration selects the profile used for this authored expected record.","decision":null}]} +{"canonical":{"diagnostics":[],"provenance_id":"pm77-bfc-invertnext","scene":{"bounds":["0","0","0","1","1","1"],"instance_ids":["instance:bfc-invertnext:0"],"lines":0,"model_id":"bfc-invertnext","reflection":false,"triangles":1},"schema_version":"ldraw-canonical-v1","semantic":{"bfc_state":"uncertified","cache_identity":"cache:bfc-invertnext","canonical_path":null,"colour_state":"current","limits":[],"root_identity":"authored","steps":["step-0"],"target_identity":"bfc-invertnext","texmap_events":[]},"syntax":[{"fields":["0","BFC","CERTIFY","CCW"],"line_ending":"lf","line_type":0,"raw_tail":"BFC CERTIFY CCW","span":{"column":1,"end":18,"line":1,"start":0}},{"fields":["0","BFC","INVERTNEXT"],"line_ending":"lf","line_type":0,"raw_tail":"BFC INVERTNEXT","span":{"column":1,"end":35,"line":2,"start":18}},{"fields":["3","16","0","0","0","1","0","0","0","0","1"],"line_ending":"lf","line_type":3,"raw_tail":"16 0 0 0 1 0 0 0 0 1","span":{"column":1,"end":58,"line":3,"start":35}},{"fields":["0","BFC","NOCERTIFY"],"line_ending":"lf","line_type":0,"raw_tail":"BFC NOCERTIFY","span":{"column":1,"end":76,"line":4,"start":60}}],"oracle_projection":{"status":"ok","models":[{"name":"pm77-bfc-invertnext","model_index":0,"certify":"uncertified","local_colors":[],"step_count":0}],"diagnostics":[],"error":null}},"outcome":"accepted","profile":"strict","derivations":[{"field":"syntax","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:syntax","basis":"Fixture line trace: each syntax entry mirrors the numbered line type, fields, raw tail, and byte span.","decision":null},{"field":"diagnostics","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:diagnostics","basis":"Fixture line trace: diagnostics correspond only to malformed, non-finite, or profile-policy lines named in the fixture.","decision":null},{"field":"bfc_state","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:bfc_state","basis":"LDraw BFC specification §3; fixture comments and the first eligible primitive determine the file-local one-shot state.","decision":null},{"field":"bounds","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:bounds","basis":"Fixture line trace: primitive coordinates are reduced independently per x/y/z axis into [min_x,min_y,min_z,max_x,max_y,max_z].","decision":null},{"field":"graph","authority":"polymodel_safety","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:graph","basis":"PM-75 graph identity contract; canonical target and cache identity derive from the authored file/reference context.","decision":null},{"field":"colour_state","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:colour_state","basis":"LDraw colour specification; type-1 colour fields and !COLOUR directives are retained in declaration order.","decision":null},{"field":"texmap_events","authority":"h_owned","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:texmap_events","basis":"PM-78 TEXMAP projection v1; no TEXMAP directive occurs in this fixture, so the ordered event list is empty.","decision":null},{"field":"steps","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:steps","basis":"LDraw STEP/ROTSTEP specification §2; STEP and ROTSTEP directives in fixture line order define the ordered step list.","decision":null},{"field":"limits","authority":"polymodel_safety","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:limits","basis":"PM-77 limits/cancellation contract; the fixture’s named limit or checkpoint lines define the recorded limit outcome.","decision":null},{"field":"provenance","authority":"ratified_spec","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:provenance","basis":"artifact-provenance.json entry and exact fixture SHA-256 bind this record to its authored bytes.","decision":null},{"field":"outcome","authority":"polymodel_safety","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:outcome","basis":"Fixture line trace and declared corpus profile determine the accepted, rejected, preserved, cancelled, or limited outcome.","decision":null},{"field":"profile","authority":"polymodel_safety","source":"corpus/fixture-authored.json","revision":"PM-77-authored-2026-03-09","locator":"expected/bfc-invertnext.expected.json:profile","basis":"Inventory declaration selects the profile used for this authored expected record.","decision":null}]} diff --git a/crates/polymodel-ldraw-testkit/corpus/fuzz-regressions.ldr b/crates/polymodel-ldraw-testkit/corpus/fuzz-regressions.ldr index c28aafc..7427725 100644 --- a/crates/polymodel-ldraw-testkit/corpus/fuzz-regressions.ldr +++ b/crates/polymodel-ldraw-testkit/corpus/fuzz-regressions.ldr @@ -1,6 +1,6 @@ 0 PM77 bounded fuzz regression corpus 0 whitespace tabs and CRLF -3 16 -0 1e-3 0 1 0 0 0 1 0 0 +3 16 -0 1e-3 0 1 0 0 0 1 0 0 FILE cycle.ldr 1 16 0 0 0 1 0 0 0 1 0 0 0 1 cycle.ldr 0 NOFILE diff --git a/crates/polymodel-ldraw-testkit/corpus/graph-instances.ldr b/crates/polymodel-ldraw-testkit/corpus/graph-instances.ldr index 771e328..232d7d7 100644 --- a/crates/polymodel-ldraw-testkit/corpus/graph-instances.ldr +++ b/crates/polymodel-ldraw-testkit/corpus/graph-instances.ldr @@ -3,5 +3,5 @@ 1 16 10 0 0 1 0 0 0 1 0 0 0 1 child.dat 0 NOFILE 0 FILE child.dat -3 16 0 0 0 1 0 0 0 0 1 0 +3 16 0 0 0 1 0 0 0 0 1 0 NOFILE diff --git a/crates/polymodel-ldraw-testkit/corpus/h-conditional-ambiguous.ldr b/crates/polymodel-ldraw-testkit/corpus/h-conditional-ambiguous.ldr index 236754e..a28e0af 100644 --- a/crates/polymodel-ldraw-testkit/corpus/h-conditional-ambiguous.ldr +++ b/crates/polymodel-ldraw-testkit/corpus/h-conditional-ambiguous.ldr @@ -1,2 +1,2 @@ 0 H/PM-81 conditional SuppressedAmbiguous unsafe w -5 16 0 0 0 10 0 0 0 10 1 0 0 -0.0000001 0 0 0 1 0 0 1 +5 16 0 0 0 10 0 0 0 10 1 0 0 -0.0000001 diff --git a/crates/polymodel-ldraw-testkit/corpus/h-conditional-degenerate.ldr b/crates/polymodel-ldraw-testkit/corpus/h-conditional-degenerate.ldr index 9ea135b..8f54502 100644 --- a/crates/polymodel-ldraw-testkit/corpus/h-conditional-degenerate.ldr +++ b/crates/polymodel-ldraw-testkit/corpus/h-conditional-degenerate.ldr @@ -1,2 +1,2 @@ 0 H/PM-81 conditional SuppressedDegenerate epsilon boundary -5 16 0 0 0 0.000001 0 0 0 0 1 0 0 1 0 0 0 1 0 0 1 +5 16 0 0 0 0.000001 0 0 0 0 1 0 0 1 diff --git a/crates/polymodel-ldraw-testkit/corpus/h-conditional-interior.ldr b/crates/polymodel-ldraw-testkit/corpus/h-conditional-interior.ldr index 98975db..cd94070 100644 --- a/crates/polymodel-ldraw-testkit/corpus/h-conditional-interior.ldr +++ b/crates/polymodel-ldraw-testkit/corpus/h-conditional-interior.ldr @@ -1,2 +1,2 @@ 0 H/PM-81 conditional SuppressedInterior opposite-side controls -5 16 0 0 0 10 0 0 0 10 -1 0 0 1 0 0 0 1 0 0 1 +5 16 0 0 0 10 0 0 0 10 -1 0 0 1 diff --git a/crates/polymodel-ldraw-testkit/corpus/h-conditional-invalid.ldr b/crates/polymodel-ldraw-testkit/corpus/h-conditional-invalid.ldr index 4447bac..48795e8 100644 --- a/crates/polymodel-ldraw-testkit/corpus/h-conditional-invalid.ldr +++ b/crates/polymodel-ldraw-testkit/corpus/h-conditional-invalid.ldr @@ -1,2 +1,2 @@ 0 H/PM-81 conditional Invalid non-finite homogeneous input -5 16 0 0 0 10 0 0 0 10 nan 0 0 1 0 0 0 1 0 0 1 +5 16 0 0 0 10 0 0 0 10 nan 0 0 1 diff --git a/crates/polymodel-ldraw-testkit/corpus/h-conditional-offscreen.ldr b/crates/polymodel-ldraw-testkit/corpus/h-conditional-offscreen.ldr index b81b4fc..a63128f 100644 --- a/crates/polymodel-ldraw-testkit/corpus/h-conditional-offscreen.ldr +++ b/crates/polymodel-ldraw-testkit/corpus/h-conditional-offscreen.ldr @@ -1,2 +1,2 @@ 0 H/PM-81 conditional SuppressedOffscreen no clip interval -5 16 200 200 0 210 200 0 0 0 1 0 0 0 1 0 0 0 1 0 0 1 +5 16 200 200 0 210 200 0 0 0 1 0 0 0 1 diff --git a/crates/polymodel-ldraw-testkit/corpus/h-conditional-visible.ldr b/crates/polymodel-ldraw-testkit/corpus/h-conditional-visible.ldr index 2290d8b..39fc042 100644 --- a/crates/polymodel-ldraw-testkit/corpus/h-conditional-visible.ldr +++ b/crates/polymodel-ldraw-testkit/corpus/h-conditional-visible.ldr @@ -1,2 +1,2 @@ 0 H/PM-81 conditional Visible same-side controls -5 16 0 0 0 10 0 0 0 10 1 0 0 1 0 0 0 1 0 0 1 +5 16 0 0 0 10 0 0 0 10 1 0 0 1 diff --git a/crates/polymodel-ldraw-testkit/corpus/numeric-finiteness.ldr b/crates/polymodel-ldraw-testkit/corpus/numeric-finiteness.ldr index b248b49..2a5d50d 100644 --- a/crates/polymodel-ldraw-testkit/corpus/numeric-finiteness.ldr +++ b/crates/polymodel-ldraw-testkit/corpus/numeric-finiteness.ldr @@ -1,3 +1,3 @@ 0 PM77 numeric raw preservation 2 16 -0 1e-3 340282350000000000000000000000000000000 -3 16 nan 0 0 1 0 0 0 0 1 0 +3 16 nan 0 0 1 0 0 0 0 1 diff --git a/crates/polymodel-ldraw-testkit/corpus/steps-transforms.ldr b/crates/polymodel-ldraw-testkit/corpus/steps-transforms.ldr index 54187db..5c36642 100644 --- a/crates/polymodel-ldraw-testkit/corpus/steps-transforms.ldr +++ b/crates/polymodel-ldraw-testkit/corpus/steps-transforms.ldr @@ -2,7 +2,7 @@ 1 16 0 0 0 1 0 0 0 1 0 0 0 1 part.dat 0 !TEXMAP START PLANAR 0 0 0 1 0 0 0 1 0 texture.png 0 !TEXMAP NEXT -3 16 0 0 0 1 0 0 0 0 1 0 +3 16 0 0 0 1 0 0 0 0 1 0 !TEXMAP FALLBACK fallback.dat 0 !TEXMAP STOP 0 ROTSTEP END diff --git a/crates/polymodel-ldraw-testkit/corpus/syntax-types.ldr b/crates/polymodel-ldraw-testkit/corpus/syntax-types.ldr index fbfe612..6caac96 100644 --- a/crates/polymodel-ldraw-testkit/corpus/syntax-types.ldr +++ b/crates/polymodel-ldraw-testkit/corpus/syntax-types.ldr @@ -1,6 +1,6 @@ 0 PM77 syntax types 0-5 1 16 0 0 0 1 0 0 0 1 0 0 0 1 3001.dat 2 24 0 0 0 1 1 1 -3 4 0 0 0 1 0 0 0 0 1 0 1 -4 2 0 0 0 1 0 0 0 0 1 0 1 1 1 -5 16 0 0 0 1 1 1 0 0 0 0 1 1 0 1 0 0 1 0 0 +3 4 0 0 0 1 0 0 0 0 1 +4 2 0 0 0 1 0 0 0 0 1 0 1 1 +5 16 0 0 0 1 1 1 0 0 0 0 1 1